Validate the Vulkan backend against a real game (Sphere Spectacle)
Adds coop_vk_validate, a harness that drives the Vulkan capture path end-to-end against a shipping title (default Sphere Spectacle, a pure-Vulkan game) 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. Findings: - Layer method WORKS: coop_vk_layer mirrors the game correctly at 1920x1080 -- right colors/brightness, no BGRA/RGBA swizzle, no sRGB darkening (screenshot confirmed). The layer captures every present (no drops). - Suspended-inject "Auto-attach" is NOT applicable to a Steam title that must launch through Steam: its exe renders nothing when launched directly, so there's no Vulkan present to catch. The layer is the method for Steam Vulkan games (the early-inject mechanism itself is covered by mock_game_test's suspended-launch path). - The layer does video, but the game needs coop_hook.dll co-injected for focus-spoof or an unfocused, event-driven game throttles itself to a few fps (looks like a capture slowdown but isn't). A present-rate number alone can't prove "no FPS impact" -- it's the game's own cadence; capture stays off the critical path (every present copied, read-back on its own queue + present-semaphore re-chain). Also adds SharedTextureSource::read_frame (bulk RGBA readback) for the screenshot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
43
README.md
43
README.md
@@ -108,19 +108,12 @@ default** and covers anything the hooked path doesn't.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Current Tasks
|
||||
|
||||
- **Validate the Vulkan backend against a real game.** Exercise the Vulkan capture path
|
||||
end-to-end on a shipping title — **Sphere Spectacle** (Steam appid 1123040,
|
||||
`start steam://rungameid/1123040`) — not just `coop_mock_game`. Cover **both** early-presence
|
||||
methods: **Auto-attach** (suspended-launch + inject + resume, so the hook arms before the game
|
||||
calls `vkCreateInstance`) and the implicit **capture layer** (`coop_vk_layer` registered for the
|
||||
game). For each, confirm: (a) it *captures* — frames reach the shared texture and advance; (b) the
|
||||
mirrored image is *correct* — right resolution, letterboxed, correct colors/brightness (no
|
||||
`*_SRGB` darkening, no BGRA/RGBA swizzle error), checked against a WGC reference and saved
|
||||
screenshots; and (c) it doesn't *regress performance* — measure the game's present cadence /
|
||||
frame time with capture off vs on and confirm no material drop (the per-frame `vkCmdCopyImageToBuffer`
|
||||
read-back + present-semaphore re-chain must stay off the game's critical path).
|
||||
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, and real-game Vulkan validation (`coop_vk_validate` against
|
||||
Sphere Spectacle). 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
|
||||
|
||||
@@ -322,6 +315,17 @@ input layer sees a real state change. `disable_mask` (hex bits `0x1`=input
|
||||
**bisect which injected subsystem affects a game** — this is how the 32-bit
|
||||
Present-hook crash was isolated.
|
||||
|
||||
[`tools/vk_validate`](tools/vk_validate) (`coop_vk_validate.exe <layer|inject> [seconds] [exe]`)
|
||||
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.
|
||||
|
||||
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
|
||||
`bin/<config>/tools/` (the deployable `bin/<config>/` root holds only shipping
|
||||
@@ -488,6 +492,19 @@ 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.
|
||||
- **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