From d2bb7d347503922b50fb8850c3f2b4b5d9e100f7 Mon Sep 17 00:00:00 2001 From: barbudreadmon Date: Wed, 28 Oct 2026 23:33:57 +0100 Subject: [PATCH] improve framepacing with video sync add vrr support --- src/burner/win32/app.rc | 1 + src/burner/win32/cona.cpp | 3 ++ src/burner/win32/menu.cpp | 1 + src/burner/win32/resource.h | 1 + src/burner/win32/run.cpp | 58 ++++++++++++++++++++++++++++++ src/burner/win32/scrn.cpp | 14 ++++++++ src/intf/interface.h | 1 + src/intf/video/vid_interface.cpp | 1 + src/intf/video/win32/vid_d3d11.cpp | 44 +++++++++++++++++++++-- 9 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/burner/win32/app.rc b/src/burner/win32/app.rc index 00f86eea9..4cbea8d53 100644 --- a/src/burner/win32/app.rc +++ b/src/burner/win32/app.rc @@ -1316,6 +1316,7 @@ BEGIN MENUITEM "Enable Windowed Fullscreen (DX9/DX11 Blitters)", MENU_WINFS MENUITEM "Enable VSync (Windowed)", MENU_VIDEOVSYNC MENUITEM "Enable DWM Sync (Windowed)", MENU_DWMFIX + MENUITEM "Enable Variable Refresh Rate (DX11, Win10+)", MENU_VRR MENUITEM "Auto Frameskip", MENU_AUTOFRAMESKIP MENUITEM "Enable RunAhead", MENU_RUNAHEAD END diff --git a/src/burner/win32/cona.cpp b/src/burner/win32/cona.cpp index b492ca2f8..b70444e71 100644 --- a/src/burner/win32/cona.cpp +++ b/src/burner/win32/cona.cpp @@ -296,6 +296,7 @@ int ConfigAppLoad() VAR(bVidDX9WinFullscreen); VAR(bVidVSync); VAR(bVidDWMSync); + VAR(bVidVRR); VAR(bVidScanlines); VAR(nVidScanIntensity); @@ -687,6 +688,8 @@ int ConfigAppSave() VAR(bVidVSync); _ftprintf(h, _T("\n// If non-zero, try to synchronise to DWM on Windows 7+, this fixes frame stuttering problems.\n")); VAR(bVidDWMSync); + _ftprintf(h, _T("\n// If non-zero, use variable refresh rate (G-Sync/FreeSync) with the DX11 blitter on Windows 10+\n")); + VAR(bVidVRR); _ftprintf(h, _T("\n// Transfer method: 0 = blit from system memory / use driver/DirectX texture management;\n")); _ftprintf(h, _T("// 1 = copy to a video memory surface, then use bltfast();\n")); _ftprintf(h, _T("// -1 = autodetect for DirectDraw, equals 1 for Direct3D\n")); diff --git a/src/burner/win32/menu.cpp b/src/burner/win32/menu.cpp index 2979386ab..cb4a68ffd 100644 --- a/src/burner/win32/menu.cpp +++ b/src/burner/win32/menu.cpp @@ -1072,6 +1072,7 @@ void MenuUpdate() CheckMenuItem(hMenu, MENU_WINFS, bVidDX9WinFullscreen ? MF_CHECKED : MF_UNCHECKED); CheckMenuItem(hMenu, MENU_DWMFIX, bVidDWMSync ? MF_CHECKED : MF_UNCHECKED); + CheckMenuItem(hMenu, MENU_VRR, bVidVRR ? MF_CHECKED : MF_UNCHECKED); var = nVidSelect + MENU_BLITTER_1; CheckMenuRadioItem(hMenu, MENU_BLITTER_1, MENU_BLITTER_8, var, MF_BYCOMMAND); diff --git a/src/burner/win32/resource.h b/src/burner/win32/resource.h index dcf9ffa64..b0225f375 100644 --- a/src/burner/win32/resource.h +++ b/src/burner/win32/resource.h @@ -602,6 +602,7 @@ #define MENU_WINFS 10101 #define MENU_FULL 10102 #define MENU_DWMFIX 10103 +#define MENU_VRR 10104 #define MENU_NOSTRETCH 10110 #define MENU_STRETCH 10111 #define MENU_ASPECT 10112 diff --git a/src/burner/win32/run.cpp b/src/burner/win32/run.cpp index bd781514d..5e5beccaf 100644 --- a/src/burner/win32/run.cpp +++ b/src/burner/win32/run.cpp @@ -257,6 +257,62 @@ void do_shonky_profile() } #endif +// Present pacing when no video sync is active. +// Frames are produced when the audio engine signals a free segment, which happens on +// its own period (~10ms on Vista+), so frame times alternate e.g. 20/20/10ms. +// Presents are held back to a steady QPC schedule which follows the audio clock. +static LARGE_INTEGER qpcPaceFreq; +static LONGLONG nPaceNext = 0; + +static bool RunPaceActive() +{ + if (bVidVSync || (bVidDWMSync && !nVidFullscreen)) return false; + if (bAppDoFast || kNetGame) return false; + + return true; +} + +static void RunPacePresent() +{ + if (!RunPaceActive()) { + nPaceNext = 0; + return; + } + + if (qpcPaceFreq.QuadPart == 0) { + QueryPerformanceFrequency(&qpcPaceFreq); + } + + LONGLONG nPeriod; + if (bAudPlaying && nBurnSoundRate > 0 && nBurnSoundLen > 0) { + nPeriod = qpcPaceFreq.QuadPart * nBurnSoundLen / nBurnSoundRate; + } else { + nPeriod = qpcPaceFreq.QuadPart * 100 / nAppVirtualFps; + } + + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + + LONGLONG nLead = nPaceNext - now.QuadPart; + if (nPaceNext == 0 || nLead < 0) { + // late (or first frame): present now, the schedule follows + nPaceNext = now.QuadPart; + } else if (nLead > nPeriod) { + // schedule drifted ahead of the frame source, pull it back + nPaceNext = now.QuadPart + nPeriod; + } + + const LONGLONG nSleepMargin = qpcPaceFreq.QuadPart * 2 / 1000; + for (;;) { + QueryPerformanceCounter(&now); + LONGLONG nRemain = nPaceNext - now.QuadPart; + if (nRemain <= 0) break; + Sleep((bEnableHighResTimer && nRemain > nSleepMargin) ? 1 : 0); + } + + nPaceNext += nPeriod; +} + // With or without sound, run one frame. // If bDraw is true, it's the last frame before we are up to date, and so we should draw the screen int RunFrame(int bDraw, int bPause) @@ -267,6 +323,7 @@ int RunFrame(int bDraw, int bPause) if (bShowFPS) DisplayFPSCheckState(); if (bPrevDraw && !bPause) { + RunPacePresent(); VidPaint(0); // paint the screen (no need to validate) if (!nVidFullscreen && bVidDWMSync) // Sync to DWM (Win7+, windowed) @@ -523,6 +580,7 @@ int RunReset() { // Reset the speed throttling code nNormalLast = 0; nNormalFrac = 0; + nPaceNext = 0; // Reset FPS display DisplayFPSInit(); diff --git a/src/burner/win32/scrn.cpp b/src/burner/win32/scrn.cpp index 083f5bebe..ccee98e59 100644 --- a/src/burner/win32/scrn.cpp +++ b/src/burner/win32/scrn.cpp @@ -2295,6 +2295,8 @@ static void OnCommand(HWND /*hDlg*/, int id, HWND /*hwndCtl*/, UINT codeNotify) bVidDWMSync = !bVidDWMSync; if (bVidDWMSync && bVidVSync) bVidVSync = 0; + if (bVidDWMSync) + bVidVRR = 0; POST_INITIALISE_MESSAGE; break; @@ -2909,6 +2911,18 @@ static void OnCommand(HWND /*hDlg*/, int id, HWND /*hwndCtl*/, UINT codeNotify) bVidVSync = !bVidVSync; if (bVidVSync && bVidDWMSync) bVidDWMSync = 0; + if (bVidVSync) + bVidVRR = 0; + + POST_INITIALISE_MESSAGE; + break; + + case MENU_VRR: + bVidVRR = !bVidVRR; + if (bVidVRR) { + bVidVSync = 0; + bVidDWMSync = 0; + } POST_INITIALISE_MESSAGE; break; diff --git a/src/intf/interface.h b/src/intf/interface.h index 89af2a132..384af7096 100644 --- a/src/intf/interface.h +++ b/src/intf/interface.h @@ -241,6 +241,7 @@ extern INT64 nVidBlitterOpt[]; extern INT32 bVidFullStretch; extern INT32 bVidTripleBuffer; extern INT32 bVidVSync; +extern INT32 bVidVRR; extern double dVidCubicB; extern double dVidCubicC; extern INT32 bVidDX9Bilinear; diff --git a/src/intf/video/vid_interface.cpp b/src/intf/video/vid_interface.cpp index d6df6436f..b6209d5fe 100644 --- a/src/intf/video/vid_interface.cpp +++ b/src/intf/video/vid_interface.cpp @@ -86,6 +86,7 @@ INT32 bVidFullStretch = 0; // 1 = stretch to fill the entire window/screen INT32 bVidCorrectAspect = 1; // 1 = stretch to fill the window/screen while maintaining the correct aspect ratio INT32 bVidIntegerScale = 0; // Wants integer scaling INT32 bVidVSync = 0; // 1 = sync blits/pageflips/presents to the screen +INT32 bVidVRR = 0; // 1 = variable refresh rate (G-Sync/FreeSync), DX11 blitter only INT32 bVidTripleBuffer = 0; // 1 = use triple buffering INT32 bVidBilinear = 1; // 1 = enable bi-linear filtering (D3D blitter) INT32 bVidScanlines = 0; // 1 = draw scanlines diff --git a/src/intf/video/win32/vid_d3d11.cpp b/src/intf/video/win32/vid_d3d11.cpp index a5d025201..a22563a37 100644 --- a/src/intf/video/win32/vid_d3d11.cpp +++ b/src/intf/video/win32/vid_d3d11.cpp @@ -53,6 +53,7 @@ static const GUID FBNEO_IID_IDXGIDevice = { 0x54ec77fa, 0x1377, 0x44e6, { 0x8c, 0x32, 0x88, 0xfd, 0x5f, 0x44, 0xc8, 0x4c } }; static const GUID FBNEO_IID_IDXGIDevice1 = { 0x77db970f, 0x6276, 0x48ba, { 0xba, 0x28, 0x07, 0x01, 0x43, 0xb4, 0x39, 0x2c } }; static const GUID FBNEO_IID_IDXGIFactory = { 0x7b7166ec, 0x21c7, 0x44ae, { 0xb2, 0x1a, 0xc9, 0xae, 0x32, 0x1a, 0xe3, 0x69 } }; +static const GUID FBNEO_IID_IDXGIFactory5 = { 0x7632e1f5, 0xee65, 0x4dca, { 0x87, 0xfd, 0x84, 0xcd, 0x75, 0xf8, 0x83, 0x8d } }; static const GUID FBNEO_IID_ID3D11Texture2D = { 0x6f15aaf2, 0xd208, 0x4e89, { 0x9a, 0xb4, 0x48, 0x95, 0x35, 0xd3, 0x4f, 0x9c } }; // ID3D11ShaderReflection changed IID between d3dcompiler versions static const GUID FBNEO_IID_ID3D11ShaderReflection_47 = { 0x8d536ca1, 0x0cca, 0x4956, { 0xa8, 0x37, 0x78, 0x69, 0x63, 0x75, 0x55, 0x84 } }; @@ -61,6 +62,9 @@ static const GUID FBNEO_IID_ID3D11ShaderReflection_43 = { 0x0a233719, 0x3960, 0x // DXGI values that older headers may not have #define FBNEO_DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL ((DXGI_SWAP_EFFECT)3) #define FBNEO_DXGI_SWAP_EFFECT_FLIP_DISCARD ((DXGI_SWAP_EFFECT)4) +#define FBNEO_DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING 0x800 +#define FBNEO_DXGI_PRESENT_ALLOW_TEARING 0x200 +#define FBNEO_DXGI_FEATURE_PRESENT_ALLOW_TEARING 0 // ---------------------------------------------------------------------------- // Runtime-loaded DLLs @@ -165,6 +169,7 @@ static ID3D11RenderTargetView* pRenderTarget = NULL; static D3D_FEATURE_LEVEL nFeatureLevel = D3D_FEATURE_LEVEL_9_1; static bool bWarpDevice = false; static DXGI_SWAP_EFFECT nSwapEffect = DXGI_SWAP_EFFECT_DISCARD; +static UINT nSwapChainFlags = 0; static UINT nSwapChainWidth = 0, nSwapChainHeight = 0; static UINT nDeviceGeneration = 0; // bumped every time a device is created (OSD textures follow it) static bool bDeviceLost = false; @@ -2579,7 +2584,7 @@ static int d3d11ResizeSwapChain(UINT nWidth, UINT nHeight) pContext->OMSetRenderTargets(0, NULL, NULL); D3D11_RELEASE(pRenderTarget); - if (FAILED(pSwapChain->ResizeBuffers(0, nWidth, nHeight, DXGI_FORMAT_UNKNOWN, 0))) { + if (FAILED(pSwapChain->ResizeBuffers(0, nWidth, nHeight, DXGI_FORMAT_UNKNOWN, nSwapChainFlags))) { return 1; } @@ -2589,6 +2594,27 @@ static int d3d11ResizeSwapChain(UINT nWidth, UINT nHeight) return d3d11CreateRenderTarget(); } +// IDXGIFactory5::CheckFeatureSupport() is called through the vtable, so no DXGI 1.5 headers are needed +static bool d3d11TearingSupported(IDXGIFactory* pFactory) +{ + typedef HRESULT (STDMETHODCALLTYPE* pfnCheckFeatureSupport)(void*, UINT, void*, UINT); + void* pFactory5 = NULL; + BOOL bAllowTearing = FALSE; + + if (FAILED(pFactory->QueryInterface(FBNEO_IID_IDXGIFactory5, &pFactory5))) { + return false; + } + + pfnCheckFeatureSupport pCheckFeatureSupport = (pfnCheckFeatureSupport)((*(void***)pFactory5)[28]); + if (FAILED(pCheckFeatureSupport(pFactory5, FBNEO_DXGI_FEATURE_PRESENT_ALLOW_TEARING, &bAllowTearing, sizeof(bAllowTearing)))) { + bAllowTearing = FALSE; + } + + ((IUnknown*)pFactory5)->Release(); + + return bAllowTearing != FALSE; +} + static int d3d11CreateSwapChain() { IDXGIDevice* pDXGIDevice = NULL; @@ -2613,6 +2639,9 @@ static int d3d11CreateSwapChain() nSwapChainWidth = (rect.right > 0) ? rect.right : 1; nSwapChainHeight = (rect.bottom > 0) ? rect.bottom : 1; + bool bTearing = bVidVRR && d3d11TearingSupported(pFactory); + nSwapChainFlags = 0; + // Prefer the flip model (Win10: FLIP_DISCARD, Win8: FLIP_SEQUENTIAL), fall back to the blt model (Win7) static const DXGI_SWAP_EFFECT SwapEffects[] = { FBNEO_DXGI_SWAP_EFFECT_FLIP_DISCARD, FBNEO_DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, DXGI_SWAP_EFFECT_DISCARD }; @@ -2630,9 +2659,11 @@ static int d3d11CreateSwapChain() scd.OutputWindow = hD3D11View; scd.Windowed = TRUE; scd.SwapEffect = SwapEffects[i]; + scd.Flags = (bFlip && bTearing) ? FBNEO_DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0; if (SUCCEEDED(pFactory->CreateSwapChain(pDevice, &scd, &pSwapChain))) { nSwapEffect = SwapEffects[i]; + nSwapChainFlags = scd.Flags; } else { pSwapChain = NULL; } @@ -3447,7 +3478,10 @@ static int d3d11Paint(int bValidate) return 1; } - HRESULT hr = pSwapChain->Present((bVidVSync && !EmulatorAppIsFast()) ? 1 : 0, 0); + UINT nSyncInterval = (bVidVSync && !EmulatorAppIsFast()) ? 1 : 0; + // VRR: present immediately, the display follows our own frame pacing + UINT nPresentFlags = (nSyncInterval == 0 && (nSwapChainFlags & FBNEO_DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING)) ? FBNEO_DXGI_PRESENT_ALLOW_TEARING : 0; + HRESULT hr = pSwapChain->Present(nSyncInterval, nPresentFlags); bInPaint = false; @@ -3489,6 +3523,12 @@ static int d3d11GetSettings(InterfaceInfo* pInfo) IntInfoAddStringModule(pInfo, _T("Using a flip-model swap chain")); } + if (nSwapChainFlags & FBNEO_DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING) { + IntInfoAddStringModule(pInfo, _T("Variable refresh rate (tearing) presents enabled")); + } else if (bVidVRR) { + IntInfoAddStringModule(pInfo, _T("Variable refresh rate requested, but not supported")); + } + if (nVidFullscreen) { if (bChangedDisplayMode) { _sntprintf(szString, MAX_PATH, _T("Fullscreen (%i x %i, display mode changed)"), nVidScrnWidth, nVidScrnHeight);