Skip to content

Commit

Permalink
GS: Improve surface refresh rate detection
Browse files Browse the repository at this point in the history
Partial backport of stenzek/duckstation@d9cc80c

DwmGetCompositionTimingInfo() returns a noisy refresh rate, at least on Win11 22H2.
  • Loading branch information
stenzek committed May 23, 2024
1 parent ae5e54b commit 2a15835
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 127 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
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
4 changes: 3 additions & 1 deletion 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,7 +43,7 @@ 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);
static std::optional<float> QueryRefreshRateForWindow(const WindowInfo& wi);

/// Enables or disables the screen saver from starting.
static bool InhibitScreensaver(const WindowInfo& wi, bool 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
10 changes: 7 additions & 3 deletions pcsx2/GS/GS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -549,12 +549,16 @@ bool GSWantsExclusiveFullscreen()
return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate);
}

bool GSGetHostRefreshRate(float* refresh_rate)
std::optional<float> GSGetHostRefreshRate()
{
if (!g_gs_device)
return false;
return std::nullopt;

return g_gs_device->GetHostRefreshRate(refresh_rate);
const float surface_refresh_rate = g_gs_device->GetWindowInfo().surface_refresh_rate;
if (surface_refresh_rate == 0.0f)
return std::nullopt;
else
return surface_refresh_rate;
}

void GSGetAdaptersAndFullscreenModes(
Expand Down
2 changes: 1 addition & 1 deletion pcsx2/GS/GS.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void GSSetVSyncMode(GSVSyncMode mode);
GSRendererType GSGetCurrentRenderer();
bool GSIsHardwareRenderer();
bool GSWantsExclusiveFullscreen();
bool GSGetHostRefreshRate(float* refresh_rate);
std::optional<float> GSGetHostRefreshRate();
void GSGetAdaptersAndFullscreenModes(
GSRendererType renderer, std::vector<std::string>* adapters, std::vector<std::string>* fullscreen_modes);
GSVideoMode GSgetDisplayMode();
Expand Down
11 changes: 0 additions & 11 deletions pcsx2/GS/Renderers/Common/GSDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,6 @@ bool GSDevice::AcquireWindow(bool recreate_window)
return true;
}

bool GSDevice::GetHostRefreshRate(float* refresh_rate)
{
if (m_window_info.surface_refresh_rate > 0.0f)
{
*refresh_rate = m_window_info.surface_refresh_rate;
return true;
}

return WindowInfo::QueryRefreshRateForWindow(m_window_info, refresh_rate);
}

void GSDevice::ClearRenderTarget(GSTexture* t, u32 c)
{
t->SetClearColor(c);
Expand Down
3 changes: 0 additions & 3 deletions pcsx2/GS/Renderers/Common/GSDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -918,9 +918,6 @@ class GSDevice : public GSAlignedClass<32>
/// Changes vsync mode for this display.
virtual void SetVSyncMode(GSVSyncMode mode) = 0;

/// Returns the effective refresh rate of this display.
virtual bool GetHostRefreshRate(float* refresh_rate);

/// Returns a string of information about the graphics driver being used.
virtual std::string GetDriverInfo() const = 0;

Expand Down
Loading

0 comments on commit 2a15835

Please sign in to comment.