Fix Vulkan capture perf collapse: read back off the present thread
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>
This commit is contained in:
45
tests/present_overhead.hpp
Normal file
45
tests/present_overhead.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
// Shared helper for the per-backend present-thread overhead guards.
|
||||
//
|
||||
// The capture path (Present / SwapBuffers hook, or the Vulkan read-back) runs on the game's present
|
||||
// thread. If it stalls there, it caps the game's frame rate -- the Vulkan layer did exactly this,
|
||||
// dropping a 144 FPS game to ~3 FPS by spending ~370 ms per present reading write-combined memory.
|
||||
// Each GPU backend's in-process hook test measures the wall time its present spends with the hook
|
||||
// live vs. removed and asserts the added cost stays under one display frame, so a future regression
|
||||
// that puts a synchronous read-back / stall back on the present thread fails the test.
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace cooptest
|
||||
{
|
||||
inline double now_ms()
|
||||
{
|
||||
LARGE_INTEGER f, c;
|
||||
QueryPerformanceFrequency(&f);
|
||||
QueryPerformanceCounter(&c);
|
||||
return 1000.0 * static_cast<double>(c.QuadPart) / static_cast<double>(f.QuadPart);
|
||||
}
|
||||
|
||||
// Average wall time of `present()` over n frames, calling `render()` (untimed) before each so a
|
||||
// fresh frame is produced. Returns milliseconds per present.
|
||||
template <class RenderFn, class PresentFn>
|
||||
double avg_present_ms(int n, RenderFn render, PresentFn present)
|
||||
{
|
||||
render();
|
||||
present(); // warm (first present/resource setup)
|
||||
double total = 0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
render();
|
||||
const double a = now_ms();
|
||||
present();
|
||||
total += now_ms() - a;
|
||||
}
|
||||
return n > 0 ? total / n : 0.0;
|
||||
}
|
||||
|
||||
// One 60 Hz display frame. The capture's added present-thread cost must stay well under this or it
|
||||
// throttles the game; the Vulkan bug added ~370 ms (22x over budget). Generous on purpose -- the
|
||||
// guard targets the catastrophic-stall class, not micro-overhead, so it never flakes on jitter.
|
||||
inline constexpr double kPresentOverheadBudgetMs = 16.7;
|
||||
} // namespace cooptest
|
||||
Reference in New Issue
Block a user