From 1b3fa6824cd5a4ffc7906e560daae122a4323f61 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sun, 21 Jun 2026 18:04:21 +0200 Subject: [PATCH] Fix DX12 mirror showing stale frames (copy the rotating back buffer) The D3D12 capture path grabbed GetBuffer(0) every Present. Unlike D3D11 flip-model -- where DXGI keeps GetBuffer(0) pointing at the live back buffer -- D3D12 rotates buffers explicitly: the game renders into the buffer at GetCurrentBackBufferIndex(), which advances each Present. So buffer 0 only holds fresh content every Nth frame; the rest copied a stale buffer, and the mirror silently ran at refresh/N with duplicate frames in between. Every metric read full rate (Present counter, published FPS, generation bump, capture->display latency) because they count Presents, not unique content -- which is why it looked fine but felt like missing frames, especially on high-refresh DX12 games (DMC5/Myst at 144, Miles Morales). Query IDXGISwapChain3::GetCurrentBackBufferIndex() before the trampoline Present (so it's the just-rendered buffer) and copy that one; fall back to 0 only if the interface is unavailable. The DX11 path is unaffected. Co-Authored-By: Claude Opus 4.8 --- hook/src/present_hook.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/hook/src/present_hook.cpp b/hook/src/present_hook.cpp index d4bbc75..02da887 100644 --- a/hook/src/present_hook.cpp +++ b/hook/src/present_hook.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -247,8 +248,25 @@ bool ensure_on12_locked(ID3D12Device* dev) // via the On12 bridge and CopyResource it into the shared texture. void capture_backbuffer_d3d12(IDXGISwapChain* sc) { + // CRITICAL: D3D12 rotates back buffers explicitly -- the game rendered into the + // buffer at GetCurrentBackBufferIndex(), and that index advances every Present. + // (Unlike D3D11 flip-model, where DXGI keeps GetBuffer(0) pointing at the live + // back buffer.) Grabbing buffer 0 unconditionally copies a stale buffer on N-1 of + // every N frames, so the mirror silently runs at refresh/N with duplicate frames + // in between -- even though the Present counter, published FPS, generation bump, + // and latency all read full rate (they count Presents, not unique content). We're + // called before the trampoline Present, so the current index is the just-rendered + // buffer. Query IDXGISwapChain3 for it; fall back to 0 only if unavailable. + UINT bb_index = 0; + IDXGISwapChain3* sc3 = nullptr; + if (SUCCEEDED(sc->QueryInterface(__uuidof(IDXGISwapChain3), reinterpret_cast(&sc3))) && sc3 != nullptr) + { + bb_index = sc3->GetCurrentBackBufferIndex(); + sc3->Release(); + } + ID3D12Resource* bb = nullptr; - if (FAILED(sc->GetBuffer(0, __uuidof(ID3D12Resource), reinterpret_cast(&bb))) || bb == nullptr) + if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast(&bb))) || bb == nullptr) { if (!g_unsupported_logged) {