Skip to content

Commit

Permalink
GS: Improve vsync mode selection
Browse files Browse the repository at this point in the history
All games use mailbox/triple buffering. Except when you enable sync to
host refresh, in which case FIFO/double buffering is used.

This means vsync enabled will ever tear, but at the same time, never
drop to 30fps on a missed frame due to frame rate differences.

To have the "best of both worlds", you should enable vsync and sync to
host refresh. Previously, this resulted in additional input lag, since
the host vsync would drive the EE frame timing. Now, this behaviour is
disabled by default, unless you enable "Use Host VSync Timing".
  • Loading branch information
stenzek committed May 24, 2024
1 parent 205ee9e commit 7cf5ca7
Show file tree
Hide file tree
Showing 40 changed files with 668 additions and 460 deletions.
2 changes: 2 additions & 0 deletions common/CocoaTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ namespace CocoaTools
{
bool CreateMetalLayer(WindowInfo* wi);
void DestroyMetalLayer(WindowInfo* wi);
std::optional<float> GetViewRefreshRate(const WindowInfo& wi);

/// Add a handler to be run when macOS changes between dark and light themes
void AddThemeChangeHandler(void* ctx, void(handler)(void* ctx));
/// Remove a handler previously added using AddThemeChangeHandler with the given context
Expand Down
21 changes: 21 additions & 0 deletions common/CocoaTools.mm
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@
[view setWantsLayer:NO];
}

