Files
CoopAllTheThings/tests/CMakeLists.txt
BlackMark 6d96531ac7 Graceful disconnect: tell the DLL to unhook everything
On an explicit Disconnect and on graceful tool exit, the host now asks the
injected DLL to remove every subsystem so the game runs exactly as if it was
never hooked (each hook restores its original bytes). The DLL stays injected but
dormant, ready for a later reconnect -- we never eject it.

Before, both paths just dropped the IPC channel (IpcServer::stop) without telling
the DLL, leaving the hooks active with frozen forwarded state until the game
exited.

- IpcServer::request_unhook_all() sets every subsystem_disabled flag (the DLL
  reconciles to fully unhooked on its next tick); all_hooks_removed() reads the
  hook registry back so the host can confirm the game is vanilla.
- InjectionPanel::disconnect_graceful() requests the unhook, waits (bounded) for
  the registry to clear, then stops. Wired into the Disconnect button (700ms) and
  the destructor (300ms). The flags persist in the section the DLL keeps alive, so
  the unhook completes even if the host exits before confirming.

Tests (failing first):
- ipc_server_test: request_unhook_all() disables all subsystems; all_hooks_removed()
  tracks the registry. Deterministic, no game.
- mock_game_test test_graceful_disconnect: inject -> hooks installed -> request
  unhook-all -> every hook removed (game vanilla) while the DLL stays alive
  (heartbeat advancing). Full suite still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:46:51 +02:00

333 lines
16 KiB
CMake

