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) {