mirror of
https://git.revela.dev/itsRevela/LCE-Revelations.git
synced 2026-05-21 19:24:55 +00:00
feat: uncap fps in fullscreen for lower input latency
Before this change, turning VSync off did not actually uncap your frame rate. The game ran in borderless fullscreen, where Windows' desktop compositor owns the display pipeline and applies its own VSync regardless of what the game asks for. So on a 240Hz monitor with a GPU that could draw 800fps, you still only saw 240 frames per second and the other 560 were thrown away. Worse, the frames you did see were up to a refresh cycle stale by the time they hit your eyes, which is input latency you can feel when moving the camera or aiming. Fullscreen now uses true DXGI exclusive mode, where the compositor is out of the way and the swap chain writes directly to the display. Every frame the GPU produces lands on screen as soon as it is ready, so "VSync off" actually does something. Expect FPS to climb well past your monitor's refresh rate and the mouse to feel noticeably more responsive. Exclusive fullscreen also displays at your monitor's native resolution with no driver-side scaling or filtering. The backbuffer is grown to match the monitor exactly before the transition, and the display mode is pinned to the backbuffer size so nothing in the output pipeline resamples your pixels on the way to the screen. The result is a crisp 1:1 image with none of the softening or greyish filter that stretched output can introduce. Yes, this is the mode where screen tearing can happen. Tearing gets a bad reputation but it is a visual tradeoff, not a bug, and many players prefer it over the latency VSync causes. If you want to avoid tearing, just turn VSync on in the settings and the game will cap cleanly to your refresh rate. You now have a real choice between the two instead of "off" quietly being broken. Under the hood, F11 and the saved fullscreen preference both route through a new ApplyExclusiveFullscreen path that does Microsoft's recommended transition: grow the window to cover the monitor, ResizeTarget with no scaling so the display mode is pinned to the backbuffer size, SetFullscreenState(TRUE), SetColorSpace1(sRGB), then a second ResizeTarget so picky drivers actually apply the mode. Exit forces a real decorated windowed state so F11 cycles cleanly between windowed and exclusive fullscreen. ResizeD3D skips its swap-chain-recreate path while in exclusive mode so it does not fight DXGI for ownership. The swap chain's RefreshRate changes from a hardcoded 60Hz to 0/0 so DXGI matches the current display mode. Fixes an "input signal out of range" error that could happen on high-refresh monitors after entering fullscreen, where the monitor was being asked to renegotiate timing off of 240Hz down to 60Hz on every launch.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
|
||||
#include <dxgi1_4.h> // IDXGISwapChain3 for SetColorSpace1
|
||||
|
||||
#include <assert.h>
|
||||
#include <iostream>
|
||||
#include <ShellScalingApi.h>
|
||||
@@ -551,6 +553,10 @@ ID3D11Texture2D* g_pDepthStencilBuffer = nullptr;
|
||||
static const float kClearColorWhite[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
|
||||
static const float kClearColorBlack[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
|
||||
|
||||
// True when the swap chain is in DXGI exclusive fullscreen. Lets ResizeD3D
|
||||
// skip its destroy-and-recreate path, which would break exclusive ownership.
|
||||
static bool g_bDxgiExclusiveFullscreen = false;
|
||||
|
||||
//
|
||||
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
|
||||
//
|
||||
@@ -905,14 +911,17 @@ HRESULT InitDevice()
|
||||
// backbuffer refs we can't release). Bitblt DISCARD has no HWND lock, so
|
||||
// the "destroy old, create new" resize path in ResizeD3D() works cleanly.
|
||||
// VSync toggle still works via the SyncInterval parameter on Present().
|
||||
// RefreshRate=0/0 so DXGI matches the current display mode. Hardcoding a
|
||||
// rate would force a mode switch on SetFullscreenState, which can produce
|
||||
// "input signal out of range" errors on high-refresh monitors.
|
||||
DXGI_SWAP_CHAIN_DESC sd;
|
||||
ZeroMemory( &sd, sizeof( sd ) );
|
||||
sd.BufferCount = 1;
|
||||
sd.BufferDesc.Width = width;
|
||||
sd.BufferDesc.Height = height;
|
||||
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
sd.BufferDesc.RefreshRate.Numerator = 60;
|
||||
sd.BufferDesc.RefreshRate.Denominator = 1;
|
||||
sd.BufferDesc.RefreshRate.Numerator = 0;
|
||||
sd.BufferDesc.RefreshRate.Denominator = 0;
|
||||
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT;
|
||||
sd.OutputWindow = g_hWnd;
|
||||
sd.SampleDesc.Count = 1;
|
||||
@@ -1005,6 +1014,9 @@ static bool ResizeD3D(int newW, int newH)
|
||||
if (newW <= 0 || newH <= 0) return false;
|
||||
if (!g_pSwapChain) return false;
|
||||
if (!g_bResizeReady) return false;
|
||||
// In exclusive fullscreen the swap chain must not be recreated.
|
||||
if (g_bDxgiExclusiveFullscreen)
|
||||
return false;
|
||||
|
||||
int bbW = newW;
|
||||
int bbH = newH;
|
||||
@@ -1101,8 +1113,9 @@ static bool ResizeD3D(int newW, int newH)
|
||||
sd.BufferDesc.Width = bbW;
|
||||
sd.BufferDesc.Height = bbH;
|
||||
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
sd.BufferDesc.RefreshRate.Numerator = 60;
|
||||
sd.BufferDesc.RefreshRate.Denominator = 1;
|
||||
// RefreshRate=0/0 matches InitDevice; see comment there.
|
||||
sd.BufferDesc.RefreshRate.Numerator = 0;
|
||||
sd.BufferDesc.RefreshRate.Denominator = 0;
|
||||
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT;
|
||||
sd.OutputWindow = g_hWnd;
|
||||
sd.SampleDesc.Count = 1;
|
||||
@@ -1318,16 +1331,99 @@ void SetExclusiveFullscreen(bool enabled)
|
||||
g_bPendingExclusiveFullscreenValue = enabled;
|
||||
}
|
||||
|
||||
// Uses borderless fullscreen (ToggleFullscreen) rather than DXGI SetFullscreenState.
|
||||
// True DXGI exclusive fullscreen is blocked by the 4J Renderer holding hidden
|
||||
// backbuffer references that prevent ResizeBuffers.
|
||||
// Enter or leave true DXGI exclusive fullscreen. With our bitblt swap chain,
|
||||
// Present(SyncInterval=0) in exclusive mode produces real screen tearing via
|
||||
// direct scanout (DWM is out of the pipeline). Flip mode with ALLOW_TEARING
|
||||
// would also work but is blocked by the 4J Renderer's deferred context refs
|
||||
// on the backbuffer, which DXGI's ResizeBuffers cannot release.
|
||||
static void ApplyExclusiveFullscreen(bool enabled)
|
||||
{
|
||||
// Toggle into/out of borderless fullscreen if state doesn't match
|
||||
if (enabled && !g_isFullscreen)
|
||||
ToggleFullscreen();
|
||||
else if (!enabled && g_isFullscreen)
|
||||
ToggleFullscreen();
|
||||
if (!g_pSwapChain)
|
||||
return;
|
||||
|
||||
LONG styleBefore = GetWindowLong(g_hWnd, GWL_STYLE);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
// Grow the window to cover the monitor first. This fires WM_SIZE which
|
||||
// runs ResizeD3D and recreates the backbuffer at monitor-native size.
|
||||
// Otherwise a small windowed backbuffer would enter exclusive fullscreen
|
||||
// at that smaller size and DXGI would scale it to fill the monitor,
|
||||
// producing a filtered / washed-out look.
|
||||
HMONITOR hMon = MonitorFromWindow(g_hWnd, MONITOR_DEFAULTTOPRIMARY);
|
||||
MONITORINFO mi = {};
|
||||
mi.cbSize = sizeof(mi);
|
||||
if (GetMonitorInfo(hMon, &mi))
|
||||
{
|
||||
int monW = mi.rcMonitor.right - mi.rcMonitor.left;
|
||||
int monH = mi.rcMonitor.bottom - mi.rcMonitor.top;
|
||||
SetWindowLong(g_hWnd, GWL_STYLE, (styleBefore & ~WS_OVERLAPPEDWINDOW) | WS_VISIBLE);
|
||||
SetWindowPos(g_hWnd, HWND_TOP,
|
||||
mi.rcMonitor.left, mi.rcMonitor.top, monW, monH,
|
||||
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
|
||||
// ResizeTarget pins the display mode to the backbuffer size with
|
||||
// no scaling. Microsoft's pattern is ResizeTarget then
|
||||
// SetFullscreenState then ResizeTarget again (see below).
|
||||
DXGI_MODE_DESC targetMode = {};
|
||||
targetMode.Width = monW;
|
||||
targetMode.Height = monH;
|
||||
targetMode.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
targetMode.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
|
||||
targetMode.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
|
||||
g_pSwapChain->ResizeTarget(&targetMode);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT hr = g_pSwapChain->SetFullscreenState(enabled ? TRUE : FALSE, nullptr);
|
||||
if (FAILED(hr))
|
||||
return;
|
||||
|
||||
g_bDxgiExclusiveFullscreen = enabled;
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
// Explicitly declare sRGB. Default for R8G8B8A8_UNORM but some drivers
|
||||
// behave differently if the color space is never set.
|
||||
IDXGISwapChain3* pSwapChain3 = nullptr;
|
||||
if (SUCCEEDED(g_pSwapChain->QueryInterface(__uuidof(IDXGISwapChain3), (void**)&pSwapChain3)) && pSwapChain3)
|
||||
{
|
||||
UINT colorSpaceSupport = 0;
|
||||
pSwapChain3->CheckColorSpaceSupport(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, &colorSpaceSupport);
|
||||
if (colorSpaceSupport & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT)
|
||||
pSwapChain3->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709);
|
||||
pSwapChain3->Release();
|
||||
}
|
||||
|
||||
// Second ResizeTarget per Microsoft's recommendation to make the mode stick.
|
||||
HMONITOR hMon2 = MonitorFromWindow(g_hWnd, MONITOR_DEFAULTTOPRIMARY);
|
||||
MONITORINFO mi2 = {};
|
||||
mi2.cbSize = sizeof(mi2);
|
||||
if (GetMonitorInfo(hMon2, &mi2))
|
||||
{
|
||||
DXGI_MODE_DESC targetMode2 = {};
|
||||
targetMode2.Width = mi2.rcMonitor.right - mi2.rcMonitor.left;
|
||||
targetMode2.Height = mi2.rcMonitor.bottom - mi2.rcMonitor.top;
|
||||
targetMode2.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
targetMode2.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
|
||||
targetMode2.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
|
||||
g_pSwapChain->ResizeTarget(&targetMode2);
|
||||
}
|
||||
g_isFullscreen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Force a real decorated windowed state on exit. DXGI would otherwise
|
||||
// restore whatever state the window had before SetFullscreenState,
|
||||
// which may still be borderless.
|
||||
SetWindowLong(g_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);
|
||||
const int w = 1280, h = 720;
|
||||
const int sw = GetSystemMetrics(SM_CXSCREEN);
|
||||
const int sh = GetSystemMetrics(SM_CYSCREEN);
|
||||
SetWindowPos(g_hWnd, HWND_TOP, (sw - w) / 2, (sh - h) / 2, w, h,
|
||||
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
g_isFullscreen = false;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
@@ -1562,10 +1658,13 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Restore fullscreen state from previous session
|
||||
if (LoadFullscreenOption() && !g_isFullscreen || launchOptions.fullscreen)
|
||||
// Restore fullscreen state from previous session. Route through the
|
||||
// deferred exclusive fullscreen path so the main loop applies it on the
|
||||
// first tick (safer than transitioning during init).
|
||||
if ((LoadFullscreenOption() && !g_isFullscreen) || launchOptions.fullscreen)
|
||||
{
|
||||
ToggleFullscreen();
|
||||
g_bPendingExclusiveFullscreen = true;
|
||||
g_bPendingExclusiveFullscreenValue = true;
|
||||
}
|
||||
|
||||
#if 0
|
||||
@@ -1857,10 +1956,9 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
|
||||
|
||||
RenderManager.Set_matrixDirty();
|
||||
#endif
|
||||
// Present the frame.
|
||||
// RenderManager.Present() hardcodes SyncInterval=1 internally.
|
||||
// When VSync is off, bypass it and call the swap chain directly
|
||||
// with SyncInterval=0.
|
||||
// Present the frame. RenderManager.Present() hardcodes SyncInterval=1,
|
||||
// so when VSync is off we bypass it for uncapped frames. In DXGI
|
||||
// exclusive fullscreen this produces real tearing via direct scanout.
|
||||
if (!g_bVSync && g_pSwapChain)
|
||||
{
|
||||
HRESULT hrPresent = g_pSwapChain->Present(0, 0);
|
||||
@@ -1962,11 +2060,11 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
|
||||
}
|
||||
#endif
|
||||
|
||||
// toggle fullscreen
|
||||
// toggle fullscreen (DXGI exclusive via ApplyExclusiveFullscreen)
|
||||
if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_FULLSCREEN))
|
||||
{
|
||||
ToggleFullscreen();
|
||||
app.SetGameSettings(ProfileManager.GetPrimaryPad(), eGameSetting_ExclusiveFullscreen, g_isFullscreen ? 1 : 0);
|
||||
ApplyExclusiveFullscreen(!g_bDxgiExclusiveFullscreen);
|
||||
app.SetGameSettings(ProfileManager.GetPrimaryPad(), eGameSetting_ExclusiveFullscreen, g_bDxgiExclusiveFullscreen ? 1 : 0);
|
||||
}
|
||||
|
||||
// Apply deferred exclusive fullscreen toggle
|
||||
|
||||
Reference in New Issue
Block a user