std::optional<float> CocoaTools::GetViewRefreshRate(const WindowInfo& wi)
{
if (![NSThread isMainThread])
{
std::optional<float> ret;
dispatch_sync(dispatch_get_main_queue(), [&ret, wi]{ ret = GetViewRefreshRate(wi); });
return ret;
}

std::optional<float> ret;
NSView* const view = (__bridge NSView*)wi.window_handle;
const u32 did = [[[[[view window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
{
ret = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
}

return ret;
}

// MARK: - Theme Change Handlers

@interface PCSX2KVOHelper : NSObject
Expand Down
3 changes: 2 additions & 1 deletion common/Darwin/DarwinMisc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if defined(__APPLE__)

#include "common/Darwin/DarwinMisc.h"
#include "common/HostSys.h"

#include <cstring>
#include <cstdlib>
Expand Down Expand Up @@ -102,7 +103,7 @@ std::string GetOSVersionString()

static IOPMAssertionID s_pm_assertion;

bool WindowInfo::InhibitScreensaver(const WindowInfo& wi, bool inhibit)
bool Common::InhibitScreensaver(bool inhibit)
{
if (s_pm_assertion)
{
Expand Down
3 changes: 3 additions & 0 deletions common/HostSys.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ extern std::string GetOSVersionString();

namespace Common
{
/// Enables or disables the screen saver from starting.
bool InhibitScreensaver(bool inhibit);

/// Abstracts platform-specific code for asynchronously playing a sound.
/// On Windows, this will use PlaySound(). On Linux, it will shell out to aplay. On MacOS, it uses NSSound.
bool PlaySoundAsync(const char* path);
Expand Down
2 changes: 1 addition & 1 deletion common/Linux/LnxMisc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ static bool SetScreensaverInhibitDBus(const bool inhibit_requested, const char*
return true;
}

bool WindowInfo::InhibitScreensaver(const WindowInfo& wi, bool inhibit)
bool Common::InhibitScreensaver(bool inhibit)
{
return SetScreensaverInhibitDBus(inhibit, "PCSX2", "PCSX2 VM is running.");
}
Expand Down
151 changes: 119 additions & 32 deletions common/WindowInfo.cpp
Original file line number Diff line number Diff line change
@@ -1,40 +1,110 @@
// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team
// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
// SPDX-License-Identifier: LGPL-3.0+

#include "WindowInfo.h"
#include "Console.h"
#include "Error.h"
#include "HeapArray.h"

#if defined(_WIN32)

#include "RedtapeWindows.h"
#include <dwmapi.h>

static bool GetRefreshRateFromDWM(HWND hwnd, float* refresh_rate)
static std::optional<float> GetRefreshRateFromDisplayConfig(HWND hwnd)
{
// Partially based on Chromium ui/display/win/display_config_helper.cc.
const HMONITOR monitor = MonitorFromWindow(hwnd, 0);
if (!monitor) [[unlikely]]
{
ERROR_LOG("{}() failed: {}", "MonitorFromWindow", Error::CreateWin32(GetLastError()).GetDescription());
return std::nullopt;
}

MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi);
if (!GetMonitorInfoW(monitor, &mi))
{
ERROR_LOG("{}() failed: {}", "GetMonitorInfoW", Error::CreateWin32(GetLastError()).GetDescription());
return std::nullopt;
}

DynamicHeapArray<DISPLAYCONFIG_PATH_INFO> path_info;
DynamicHeapArray<DISPLAYCONFIG_MODE_INFO> mode_info;

// I guess this could fail if it changes inbetween two calls... unlikely.
for (;;)
{
UINT32 path_size = 0, mode_size = 0;
LONG res = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_size, &mode_size);
if (res != ERROR_SUCCESS)
{
ERROR_LOG("{}() failed: {}", "GetDisplayConfigBufferSizes", Error::CreateWin32(res).GetDescription());
return std::nullopt;
}

path_info.resize(path_size);
mode_info.resize(mode_size);
res =
QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_size, path_info.data(), &mode_size, mode_info.data(), nullptr);
if (res == ERROR_SUCCESS)
break;
if (res != ERROR_INSUFFICIENT_BUFFER)
{
ERROR_LOG("{}() failed: {}", "QueryDisplayConfig", Error::CreateWin32(res).GetDescription());
return std::nullopt;
}
}

for (const DISPLAYCONFIG_PATH_INFO& pi : path_info)
{
DISPLAYCONFIG_SOURCE_DEVICE_NAME sdn = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME),
.adapterId = pi.sourceInfo.adapterId,
.id = pi.sourceInfo.id}};
LONG res = DisplayConfigGetDeviceInfo(&sdn.header);
if (res != ERROR_SUCCESS)
{
ERROR_LOG("{}() failed: {}", "DisplayConfigGetDeviceInfo", Error::CreateWin32(res).GetDescription());
continue;
}

if (std::wcscmp(sdn.viewGdiDeviceName, mi.szDevice) == 0)
{
// Found the monitor!
return static_cast<float>(static_cast<double>(pi.targetInfo.refreshRate.Numerator) /
static_cast<double>(pi.targetInfo.refreshRate.Denominator));
}
}

return std::nullopt;
}

static std::optional<float> GetRefreshRateFromDWM(HWND hwnd)
{
BOOL composition_enabled;
if (FAILED(DwmIsCompositionEnabled(&composition_enabled)))
return false;
return std::nullopt;

DWM_TIMING_INFO ti = {};
ti.cbSize = sizeof(ti);
HRESULT hr = DwmGetCompositionTimingInfo(nullptr, &ti);
if (SUCCEEDED(hr))
{
if (ti.rateRefresh.uiNumerator == 0 || ti.rateRefresh.uiDenominator == 0)
return false;
return std::nullopt;

*refresh_rate = static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
return true;
return static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
}

return false;
return std::nullopt;
}

static bool GetRefreshRateFromMonitor(HWND hwnd, float* refresh_rate)
static std::optional<float> GetRefreshRateFromMonitor(HWND hwnd)
{
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (!mon)
return false;
return std::nullopt;

MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi);
Expand All @@ -45,23 +115,41 @@ static bool GetRefreshRateFromMonitor(HWND hwnd, float* refresh_rate)

// 0/1 are reserved for "defaults".
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
{
*refresh_rate = static_cast<float>(dm.dmDisplayFrequency);
return true;
}
return static_cast<float>(dm.dmDisplayFrequency);
}

return false;
return std::nullopt;
}

bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate)
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
std::optional<float> ret;
if (wi.type != Type::Win32 || !wi.window_handle)
return false;
return ret;

// Try DWM first, then fall back to integer values.
const HWND hwnd = static_cast<HWND>(wi.window_handle);
return GetRefreshRateFromDWM(hwnd, refresh_rate) || GetRefreshRateFromMonitor(hwnd, refresh_rate);
ret = GetRefreshRateFromDisplayConfig(hwnd);
if (!ret.has_value())
{
ret = GetRefreshRateFromDWM(hwnd);
if (!ret.has_value())
ret = GetRefreshRateFromMonitor(hwnd);
}

return ret;
}

#elif defined(__APPLE__)

#include "common/CocoaTools.h"

