Doing the "failing perf test first": measured the OpenGL capture's present-thread
overhead at a real resolution instead of the test's 64x64 toy. It is ~0.68 ms at
1280x720 (and ~0.06 ms when it overlaps a busy present at 1080p) -- well under one
frame, even at 144 Hz. No budget makes it fail, so the off-thread / async-PBO
refactor is NOT warranted.
The Vulkan 144->3 FPS stall was catastrophic specifically because it read
WRITE-COMBINED staging memory (~370 ms/frame), not because read-back is
synchronous. D3D9 GetRenderTargetData (a D3DPOOL_SYSTEMMEM surface) and glReadPixels
(normal CPU memory) read CACHED memory, so there is no comparable stall.
What changed instead:
- opengl_hook_test now runs the present-overhead guard at 1280x720 (not 64x64), so
it is meaningful -- a future write-combined-class regression trips the budget.
- README Lessons learned records the cached-vs-write-combined distinction so nobody
needlessly off-threads the other backends.
The matching D3D9 present-overhead guard ships with the new d3d9_hook_test (test
coverage). Drops both Performance items from the roadmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Against Sphere Spectacle (144 FPS, runs without Steam) the implicit-layer
capture dropped the game to ~3 FPS. Measured cause (per-stage trace in the
layer): the read-back ran on the game's PRESENT THREAD and spent ~370 ms per
1080p frame -- not the GPU copy (~2 ms) but the CPU swizzle, because the staging
buffer was a plain HOST_VISIBLE|HOST_COHERENT type (write-combined / uncached on
a discrete GPU), where a scattered CPU read runs at PCIe latency. 3 captures/s =
the 3 FPS the user saw.
Test-first: tests/vk_capture_perf_test reproduces the stall as a deterministic
unit test (372 ms/present, ratio 1.0 -> FAIL via `--sync`), then proves the fix
(0.02 ms/present, byte-correct BGRA->RGBA, ratio ~0 -> PASS).
Fix: extract the near-identical read-back from vk_hook.cpp and coop_vk_layer.cpp
into one shared coop::hook::VkCapture that:
* has the present thread only record + submit the copy (sub-ms) and return;
* runs a dedicated reaper thread for the fence wait + swizzle + D3D upload, off
the critical path, with a ring of in-flight slots (game never waits);
* allocates HOST_CACHED staging (fast CPU read), invalidating when non-coherent;
* throttles capture to ~150 Hz (a guest stream is <= the host refresh; no point
mirroring an uncapped 400+ FPS game and burning reaper CPU).
Real-game A/B: present rate now matches the no-capture baseline (605->470 vs
593->405 over the same ramp) with the mirror at ~130 fps -- no measurable impact.
Also adds present-thread overhead guards to the other GPU backends' hook tests
(present_overhead.hpp): DX11 0.05 ms, DX12 0.34 ms, OpenGL 0.09 ms overhead, all
asserted < one 60 Hz frame, so any future synchronous-stall regression fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Present hook never fired in Phantom Brave because it's an OpenGL game
(OPENGL32.dll loaded, IDXGISwapChain::Present calls=0), so the hooked video source
showed no image. Add an OpenGL producer under the video subsystem: inline-hook
gdi32!SwapBuffers + opengl32!wglSwapBuffers (with a re-entrancy guard, since
SwapBuffers calls wglSwapBuffers), glReadPixels the backbuffer, flip it, and upload
it into the same shared keyed-mutex texture the host already samples -- so the host
is unchanged. DXGI games still hit the Present hook; both producers are installed
and whichever the game uses fills the texture.
Validated by opengl_hook_test (real GL context, clears to a known color, reads the
exact pixels back through the shared texture) and against Phantom Brave (SwapBuffers
~75/s, present=0, shared texture 1920x1080, generation advancing). Vulkan
(vkQueuePresentKHR) still needs WGC -- documented. All 7 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>