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:
81
README.md
81
README.md
@@ -108,27 +108,18 @@ default** and covers anything the hooked path doesn't.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Current tasks
|
||||
|
||||
- **The inline-hook (suspended-inject) Vulkan path must work, or be proven a true limitation.**
|
||||
Sphere Spectacle does *not* require Steam — the exe can be launched directly (and suspended, with
|
||||
`coop_hook.dll` injected before it runs any code). Either make the early inline `vk_hook` capture
|
||||
this game, or determine and document the genuine reason it cannot (Steam is not the reason).
|
||||
Final verification of both paths must be with the game launched **through Steam**
|
||||
(`steam://rungameid/1123040`).
|
||||
|
||||
### Done
|
||||
|
||||
The earlier tracked tasks are complete: injection hardening (the cross-backend safe-unhook drain),
|
||||
The near-term tracked tasks are complete: injection hardening (the cross-backend safe-unhook drain),
|
||||
two-path audio-format correlation (rate + channels/bit-depth recovery), mouse + keyboard forwarding
|
||||
for DirectInput and Raw Input games, real-game Vulkan validation (`coop_vk_validate` against Sphere
|
||||
Spectacle), and the **Vulkan capture performance fix** — the read-back that dropped a 144 FPS game
|
||||
to ~3 FPS now runs off the present thread (shared `coop::hook::VkCapture`), so the game keeps its
|
||||
frame rate while mirroring (guarded by `vk_capture_perf_test`; every GPU backend's hook test also
|
||||
asserts a present-thread overhead bound). See **Lessons learned** and the test suite for each. Open
|
||||
directions: per-game profiles, multi-guest
|
||||
virtual-pad mapping, and continuous raw-mouse *movement* forwarding (the MKB event stream is
|
||||
position-based today).
|
||||
Spectacle), the **Vulkan capture performance fix** — the read-back that dropped a 144 FPS game to ~3
|
||||
FPS now runs off the present thread (shared `coop::hook::VkCapture`), so the game keeps its frame
|
||||
rate while mirroring (guarded by `vk_capture_perf_test`; every GPU backend's hook test also asserts a
|
||||
present-thread overhead bound) — and the **inline-hook (suspended-inject) Vulkan path**, which now
|
||||
captures Sphere Spectacle correctly once the harness launches the exe with the right working
|
||||
directory and `vk_hook` intercepts present/swapchain resolved via `vkGetInstanceProcAddr` (not just
|
||||
`vkGetDeviceProcAddr`). Both Vulkan paths are verified on the real game. See **Lessons learned** and
|
||||
the test suite for each. Open directions: per-game profiles, multi-guest virtual-pad mapping, and
|
||||
continuous raw-mouse *movement* forwarding (the MKB event stream is position-based today).
|
||||
|
||||
## Building
|
||||
|
||||
@@ -334,12 +325,16 @@ Present-hook crash was isolated.
|
||||
validates the **Vulkan capture backend against a real game** (defaults to Sphere Spectacle). It
|
||||
drives both early-presence methods — the implicit **layer** (registers `coop_vk_layer` scoped to the
|
||||
game, launches via Steam, also late-injects `coop_hook.dll` for focus-spoofing so the game renders
|
||||
unfocused) and **inject** (suspended-launch the exe + early-inject before `vkCreateInstance`) — and
|
||||
asserts frames reach the shared texture and advance, the captured resolution/colors are sane, saves
|
||||
a BMP screenshot for visual confirmation, and reports the present rate while capturing. Confirmed:
|
||||
the layer path mirrors Sphere Spectacle correctly (1920×1080, right colors, no swizzle/darkening);
|
||||
the suspended-inject path is **not applicable** to titles that must launch through Steam (their exe
|
||||
renders nothing when launched directly) — the layer is the method there.
|
||||
unfocused) and **inject** (suspended-launch the exe *with the game's own folder as the working
|
||||
directory* + early-inject before `vkCreateInstance`) — and asserts frames reach the shared texture
|
||||
and advance, the captured resolution/colors are sane, saves a BMP screenshot for visual
|
||||
confirmation, and **gates on the game keeping a healthy present rate while capturing** (so a
|
||||
present-thread stall fails the tool, not just gets reported). Confirmed against Sphere Spectacle:
|
||||
**both** paths mirror it correctly (1920×1080, right colors, no swizzle/darkening) at the game's full
|
||||
present rate. The suspended-inject path needs the correct working directory (the game loads
|
||||
`steam_api64.dll` / `resources/` relative to cwd) and only applies to titles that actually run when
|
||||
launched directly; a title that refuses to run outside Steam produces no presents, and the tool
|
||||
SKIPs inject for it (use the layer).
|
||||
|
||||
Both auto-detect a 32-bit (WOW64) target and inject via `coop_inject_x86.exe` +
|
||||
`coop_hook_x86.dll`, exactly like the host. The probes build into
|
||||
@@ -507,19 +502,29 @@ Non-obvious things that cost time and constrain the design:
|
||||
app, so it self-scopes: capture only when the process image matches the host-written target file,
|
||||
else pure pass-through. Register it per-user (HKCU `…\Vulkan\ImplicitLayers`, no admin) and
|
||||
unregister on untick / host exit.
|
||||
- **On a real Steam Vulkan game, the layer is the *only* usable early-presence method.** Validating
|
||||
against Sphere Spectacle (`coop_vk_validate`) confirmed the layer path mirrors it correctly, but
|
||||
also that the suspended-launch "Auto-attach" (the early-inject path the mock uses) **doesn't apply
|
||||
to a Steam title**: launching its `.exe` directly — even with Steam running — renders nothing
|
||||
under our injected process (the title requires launching *through* Steam, which we can't suspend),
|
||||
so there's no Vulkan present to catch. The implicit layer sidesteps this entirely (it's in the
|
||||
loader chain however Steam launches the game), which is why it's the productized path. Two more
|
||||
real-game lessons: (1) **the layer does video, but the game still needs `coop_hook.dll` co-injected
|
||||
for focus-spoofing** — without it an unfocused game throttles *itself* to a few fps (an
|
||||
event-driven title presents only on change), which looks like a capture slowdown but isn't; (2)
|
||||
the layer captures **every** present (copied ≈ present_calls, no drops), so the read-back stays off
|
||||
the critical path — a present-*rate* number alone can't prove "no FPS impact" because it's the
|
||||
game's own cadence, so a hard FPS gate there is meaningless; validate feel by playing.
|
||||
- **Both Vulkan early-presence paths work on a real game — the "only the layer works" belief was two
|
||||
bugs in disguise.** Against Sphere Spectacle (`coop_vk_validate`), the inline **inject** path
|
||||
(suspended-launch + early-inject before `vkCreateInstance`) initially captured nothing, which was
|
||||
wrongly written off as "the title requires launching through Steam." It does not. Two real causes:
|
||||
(1) the harness launched the exe with **our** working directory, so the game couldn't load
|
||||
`steam_api64.dll` / `resources/` and never rendered — launch it with the game's own folder as cwd
|
||||
and it runs fine directly; and (2) the game (volk-style) resolves `vkQueuePresentKHR` /
|
||||
`vkCreateSwapchainKHR` via **`vkGetInstanceProcAddr`**, but `vk_hook` only substituted those when
|
||||
resolved via `vkGetDeviceProcAddr` — so the present bypassed us. Intercepting them in the
|
||||
instance-level resolver too fixed it. The division of labor is about *who launches the game*, not
|
||||
capability: for a game launched **through Steam** we can't suspend the launch, so the implicit
|
||||
**layer** (always in the loader chain) is the path; when **we** launch the exe, suspended-inject
|
||||
works (and is how you'd debug). When the layer runs, the game still needs `coop_hook.dll`
|
||||
co-injected for **focus-spoofing** so an unfocused window doesn't throttle itself.
|
||||
- **A Vulkan present-rate *is* a valid no-FPS-impact gate — once the read-back is off the present
|
||||
thread.** The earlier validator reported the present rate and rationalized it ("the game's own
|
||||
cadence; a hard FPS gate is meaningless"), which hid a real **144→3 FPS** stall: the read-back ran
|
||||
on the present thread and spent ~370 ms/frame doing a CPU read of write-combined staging memory.
|
||||
After moving the read-back to a reaper thread (`coop::hook::VkCapture`), `present_calls` (counted
|
||||
every present, independent of the throttled mirror) reflects the game's true rate, so the tool now
|
||||
**asserts** it stays healthy while capturing. The mirror is deliberately throttled to ~150 Hz, so
|
||||
`copied < present_calls` is expected, not a drop. Lesson: a perf check must assert a bound — if you
|
||||
find yourself explaining why a number is fine, make the test prove it.
|
||||
- **A render client that predates our injection has no knowable format — measure it.**
|
||||
We inject into already-running games, so we usually never see the game's
|
||||
`IAudioClient::Initialize`; the render-hook then assumes the device mix format for that
|
||||
|
||||
Reference in New Issue
Block a user