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 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 18:04:21 +02:00
parent 4121ad4373
commit 1b3fa6824c

View File

@@ -9,6 +9,7 @@
#include <d3d11on12.h>
#include <d3d12.h>
#include <dxgi1_2.h>
#include <dxgi1_4.h>
#include <safetyhook.hpp>
@@ -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<void**>(&sc3))) && sc3 != nullptr)
{
bb_index = sc3->GetCurrentBackBufferIndex();
sc3->Release();
}
ID3D12Resource* bb = nullptr;
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr)
if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr)
{
if (!g_unsupported_logged)
{