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

@@ -227,6 +227,7 @@ DWORD WINAPI worker_thread(LPVOID)
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
coop::hook::release_cursor_tick(); // free the operator's mouse if requested (no-op otherwise)
coop::hook::hook_publish(g_ipc); // installed-hooks list + call counts
g_ipc.set_vk_too_late(coop::hook::vk_injected_too_late()); // Vulkan attached-too-late banner
g_ipc.heartbeat();
// Reconcile ~4x/s, but drain MKB events far more often (input must be

View File

@@ -147,6 +147,14 @@ public:
}
}
void set_vk_too_late(bool too_late)
{
if (block_ != nullptr)
{
block_->status.vk_too_late = too_late ? 1u : 0u;
}
}
// Record the rumble the game requested for a slot (so the host can forward it to
// the guest's controller). Plain stores; the hook is the sole writer.
void note_rumble(std::uint32_t slot, std::uint16_t left, std::uint16_t right)

View File

@@ -36,6 +36,7 @@ std::atomic<std::uint64_t> g_presents{0};
std::atomic<std::uint64_t> g_frames_shared{0};
std::atomic<bool> g_present_captured{false}; // set once we successfully read a present back
bool g_unsupported_logged = false;
ULONGLONG g_install_tick = 0; // GetTickCount64 when the GPA hook went in (for the too-late grace)
// Real entry points. g_real_gdpa and below are unhooked exports/results, so they're plain PFNs;
// the real vkGetInstanceProcAddr is reached through the inline hook's trampoline (real_gipa()).
@@ -638,6 +639,7 @@ bool install_vk_hooks(IpcClient& ipc)
}
g_unsupported_logged = false;
g_hk_gipa = safetyhook::create_inline(gipa, reinterpret_cast<void*>(&hk_vkGetInstanceProcAddr));
g_install_tick = GetTickCount64();
hook_set_installed(g_id_present, static_cast<bool>(g_hk_gipa));
logf("install_vk_hooks: vkGetInstanceProcAddr=%p hooked=%d", gipa, static_cast<bool>(g_hk_gipa) ? 1 : 0);
return static_cast<bool>(g_hk_gipa);
@@ -685,6 +687,7 @@ void remove_vk_hooks()
g_device = VK_NULL_HANDLE;
g_instance = VK_NULL_HANDLE;
g_real_gdpa = nullptr;
g_install_tick = 0;
g_present_captured.store(false, std::memory_order_relaxed);
g_presents.store(0, std::memory_order_relaxed);
g_frames_shared.store(0, std::memory_order_relaxed);
@@ -703,10 +706,16 @@ std::uint64_t vk_frames_shared()
bool vk_injected_too_late()
{
// vulkan-1.dll is loaded but we never captured a present -> the app resolved its present
// pointer before we hooked (or doesn't go through our chain). The host shows the banner.
return GetModuleHandleW(L"vulkan-1.dll") != nullptr && g_presents.load(std::memory_order_relaxed) == 0 &&
!g_present_captured.load(std::memory_order_relaxed);
// Too late = vulkan-1.dll is loaded and our GPA hook has been in for a few seconds, but we
// never even saw the app create its device -> it resolved its Vulkan functions before our
// hook (we're not in the chain). A game we hooked early always trips hk_vkCreateDevice
// (g_device != null) well within the grace window, even before it presents. The host shows
// the "relaunch with Auto-attach / Vulkan layer" banner on this.
if (GetModuleHandleW(L"vulkan-1.dll") == nullptr || g_device != VK_NULL_HANDLE || g_install_tick == 0)
{
return false;
}
return (GetTickCount64() - g_install_tick) > 4000;
}
} // namespace coop::hook