DX12 capture: fence the copy off the game's queue + add drop detection

Resolves the DX12 mirror stutter and makes dropped frames observable.

Decouple the D3D11On12 copy from the game's present queue. Submitting the
copy on the game's own present queue (the prior approach) ordered it
correctly but stalled the game's presents: GPU back-pressure, plus the
shared keyed-mutex AcquireSync is a CPU-blocking call on the render
thread. Running it on an independent queue avoids the stall but races the
game's render -> stale frames. Do both: run the copy on our own queue and
order it after the frame with an ID3D12Fence the game's present queue
signals (near-free) and our queue waits on. The present queue is still
recovered for late injection via the ExecuteCommandLists hook (now used to
signal the fence, not host the copy). Producer AcquireSync stays
non-blocking (timeout 0) so a busy mutex drops a mirror frame instead of
stalling the game.

Add drop detection (protocol v12 -> v13). The hook counts captures skipped
because the keyed mutex was busy (VideoShare.frames_dropped); the host
counts published frames it never displayed (generation gaps). The Video
panel shows "Frames lost: N/s capture  N/s display", red when nonzero.
This confirmed the game-window-vs-mirror behavior is a display-path
artifact (unfocused windows lose VRR/independent flip), not a capture loss.

Add a one-shot present-pattern log: per distinct swapchain (size/format/
buffer index) and per distinct present-flags value, with DXGI_PRESENT_TEST
spelled out as an occlusion probe that draws nothing -- which is why
Miles Morales shows ~2 presents per captured frame (the test present is
counted but produces no frame).

Docs: add the DX12 capture lessons to the README (rotating back buffer,
fence/own-queue, capture-at-Present decoupling from DWM) and drop the now
-moot DX12 overhead future-work item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 19:55:27 +02:00
parent efc16b5eea
commit ffaad6c4ae
10 changed files with 241 additions and 49 deletions

View File

@@ -47,6 +47,12 @@ public:
{
return frames_copied_;
}
// Cumulative published frames the host never displayed because the generation
// advanced by more than one between copies (host render rate < hook publish rate).
[[nodiscard]] std::uint64_t frames_missed() const
{
return frames_missed_;
}
private:
bool reopen(unsigned long pid, const VideoShareView& share);
@@ -64,6 +70,7 @@ private:
std::uint32_t format_ = 0;
std::uint32_t last_generation_ = 0;
std::uint64_t frames_copied_ = 0;
std::uint64_t frames_missed_ = 0;
};
} // namespace coop