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:
@@ -22,6 +22,7 @@
|
||||
#include "coop/shared_memory.hpp"
|
||||
#include "ipc_client.hpp"
|
||||
#include "present_hook.hpp"
|
||||
#include "present_overhead.hpp"
|
||||
|
||||
using namespace coop;
|
||||
|
||||
@@ -265,6 +266,45 @@ int main()
|
||||
}
|
||||
release(ctxB);
|
||||
release(devB);
|
||||
|
||||
// --- Performance regression guard: the D3D11On12 bridge + copy the hook does inside Present
|
||||
// must stay off the present thread (measure a full frame with the hook live vs. removed). ---
|
||||
auto frame = [&] {
|
||||
const UINT idx = sc3->GetCurrentBackBufferIndex();
|
||||
allocator->Reset();
|
||||
cmdlist->Reset(allocator, nullptr);
|
||||
D3D12_RESOURCE_BARRIER b{};
|
||||
b.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
b.Transition.pResource = render_targets[idx];
|
||||
b.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
b.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
|
||||
b.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
|
||||
cmdlist->ResourceBarrier(1, &b);
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_heap->GetCPUDescriptorHandleForHeapStart();
|
||||
rtv.ptr += static_cast<SIZE_T>(idx) * rtv_size;
|
||||
cmdlist->ClearRenderTargetView(rtv, kClear, 0, nullptr);
|
||||
std::swap(b.Transition.StateBefore, b.Transition.StateAfter);
|
||||
cmdlist->ResourceBarrier(1, &b);
|
||||
cmdlist->Close();
|
||||
ID3D12CommandList* lists[] = {cmdlist};
|
||||
queue->ExecuteCommandLists(1, lists);
|
||||
};
|
||||
auto present = [&] {
|
||||
swapchain->Present(0, 0);
|
||||
queue->Signal(fence, ++fence_value);
|
||||
if (fence->GetCompletedValue() < fence_value)
|
||||
{
|
||||
fence->SetEventOnCompletion(fence_value, fence_event);
|
||||
WaitForSingleObject(fence_event, 1000);
|
||||
}
|
||||
};
|
||||
const double hooked = cooptest::avg_present_ms(60, frame, present);
|
||||
hook::remove_present_hooks(); // baseline: same swapchain, hook removed
|
||||
const double base = cooptest::avg_present_ms(60, frame, present);
|
||||
std::printf("present-thread: hooked %.3f ms, unhooked %.3f ms, capture overhead %.3f ms\n", hooked, base,
|
||||
hooked - base);
|
||||
check(hooked - base < cooptest::kPresentOverheadBudgetMs,
|
||||
"D3D12 present hook stays off the present thread (overhead < one 60 Hz frame)");
|
||||
}
|
||||
|
||||
hook::remove_present_hooks();
|
||||
|
||||
Reference in New Issue
Block a user