# Self-contained verification of the forwarding core (IPC + XInput hook).
# Reuses the hook's xinput_hook.cpp directly so it exercises the shipping code.
add_executable(hook_selftest
hook_selftest.cpp
${CMAKE_SOURCE_DIR}/hook/src/xinput_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(hook_selftest PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(hook_selftest PRIVATE
coop_common
safetyhook::safetyhook
xinput)
add_test(NAME hook_selftest COMMAND hook_selftest)
# In-process self-test for the DirectInput forwarding path: reuses mkb_hook.cpp, installs the MKB
# hooks (which vtable-swap IDirectInputDevice8::GetDeviceState), forwards a synthetic key + mouse
# button through the ring, then creates a real DI keyboard + mouse device and asserts GetDeviceState
# returns the forwarded input. Skips cleanly if DirectInput can't acquire a device.
add_executable(dinput_hook_test
dinput_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/mkb_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(dinput_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(dinput_hook_test PRIVATE
coop_common
safetyhook::safetyhook
user32
dinput8
dxguid
ole32)
add_test(NAME dinput_hook_test COMMAND dinput_hook_test)
# Unit test for the shared audio ring (lock-free SPSC push/pop, wrap-around,
# format handshake, overrun policy). Header-only, no hook or audio device.
add_executable(audio_ring_test audio_ring_test.cpp)
target_link_libraries(audio_ring_test PRIVATE coop_common)
add_test(NAME audio_ring_test COMMAND audio_ring_test)
# Unit test for the host-side IpcServer control ops behind graceful disconnect:
# request_unhook_all() disables every subsystem and all_hooks_removed() reads the hook
# registry back. Opens a second view of the section to stand in for the hook. No game.
add_executable(ipc_server_test
ipc_server_test.cpp
${CMAKE_SOURCE_DIR}/host/src/ipc/ipc_server.cpp)
target_include_directories(ipc_server_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
target_link_libraries(ipc_server_test PRIVATE coop_common)
add_test(NAME ipc_server_test COMMAND ipc_server_test)
# Unit test for the DetourGate safe-unhook coordinator (hook/src/hook_guard.hpp): drain() must block
# while a detour Guard is in flight and return promptly otherwise. Fast/deterministic complement to
# the mock_game_test hook/unhook storm. Header-only (just needs the hook include dir + threads).
add_executable(detour_gate_test detour_gate_test.cpp)
target_include_directories(detour_gate_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
add_test(NAME detour_gate_test COMMAND detour_gate_test)
# Deterministic contract test for the persistent inline-hook model (hook/src/hook_install.hpp):
# install_inline creates a hook once and re-enables it on re-install, reusing the trampoline (never
# freed) so a stale detour can't UAF. Single-threaded, so it can't flake on enable/disable atomicity.
#
# A multithreaded TIGHT-LOOP toggle test used to live here and flaked ~1/10: it was asserting an
# atomicity that SafetyHook's enable()/disable() don't provide and that our code never needs (we
# reconcile install/remove from a single, tick-bounded worker thread -- never a tight loop). That
# valid contract is now covered deterministically here + by detour_gate_test; the realistic concurrent
# behaviour by mock_game_test's storm; and the SafetyHook limitation itself is captured as a committed
# upstream reproducer at tools/sh_concurrency_repro/ (see docs/safetyhook-concurrency.md) rather than a
# flaky CTest.
add_executable(hook_install_test hook_install_test.cpp)
target_include_directories(hook_install_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(hook_install_test PRIVATE safetyhook::safetyhook)
add_test(NAME hook_install_test COMMAND hook_install_test)
# Unit test for the MKB event ring (SPSC push/pop, wrap-around, full/empty).
add_executable(mkb_ring_test mkb_ring_test.cpp)
target_link_libraries(mkb_ring_test PRIVATE coop_common)
add_test(NAME mkb_ring_test COMMAND mkb_ring_test)
# Unit test for the audio mixer math (decode/sum/soft-clip/encode). Header-only.
add_executable(audio_mix_test audio_mix_test.cpp)
target_include_directories(audio_mix_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
add_test(NAME audio_mix_test COMMAND audio_mix_test)
# Unit test for the robust sample-rate estimator (consensus / reject-non-standard /
# low-confidence). Pure header logic fed synthetic adversarial cadences. No device.
add_executable(rate_estimator_test rate_estimator_test.cpp)
target_include_directories(rate_estimator_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
add_test(NAME rate_estimator_test COMMAND rate_estimator_test)
# Unit test for the audio fidelity analyzer (pitch error in cents, SNR/THD, click +
# dropout detection) and the WAV reader/writer. Synthesizes adversarial signals (clean /
# wrong-rate pitch shift / injected clicks / silence gaps) and asserts the metrics. Pure
# header logic, no device. Underpins the coop_audio_validate diagnostic tool.
add_executable(tone_analysis_test tone_analysis_test.cpp)
target_link_libraries(tone_analysis_test PRIVATE coop_common)
add_test(NAME tone_analysis_test COMMAND tone_analysis_test)
# Unit test for the two-path audio-format correlator (common/include/coop/audio_correlate.hpp).
# Synthesizes one continuous signal sampled at two rates (the hook's true rate + the device rate,
# with capture skew + noise) and asserts correlate_rate() recovers the true rate -- including the
# 44100-vs-48000 case the cadence method can misread. Pure header logic, no device.
add_executable(audio_correlation_test audio_correlation_test.cpp)
target_link_libraries(audio_correlation_test PRIVATE coop_common)
add_test(NAME audio_correlation_test COMMAND audio_correlation_test)
# Unit test for the audio-mirror render pacing policy (host/src/audio/render_pacer.hpp).
# Simulates a producer/consumer device timeline and asserts the shipping RenderPacer rides
# through producer jitter that makes the old re-prime-on-partial-fill policy glitch
# repeatedly (the under-run / "metallic" bug coop_audio_validate found). Header-only, no device.
add_executable(render_pacer_test render_pacer_test.cpp)
target_include_directories(render_pacer_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
add_test(NAME render_pacer_test COMMAND render_pacer_test)
# Unit test for the per-game audio override store (persist/reload, case-insensitive
# lookup, differing-overwrite detection). Reuses the shipping source. No device.
add_executable(audio_overrides_test
audio_overrides_test.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/audio_overrides.cpp)
target_include_directories(audio_overrides_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
target_link_libraries(audio_overrides_test PRIVATE coop_common)
add_test(NAME audio_overrides_test COMMAND audio_overrides_test)
# Unit test for the host->game mouse coordinate mapping (letterbox inverse +
# decorated-window client offset). Header-only, no device.
add_executable(mkb_map_test mkb_map_test.cpp)
target_include_directories(mkb_map_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
add_test(NAME mkb_map_test COMMAND mkb_map_test)
# Integration test for WASAPI process-loopback capture. Reuses the shipping
# capture code and captures from coop_tone (a known sine-wave render process).
add_executable(audio_loopback_test
audio_loopback_test.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/process_loopback_capture.cpp)
target_include_directories(audio_loopback_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
# Process loopback needs the Windows 10 20H1 (NTDDI_WIN10_CO) headers.
target_compile_definitions(audio_loopback_test PRIVATE NTDDI_VERSION=0x0A00000B)
target_link_libraries(audio_loopback_test PRIVATE mmdevapi ole32)
add_dependencies(audio_loopback_test coop_tone)
add_test(NAME audio_loopback_test COMMAND audio_loopback_test)
# In-process self-test for the WASAPI render-hook. Reuses the shipping
# audio_hook.cpp and drives a real WASAPI render path in the same process, so it
# exercises COM vtable discovery + GetBuffer/ReleaseBuffer interception with no
# game and no second Steam account (the audio analogue of hook_selftest).
add_executable(audio_hook_test
audio_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/audio_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(audio_hook_test PRIVATE
${CMAKE_SOURCE_DIR}/hook/src
${CMAKE_SOURCE_DIR}/tools/audio_tone) # shared configurable ToneSource (also used by coop_tone)
# IAudioClient3 / process-audio APIs want the Windows 10 20H1 (NTDDI_WIN10_CO) headers.
target_compile_definitions(audio_hook_test PRIVATE NTDDI_VERSION=0x0A00000B)
target_link_libraries(audio_hook_test PRIVATE
coop_common
safetyhook::safetyhook
ole32
mmdevapi)
add_test(NAME audio_hook_test COMMAND audio_hook_test)
# Unit test for the sRGB->UNORM format mapping the hooked video path uses to avoid
# darkening *_SRGB-backbuffer games. Header-only, no device.
add_executable(srgb_format_test srgb_format_test.cpp)
target_include_directories(srgb_format_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
add_test(NAME srgb_format_test COMMAND srgb_format_test)
# In-process self-test for the Present-hook video path. Reuses the shipping
# present_hook.cpp and drives a real D3D11 swapchain in the same process, so it
# exercises the IDXGISwapChain::Present inline hook, the shared keyed-mutex
# texture, and the open-by-name / readback contract (the video analogue of
# audio_hook_test). Skips on a machine without a D3D11 device.
add_executable(present_hook_test
present_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/present_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(present_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(present_hook_test PRIVATE
coop_common
safetyhook::safetyhook
d3d11
dxgi)
add_test(NAME present_hook_test COMMAND present_hook_test)
# In-process self-test for the D3D12 path of the Present hook: drives a real D3D12
# swapchain through the (shared) IDXGISwapChain::Present vtable and verifies the
# D3D11On12 bridge copies the D3D12 backbuffer into the shared texture. Skips on a
# machine without a D3D12 device.
add_executable(dx12_present_hook_test
dx12_present_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/present_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(dx12_present_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(dx12_present_hook_test PRIVATE
coop_common
safetyhook::safetyhook
d3d11
d3d12
dxgi)
add_test(NAME dx12_present_hook_test COMMAND dx12_present_hook_test)
# Comprehensive capture/audio/hook stress test against coop_mock_game: launches the
# animated, frame-numbered A/V game, injects coop_hook.dll, decodes captured frame
# numbers (DX11 + DX12) to assert a monotonic/advancing mirror, checks audio capture, and
# stresses hook/unhook cycles. Reuses the shipping shared-texture reader. Skips without D3D11.
add_executable(mock_game_test
mock_game_test.cpp
${CMAKE_SOURCE_DIR}/host/src/capture/shared_texture.cpp)
target_include_directories(mock_game_test PRIVATE
${CMAKE_SOURCE_DIR}/host/src
${CMAKE_SOURCE_DIR}/tools/mock_game)
target_link_libraries(mock_game_test PRIVATE coop_common d3d11 dxgi)
add_dependencies(mock_game_test coop_mock_game coop_hook)
add_test(NAME mock_game_test COMMAND mock_game_test)
# Integration test for the two-path audio-format verifier: launches coop_mock_game rendering a
# NON-device rate (44100 on a 48000 endpoint), injects coop_hook.dll late (guessed stream), and
# runs the real verify_stream_format() -- co-capturing the hook (pre-mix) + a parallel loopback
# (post-mix) and correlating to recover the true rate. Skips cleanly without an audio endpoint.
add_executable(audio_verify_test
audio_verify_test.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/audio_format_verifier.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/process_loopback_capture.cpp)
target_include_directories(audio_verify_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
target_compile_definitions(audio_verify_test PRIVATE NTDDI_VERSION=0x0A00000B)
target_link_libraries(audio_verify_test PRIVATE coop_common ole32 mmdevapi)
add_dependencies(audio_verify_test coop_mock_game coop_hook)
add_test(NAME audio_verify_test COMMAND audio_verify_test)
# In-process self-test for the OpenGL capture path. Reuses the shipping
# opengl_hook.cpp and drives a real OpenGL context in the same process, so it
# exercises the SwapBuffers hook, the glReadPixels readback, and the upload into
# the shared texture (opened by name + read back). Skips without GL / D3D11.
add_executable(opengl_hook_test
opengl_hook_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/opengl_hook.cpp
${CMAKE_SOURCE_DIR}/hook/src/debug_log.cpp
${CMAKE_SOURCE_DIR}/hook/src/hook_registry.cpp)
target_include_directories(opengl_hook_test PRIVATE ${CMAKE_SOURCE_DIR}/hook/src)
target_link_libraries(opengl_hook_test PRIVATE
coop_common
safetyhook::safetyhook
d3d11
dxgi
opengl32
gdi32)
add_test(NAME opengl_hook_test COMMAND opengl_hook_test)
# Reproducer + regression guard for the Vulkan capture performance collapse (the ~370 ms/frame
# present-thread stall that dropped a 144 FPS game to ~3 FPS). Reuses the shipping VkCapture; builds
# a known image on a real device and asserts the fixed present-thread cost is a small fraction of a
# synchronous read-back (and the captured image is byte-correct). `--sync` re-demonstrates the
# regression (the assertion fails on the synchronous design). Skips without a Vulkan ICD.
add_executable(vk_capture_perf_test
vk_capture_perf_test.cpp
${CMAKE_SOURCE_DIR}/hook/src/vk_capture.cpp)
target_include_directories(vk_capture_perf_test PRIVATE
${CMAKE_SOURCE_DIR}/hook/src
${CMAKE_SOURCE_DIR}/third_party/Vulkan-Headers/include)
target_link_libraries(vk_capture_perf_test PRIVATE coop_common d3d11 dxgi)
add_test(NAME vk_capture_perf_test COMMAND vk_capture_perf_test)
# Headless UI-fit check (M1): drives the real Controllers + Audio panels at reference
# resolutions with Debug details on and the richest content, and asserts no panel overflows
# its assigned size. Pure ImGui layout (no GPU / window). The Audio panel renders in its
# demo mode so the full content path shows without a live audio device.
add_executable(ui_fit_test
ui_fit_test.cpp
${CMAKE_SOURCE_DIR}/host/src/audio_panel.cpp
${CMAKE_SOURCE_DIR}/host/src/controllers_panel.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/audio_loopback.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/audio_format_verifier.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/process_loopback_capture.cpp
${CMAKE_SOURCE_DIR}/host/src/audio/audio_overrides.cpp
${CMAKE_SOURCE_DIR}/host/src/ui/app_chrome.cpp)
target_include_directories(ui_fit_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
# process_loopback_capture (pulled in via AudioMirror) needs the Win10 20H1 headers.
target_compile_definitions(ui_fit_test PRIVATE NTDDI_VERSION=0x0A00000B)
target_link_libraries(ui_fit_test PRIVATE coop_common imgui ole32 mmdevapi)
add_test(NAME ui_fit_test COMMAND ui_fit_test)
# Keep the bin/<config>/ root deployable: stage every test exe under tests/. coop_tone
# (the audio_loopback_test fixture) is staged there too, next to its consumer, so the
# test's "coop_tone.exe alongside me" lookup keeps working.
coop_output_subdir(tests
hook_selftest
dinput_hook_test
audio_ring_test
ipc_server_test
detour_gate_test
hook_install_test
mkb_ring_test
mkb_map_test
audio_mix_test
tone_analysis_test
audio_correlation_test
render_pacer_test
rate_estimator_test
audio_overrides_test
audio_loopback_test
audio_hook_test
srgb_format_test
present_hook_test
dx12_present_hook_test
opengl_hook_test
mock_game_test
audio_verify_test
vk_capture_perf_test
ui_fit_test)