DX12 capture: document why it's pricier than DX11/GL (measured breakdown)

Investigated the DX12 present-thread overhead (~0.38 ms vs DX11 ~0.05 / GL ~0.09).
Per-stage timing of the D3D11On12 path showed the cost is NOT where you'd assume:

  fence 0.005 + wrap(CreateWrappedResource+Acquire) 0.012 + copy 0.133 + flush 0.057 ms

CreateWrappedResource is cheap. The cost is the CopyResource issued on the 11On12
immediate context plus the mandatory Flush to make the shared copy visible to the
host -- both inherent to the bridge and not paid by the native-D3D11 path. The
per-frame GetDevice can't be skipped either (it's how device recreation is
detected). Documented this in the capture path.

Improving it means a native-D3D12 copy-queue path into a D3D12-shared texture, but
the host consumes the shared surface via IDXGIKeyedMutex (a D3D11 concept), so that
also requires switching the DX12 producer<->host sync to a shared ID3D12Fence -- a
cross-API rewrite. Deferred: the overhead is ~5% of a 144 Hz frame and correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 08:55:00 +02:00
parent 9fee13789d
commit c480dfe152

View File

@@ -424,6 +424,16 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
std::scoped_lock lock(g_tex_mutex);
if (ensure_on12_locked(dev))
{
// DX12 capture is pricier than DX11/OpenGL (measured ~0.38 ms vs ~0.05/0.09 ms of
// present-thread overhead) because it goes through the D3D11On12 bridge. The cost is NOT
// CreateWrappedResource (measured ~0.012 ms) -- it's the CopyResource issued on the 11On12
// immediate context (~0.13 ms) plus the mandatory Flush to make the shared copy visible to
// the host (~0.06 ms), neither of which the native-D3D11 path pays. Eliminating it needs a
// native-D3D12 copy-queue path into a D3D12-shared texture, but the host consumes the
// shared surface via an IDXGIKeyedMutex (a D3D11 concept), so that also means switching the
// DX12 producer<->host sync to a shared ID3D12Fence -- a cross-API rewrite. Deferred: the
// overhead is ~5% of a 144 Hz frame and the capture is correct; the bridge stays for now.
//
// Order our copy after the game's frame without burdening the game's queue: the
// game queue signals the fence (cheap), our copy queue waits on it. Skipped if the
// queue isn't captured yet or the fence is missing (one possibly-early frame).