std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
if (wi.type == WindowInfo::Type::MacOS)
return CocoaTools::GetViewRefreshRate(wi);

return std::nullopt;
}

#else
Expand All @@ -72,18 +160,18 @@ bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_
#include <X11/extensions/Xrandr.h>
#include <X11/Xlib.h>

static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
static std::optional<float> GetRefreshRateFromXRandR(const WindowInfo& wi)
{
Display* display = static_cast<Display*>(wi.display_connection);
Window window = static_cast<Window>(reinterpret_cast<uintptr_t>(wi.window_handle));
if (!display || !window)
return false;
return std::nullopt;

XRRScreenResources* res = XRRGetScreenResources(display, window);
if (!res)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetScreenResources() failed");
return false;
return std::nullopt;
}

ScopedGuard res_guard([res]() { XRRFreeScreenResources(res); });
Expand All @@ -93,7 +181,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (num_monitors < 0)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetMonitors() failed");
return false;
return std::nullopt;
}
else if (num_monitors > 1)
{
Expand All @@ -104,7 +192,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (mi->noutput <= 0)
{
Console.Error("(GetRefreshRateFromXRandR) Monitor has no outputs");
return false;
return std::nullopt;
}
else if (mi->noutput > 1)
{
Expand All @@ -115,7 +203,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!oi)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetOutputInfo() failed");
return false;
return std::nullopt;
}

ScopedGuard oi_guard([oi]() { XRRFreeOutputInfo(oi); });
Expand All @@ -124,7 +212,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!ci)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetCrtcInfo() failed");
return false;
return std::nullopt;
}

ScopedGuard ci_guard([ci]() { XRRFreeCrtcInfo(ci); });
Expand All @@ -141,30 +229,29 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!mode)
{
Console.Error("(GetRefreshRateFromXRandR) Failed to look up mode %d (of %d)", static_cast<int>(ci->mode), res->nmode);
return false;
return std::nullopt;
}

if (mode->dotClock == 0 || mode->hTotal == 0 || mode->vTotal == 0)
{
Console.Error("(GetRefreshRateFromXRandR) Modeline is invalid: %ld/%d/%d", mode->dotClock, mode->hTotal, mode->vTotal);
return false;
return std::nullopt;
}

*refresh_rate =
static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal));
return true;
return static_cast<float>(
static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal)));
}

#endif // X11_API

bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate)
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
#if defined(X11_API)
if (wi.type == WindowInfo::Type::X11)
return GetRefreshRateFromXRandR(wi, refresh_rate);
return GetRefreshRateFromXRandR(wi);
#endif

return false;
return std::nullopt;
}

#endif
7 changes: 3 additions & 4 deletions common/WindowInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#pragma once
#include "Pcsx2Defs.h"

#include <optional>

/// Contains the information required to create a graphics context in a window.
struct WindowInfo
{
Expand Down Expand Up @@ -41,8 +43,5 @@ struct WindowInfo
float surface_refresh_rate = 0.0f;

/// Returns the host's refresh rate for the given window, if available.
static bool QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate);

/// Enables or disables the screen saver from starting.
static bool InhibitScreensaver(const WindowInfo& wi, bool inhibit);
static std::optional<float> QueryRefreshRateForWindow(const WindowInfo& wi);
};
2 changes: 1 addition & 1 deletion common/Windows/WinMisc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ std::string GetOSVersionString()
return retval;
}

bool WindowInfo::InhibitScreensaver(const WindowInfo& wi, bool inhibit)
bool Common::InhibitScreensaver(bool inhibit)
{
EXECUTION_STATE flags = ES_CONTINUOUS;
if (inhibit)
Expand Down
4 changes: 4 additions & 0 deletions pcsx2-qt/QtUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ namespace QtUtils
wi.surface_width = static_cast<u32>(static_cast<qreal>(widget->width()) * dpr);
wi.surface_height = static_cast<u32>(static_cast<qreal>(widget->height()) * dpr);
wi.surface_scale = static_cast<float>(dpr);

// Query refresh rate, we need it for sync.
wi.surface_refresh_rate = WindowInfo::QueryRefreshRateForWindow(wi).value_or(0.0f);
INFO_LOG("Surface refresh rate: {} hz", wi.surface_refresh_rate);
return wi;
}

Expand Down
Loading

0 comments on commit 7cf5ca7

Please sign in to comment.