Make the inline-hook (suspended-inject) Vulkan path work on Sphere Spectacle

The earlier validation concluded suspended-inject was "not applicable -- the
title requires launching through Steam." That was wrong; it was two bugs:

1. coop_vk_validate's inject mode launched the exe with CreateProcessW and a
   null working directory, so the game couldn't load steam_api64.dll / resources/
   (loaded relative to cwd) and never rendered -> no presents. Launch with the
   game's own folder as cwd and it runs fine directly, no Steam needed.
2. The game resolves vkQueuePresentKHR / vkCreateSwapchainKHR via
   vkGetInstanceProcAddr (volk's volkLoadInstance does this), but vk_hook only
   substituted our detours when they were resolved via vkGetDeviceProcAddr -- so
   the present bypassed the hook. Intercept those names in hk_vkGetInstanceProcAddr
   too (our detours already gate on g_capture_enabled/g_device, so handing them out
   before the device exists is safe).

With both fixed, inject mode captures Sphere Spectacle correctly: 1920x1080,
correct colors/orientation (screenshot), ~480 fps present while mirroring at the
~150 Hz throttle -- no present-thread impact (the VkCapture fix is shared).

Also makes the validator ASSERT a present-rate floor while capturing (it used to
report the rate and rationalize it, which is exactly what hid the 144->3 FPS
stall), and reports the true mirror rate from video.generation. Division of labor
is about who launches the game: layer for Steam-launched (can't suspend), inject
when we control the launch. README lessons-learned corrected accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 08:31:11 +02:00
parent 304857dcf0
commit 15d6da92bc
3 changed files with 86 additions and 55 deletions

View File

@@ -271,6 +271,20 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance ins
{
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetDeviceProcAddr);
}
// vkGetInstanceProcAddr can also resolve device-level functions (the loader returns a
// dispatch trampoline). A game -- or volk's volkLoadInstance -- that resolves the present /
// swapchain entry points this way (rather than via vkGetDeviceProcAddr) would otherwise get
// the real loader pointer and bypass our capture. Sphere Spectacle does exactly this, so
// intercept them here too. (Our detours gate on g_capture_enabled / g_device, so handing them
// out before the device exists is safe.)
if (std::strcmp(name, "vkQueuePresentKHR") == 0)
{
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
}
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0)
{
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
}
}
return real_gipa(instance, name);
}