// Comprehensive capture/audio/hook stress test against coop_mock_game. // // Launches the mock game (an animated, frame-numbered A/V source), injects coop_hook.dll, // and drives the real shared-memory paths the host uses -- no Steam, no host UI. For each // graphics backend (DX11, DX12) it opens the hook's shared video texture, decodes the // frame number out of the captured pixels, and asserts the mirror sees a *monotonic, // advancing* sequence (this is what the DX12 rotating-backbuffer bug broke -- it showed // stale/repeated frames). A final A/V test injects with audio + video, checks both stream, // cycles the audio subsystem off/on a few times (hook/unhook stress), and confirms the // game never freezes/crashes. Skips cleanly without a D3D11 device. #include #include #include #include #include #include #include #include #include #include #include #include "capture/shared_texture.hpp" #include "coop/audio_ring.hpp" #include "coop/log_ring.hpp" #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" #include "coop/tool_paths.hpp" #include "render_backend.hpp" // coop::mock::rgb_to_frame / kFrameBlock using namespace coop; namespace { int g_failures = 0; void check(bool ok, const char* what) { std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what); if (!ok) { ++g_failures; } } std::wstring tool_path(const wchar_t* name) { return exe_directory() + name; // coop_mock_game.exe is staged next to this test } // Kill any leftover mock games from a previous (crashed/interrupted) run, so a stray one // holding coop_hook.dll can't perturb this run. void kill_stray_mock_games() { HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap == INVALID_HANDLE_VALUE) { return; } PROCESSENTRY32W pe{}; pe.dwSize = sizeof(pe); for (BOOL ok = Process32FirstW(snap, &pe); ok; ok = Process32NextW(snap, &pe)) { if (_wcsicmp(pe.szExeFile, L"coop_mock_game.exe") == 0) { HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID); if (h != nullptr) { TerminateProcess(h, 0); CloseHandle(h); } } } CloseHandle(snap); } // Inject coop_hook.dll (x64 -> the mock game is x64) into `pid` via LoadLibrary remote thread. bool inject(unsigned long pid) { const std::wstring dll = deployed_artifact_path(L"coop_hook.dll"); // root is one dir up from tests/ if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES) { return false; } const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; HANDLE process = OpenProcess(access, FALSE, pid); if (process == nullptr) { return false; } const SIZE_T bytes = (dll.size() + 1) * sizeof(wchar_t); void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); bool ok = false; if (remote != nullptr && WriteProcessMemory(process, remote, dll.c_str(), bytes, nullptr)) { auto load = reinterpret_cast( GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW")); HANDLE th = CreateRemoteThread(process, nullptr, 0, load, remote, 0, nullptr); if (th != nullptr) { WaitForSingleObject(th, INFINITE); DWORD code = 0; GetExitCodeThread(th, &code); CloseHandle(th); ok = code != 0; } } if (remote != nullptr) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); } CloseHandle(process); return ok; } // Inject with a few retries: a freshly-launched process can briefly refuse a remote thread. bool inject_retry(unsigned long pid) { for (int i = 0; i < 4; ++i) { if (inject(pid)) { return true; } Sleep(300); } return false; } struct MockGame { PROCESS_INFORMATION pi{}; bool ok = false; // Launch coop_mock_game.exe with the given args (e.g. L"dx12 30" or L"dx11 30 48000 2 32 float"). static MockGame launch(const std::wstring& args) { MockGame g; const std::wstring exe = tool_path(L"coop_mock_game.exe"); std::wstring cmd = L"\"" + exe + L"\" " + args; STARTUPINFOW si{}; si.cb = sizeof(si); g.ok = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &g.pi) != 0; return g; } unsigned long pid() const { return pi.dwProcessId; } bool alive() const { return pi.hProcess != nullptr && WaitForSingleObject(pi.hProcess, 0) == WAIT_TIMEOUT; } unsigned long exit_code() const { DWORD code = 0; if (pi.hProcess != nullptr) { GetExitCodeProcess(pi.hProcess, &code); } return code; } void kill() { if (pi.hProcess != nullptr) { TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); pi = PROCESS_INFORMATION{}; } } }; // Create the input SharedBlock the hook needs, with the given subsystems disabled (bit per // HookSubsystem). Keeps the mapping alive in `shm`. SharedBlock* make_ipc(SharedMemory& shm, unsigned long pid, std::uint32_t disabled_mask) { if (!shm.create(shared_memory_name(pid), sizeof(SharedBlock))) { return nullptr; } auto* block = shm.as(); block->version = kProtocolVersion; block->pad_count = 0; block->sequence.store(0, std::memory_order_relaxed); for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store((disabled_mask >> s) & 1u, std::memory_order_release); } block->magic = kProtocolMagic; return block; } ID3D11Device* make_device() { ID3D11Device* dev = nullptr; const D3D_FEATURE_LEVEL fl[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0}; if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, fl, static_cast(std::size(fl)), D3D11_SDK_VERSION, &dev, nullptr, nullptr))) { return nullptr; } return dev; } VideoShareView read_video_share(const SharedBlock* block) { VideoShareView v; v.generation = block->video.generation.load(std::memory_order_acquire); v.width = block->video.width; v.height = block->video.height; v.format = block->video.format; v.present_calls = block->video.present_calls; return v; } // Capture from the mock game on `backend` and assert the decoded frame numbers form a // monotonic, advancing sequence. void test_video_capture(const char* backend, ID3D11Device* device) { std::printf("== video capture: %s ==\n", backend); std::wstring args; for (const char* p = backend; *p != '\0'; ++p) // backend names are ASCII (dx10/dx11/dx12/...) { args.push_back(static_cast(*p)); } args += L" 30"; MockGame game = MockGame::launch(args); if (!game.ok) { check(false, "launch coop_mock_game"); return; } Sleep(800); // let the window + swap chain come up // Only the video subsystem (disable input/focus/audio/mkb to keep the test focused). SharedMemory shm; const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); if (block == nullptr || !inject_retry(game.pid())) { check(false, "inject into mock game"); game.kill(); return; } SharedTextureSource src; src.init(device); std::vector seq; std::uint64_t backward = 0; std::uint32_t last = 0; bool have_last = false; for (int i = 0; i < 80 && game.alive(); ++i) // ~4 s at 50 ms { Sleep(50); const VideoShareView share = read_video_share(block); if (!src.update(share, game.pid())) { continue; } std::uint8_t px[4] = {}; if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { continue; } const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); if (have_last && f < last) { ++backward; // a stale / wrong (rotated) buffer -> frame number went backwards } last = f; have_last = true; seq.push_back(f); } const std::uint32_t present = static_cast(block->video.present_calls); std::printf(" present_calls=%u copied=%llu samples=%zu backward=%llu\n", present, static_cast(src.frames_copied()), seq.size(), static_cast(backward)); check(present > 0, "hook captured Present calls"); check(src.frames_copied() >= 5, "host copied multiple shared frames"); check(seq.size() >= 5, "decoded multiple frame numbers from the captured pixels"); check(backward == 0, "captured frame numbers never go backwards (no stale/rotated buffer)"); if (!seq.empty()) { const std::uint32_t span = seq.back() - seq.front(); std::printf(" frame# %u..%u (span %u)\n", seq.front(), seq.back(), span); check(span >= 20, "captured frame numbers advance (mirror gets fresh frames)"); const bool all_same = std::all_of(seq.begin(), seq.end(), [&](std::uint32_t v) { return v == seq[0]; }); check(!all_same, "captured frames are not stuck on one number"); } game.kill(); } // Vulkan capture: Vulkan caches its present pointer at init, so late injection can't hook it. // We launch the mock **suspended**, inject the hook, and resume; under COOP_MOCK_VK_EARLY the mock // loads vulkan-1.dll and waits, giving the hook's worker time to hook vkGetInstanceProcAddr before // the mock calls vkCreateInstance. Then we decode frames out of the captured pixels like the other // backends. Exit code 2 = no Vulkan driver -> skip without failing. void test_vk_capture(ID3D11Device* device) { std::printf("== video capture: vk (early-load) ==\n"); SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", L"1"); PROCESS_INFORMATION pi{}; STARTUPINFOW si{}; si.cb = sizeof(si); const std::wstring exe = tool_path(L"coop_mock_game.exe"); std::wstring cmd = L"\"" + exe + L"\" vk 30"; const BOOL ok = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", nullptr); if (!ok) { check(false, "launch suspended vk mock"); return; } SharedMemory shm; const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, pi.dwProcessId, disabled); const bool injected = block != nullptr && inject_retry(pi.dwProcessId); ResumeThread(pi.hThread); // the mock loads vulkan + waits, then renders auto cleanup = [&] { TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); }; if (!injected) { check(false, "inject suspended vk mock"); cleanup(); return; } auto alive = [&] { return WaitForSingleObject(pi.hProcess, 0) == WAIT_TIMEOUT; }; auto exit_code = [&] { DWORD c = 0; GetExitCodeProcess(pi.hProcess, &c); return c; }; SharedTextureSource src; src.init(device); std::vector seq; std::uint64_t backward = 0; std::uint32_t last = 0; bool have_last = false; for (int i = 0; i < 200 && alive(); ++i) // up to ~10 s (the mock waits 1.5 s at start) { Sleep(50); const VideoShareView share = read_video_share(block); if (!src.update(share, pi.dwProcessId)) { continue; } std::uint8_t px[4] = {}; if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { continue; } const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); if (have_last && f < last) { ++backward; } last = f; have_last = true; seq.push_back(f); if (seq.size() >= 40) { break; } } if (!alive() && exit_code() == 2 && seq.empty()) { std::printf(" Vulkan unavailable on this machine -- skipping vk capture\n"); cleanup(); return; } const std::uint32_t present = static_cast(block->video.present_calls); std::printf(" present_calls=%u copied=%llu samples=%zu backward=%llu\n", present, static_cast(src.frames_copied()), seq.size(), static_cast(backward)); check(present > 0, "hook captured Vulkan present calls"); check(src.frames_copied() >= 5, "host copied multiple shared frames (vk)"); check(seq.size() >= 5, "decoded multiple frame numbers from the captured pixels (vk)"); check(backward == 0, "captured vk frame numbers never go backwards"); if (!seq.empty()) { check(seq.back() - seq.front() >= 10, "captured vk frame numbers advance"); } cleanup(); } // Vulkan capture via the implicit layer: register coop_vk_layer through the loader (VK_LAYER_PATH // + VK_INSTANCE_LAYERS, with COOP_VK_LAYER_FORCE so it captures this process), launch the vk mock // normally (the layer is in the chain from the first frame -- no early-load games), and decode // frames out of the captured pixels. This is the productized form of the early-load path. void test_vk_layer_capture(ID3D11Device* device) { std::printf("== video capture: vk implicit layer ==\n"); const std::wstring manifest = deployed_artifact_path(L"coop_vk_layer.json"); if (GetFileAttributesW(manifest.c_str()) == INVALID_FILE_ATTRIBUTES) { check(false, "coop_vk_layer.json staged"); return; } const std::wstring layer_dir = manifest.substr(0, manifest.find_last_of(L"\\/")); SetEnvironmentVariableW(L"VK_LAYER_PATH", layer_dir.c_str()); SetEnvironmentVariableW(L"VK_INSTANCE_LAYERS", L"VK_LAYER_coop_capture"); SetEnvironmentVariableW(L"COOP_VK_LAYER_FORCE", L"1"); MockGame game = MockGame::launch(L"vk 30"); // normal launch; the layer is already in the chain auto unset_env = [] { SetEnvironmentVariableW(L"VK_LAYER_PATH", nullptr); SetEnvironmentVariableW(L"VK_INSTANCE_LAYERS", nullptr); SetEnvironmentVariableW(L"COOP_VK_LAYER_FORCE", nullptr); }; unset_env(); if (!game.ok) { check(false, "launch vk mock (layer)"); return; } SharedMemory shm; const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); // the layer connects to this + publishes if (block == nullptr) { check(false, "ipc block (layer)"); game.kill(); return; } SharedTextureSource src; src.init(device); std::vector seq; std::uint64_t backward = 0; std::uint32_t last = 0; bool have_last = false; for (int i = 0; i < 160 && game.alive(); ++i) // ~8 s { Sleep(50); const VideoShareView share = read_video_share(block); if (!src.update(share, game.pid())) { continue; } std::uint8_t px[4] = {}; if (!src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { continue; } const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); if (have_last && f < last) { ++backward; } last = f; have_last = true; seq.push_back(f); if (seq.size() >= 40) { break; } } if (!game.alive() && game.exit_code() == 2 && seq.empty()) { std::printf(" Vulkan unavailable on this machine -- skipping layer capture\n"); game.kill(); return; } std::printf(" copied=%llu samples=%zu backward=%llu\n", static_cast(src.frames_copied()), seq.size(), static_cast(backward)); check(src.frames_copied() >= 5, "layer copied multiple shared frames"); check(seq.size() >= 5, "decoded multiple frame numbers via the layer"); check(backward == 0, "layer-captured frame numbers never go backwards"); if (!seq.empty()) { check(seq.back() - seq.front() >= 10, "layer-captured frame numbers advance"); } game.kill(); } // Vulkan too-late detection: launch the vk mock normally (it inits Vulkan immediately), inject // *late* (the realistic case), and assert the hook reports vk_too_late -- it sees vulkan-1.dll // loaded but never caught the device, because the app resolved its present pointer first. This is // what drives the host's relaunch banner. void test_vk_too_late() { std::printf("== vk too-late detection (late inject) ==\n"); MockGame game = MockGame::launch(L"vk 30"); if (!game.ok) { check(false, "launch vk mock (too-late)"); return; } Sleep(1200); // let it create its instance/device and start presenting if (!game.alive() && game.exit_code() == 2) { std::printf(" Vulkan unavailable on this machine -- skipping\n"); game.kill(); return; } SharedMemory shm; const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Audio) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); if (block == nullptr || !inject_retry(game.pid())) { if (!game.alive() && game.exit_code() == 2) { std::printf(" Vulkan unavailable -- skipping\n"); } else { check(false, "inject vk mock (too-late)"); } game.kill(); return; } bool too_late = false; for (int i = 0; i < 160 && game.alive(); ++i) // ~8 s (past the hook's 4 s grace) { Sleep(50); if (block->status.vk_too_late != 0) { too_late = true; break; } } check(too_late, "hook reports vk_too_late after a late inject into a Vulkan game"); game.kill(); } // Launch the mock game rendering audio at `rate`/`channels`/`bits`/`fmt`, inject the audio // hook (late, so it's the guessed path), and verify the hook MEASURES the right sample rate // for this variant and captures non-silent audio. (Channels/bit-depth aren't recoverable for // a guessed stream -- that's the documented limitation -- so the rate is the variant check.) void test_audio_variant(unsigned rate, unsigned channels, unsigned bits, const wchar_t* fmt) { wchar_t args[64]; swprintf(args, static_cast(std::size(args)), L"dx11 30 %u %u %u %ls", rate, channels, bits, fmt); std::printf("== audio variant: %u Hz %u ch %u-bit %ls ==\n", rate, channels, bits, fmt); MockGame game = MockGame::launch(args); if (!game.ok) { check(false, "launch coop_mock_game (audio variant)"); return; } Sleep(800); // Audio subsystem only. SharedMemory shm; const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) | (1u << HookSubsys_Video) | (1u << HookSubsys_Mkb); SharedBlock* block = make_ipc(shm, game.pid(), disabled); SharedMemory ring_shm; AudioRingHeader* ring = nullptr; if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) { ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); } if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) { check(false, "inject into mock game (audio variant)"); game.kill(); return; } // Wait for the rate to be measured + published (Measured / LowConfidence / Exact), then // drain to prove non-silent capture. std::vector drain(kAudioRingCapacity); std::uint32_t measured = 0; std::uint32_t state = 0; double peak = 0.0; for (int i = 0; i < 120 && game.alive(); ++i) // up to ~6 s { Sleep(50); std::uint32_t got = 0; while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { if (ring->bits == 32 && ring->format_tag == 3) { const auto* f = reinterpret_cast(drain.data()); for (std::uint32_t k = 0; k < got / 4; ++k) { peak = std::max(peak, static_cast(std::fabs(f[k]))); } } else if (ring->bits == 16) { const auto* s = reinterpret_cast(drain.data()); for (std::uint32_t k = 0; k < got / 2; ++k) { peak = std::max(peak, std::abs(s[k]) / 32768.0); } } if (got < drain.size()) { break; } } state = block->status.audio_streams[0].format_state; if (state == AudioFormat_Measured || state == AudioFormat_LowConfidence || state == AudioFormat_Exact) { measured = block->status.audio_streams[0].sample_rate; if (measured != 0 && peak > 0.01) { break; } } } std::printf(" measured rate=%u Hz (state=%u) peak=%.4f\n", measured, state, peak); check(measured == rate, "hook measured this variant's sample rate"); check(peak > 0.01, "captured non-silent audio for this variant"); game.kill(); } // Inject with audio + video, verify both stream, then cycle the audio subsystem off/on a // few times (hook/unhook stress) and confirm the game stays alive (heartbeat advances). void test_av_and_hook_cycles(ID3D11Device* device) { std::printf("== A/V + hook/unhook stress (dx11 + audio) ==\n"); MockGame game = MockGame::launch(L"dx11 30 48000 2 32 float"); if (!game.ok) { check(false, "launch coop_mock_game (A/V)"); return; } Sleep(800); SharedMemory shm; SharedBlock* block = make_ipc(shm, game.pid(), /*disabled=*/0); // all subsystems on SharedMemory ring_shm; AudioRingHeader* ring = nullptr; if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity))) { ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); } if (block == nullptr || ring == nullptr || !inject_retry(game.pid())) { check(false, "inject into mock game (A/V)"); game.kill(); return; } SharedTextureSource src; src.init(device); // Let it settle, then verify video advances and audio is non-silent. double peak = 0.0; std::uint32_t first_frame = 0, last_frame = 0; std::vector drain(kAudioRingCapacity); for (int i = 0; i < 60 && game.alive(); ++i) // ~3 s { Sleep(50); const VideoShareView share = read_video_share(block); if (src.update(share, game.pid())) { std::uint8_t px[4] = {}; if (src.read_pixel(coop::mock::kFrameBlock / 2, coop::mock::kFrameBlock / 2, px)) { const std::uint32_t f = coop::mock::rgb_to_frame(px[0], px[1], px[2]); if (first_frame == 0) { first_frame = f; } last_frame = f; } } std::uint32_t got = 0; while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { if (ring->bits == 32 && ring->format_tag == 3) { const auto* f = reinterpret_cast(drain.data()); for (std::uint32_t k = 0; k < got / 4; ++k) { peak = std::max(peak, static_cast(std::fabs(f[k]))); } } if (got < drain.size()) { break; } } } check(last_frame > first_frame, "A/V: video frames advance"); check(peak > 0.01, "A/V: audio captured non-silent"); // Hook/unhook stress: toggle the audio subsystem off->on a few times. Each phase is > // the worker's ~250 ms reconcile tick, so the install/remove fully completes each time // (this is the realistic cadence -- an operator toggling a checkbox, not thrashing it). const std::uint32_t hb_start = block->status.heartbeat.load(std::memory_order_relaxed); for (int c = 0; c < 3 && game.alive(); ++c) { block->control.subsystem_disabled[HookSubsys_Audio].store(1, std::memory_order_release); Sleep(400); block->control.subsystem_disabled[HookSubsys_Audio].store(0, std::memory_order_release); Sleep(400); } const std::uint32_t hb_end = block->status.heartbeat.load(std::memory_order_relaxed); if (!game.alive()) { std::printf(" game exit code = 0x%08lX\n", game.exit_code()); } check(game.alive(), "game survived hook/unhook cycles (no crash)"); check(hb_end > hb_start, "hook heartbeat kept advancing through the cycles (no freeze)"); // After the final re-enable, capture must resume: the audio must come back (proving the // rehook works end-to-end, not just that nothing crashed). std::uint64_t produced_before = ring->frames_produced.load(std::memory_order_relaxed); double peak2 = 0.0; for (int i = 0; i < 30 && game.alive(); ++i) // ~1.5 s { Sleep(50); std::uint32_t got = 0; while ((got = audio_ring_pop(*ring, drain.data(), static_cast(drain.size()))) > 0) { if (ring->bits == 32 && ring->format_tag == 3) { const auto* f = reinterpret_cast(drain.data()); for (std::uint32_t k = 0; k < got / 4; ++k) { peak2 = std::max(peak2, static_cast(std::fabs(f[k]))); } } if (got < drain.size()) { break; } } } const std::uint64_t produced_after = ring->frames_produced.load(std::memory_order_relaxed); check(produced_after > produced_before && peak2 > 0.01, "audio capture resumed after re-enable"); game.kill(); } // Aggressively toggle every injected subsystem on/off from a separate thread while the game is // actively presenting, to provoke an unsafe hook install/remove race. The known failure mode: // remove_*_hooks frees the hook's shared D3D state (device / context / keyed-mutex texture, and // the Vulkan read-back resources) while a capture detour on the game's render thread is still // using it -> use-after-free -> the game crashes. This is the "spamming the Mirror video button // crashed Brotato" bug; the gentle, audio-only cycles in test_av_and_hook_cycles never exercised // the video teardown, so they missed it. We storm ALL subsystems (especially video) across every // backend, then confirm the game never crashed/froze and that capture resumes. vk_early uses the // suspended-launch + early-inject path (Vulkan caches its present pointer at init, so a late inject // can't hook it). void test_hook_storm(const char* backend, ID3D11Device* device, bool vk_early) { std::printf("== hook/unhook storm: %s ==\n", backend); std::wstring wbackend; for (const char* p = backend; *p != '\0'; ++p) // backend names are ASCII { wbackend.push_back(static_cast(*p)); } PROCESS_INFORMATION pi{}; STARTUPINFOW si{}; si.cb = sizeof(si); const std::wstring exe = tool_path(L"coop_mock_game.exe"); std::wstring cmd = L"\"" + exe + L"\" " + wbackend + L" 60"; if (vk_early) { SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", L"1"); } const DWORD launch_flags = vk_early ? CREATE_SUSPENDED : 0; const BOOL launched = CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, launch_flags, nullptr, nullptr, &si, &pi); if (vk_early) { SetEnvironmentVariableW(L"COOP_MOCK_VK_EARLY", nullptr); } if (!launched) { check(false, "launch mock game (storm)"); return; } auto alive = [&] { return WaitForSingleObject(pi.hProcess, 0) == WAIT_TIMEOUT; }; auto exit_code = [&] { DWORD c = 0; GetExitCodeProcess(pi.hProcess, &c); return c; }; auto cleanup = [&] { TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 2000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); }; SharedMemory shm; SharedBlock* block = make_ipc(shm, pi.dwProcessId, /*disabled=*/0); // all subsystems on SharedMemory ring_shm; AudioRingHeader* ring = nullptr; if (ring_shm.create(audio_ring_name(pi.dwProcessId), audio_ring_total_size(kAudioRingCapacity))) { ring = ring_shm.as(); audio_ring_init(*ring, kAudioRingCapacity); ring->capture_enabled.store(1, std::memory_order_release); } const bool injected = block != nullptr && inject_retry(pi.dwProcessId); if (vk_early) { ResumeThread(pi.hThread); // the mock loads Vulkan + waits, then renders } if (!injected) { if (vk_early && !alive() && exit_code() == 2) { std::printf(" Vulkan unavailable -- skipping storm\n"); } else { check(false, "inject mock game (storm)"); } cleanup(); return; } Sleep(vk_early ? 2500 : 1000); // let the hook attach + the game start presenting if (vk_early && !alive() && exit_code() == 2) { std::printf(" Vulkan unavailable -- skipping storm\n"); cleanup(); return; } const std::uint32_t hb_start = block->status.heartbeat.load(std::memory_order_relaxed); // Storm thread: flip every subsystem on/off as fast as it can while the game presents, so a // remove lands while a capture detour is mid-flight on the game's render thread. std::atomic stop{false}; std::thread storm([&] { bool off = false; while (!stop.load(std::memory_order_relaxed)) { off = !off; for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store(off ? 1u : 0u, std::memory_order_release); } Sleep(60); } }); bool crashed = false; for (int i = 0; i < 170 && !crashed; ++i) // ~10 s of storming { Sleep(60); if (!alive()) { crashed = true; } } stop.store(true, std::memory_order_relaxed); storm.join(); if (crashed) { std::printf(" game CRASHED during the storm (exit 0x%08lX)\n", exit_code()); } check(!crashed, "game survived the hook/unhook storm (no crash)"); if (crashed) { cleanup(); return; } // Re-enable everything and confirm the game is still alive + the hook still beating. for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) { block->control.subsystem_disabled[s].store(0, std::memory_order_release); } Sleep(600); check(alive(), "game alive after the storm settles"); check(block->status.heartbeat.load(std::memory_order_relaxed) > hb_start, "hook heartbeat advanced across the storm (no freeze)"); // Capture must resume (the rehook works end-to-end). Vulkan can't re-arm after a toggle (its // present pointer was cached at init), so only assert resume for the other backends. if (!vk_early) { SharedTextureSource src; src.init(device); const std::uint64_t frames0 = src.frames_copied(); bool advanced = false; for (int i = 0; i < 80 && alive(); ++i) // ~4 s { Sleep(50); const VideoShareView share = read_video_share(block); if (src.update(share, pi.dwProcessId) && src.frames_copied() > frames0 + 3) { advanced = true; break; } } check(advanced, "video capture resumed after the storm"); } cleanup(); } } // namespace int main() { kill_stray_mock_games(); // clean slate: no leftover game holding coop_hook.dll ID3D11Device* device = make_device(); if (device == nullptr) { std::printf("No D3D11 device -- skipping mock_game_test.\n"); return 0; } // Vulkan: present pointer cached at init -> can't be late-hooked, so we capture via the // early-load path (suspended launch + inject + resume; the mock loads Vulkan and waits). test_vk_capture(device); test_vk_layer_capture(device); test_vk_too_late(); test_video_capture("gl", device); test_video_capture("dx9ex", device); test_video_capture("dx9", device); test_video_capture("dx10", device); test_video_capture("dx11", device); test_video_capture("dx12", device); // Audio variants: the hook must measure each variant's rate through the full inject path. test_audio_variant(44100, 2, 16, L"pcm"); test_audio_variant(48000, 2, 32, L"float"); test_audio_variant(96000, 2, 32, L"float"); test_av_and_hook_cycles(device); // Aggressive hook/unhook storm across every backend: a separate thread thrashes every // subsystem on/off while the game presents, to catch an unsafe install/remove race (the // "spamming Mirror video crashed Brotato" use-after-free). vk uses the early-load path. test_hook_storm("gl", device, /*vk_early=*/false); test_hook_storm("dx9", device, /*vk_early=*/false); test_hook_storm("dx10", device, /*vk_early=*/false); test_hook_storm("dx11", device, /*vk_early=*/false); test_hook_storm("dx12", device, /*vk_early=*/false); test_hook_storm("vk", device, /*vk_early=*/true); device->Release(); kill_stray_mock_games(); // belt-and-suspenders: ensure nothing is left running if (g_failures == 0) { std::printf("PASS mock_game_test\n"); return 0; } std::printf("FAILED mock_game_test (%d)\n", g_failures); return 1; }