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

@@ -139,6 +139,12 @@ std::string apply_test_command(const std::string& cmd, coop::UiState& ui, coop::
audio.dev_set_enabled(on);
return "ok";
}
if (v == "video")
{
// Install/remove the hooked video subsystem (Present/GL/D3D9/Vulkan capture hooks).
injection.request_video(arg(1) == "on");
return "ok";
}
if (v == "debug")
{
ui.debug_details = (arg(1) == "on");
@@ -219,6 +225,38 @@ std::string steam_manifest_path()
}
#endif
// Red banner on the mirror window when a Vulkan game was injected too late for the hooked
// capture (Vulkan caches its present pointer at startup, so the hook must be present before
// vkCreateInstance). WGC still mirrors the window; this prompts the operator to relaunch with
// Auto-attach so the hook arms before the game initializes Vulkan. Shown regardless of overlay
// visibility, since it's an actionable alert about the mirror itself.
void draw_vk_too_late_banner()
{
const ImGuiViewport* vp = ImGui::GetMainViewport();
float w = vp->WorkSize.x - 40.0f;
if (w > 760.0f)
{
w = 760.0f;
}
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + 16.0f),
ImGuiCond_Always, ImVec2(0.5f, 0.0f));
ImGui::SetNextWindowSize(ImVec2(w, 0.0f));
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_AlwaysAutoResize;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.28f, 0.03f, 0.03f, 0.92f));
ImGui::Begin("##vk_too_late", nullptr, flags);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.45f, 1.0f));
ImGui::TextWrapped("Detected a Vulkan game, but the mirror hook attached too late to capture it "
"with low latency (Vulkan resolves its present function at startup). The window "
"is mirroring via WGC meanwhile. For the hooked path, enable \"Auto re-attach "
"this game on relaunch\" (and \"Set up Vulkan layer\" if it persists) in the "
"Injection panel, then relaunch the game.");
ImGui::PopStyleColor();
ImGui::End();
ImGui::PopStyleColor();
}
// When the overlay is hidden, briefly show a fading hint so the operator can find
// the way back. The window is borderless and non-interactive so it never steals a
// click or a frame from the mirror underneath.
@@ -431,6 +469,10 @@ int run()
{
draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at);
}
if (injection.hook_status().vk_too_late) // Vulkan game injected too late -> relaunch prompt
{
draw_vk_too_late_banner();
}
coop::apply_layout_end_frame(); // clear the one-shot "Reset layout" force
if (ui.request_quit) // File -> Exit