M2(Vulkan): too-late detection + red relaunch banner on the mirror window

The hook reports vk_too_late when vulkan-1.dll is loaded and the GPA hook has
been in for >4s but it never caught the app creating its device -- i.e. the app
resolved its Vulkan functions before we hooked (injected too late). Surfaced
through a new HookStatus.vk_too_late field (protocol 15->16) and shown by the
host as a red banner on the mirror window: "Detected a Vulkan game, but the
mirror hook attached too late... enable Auto re-attach (and Set up Vulkan layer
if it persists) and relaunch." WGC keeps mirroring meanwhile.

mock_game_test gains a too-late detection check (late-inject a Vulkan game ->
vk_too_late trips). Verified live: the banner shows over the running tool when a
Vulkan game is injected late with the video subsystem on (added a debug-harness
`video on/off` command to drive it). 15/15 ctest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 12:51:34 +02:00
parent dd976979a1
commit 894285459c
8 changed files with 124 additions and 5 deletions

View File

@@ -403,6 +403,57 @@ void test_vk_capture(ID3D11Device* device)
cleanup();
}
// 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
@@ -628,6 +679,7 @@ int main()
// 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_too_late();
test_video_capture("gl", device);
test_video_capture("dx9ex", device);
test_video_capture("dx9", device);