GS: Improve vsync mode selection

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".
This commit is contained in:
Stenzek 2024-05-23 23:35:34 +10:00 committed by Connor McLaughlin
parent 82fbf34f5b
commit c7a21a60cf
40 changed files with 707 additions and 460 deletions

View File

@ -13,6 +13,8 @@ namespace CocoaTools
{ {
bool CreateMetalLayer(WindowInfo* wi); bool CreateMetalLayer(WindowInfo* wi);
void DestroyMetalLayer(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 /// Add a handler to be run when macOS changes between dark and light themes
void AddThemeChangeHandler(void* ctx, void(handler)(void* ctx)); void AddThemeChangeHandler(void* ctx, void(handler)(void* ctx));
/// Remove a handler previously added using AddThemeChangeHandler with the given context /// Remove a handler previously added using AddThemeChangeHandler with the given context

View File

@ -64,6 +64,27 @@ void CocoaTools::DestroyMetalLayer(WindowInfo* wi)
[view setWantsLayer:NO]; [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 // MARK: - Theme Change Handlers
@interface PCSX2KVOHelper : NSObject @interface PCSX2KVOHelper : NSObject

View File

@ -4,6 +4,7 @@
#if defined(__APPLE__) #if defined(__APPLE__)
#include "common/Darwin/DarwinMisc.h" #include "common/Darwin/DarwinMisc.h"
#include "common/HostSys.h"
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
@ -102,7 +103,7 @@ std::string GetOSVersionString()
static IOPMAssertionID s_pm_assertion; static IOPMAssertionID s_pm_assertion;
bool WindowInfo::InhibitScreensaver(const WindowInfo& wi, bool inhibit) bool Common::InhibitScreensaver(bool inhibit)
{ {
if (s_pm_assertion) if (s_pm_assertion)
{ {

View File

@ -180,6 +180,9 @@ extern std::string GetOSVersionString();
namespace Common namespace Common
{ {
/// Enables or disables the screen saver from starting.
bool InhibitScreensaver(bool inhibit);
/// Abstracts platform-specific code for asynchronously playing a sound. /// 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. /// On Windows, this will use PlaySound(). On Linux, it will shell out to aplay. On MacOS, it uses NSSound.
bool PlaySoundAsync(const char* path); bool PlaySoundAsync(const char* path);

View File

@ -161,7 +161,7 @@ static bool SetScreensaverInhibitDBus(const bool inhibit_requested, const char*
return true; return true;
} }
bool WindowInfo::InhibitScreensaver(const WindowInfo& wi, bool inhibit) bool Common::InhibitScreensaver(bool inhibit)
{ {
return SetScreensaverInhibitDBus(inhibit, "PCSX2", "PCSX2 VM is running."); return SetScreensaverInhibitDBus(inhibit, "PCSX2", "PCSX2 VM is running.");
} }

View File

@ -1,19 +1,90 @@
// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team // SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team
// SPDX-License-Identifier: LGPL-3.0+ // SPDX-License-Identifier: LGPL-3.0+
#include "WindowInfo.h" #include "WindowInfo.h"
#include "Console.h" #include "Console.h"
#include "Error.h"
#include "HeapArray.h"
#if defined(_WIN32) #if defined(_WIN32)
#include "RedtapeWindows.h" #include "RedtapeWindows.h"
#include <dwmapi.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; BOOL composition_enabled;
if (FAILED(DwmIsCompositionEnabled(&composition_enabled))) if (FAILED(DwmIsCompositionEnabled(&composition_enabled)))
return false; return std::nullopt;
DWM_TIMING_INFO ti = {}; DWM_TIMING_INFO ti = {};
ti.cbSize = sizeof(ti); ti.cbSize = sizeof(ti);
@ -21,20 +92,19 @@ static bool GetRefreshRateFromDWM(HWND hwnd, float* refresh_rate)
if (SUCCEEDED(hr)) if (SUCCEEDED(hr))
{ {
if (ti.rateRefresh.uiNumerator == 0 || ti.rateRefresh.uiDenominator == 0) 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 static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
return true;
} }
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); HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (!mon) if (!mon)
return false; return std::nullopt;
MONITORINFOEXW mi = {}; MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi); mi.cbSize = sizeof(mi);
@ -45,23 +115,41 @@ static bool GetRefreshRateFromMonitor(HWND hwnd, float* refresh_rate)
// 0/1 are reserved for "defaults". // 0/1 are reserved for "defaults".
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1) if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
{ return static_cast<float>(dm.dmDisplayFrequency);
*refresh_rate = static_cast<float>(dm.dmDisplayFrequency);
return true;
}
} }
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) if (wi.type != Type::Win32 || !wi.window_handle)
return false; return ret;
// Try DWM first, then fall back to integer values. // Try DWM first, then fall back to integer values.
const HWND hwnd = static_cast<HWND>(wi.window_handle); 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 #else
@ -72,18 +160,18 @@ bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_
#include <X11/extensions/Xrandr.h> #include <X11/extensions/Xrandr.h>
#include <X11/Xlib.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); Display* display = static_cast<Display*>(wi.display_connection);
Window window = static_cast<Window>(reinterpret_cast<uintptr_t>(wi.window_handle)); Window window = static_cast<Window>(reinterpret_cast<uintptr_t>(wi.window_handle));
if (!display || !window) if (!display || !window)
return false; return std::nullopt;
XRRScreenResources* res = XRRGetScreenResources(display, window); XRRScreenResources* res = XRRGetScreenResources(display, window);
if (!res) if (!res)
{ {
Console.Error("(GetRefreshRateFromXRandR) XRRGetScreenResources() failed"); Console.Error("(GetRefreshRateFromXRandR) XRRGetScreenResources() failed");
return false; return std::nullopt;
} }
ScopedGuard res_guard([res]() { XRRFreeScreenResources(res); }); ScopedGuard res_guard([res]() { XRRFreeScreenResources(res); });
@ -93,7 +181,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (num_monitors < 0) if (num_monitors < 0)
{ {
Console.Error("(GetRefreshRateFromXRandR) XRRGetMonitors() failed"); Console.Error("(GetRefreshRateFromXRandR) XRRGetMonitors() failed");
return false; return std::nullopt;
} }
else if (num_monitors > 1) else if (num_monitors > 1)
{ {
@ -104,7 +192,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (mi->noutput <= 0) if (mi->noutput <= 0)
{ {
Console.Error("(GetRefreshRateFromXRandR) Monitor has no outputs"); Console.Error("(GetRefreshRateFromXRandR) Monitor has no outputs");
return false; return std::nullopt;
} }
else if (mi->noutput > 1) else if (mi->noutput > 1)
{ {
@ -115,7 +203,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!oi) if (!oi)
{ {
Console.Error("(GetRefreshRateFromXRandR) XRRGetOutputInfo() failed"); Console.Error("(GetRefreshRateFromXRandR) XRRGetOutputInfo() failed");
return false; return std::nullopt;
} }
ScopedGuard oi_guard([oi]() { XRRFreeOutputInfo(oi); }); ScopedGuard oi_guard([oi]() { XRRFreeOutputInfo(oi); });
@ -124,7 +212,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!ci) if (!ci)
{ {
Console.Error("(GetRefreshRateFromXRandR) XRRGetCrtcInfo() failed"); Console.Error("(GetRefreshRateFromXRandR) XRRGetCrtcInfo() failed");
return false; return std::nullopt;
} }
ScopedGuard ci_guard([ci]() { XRRFreeCrtcInfo(ci); }); ScopedGuard ci_guard([ci]() { XRRFreeCrtcInfo(ci); });
@ -141,30 +229,29 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!mode) if (!mode)
{ {
Console.Error("(GetRefreshRateFromXRandR) Failed to look up mode %d (of %d)", static_cast<int>(ci->mode), res->nmode); 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) 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); Console.Error("(GetRefreshRateFromXRandR) Modeline is invalid: %ld/%d/%d", mode->dotClock, mode->hTotal, mode->vTotal);
return false; return std::nullopt;
} }
*refresh_rate = return static_cast<float>(
static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal)); static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal)));
return true;
} }
#endif // X11_API #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 defined(X11_API)
if (wi.type == WindowInfo::Type::X11) if (wi.type == WindowInfo::Type::X11)
return GetRefreshRateFromXRandR(wi, refresh_rate); return GetRefreshRateFromXRandR(wi);
#endif #endif
return false; return std::nullopt;
} }
#endif #endif

View File

@ -4,6 +4,8 @@
#pragma once #pragma once
#include "Pcsx2Defs.h" #include "Pcsx2Defs.h"
#include <optional>
/// Contains the information required to create a graphics context in a window. /// Contains the information required to create a graphics context in a window.
struct WindowInfo struct WindowInfo
{ {
@ -41,8 +43,5 @@ struct WindowInfo
float surface_refresh_rate = 0.0f; float surface_refresh_rate = 0.0f;
/// Returns the host's refresh rate for the given window, if available. /// 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);
}; };

View File

@ -81,7 +81,7 @@ std::string GetOSVersionString()
return retval; return retval;
} }
bool WindowInfo::InhibitScreensaver(const WindowInfo& wi, bool inhibit) bool Common::InhibitScreensaver(bool inhibit)
{ {
EXECUTION_STATE flags = ES_CONTINUOUS; EXECUTION_STATE flags = ES_CONTINUOUS;
if (inhibit) if (inhibit)

View File

@ -302,6 +302,21 @@ namespace QtUtils
wi.surface_width = static_cast<u32>(static_cast<qreal>(widget->width()) * dpr); 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_height = static_cast<u32>(static_cast<qreal>(widget->height()) * dpr);
wi.surface_scale = static_cast<float>(dpr); wi.surface_scale = static_cast<float>(dpr);
// Query refresh rate, we need it for sync.
std::optional<float> surface_refresh_rate = WindowInfo::QueryRefreshRateForWindow(wi);
if (!surface_refresh_rate.has_value())
{
// Fallback to using the screen, getting the rate for Wayland is an utter mess otherwise.
const QScreen* widget_screen = widget->screen();
if (!widget_screen)
widget_screen = QGuiApplication::primaryScreen();
surface_refresh_rate = widget_screen ? static_cast<float>(widget_screen->refreshRate()) : 0.0f;
}
wi.surface_refresh_rate = surface_refresh_rate.value();
INFO_LOG("Surface refresh rate: {} hz", wi.surface_refresh_rate);
return wi; return wi;
} }

View File

@ -31,8 +31,12 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
initializeSpeedCombo(m_ui.slowMotionSpeed, "Framerate", "SlomoScalar", 0.5f); initializeSpeedCombo(m_ui.slowMotionSpeed, "Framerate", "SlomoScalar", 0.5f);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.maxFrameLatency, "EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.maxFrameLatency, "EmuCore/GS", "VsyncQueueSize", DEFAULT_FRAME_LATENCY);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.vsync, "EmuCore/GS", "VsyncEnable", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToHostRefreshRate, "EmuCore/GS", "SyncToHostRefreshRate", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.syncToHostRefreshRate, "EmuCore/GS", "SyncToHostRefreshRate", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useVSyncForTiming, "EmuCore/GS", "UseVSyncForTiming", false);
connect(m_ui.optimalFramePacing, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::onOptimalFramePacingChanged); connect(m_ui.optimalFramePacing, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::onOptimalFramePacingChanged);
connect(m_ui.vsync, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::updateUseVSyncForTimingEnabled);
connect(m_ui.syncToHostRefreshRate, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::updateUseVSyncForTimingEnabled);
m_ui.optimalFramePacing->setTristate(dialog->isPerGameSettings()); m_ui.optimalFramePacing->setTristate(dialog->isPerGameSettings());
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.eeCycleSkipping, "EmuCore/Speedhacks", "EECycleSkip", DEFAULT_EE_CYCLE_SKIP); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.eeCycleSkipping, "EmuCore/Speedhacks", "EECycleSkip", DEFAULT_EE_CYCLE_SKIP);
@ -131,13 +135,20 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
dialog->registerWidgetHelp(m_ui.maxFrameLatency, tr("Maximum Frame Latency"), tr("2 Frames"), dialog->registerWidgetHelp(m_ui.maxFrameLatency, tr("Maximum Frame Latency"), tr("2 Frames"),
tr("Sets the maximum number of frames that can be queued up to the GS, before the CPU thread will wait for one of them to complete before continuing. " tr("Sets the maximum number of frames that can be queued up to the GS, before the CPU thread will wait for one of them to complete before continuing. "
"Higher values can assist with smoothing out irregular frame times, but add additional input lag.")); "Higher values can assist with smoothing out irregular frame times, but add additional input lag."));
dialog->registerWidgetHelp(m_ui.syncToHostRefreshRate, tr("Scale To Host Refresh Rate"), tr("Unchecked"), dialog->registerWidgetHelp(m_ui.syncToHostRefreshRate, tr("Sync to Host Refresh Rate"), tr("Unchecked"),
tr("Speeds up emulation so that the guest refresh rate matches the host. This results in the smoothest animations possible, at the cost of " tr("Speeds up emulation so that the guest refresh rate matches the host. This results in the smoothest animations possible, at the cost of "
"potentially increasing the emulation speed by less than 1%. Scale To Host Refresh Rate will not take effect if " "potentially increasing the emulation speed by less than 1%. Sync to Host Refresh Rate will not take effect if "
"the console's refresh rate is too far from the host's refresh rate. Users with variable refresh rate displays " "the console's refresh rate is too far from the host's refresh rate. Users with variable refresh rate displays "
"should disable this option.")); "should disable this option."));
dialog->registerWidgetHelp(m_ui.vsync, tr("Vertical Sync (VSync)"), tr("Unchecked"),
tr("Enable this option to match PCSX2's refresh rate with your current monitor or screen. VSync is automatically disabled when "
"it is not possible (eg. running at non-100% speed)."));
dialog->registerWidgetHelp(m_ui.useVSyncForTiming, tr("Use Host VSync Timing"), tr("Unchecked"),
tr("When synchronizing with the host refresh rate, this option disable's PCSX2's internal frame timing, and uses the host instead. "
"Can result in smoother frame pacing, <strong>but at the cost of increased input latency</strong>."));
updateOptimalFramePacing(); updateOptimalFramePacing();
updateUseVSyncForTimingEnabled();
} }
EmulationSettingsWidget::~EmulationSettingsWidget() = default; EmulationSettingsWidget::~EmulationSettingsWidget() = default;
@ -274,3 +285,10 @@ void EmulationSettingsWidget::updateOptimalFramePacing()
m_ui.maxFrameLatency->setMinimum(optimal ? 0 : 1); m_ui.maxFrameLatency->setMinimum(optimal ? 0 : 1);
m_ui.maxFrameLatency->setValue(optimal ? 0 : value); m_ui.maxFrameLatency->setValue(optimal ? 0 : value);
} }
void EmulationSettingsWidget::updateUseVSyncForTimingEnabled()
{
const bool vsync = m_dialog->getEffectiveBoolValue("EmuCore/GS", "VsyncEnable", false);
const bool sync_to_host_refresh = m_dialog->getEffectiveBoolValue("EmuCore/GS", "SyncToHostRefreshRate", false);
m_ui.useVSyncForTiming->setEnabled(vsync && sync_to_host_refresh);
}

View File

@ -24,6 +24,7 @@ private:
void initializeSpeedCombo(QComboBox* cb, const char* section, const char* key, float default_value); void initializeSpeedCombo(QComboBox* cb, const char* section, const char* key, float default_value);
void handleSpeedComboChange(QComboBox* cb, const char* section, const char* key); void handleSpeedComboChange(QComboBox* cb, const char* section, const char* key);
void updateOptimalFramePacing(); void updateOptimalFramePacing();
void updateUseVSyncForTimingEnabled();
SettingsWindow* m_dialog; SettingsWindow* m_dialog;

View File

@ -261,6 +261,20 @@
</item> </item>
<item row="3" column="0" colspan="2"> <item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="basicCheckboxGridLayout"> <layout class="QGridLayout" name="basicCheckboxGridLayout">
<item row="1" column="1">
<widget class="QCheckBox" name="useVSyncForTiming">
<property name="text">
<string>Use Host VSync Timing</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="syncToHostRefreshRate">
<property name="text">
<string>Sync to Host Refresh Rate</string>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="optimalFramePacing"> <widget class="QCheckBox" name="optimalFramePacing">
<property name="text"> <property name="text">
@ -268,10 +282,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="1" column="0">
<widget class="QCheckBox" name="syncToHostRefreshRate"> <widget class="QCheckBox" name="vsync">
<property name="text"> <property name="text">
<string>Scale To Host Refresh Rate</string> <string>Vertical Sync (VSync)</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -283,7 +297,7 @@
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Orientation::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>

View File

@ -74,7 +74,6 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
// Global Settings // Global Settings
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.adapter, "EmuCore/GS", "Adapter"); SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.adapter, "EmuCore/GS", "Adapter");
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.vsync, "EmuCore/GS", "VsyncEnable", 0);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableHWFixes, "EmuCore/GS", "UserHacks", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableHWFixes, "EmuCore/GS", "UserHacks", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spinGPUDuringReadbacks, "EmuCore/GS", "HWSpinGPUForReadbacks", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spinGPUDuringReadbacks, "EmuCore/GS", "HWSpinGPUForReadbacks", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spinCPUDuringReadbacks, "EmuCore/GS", "HWSpinCPUForReadbacks", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spinCPUDuringReadbacks, "EmuCore/GS", "HWSpinCPUForReadbacks", false);
@ -422,10 +421,6 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
dialog->registerWidgetHelp(m_ui.PCRTCAntiBlur, tr("Anti-Blur"), tr("Checked"), dialog->registerWidgetHelp(m_ui.PCRTCAntiBlur, tr("Anti-Blur"), tr("Checked"),
tr("Enables internal Anti-Blur hacks. Less accurate to PS2 rendering but will make a lot of games look less blurry.")); tr("Enables internal Anti-Blur hacks. Less accurate to PS2 rendering but will make a lot of games look less blurry."));
dialog->registerWidgetHelp(m_ui.vsync, tr("VSync"), tr("Unchecked"),
tr("Enable this option to match PCSX2's refresh rate with your current monitor or screen. VSync is automatically disabled when "
"it is not possible (eg. running at non-100% speed)."));
dialog->registerWidgetHelp(m_ui.integerScaling, tr("Integer Scaling"), tr("Unchecked"), dialog->registerWidgetHelp(m_ui.integerScaling, tr("Integer Scaling"), tr("Unchecked"),
tr("Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an " tr("Adds padding to the display area to ensure that the ratio between pixels on the host to pixels in the console is an "
"integer number. May result in a sharper image in some 2D games.")); "integer number. May result in a sharper image in some 2D games."));

View File

@ -224,6 +224,75 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Screenshot Size:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,0,0">
<item>
<widget class="QComboBox" name="screenshotSize">
<item>
<property name="text">
<string>Window Resolution (Aspect Corrected)</string>
</property>
</item>
<item>
<property name="text">
<string>Internal Resolution (Aspect Corrected)</string>
</property>
</item>
<item>
<property name="text">
<string>Internal Resolution (No Aspect Correction)</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="screenshotFormat">
<item>
<property name="text">
<string>PNG</string>
</property>
</item>
<item>
<property name="text">
<string>JPEG</string>
</property>
</item>
<item>
<property name="text">
<string>WebP</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_44">
<property name="text">
<string>Quality:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="screenshotQuality">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_24"> <widget class="QLabel" name="label_24">
<property name="text"> <property name="text">
@ -325,13 +394,6 @@
</item> </item>
<item row="8" column="0" colspan="2"> <item row="8" column="0" colspan="2">
<layout class="QGridLayout" name="displayGridLayout"> <layout class="QGridLayout" name="displayGridLayout">
<item row="3" column="0">
<widget class="QCheckBox" name="PCRTCOffsets">
<property name="text">
<string>Screen Offsets</string>
</property>
</widget>
</item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QCheckBox" name="integerScaling"> <widget class="QCheckBox" name="integerScaling">
<property name="text"> <property name="text">
@ -339,20 +401,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QCheckBox" name="vsync">
<property name="text">
<string>VSync</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="PCRTCOverscan">
<property name="text">
<string>Show Overscan</string>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="widescreenPatches"> <widget class="QCheckBox" name="widescreenPatches">
<property name="text"> <property name="text">
@ -384,72 +432,17 @@
</property> </property>
</widget> </widget>
</item> </item>
</layout> <item row="2" column="0">
</item> <widget class="QCheckBox" name="PCRTCOffsets">
<item row="5" column="0">
<widget class="QLabel" name="label_21">
<property name="text"> <property name="text">
<string>Screenshot Size:</string> <string>Screen Offsets</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="3" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,0,0"> <widget class="QCheckBox" name="PCRTCOverscan">
<item>
<widget class="QComboBox" name="screenshotSize">
<item>
<property name="text"> <property name="text">
<string>Window Resolution (Aspect Corrected)</string> <string>Show Overscan</string>
</property>
</item>
<item>
<property name="text">
<string>Internal Resolution (Aspect Corrected)</string>
</property>
</item>
<item>
<property name="text">
<string>Internal Resolution (No Aspect Correction)</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="screenshotFormat">
<item>
<property name="text">
<string>PNG</string>
</property>
</item>
<item>
<property name="text">
<string>JPEG</string>
</property>
</item>
<item>
<property name="text">
<string>WebP</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_44">
<property name="text">
<string>Quality:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="screenshotQuality">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -247,6 +247,14 @@ enum class GSRendererType : s8
DX12 = 15, DX12 = 15,
}; };
enum class GSVSyncMode : u8
{
Disabled,
FIFO,
Mailbox,
Count
};
enum class GSInterlaceMode : u8 enum class GSInterlaceMode : u8
{ {
Automatic, Automatic,
@ -971,6 +979,7 @@ struct Pcsx2Config
{ {
BITFIELD32() BITFIELD32()
bool SyncToHostRefreshRate : 1; bool SyncToHostRefreshRate : 1;
bool UseVSyncForTiming : 1;
BITFIELD_END BITFIELD_END
float NominalScalar{1.0f}; float NominalScalar{1.0f};

View File

@ -56,8 +56,6 @@ Pcsx2Config::GSOptions GSConfig;
static GSRendererType GSCurrentRenderer; static GSRendererType GSCurrentRenderer;
static u64 s_next_manual_present_time;
GSRendererType GSGetCurrentRenderer() GSRendererType GSGetCurrentRenderer()
{ {
return GSCurrentRenderer; return GSCurrentRenderer;
@ -98,7 +96,8 @@ static RenderAPI GetAPIForRenderer(GSRendererType renderer)
} }
} }
static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool recreate_window) static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool recreate_window,
GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
const RenderAPI new_api = GetAPIForRenderer(renderer); const RenderAPI new_api = GetAPIForRenderer(renderer);
switch (new_api) switch (new_api)
@ -133,7 +132,7 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool
return false; return false;
} }
bool okay = g_gs_device->Create(); bool okay = g_gs_device->Create(vsync_mode, allow_present_throttle);
if (okay) if (okay)
{ {
okay = ImGuiManager::Initialize(); okay = ImGuiManager::Initialize();
@ -266,9 +265,11 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
{ {
// We need a new render window when changing APIs. // We need a new render window when changing APIs.
const bool recreate_window = (g_gs_device->GetRenderAPI() != GetAPIForRenderer(GSConfig.Renderer)); const bool recreate_window = (g_gs_device->GetRenderAPI() != GetAPIForRenderer(GSConfig.Renderer));
const GSVSyncMode vsync_mode = g_gs_device->GetVSyncMode();
const bool allow_present_throttle = g_gs_device->IsPresentThrottleAllowed();
CloseGSDevice(false); CloseGSDevice(false);
if (!OpenGSDevice(new_renderer, false, recreate_window)) if (!OpenGSDevice(new_renderer, false, recreate_window, vsync_mode, allow_present_throttle))
{ {
Host::AddKeyedOSDMessage("GSReopenFailed", Host::AddKeyedOSDMessage("GSReopenFailed",
TRANSLATE_STR("GS", "Failed to reopen, restoring old configuration."), TRANSLATE_STR("GS", "Failed to reopen, restoring old configuration."),
@ -279,7 +280,7 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
if (old_config.has_value()) if (old_config.has_value())
GSConfig = *old_config.value(); GSConfig = *old_config.value();
if (!OpenGSDevice(GSConfig.Renderer, false, recreate_window)) if (!OpenGSDevice(GSConfig.Renderer, false, recreate_window, vsync_mode, allow_present_throttle))
{ {
pxFailRel("Failed to reopen GS on old config"); pxFailRel("Failed to reopen GS on old config");
Host::ReleaseRenderWindow(); Host::ReleaseRenderWindow();
@ -309,14 +310,15 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
return true; return true;
} }
bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* basemem) bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* basemem,
GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
GSConfig = config; GSConfig = config;
if (renderer == GSRendererType::Auto) if (renderer == GSRendererType::Auto)
renderer = GSUtil::GetPreferredRenderer(); renderer = GSUtil::GetPreferredRenderer();
bool res = OpenGSDevice(renderer, true, false); bool res = OpenGSDevice(renderer, true, false, vsync_mode, allow_present_throttle);
if (res) if (res)
{ {
res = OpenGSRenderer(renderer, basemem); res = OpenGSRenderer(renderer, basemem);
@ -471,29 +473,13 @@ void GSPresentCurrentFrame()
void GSThrottlePresentation() void GSThrottlePresentation()
{ {
if (g_gs_device->IsVSyncEnabled()) if (g_gs_device->GetVSyncMode() == GSVSyncMode::FIFO)
{ {
// Let vsync take care of throttling. // Let vsync take care of throttling.
return; return;
} }
// Manually throttle presentation when vsync isn't enabled, so we don't try to render the g_gs_device->ThrottlePresentation();
// fullscreen UI at thousands of FPS and make the gpu go brrrrrrrr.
const float surface_refresh_rate = g_gs_device->GetWindowInfo().surface_refresh_rate;
const float throttle_rate = (surface_refresh_rate > 0.0f) ? surface_refresh_rate : 60.0f;
const u64 sleep_period = static_cast<u64>(static_cast<double>(GetTickFrequency()) / static_cast<double>(throttle_rate));
const u64 current_ts = GetCPUTicks();
// Allow it to fall behind/run ahead up to 2*period. Sleep isn't that precise, plus we need to
// allow time for the actual rendering.
const u64 max_variance = sleep_period * 2;
if (static_cast<u64>(std::abs(static_cast<s64>(current_ts - s_next_manual_present_time))) > max_variance)
s_next_manual_present_time = current_ts + sleep_period;
else
s_next_manual_present_time += sleep_period;
Threading::SleepUntil(s_next_manual_present_time);
} }
void GSGameChanged() void GSGameChanged()
@ -528,9 +514,16 @@ void GSUpdateDisplayWindow()
ImGuiManager::WindowResized(); ImGuiManager::WindowResized();
} }
void GSSetVSyncEnabled(bool enabled) void GSSetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
g_gs_device->SetVSyncEnabled(enabled); static constexpr std::array<const char*, static_cast<size_t>(GSVSyncMode::Count)> modes = {{
"Disabled",
"FIFO",
"Mailbox",
}};
Console.WriteLnFmt(Color_StrongCyan, "Setting vsync mode: {}{}", modes[static_cast<size_t>(mode)],
allow_present_throttle ? " (throttle allowed)" : "");
g_gs_device->SetVSyncMode(mode, allow_present_throttle);
} }
bool GSWantsExclusiveFullscreen() bool GSWantsExclusiveFullscreen()
@ -543,12 +536,16 @@ bool GSWantsExclusiveFullscreen()
return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate); return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate);
} }
bool GSGetHostRefreshRate(float* refresh_rate) std::optional<float> GSGetHostRefreshRate()
{ {
if (!g_gs_device) 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( void GSGetAdaptersAndFullscreenModes(

View File

@ -56,7 +56,8 @@ s16 GSLookupGetSkipCountFunctionId(const std::string_view name);
s16 GSLookupBeforeDrawFunctionId(const std::string_view name); s16 GSLookupBeforeDrawFunctionId(const std::string_view name);
s16 GSLookupMoveHandlerFunctionId(const std::string_view name); s16 GSLookupMoveHandlerFunctionId(const std::string_view name);
bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* basemem); bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* basemem,
GSVSyncMode vsync_mode, bool allow_present_throttle);
bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_renderer, bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_renderer,
std::optional<const Pcsx2Config::GSOptions*> old_config); std::optional<const Pcsx2Config::GSOptions*> old_config);
void GSreset(bool hardware_reset); void GSreset(bool hardware_reset);
@ -84,12 +85,12 @@ void GSSetDisplayAlignment(GSDisplayAlignment alignment);
bool GSHasDisplayWindow(); bool GSHasDisplayWindow();
void GSResizeDisplayWindow(int width, int height, float scale); void GSResizeDisplayWindow(int width, int height, float scale);
void GSUpdateDisplayWindow(); void GSUpdateDisplayWindow();
void GSSetVSyncEnabled(bool enabled); void GSSetVSyncMode(GSVSyncMode mode, bool allow_present_throttle);
GSRendererType GSGetCurrentRenderer(); GSRendererType GSGetCurrentRenderer();
bool GSIsHardwareRenderer(); bool GSIsHardwareRenderer();
bool GSWantsExclusiveFullscreen(); bool GSWantsExclusiveFullscreen();
bool GSGetHostRefreshRate(float* refresh_rate); std::optional<float> GSGetHostRefreshRate();
void GSGetAdaptersAndFullscreenModes( void GSGetAdaptersAndFullscreenModes(
GSRendererType renderer, std::vector<std::string>* adapters, std::vector<std::string>* fullscreen_modes); GSRendererType renderer, std::vector<std::string>* adapters, std::vector<std::string>* fullscreen_modes);
GSVideoMode GSgetDisplayMode(); GSVideoMode GSgetDisplayMode();
@ -126,9 +127,6 @@ namespace Host
/// Alters fullscreen state of hosting application. /// Alters fullscreen state of hosting application.
void SetFullscreen(bool enabled); void SetFullscreen(bool enabled);
/// Returns the desired vsync mode, depending on the runtime environment.
bool IsVsyncEffectivelyEnabled();
/// Called when video capture starts or stops. Called on the MTGS thread. /// Called when video capture starts or stops. Called on the MTGS thread.
void OnCaptureStarted(const std::string& filename); void OnCaptureStarted(const std::string& filename);
void OnCaptureStopped(); void OnCaptureStopped();

View File

@ -971,7 +971,7 @@ void GSCapture::StopEncoderThread(std::unique_lock<std::mutex>& lock)
bool GSCapture::SendFrame(const PendingFrame& pf) bool GSCapture::SendFrame(const PendingFrame& pf)
{ {
const AVPixelFormat source_format = g_gs_device->IsRBSwapped() ? AV_PIX_FMT_BGRA : AV_PIX_FMT_RGBA; const AVPixelFormat source_format = AV_PIX_FMT_RGBA;
const u8* source_ptr = pf.tex->GetMapPointer(); const u8* source_ptr = pf.tex->GetMapPointer();
const int source_width = static_cast<int>(pf.tex->GetWidth()); const int source_width = static_cast<int>(pf.tex->GetWidth());
const int source_height = static_cast<int>(pf.tex->GetHeight()); const int source_height = static_cast<int>(pf.tex->GetHeight());

View File

@ -9,9 +9,11 @@
#include "common/Console.h" #include "common/Console.h"
#include "common/BitUtils.h" #include "common/BitUtils.h"
#include "common/FileSystem.h" #include "common/FileSystem.h"
#include "common/HostSys.h"
#include "common/Path.h" #include "common/Path.h"
#include "common/SmallString.h" #include "common/SmallString.h"
#include "common/StringUtil.h" #include "common/StringUtil.h"
#include "common/Threading.h"
#include "imgui.h" #include "imgui.h"
@ -305,9 +307,10 @@ int GSDevice::GetMipmapLevelsForSize(int width, int height)
return std::min(static_cast<int>(std::log2(std::max(width, height))) + 1, MAXIMUM_TEXTURE_MIPMAP_LEVELS); return std::min(static_cast<int>(std::log2(std::max(width, height))) + 1, MAXIMUM_TEXTURE_MIPMAP_LEVELS);
} }
bool GSDevice::Create() bool GSDevice::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
m_vsync_enabled = Host::IsVsyncEffectivelyEnabled(); m_vsync_mode = vsync_mode;
m_allow_present_throttle = allow_present_throttle;
return true; return true;
} }
@ -337,15 +340,42 @@ bool GSDevice::AcquireWindow(bool recreate_window)
return true; return true;
} }
bool GSDevice::GetHostRefreshRate(float* refresh_rate) bool GSDevice::ShouldSkipPresentingFrame()
{ {
if (m_window_info.surface_refresh_rate > 0.0f) // Only needed with FIFO.
{ if (!m_allow_present_throttle || m_vsync_mode != GSVSyncMode::FIFO)
*refresh_rate = m_window_info.surface_refresh_rate; return false;
const float throttle_rate = (m_window_info.surface_refresh_rate > 0.0f) ? m_window_info.surface_refresh_rate : 60.0f;
const u64 throttle_period = static_cast<u64>(static_cast<double>(GetTickFrequency()) / static_cast<double>(throttle_rate));
const u64 now = GetCPUTicks();
const double diff = now - m_last_frame_displayed_time;
if (diff < throttle_period)
return true; return true;
m_last_frame_displayed_time = now;
return false;
} }
return WindowInfo::QueryRefreshRateForWindow(m_window_info, refresh_rate); void GSDevice::ThrottlePresentation()
{
// Manually throttle presentation when vsync isn't enabled, so we don't try to render the
// fullscreen UI at thousands of FPS and make the gpu go brrrrrrrr.
const float throttle_rate = (m_window_info.surface_refresh_rate > 0.0f) ? m_window_info.surface_refresh_rate : 60.0f;
const u64 sleep_period = static_cast<u64>(static_cast<double>(GetTickFrequency()) / static_cast<double>(throttle_rate));
const u64 current_ts = GetCPUTicks();
// Allow it to fall behind/run ahead up to 2*period. Sleep isn't that precise, plus we need to
// allow time for the actual rendering.
const u64 max_variance = sleep_period * 2;
if (static_cast<u64>(std::abs(static_cast<s64>(current_ts - m_last_frame_displayed_time))) > max_variance)
m_last_frame_displayed_time = current_ts + sleep_period;
else
m_last_frame_displayed_time += sleep_period;
Threading::SleepUntil(m_last_frame_displayed_time);
} }
void GSDevice::ClearRenderTarget(GSTexture* t, u32 c) void GSDevice::ClearRenderTarget(GSTexture* t, u32 c)

View File

@ -786,6 +786,20 @@ public:
}; };
// clang-format on // clang-format on
protected:
FeatureSupport m_features;
struct
{
u32 start, count;
} m_vertex = {};
struct
{
u32 start, count;
} m_index = {};
u32 m_frame = 0; // for ageing the pool
private: private:
std::array<FastList<GSTexture*>, 2> m_pool; // [texture, target] std::array<FastList<GSTexture*>, 2> m_pool; // [texture, target]
u64 m_pool_memory_usage = 0; u64 m_pool_memory_usage = 0;
@ -803,6 +817,9 @@ protected:
static constexpr u32 EXPAND_BUFFER_SIZE = sizeof(u16) * 16383 * 6; static constexpr u32 EXPAND_BUFFER_SIZE = sizeof(u16) * 16383 * 6;
WindowInfo m_window_info; WindowInfo m_window_info;
GSVSyncMode m_vsync_mode = GSVSyncMode::Disabled;
bool m_allow_present_throttle = false;
u64 m_last_frame_displayed_time = 0;
GSTexture* m_imgui_font = nullptr; GSTexture* m_imgui_font = nullptr;
@ -814,19 +831,6 @@ protected:
GSTexture* m_current = nullptr; GSTexture* m_current = nullptr;
GSTexture* m_cas = nullptr; GSTexture* m_cas = nullptr;
struct
{
u32 start, count;
} m_vertex = {};
struct
{
u32 start, count;
} m_index = {};
unsigned int m_frame = 0; // for ageing the pool
bool m_vsync_enabled = false;
bool m_rbswapped = false;
FeatureSupport m_features;
bool AcquireWindow(bool recreate_window); bool AcquireWindow(bool recreate_window);
virtual GSTexture* CreateSurface(GSTexture::Type type, int width, int height, int levels, GSTexture::Format format) = 0; virtual GSTexture* CreateSurface(GSTexture::Type type, int width, int height, int levels, GSTexture::Format format) = 0;
@ -874,7 +878,8 @@ public:
__fi s32 GetWindowHeight() const { return static_cast<s32>(m_window_info.surface_height); } __fi s32 GetWindowHeight() const { return static_cast<s32>(m_window_info.surface_height); }
__fi GSVector2i GetWindowSize() const { return GSVector2i(static_cast<s32>(m_window_info.surface_width), static_cast<s32>(m_window_info.surface_height)); } __fi GSVector2i GetWindowSize() const { return GSVector2i(static_cast<s32>(m_window_info.surface_width), static_cast<s32>(m_window_info.surface_height)); }
__fi float GetWindowScale() const { return m_window_info.surface_scale; } __fi float GetWindowScale() const { return m_window_info.surface_scale; }
__fi bool IsVSyncEnabled() const { return m_vsync_enabled; } __fi GSVSyncMode GetVSyncMode() const { return m_vsync_mode; }
__fi bool IsPresentThrottleAllowed() const { return m_allow_present_throttle; }
__fi GSTexture* GetCurrent() const { return m_current; } __fi GSTexture* GetCurrent() const { return m_current; }
@ -886,7 +891,7 @@ public:
/// Recreates the font, call when the window scaling changes. /// Recreates the font, call when the window scaling changes.
bool UpdateImGuiFontTexture(); bool UpdateImGuiFontTexture();
virtual bool Create(); virtual bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle);
virtual void Destroy(); virtual void Destroy();
/// Returns the graphics API used by this device. /// Returns the graphics API used by this device.
@ -915,10 +920,7 @@ public:
virtual void EndPresent() = 0; virtual void EndPresent() = 0;
/// Changes vsync mode for this display. /// Changes vsync mode for this display.
virtual void SetVSyncEnabled(bool enabled) = 0; virtual void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) = 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. /// Returns a string of information about the graphics driver being used.
virtual std::string GetDriverInfo() const = 0; virtual std::string GetDriverInfo() const = 0;
@ -929,6 +931,12 @@ public:
/// Returns the amount of GPU time utilized since the last time this method was called. /// Returns the amount of GPU time utilized since the last time this method was called.
virtual float GetAndResetAccumulatedGPUTime() = 0; virtual float GetAndResetAccumulatedGPUTime() = 0;
/// Returns true if not enough time has passed for present to not block.
bool ShouldSkipPresentingFrame();
/// Sleeps to the time the next frame can be displayed.
void ThrottlePresentation();
void ClearRenderTarget(GSTexture* t, u32 c); void ClearRenderTarget(GSTexture* t, u32 c);
void ClearDepth(GSTexture* t, float d); void ClearDepth(GSTexture* t, float d);
void InvalidateRenderTarget(GSTexture* t); void InvalidateRenderTarget(GSTexture* t);
@ -980,8 +988,6 @@ public:
bool ResizeRenderTarget(GSTexture** t, int w, int h, bool preserve_contents, bool recycle); bool ResizeRenderTarget(GSTexture** t, int w, int h, bool preserve_contents, bool recycle);
bool IsRBSwapped() { return m_rbswapped; }
void AgePool(); void AgePool();
void PurgePool(); void PurgePool();

View File

@ -580,12 +580,13 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
m_last_draw_n = s_n; m_last_draw_n = s_n;
m_last_transfer_n = s_transfer_n; m_last_transfer_n = s_transfer_n;
if (skip_frame) // Skip presentation when running uncapped while vsync is on.
if (skip_frame || g_gs_device->ShouldSkipPresentingFrame())
{ {
if (BeginPresentFrame(true)) if (BeginPresentFrame(true))
EndPresentFrame(); EndPresentFrame();
PerformanceMetrics::Update(registers_written, fb_sprite_frame, true); PerformanceMetrics::Update(registers_written, fb_sprite_frame, skip_frame);
return; return;
} }

View File

@ -61,7 +61,7 @@ bool GSTexture::Save(const std::string& fn)
} }
const int compression = GSConfig.PNGCompressionLevel; const int compression = GSConfig.PNGCompressionLevel;
return GSPng::Save(format, fn, dl->GetMapPointer(), m_size.x, m_size.y, dl->GetMapPitch(), compression, g_gs_device->IsRBSwapped()); return GSPng::Save(format, fn, dl->GetMapPointer(), m_size.x, m_size.y, dl->GetMapPitch(), compression, false);
} }
const char* GSTexture::GetFormatName(Format format) const char* GSTexture::GetFormatName(Format format)

View File

@ -80,9 +80,9 @@ RenderAPI GSDevice11::GetRenderAPI() const
return RenderAPI::D3D11; return RenderAPI::D3D11;
} }
bool GSDevice11::Create() bool GSDevice11::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
if (!GSDevice::Create()) if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false; return false;
UINT create_flags = 0; UINT create_flags = 0;
@ -609,28 +609,38 @@ bool GSDevice11::HasSurface() const
return static_cast<bool>(m_swap_chain); return static_cast<bool>(m_swap_chain);
} }
bool GSDevice11::GetHostRefreshRate(float* refresh_rate) void GSDevice11::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
if (m_swap_chain && m_is_exclusive_fullscreen) m_allow_present_throttle = allow_present_throttle;
// Using mailbox-style no-allow-tearing causes tearing in exclusive fullscreen.
if (mode == GSVSyncMode::Mailbox && m_is_exclusive_fullscreen)
{ {
DXGI_SWAP_CHAIN_DESC desc; WARNING_LOG("Using FIFO instead of Mailbox vsync due to exclusive fullscreen.");
if (SUCCEEDED(m_swap_chain->GetDesc(&desc)) && desc.BufferDesc.RefreshRate.Numerator > 0 && mode = GSVSyncMode::FIFO;
desc.BufferDesc.RefreshRate.Denominator > 0) }
if (m_vsync_mode == mode)
return;
const u32 old_buffer_count = GetSwapChainBufferCount();
m_vsync_mode = mode;
if (!m_swap_chain)
return;
if (GetSwapChainBufferCount() != old_buffer_count)
{ {
DevCon.WriteLn( DestroySwapChain();
"using fs rr: %u %u", desc.BufferDesc.RefreshRate.Numerator, desc.BufferDesc.RefreshRate.Denominator); if (!CreateSwapChain())
*refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) / pxFailRel("Failed to recreate swap chain after vsync change.");
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
return true;
} }
} }
return GSDevice::GetHostRefreshRate(refresh_rate); u32 GSDevice11::GetSwapChainBufferCount() const
}
void GSDevice11::SetVSyncEnabled(bool enabled)
{ {
m_vsync_enabled = enabled; // With vsync off, we only need two buffers. Same for blocking vsync.
// With triple buffering, we need three.
return (m_vsync_mode == GSVSyncMode::Mailbox) ? 3 : 2;
} }
bool GSDevice11::CreateSwapChain() bool GSDevice11::CreateSwapChain()
@ -655,6 +665,13 @@ bool GSDevice11::CreateSwapChain()
D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width, D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width,
fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode, fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode,
fullscreen_output.put()); fullscreen_output.put());
// Using mailbox-style no-allow-tearing causes tearing in exclusive fullscreen.
if (m_vsync_mode == GSVSyncMode::Mailbox && m_is_exclusive_fullscreen)
{
WARNING_LOG("Using FIFO instead of Mailbox vsync due to exclusive fullscreen.");
m_vsync_mode = GSVSyncMode::FIFO;
}
} }
else else
{ {
@ -668,7 +685,7 @@ bool GSDevice11::CreateSwapChain()
swap_chain_desc.Height = static_cast<u32>(client_rc.bottom - client_rc.top); swap_chain_desc.Height = static_cast<u32>(client_rc.bottom - client_rc.top);
swap_chain_desc.Format = swap_chain_format; swap_chain_desc.Format = swap_chain_format;
swap_chain_desc.SampleDesc.Count = 1; swap_chain_desc.SampleDesc.Count = 1;
swap_chain_desc.BufferCount = 3; swap_chain_desc.BufferCount = GetSwapChainBufferCount();
swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swap_chain_desc.SwapEffect = swap_chain_desc.SwapEffect =
m_using_flip_model_swap_chain ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_DISCARD; m_using_flip_model_swap_chain ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_DISCARD;
@ -791,10 +808,6 @@ bool GSDevice11::CreateSwapChainRTV()
m_window_info.surface_refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) / m_window_info.surface_refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) /
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator); static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
} }
else
{
m_window_info.surface_refresh_rate = 0.0f;
}
} }
return true; return true;
@ -928,7 +941,7 @@ GSDevice::PresentResult GSDevice11::BeginPresent(bool frame_skip)
// This blows our our GPU usage number considerably, so read the timestamp before the final blit // This blows our our GPU usage number considerably, so read the timestamp before the final blit
// in this configuration. It does reduce accuracy a little, but better than seeing 100% all of // in this configuration. It does reduce accuracy a little, but better than seeing 100% all of
// the time, when it's more like a couple of percent. // the time, when it's more like a couple of percent.
if (m_vsync_enabled && m_gpu_timing_enabled) if (m_vsync_mode == GSVSyncMode::FIFO && m_gpu_timing_enabled)
PopTimestampQuery(); PopTimestampQuery();
m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), s_present_clear_color.data()); m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), s_present_clear_color.data());
@ -957,13 +970,12 @@ void GSDevice11::EndPresent()
RenderImGui(); RenderImGui();
// See note in BeginPresent() for why it's conditional on vsync-off. // See note in BeginPresent() for why it's conditional on vsync-off.
if (!m_vsync_enabled && m_gpu_timing_enabled) if (m_vsync_mode != GSVSyncMode::FIFO && m_gpu_timing_enabled)
PopTimestampQuery(); PopTimestampQuery();
if (!m_vsync_enabled && m_using_allow_tearing) const UINT sync_interval = static_cast<UINT>(m_vsync_mode == GSVSyncMode::FIFO);
m_swap_chain->Present(0, DXGI_PRESENT_ALLOW_TEARING); const UINT flags = (m_vsync_mode == GSVSyncMode::Disabled && m_using_allow_tearing) ? DXGI_PRESENT_ALLOW_TEARING : 0;
else m_swap_chain->Present(sync_interval, flags);
m_swap_chain->Present(static_cast<UINT>(m_vsync_enabled), 0);
if (m_gpu_timing_enabled) if (m_gpu_timing_enabled)
KickTimestampQuery(); KickTimestampQuery();

View File

@ -93,6 +93,7 @@ private:
void SetFeatures(IDXGIAdapter1* adapter); void SetFeatures(IDXGIAdapter1* adapter);
int GetMaxTextureSize() const; int GetMaxTextureSize() const;
u32 GetSwapChainBufferCount() const;
bool CreateSwapChain(); bool CreateSwapChain();
bool CreateSwapChainRTV(); bool CreateSwapChainRTV();
void DestroySwapChain(); void DestroySwapChain();
@ -259,7 +260,7 @@ public:
__fi ID3D11Device1* GetD3DDevice() const { return m_dev.get(); } __fi ID3D11Device1* GetD3DDevice() const { return m_dev.get(); }
__fi ID3D11DeviceContext1* GetD3DContext() const { return m_ctx.get(); } __fi ID3D11DeviceContext1* GetD3DContext() const { return m_ctx.get(); }
bool Create() override; bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override; void Destroy() override;
RenderAPI GetRenderAPI() const override; RenderAPI GetRenderAPI() const override;
@ -271,9 +272,7 @@ public:
void DestroySurface() override; void DestroySurface() override;
std::string GetDriverInfo() const override; std::string GetDriverInfo() const override;
bool GetHostRefreshRate(float* refresh_rate) override; void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
void SetVSyncEnabled(bool enabled) override;
PresentResult BeginPresent(bool frame_skip) override; PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override; void EndPresent() override;

View File

@ -676,9 +676,9 @@ bool GSDevice12::HasSurface() const
return static_cast<bool>(m_swap_chain); return static_cast<bool>(m_swap_chain);
} }
bool GSDevice12::Create() bool GSDevice12::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
if (!GSDevice::Create()) if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false; return false;
if (!CreateDevice()) if (!CreateDevice())
@ -755,28 +755,38 @@ void GSDevice12::Destroy()
DestroyResources(); DestroyResources();
} }
bool GSDevice12::GetHostRefreshRate(float* refresh_rate) void GSDevice12::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
if (m_swap_chain && m_is_exclusive_fullscreen) m_allow_present_throttle = allow_present_throttle;
// Using mailbox-style no-allow-tearing causes tearing in exclusive fullscreen.
if (mode == GSVSyncMode::Mailbox && m_is_exclusive_fullscreen)
{ {
DXGI_SWAP_CHAIN_DESC desc; WARNING_LOG("Using FIFO instead of Mailbox vsync due to exclusive fullscreen.");
if (SUCCEEDED(m_swap_chain->GetDesc(&desc)) && desc.BufferDesc.RefreshRate.Numerator > 0 && mode = GSVSyncMode::FIFO;
desc.BufferDesc.RefreshRate.Denominator > 0) }
if (m_vsync_mode == mode)
return;
const u32 old_buffer_count = GetSwapChainBufferCount();
m_vsync_mode = mode;
if (!m_swap_chain)
return;
if (GetSwapChainBufferCount() != old_buffer_count)
{ {
DevCon.WriteLn( DestroySwapChain();
"using fs rr: %u %u", desc.BufferDesc.RefreshRate.Numerator, desc.BufferDesc.RefreshRate.Denominator); if (!CreateSwapChain())
*refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) / pxFailRel("Failed to recreate swap chain after vsync change.");
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
return true;
} }
} }
return GSDevice::GetHostRefreshRate(refresh_rate); u32 GSDevice12::GetSwapChainBufferCount() const
}
void GSDevice12::SetVSyncEnabled(bool enabled)
{ {
m_vsync_enabled = enabled; // With vsync off, we only need two buffers. Same for blocking vsync.
// With triple buffering, we need three.
return (m_vsync_mode == GSVSyncMode::Mailbox) ? 3 : 2;
} }
bool GSDevice12::CreateSwapChain() bool GSDevice12::CreateSwapChain()
@ -801,6 +811,13 @@ bool GSDevice12::CreateSwapChain()
D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width, D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width,
fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode, fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode,
fullscreen_output.put()); fullscreen_output.put());
// Using mailbox-style no-allow-tearing causes tearing in exclusive fullscreen.
if (m_vsync_mode == GSVSyncMode::Mailbox && m_is_exclusive_fullscreen)
{
WARNING_LOG("Using FIFO instead of Mailbox vsync due to exclusive fullscreen.");
m_vsync_mode = GSVSyncMode::FIFO;
}
} }
else else
{ {
@ -812,7 +829,7 @@ bool GSDevice12::CreateSwapChain()
swap_chain_desc.Height = static_cast<u32>(client_rc.bottom - client_rc.top); swap_chain_desc.Height = static_cast<u32>(client_rc.bottom - client_rc.top);
swap_chain_desc.Format = swap_chain_format; swap_chain_desc.Format = swap_chain_format;
swap_chain_desc.SampleDesc.Count = 1; swap_chain_desc.SampleDesc.Count = 1;
swap_chain_desc.BufferCount = 3; swap_chain_desc.BufferCount = GetSwapChainBufferCount();
swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
@ -934,10 +951,6 @@ bool GSDevice12::CreateSwapChainRTV()
m_window_info.surface_refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) / m_window_info.surface_refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) /
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator); static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
} }
else
{
m_window_info.surface_refresh_rate = 0.0f;
}
} }
m_current_swap_chain_buffer = 0; m_current_swap_chain_buffer = 0;
@ -1109,10 +1122,9 @@ void GSDevice12::EndPresent()
return; return;
} }
if (!m_vsync_enabled && m_using_allow_tearing) const UINT sync_interval = static_cast<UINT>(m_vsync_mode == GSVSyncMode::FIFO);
m_swap_chain->Present(0, DXGI_PRESENT_ALLOW_TEARING); const UINT flags = (m_vsync_mode == GSVSyncMode::Disabled && m_using_allow_tearing) ? DXGI_PRESENT_ALLOW_TEARING : 0;
else m_swap_chain->Present(sync_interval, flags);
m_swap_chain->Present(static_cast<UINT>(m_vsync_enabled), 0);
InvalidateCachedState(); InvalidateCachedState();
} }

View File

@ -341,6 +341,7 @@ private:
void LookupNativeFormat(GSTexture::Format format, DXGI_FORMAT* d3d_format, DXGI_FORMAT* srv_format, void LookupNativeFormat(GSTexture::Format format, DXGI_FORMAT* d3d_format, DXGI_FORMAT* srv_format,
DXGI_FORMAT* rtv_format, DXGI_FORMAT* dsv_format) const; DXGI_FORMAT* rtv_format, DXGI_FORMAT* dsv_format) const;
u32 GetSwapChainBufferCount() const;
bool CreateSwapChain(); bool CreateSwapChain();
bool CreateSwapChainRTV(); bool CreateSwapChainRTV();
void DestroySwapChainRTVs(); void DestroySwapChainRTVs();
@ -398,7 +399,7 @@ public:
RenderAPI GetRenderAPI() const override; RenderAPI GetRenderAPI() const override;
bool HasSurface() const override; bool HasSurface() const override;
bool Create() override; bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override; void Destroy() override;
bool UpdateWindow() override; bool UpdateWindow() override;
@ -407,9 +408,7 @@ public:
void DestroySurface() override; void DestroySurface() override;
std::string GetDriverInfo() const override; std::string GetDriverInfo() const override;
bool GetHostRefreshRate(float* refresh_rate) override; void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
void SetVSyncEnabled(bool enabled) override;
PresentResult BeginPresent(bool frame_skip) override; PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override; void EndPresent() override;

View File

@ -373,7 +373,7 @@ public:
MRCOwned<id<MTLFunction>> LoadShader(NSString* name); MRCOwned<id<MTLFunction>> LoadShader(NSString* name);
MRCOwned<id<MTLRenderPipelineState>> MakePipeline(MTLRenderPipelineDescriptor* desc, id<MTLFunction> vertex, id<MTLFunction> fragment, NSString* name); MRCOwned<id<MTLRenderPipelineState>> MakePipeline(MTLRenderPipelineDescriptor* desc, id<MTLFunction> vertex, id<MTLFunction> fragment, NSString* name);
MRCOwned<id<MTLComputePipelineState>> MakeComputePipeline(id<MTLFunction> compute, NSString* name); MRCOwned<id<MTLComputePipelineState>> MakeComputePipeline(id<MTLFunction> compute, NSString* name);
bool Create() override; bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override; void Destroy() override;
void AttachSurfaceOnMainThread(); void AttachSurfaceOnMainThread();
@ -392,9 +392,7 @@ public:
PresentResult BeginPresent(bool frame_skip) override; PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override; void EndPresent() override;
void SetVSyncEnabled(bool enabled) override; void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
bool GetHostRefreshRate(float* refresh_rate) override;
bool SetGPUTimingEnabled(bool enabled) override; bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override; float GetAndResetAccumulatedGPUTime() override;

View File

@ -822,9 +822,9 @@ static MRCOwned<id<MTLSamplerState>> CreateSampler(id<MTLDevice> dev, GSHWDrawCo
return ret; return ret;
} }
bool GSDeviceMTL::Create() bool GSDeviceMTL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ @autoreleasepool { { @autoreleasepool {
if (!GSDevice::Create()) if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false; return false;
NSString* ns_adapter_name = [NSString stringWithUTF8String:GSConfig.Adapter.c_str()]; NSString* ns_adapter_name = [NSString stringWithUTF8String:GSConfig.Adapter.c_str()];
@ -877,7 +877,10 @@ bool GSDeviceMTL::Create()
{ {
AttachSurfaceOnMainThread(); AttachSurfaceOnMainThread();
}); });
[m_layer setDisplaySyncEnabled:m_vsync_enabled];
// Metal does not support mailbox.
m_vsync_mode = (m_vsync_mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : m_vsync_mode;
[m_layer setDisplaySyncEnabled:m_vsync_mode == GSVSyncMode::FIFO];
} }
else else
{ {
@ -1305,7 +1308,7 @@ void GSDeviceMTL::EndPresent()
if (m_current_drawable) if (m_current_drawable)
{ {
const bool use_present_drawable = m_use_present_drawable == UsePresentDrawable::Always || const bool use_present_drawable = m_use_present_drawable == UsePresentDrawable::Always ||
(m_use_present_drawable == UsePresentDrawable::IfVsync && m_vsync_enabled); (m_use_present_drawable == UsePresentDrawable::IfVsync && m_vsync_mode == GSVSyncMode::FIFO);
if (use_present_drawable) if (use_present_drawable)
[m_current_render_cmdbuf presentDrawable:m_current_drawable]; [m_current_render_cmdbuf presentDrawable:m_current_drawable];
@ -1367,31 +1370,15 @@ void GSDeviceMTL::EndPresent()
} }
}} }}
void GSDeviceMTL::SetVSyncEnabled(bool enabled) void GSDeviceMTL::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
if (m_vsync_enabled == enabled) m_allow_present_throttle = allow_present_throttle;
if (m_vsync_mode == mode)
return; return;
[m_layer setDisplaySyncEnabled:enabled]; m_vsync_mode = (mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : mode;
m_vsync_enabled = enabled; [m_layer setDisplaySyncEnabled:m_vsync_mode == GSVSyncMode::FIFO];
}
bool GSDeviceMTL::GetHostRefreshRate(float* refresh_rate)
{
OnMainThread([this, refresh_rate]
{
u32 did = [[[[[m_view window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
{
*refresh_rate = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
}
else
{
*refresh_rate = 0;
}
});
return *refresh_rate != 0;
} }
bool GSDeviceMTL::SetGPUTimingEnabled(bool enabled) bool GSDeviceMTL::SetGPUTimingEnabled(bool enabled)

View File

@ -146,18 +146,20 @@ bool GSDeviceOGL::HasSurface() const
return m_window_info.type != WindowInfo::Type::Surfaceless; return m_window_info.type != WindowInfo::Type::Surfaceless;
} }
void GSDeviceOGL::SetVSyncEnabled(bool enabled) void GSDeviceOGL::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
if (m_vsync_enabled == enabled) m_allow_present_throttle = allow_present_throttle;
if (m_vsync_mode == mode)
return; return;
m_vsync_enabled = enabled; m_vsync_mode = mode;
SetSwapInterval(); SetSwapInterval();
} }
bool GSDeviceOGL::Create() bool GSDeviceOGL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
if (!GSDevice::Create()) if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false; return false;
// GL is a pain and needs the window super early to create the context. // GL is a pain and needs the window super early to create the context.
@ -772,8 +774,12 @@ void GSDeviceOGL::SetSwapInterval()
if (m_window_info.type == WindowInfo::Type::Surfaceless) if (m_window_info.type == WindowInfo::Type::Surfaceless)
return; return;
// OpenGL does not support mailbox, only effectively FIFO.
// Fall back to manual throttling in this case.
m_vsync_mode = (m_vsync_mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : m_vsync_mode;
// Window framebuffer has to be bound to call SetSwapInterval. // Window framebuffer has to be bound to call SetSwapInterval.
const s32 interval = m_vsync_enabled ? (m_gl_context->SupportsNegativeSwapInterval() ? -1 : 1) : 0; const s32 interval = static_cast<s32>(m_vsync_mode == GSVSyncMode::FIFO);
GLint current_fbo = 0; GLint current_fbo = 0;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &current_fbo); glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &current_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

View File

@ -283,7 +283,7 @@ public:
RenderAPI GetRenderAPI() const override; RenderAPI GetRenderAPI() const override;
bool HasSurface() const override; bool HasSurface() const override;
bool Create() override; bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override; void Destroy() override;
bool UpdateWindow() override; bool UpdateWindow() override;
@ -292,7 +292,7 @@ public:
void DestroySurface() override; void DestroySurface() override;
std::string GetDriverInfo() const override; std::string GetDriverInfo() const override;
void SetVSyncEnabled(bool enabled) override; void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
PresentResult BeginPresent(bool frame_skip) override; PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override; void EndPresent() override;

View File

@ -2088,9 +2088,9 @@ bool GSDeviceVK::HasSurface() const
return static_cast<bool>(m_swap_chain); return static_cast<bool>(m_swap_chain);
} }
bool GSDeviceVK::Create() bool GSDeviceVK::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ {
if (!GSDevice::Create()) if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false; return false;
if (!CreateDeviceAndSwapChain()) if (!CreateDeviceAndSwapChain())
@ -2222,9 +2222,10 @@ bool GSDeviceVK::UpdateWindow()
return false; return false;
} }
m_swap_chain = VKSwapChain::Create(m_window_info, surface, m_vsync_enabled, VkPresentModeKHR present_mode;
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl)); if (!VKSwapChain::SelectPresentMode(surface, &m_vsync_mode, &present_mode) ||
if (!m_swap_chain) !(m_swap_chain = VKSwapChain::Create(m_window_info, surface, present_mode,
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl))))
{ {
Console.Error("Failed to create swap chain"); Console.Error("Failed to create swap chain");
VKSwapChain::DestroyVulkanSurface(m_instance, &m_window_info, surface); VKSwapChain::DestroyVulkanSurface(m_instance, &m_window_info, surface);
@ -2297,27 +2298,36 @@ std::string GSDeviceVK::GetDriverInfo() const
return ret; return ret;
} }
void GSDeviceVK::SetVSyncEnabled(bool enabled) void GSDeviceVK::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
if (!m_swap_chain || m_vsync_enabled == enabled) m_allow_present_throttle = allow_present_throttle;
if (!m_swap_chain)
{ {
m_vsync_enabled = enabled; // For when it is re-created.
m_vsync_mode = mode;
return; return;
} }
// This swap chain should not be used by the current buffer, thus safe to destroy. VkPresentModeKHR present_mode;
WaitForGPUIdle(); if (!VKSwapChain::SelectPresentMode(m_swap_chain->GetSurface(), &mode, &present_mode))
if (!m_swap_chain->SetVSyncEnabled(enabled))
{ {
// Try switching back to the old mode.. ERROR_LOG("Ignoring vsync mode change.");
if (!m_swap_chain->SetVSyncEnabled(m_vsync_enabled)) return;
{
pxFailRel("Failed to reset old vsync mode after failure");
m_swap_chain.reset();
}
} }
m_vsync_enabled = enabled; // Actually changed? If using a fallback, it might not have.
if (m_vsync_mode == mode)
return;
m_vsync_mode = mode;
// This swap chain should not be used by the current buffer, thus safe to destroy.
WaitForGPUIdle();
if (!m_swap_chain->SetPresentMode(present_mode))
{
pxFailRel("Failed to update swap chain present mode.");
m_swap_chain.reset();
}
} }
GSDevice::PresentResult GSDeviceVK::BeginPresent(bool frame_skip) GSDevice::PresentResult GSDeviceVK::BeginPresent(bool frame_skip)
@ -2616,11 +2626,12 @@ bool GSDeviceVK::CreateDeviceAndSwapChain()
if (surface != VK_NULL_HANDLE) if (surface != VK_NULL_HANDLE)
{ {
m_swap_chain = VKSwapChain::Create(m_window_info, surface, m_vsync_enabled, VkPresentModeKHR present_mode;
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl)); if (!VKSwapChain::SelectPresentMode(surface, &m_vsync_mode, &present_mode) ||
if (!m_swap_chain) !(m_swap_chain = VKSwapChain::Create(m_window_info, surface, present_mode,
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl))))
{ {
Console.Error("Failed to create swap chain"); ERROR_LOG("Failed to create swap chain");
return false; return false;
} }

View File

@ -515,7 +515,7 @@ public:
RenderAPI GetRenderAPI() const override; RenderAPI GetRenderAPI() const override;
bool HasSurface() const override; bool HasSurface() const override;
bool Create() override; bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override; void Destroy() override;
bool UpdateWindow() override; bool UpdateWindow() override;
@ -524,7 +524,7 @@ public:
void DestroySurface() override; void DestroySurface() override;
std::string GetDriverInfo() const override; std::string GetDriverInfo() const override;
void SetVSyncEnabled(bool enabled) override; void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
PresentResult BeginPresent(bool frame_skip) override; PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override; void EndPresent() override;

View File

@ -18,12 +18,12 @@
#include <X11/Xlib.h> #include <X11/Xlib.h>
#endif #endif
VKSwapChain::VKSwapChain( VKSwapChain::VKSwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode,
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control) std::optional<bool> exclusive_fullscreen_control)
: m_window_info(wi) : m_window_info(wi)
, m_surface(surface) , m_surface(surface)
, m_present_mode(present_mode)
, m_exclusive_fullscreen_control(exclusive_fullscreen_control) , m_exclusive_fullscreen_control(exclusive_fullscreen_control)
, m_vsync_enabled(vsync)
{ {
} }
@ -135,11 +135,11 @@ void VKSwapChain::DestroyVulkanSurface(VkInstance instance, WindowInfo* wi, VkSu
#endif #endif
} }
std::unique_ptr<VKSwapChain> VKSwapChain::Create( std::unique_ptr<VKSwapChain> VKSwapChain::Create(const WindowInfo& wi, VkSurfaceKHR surface,
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control) VkPresentModeKHR present_mode, std::optional<bool> exclusive_fullscreen_control)
{ {
std::unique_ptr<VKSwapChain> swap_chain = std::unique_ptr<VKSwapChain> swap_chain =
std::unique_ptr<VKSwapChain>(new VKSwapChain(wi, surface, vsync, exclusive_fullscreen_control)); std::unique_ptr<VKSwapChain>(new VKSwapChain(wi, surface, present_mode, exclusive_fullscreen_control));
if (!swap_chain->CreateSwapChain()) if (!swap_chain->CreateSwapChain())
return nullptr; return nullptr;
@ -227,7 +227,7 @@ static const char* PresentModeToString(VkPresentModeKHR mode)
} }
} }
std::optional<VkPresentModeKHR> VKSwapChain::SelectPresentMode(VkSurfaceKHR surface, VkPresentModeKHR requested_mode) bool VKSwapChain::SelectPresentMode(VkSurfaceKHR surface, GSVSyncMode* vsync_mode, VkPresentModeKHR* present_mode)
{ {
VkResult res; VkResult res;
u32 mode_count; u32 mode_count;
@ -236,7 +236,7 @@ std::optional<VkPresentModeKHR> VKSwapChain::SelectPresentMode(VkSurfaceKHR surf
if (res != VK_SUCCESS || mode_count == 0) if (res != VK_SUCCESS || mode_count == 0)
{ {
LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceSurfaceFormatsKHR failed: "); LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceSurfaceFormatsKHR failed: ");
return std::nullopt; return false;
} }
std::vector<VkPresentModeKHR> present_modes(mode_count); std::vector<VkPresentModeKHR> present_modes(mode_count);
@ -245,48 +245,70 @@ std::optional<VkPresentModeKHR> VKSwapChain::SelectPresentMode(VkSurfaceKHR surf
pxAssert(res == VK_SUCCESS); pxAssert(res == VK_SUCCESS);
// Checks if a particular mode is supported, if it is, returns that mode. // Checks if a particular mode is supported, if it is, returns that mode.
auto CheckForMode = [&present_modes](VkPresentModeKHR check_mode) { const auto CheckForMode = [&present_modes](VkPresentModeKHR check_mode) {
auto it = std::find_if(present_modes.begin(), present_modes.end(), auto it = std::find_if(present_modes.begin(), present_modes.end(),
[check_mode](VkPresentModeKHR mode) { return check_mode == mode; }); [check_mode](VkPresentModeKHR mode) { return check_mode == mode; });
return it != present_modes.end(); return it != present_modes.end();
}; };
// Use preferred mode if available. switch (*vsync_mode)
VkPresentModeKHR selected_mode;
if (CheckForMode(requested_mode))
{ {
selected_mode = requested_mode; case GSVSyncMode::Disabled:
{
// Prefer immediate > mailbox > fifo.
if (CheckForMode(VK_PRESENT_MODE_IMMEDIATE_KHR))
{
*present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
} }
else if (requested_mode == VK_PRESENT_MODE_IMMEDIATE_KHR && CheckForMode(VK_PRESENT_MODE_MAILBOX_KHR)) else if (CheckForMode(VK_PRESENT_MODE_MAILBOX_KHR))
{ {
// Prefer mailbox over FIFO for vsync-off, since we don't want to block. WARNING_LOG("Immediate not supported for vsync-disabled, using mailbox.");
selected_mode = VK_PRESENT_MODE_MAILBOX_KHR; *present_mode = VK_PRESENT_MODE_MAILBOX_KHR;
*vsync_mode = GSVSyncMode::Mailbox;
} }
else else
{ {
// Fallback to FIFO if we we can't use mailbox. This should never fail, FIFO is mandated. WARNING_LOG("Mailbox not supported for vsync-disabled, using FIFO.");
selected_mode = VK_PRESENT_MODE_FIFO_KHR; *present_mode = VK_PRESENT_MODE_FIFO_KHR;
*vsync_mode = GSVSyncMode::FIFO;
}
}
break;
case GSVSyncMode::FIFO:
{
// FIFO is always available.
*present_mode = VK_PRESENT_MODE_FIFO_KHR;
}
break;
case GSVSyncMode::Mailbox:
{
// Mailbox > fifo.
if (CheckForMode(VK_PRESENT_MODE_MAILBOX_KHR))
{
*present_mode = VK_PRESENT_MODE_MAILBOX_KHR;
}
else
{
WARNING_LOG("Mailbox not supported for vsync-mailbox, using FIFO.");
*present_mode = VK_PRESENT_MODE_FIFO_KHR;
*vsync_mode = GSVSyncMode::FIFO;
}
}
break;
jNO_DEFAULT
} }
DevCon.WriteLn("(SwapChain) Preferred present mode: %s, selected: %s", PresentModeToString(requested_mode), return true;
PresentModeToString(selected_mode));
return selected_mode;
} }
bool VKSwapChain::CreateSwapChain() bool VKSwapChain::CreateSwapChain()
{ {
// Select swap chain format and present mode // Select swap chain format
std::optional<VkSurfaceFormatKHR> surface_format = SelectSurfaceFormat(m_surface); std::optional<VkSurfaceFormatKHR> surface_format = SelectSurfaceFormat(m_surface);
if (!surface_format.has_value())
// Prefer mailbox if not syncing to host refresh, because that requires "real" vsync.
const VkPresentModeKHR requested_mode =
m_vsync_enabled ? (VMManager::IsUsingVSyncForTiming() ?
VK_PRESENT_MODE_FIFO_KHR :
VK_PRESENT_MODE_MAILBOX_KHR) :
VK_PRESENT_MODE_IMMEDIATE_KHR;
std::optional<VkPresentModeKHR> present_mode = SelectPresentMode(m_surface, requested_mode);
if (!surface_format.has_value() || !present_mode.has_value())
return false; return false;
// Look up surface properties to determine image count and dimensions // Look up surface properties to determine image count and dimensions
@ -299,12 +321,12 @@ bool VKSwapChain::CreateSwapChain()
return false; return false;
} }
// Select number of images in swap chain, we prefer one buffer in the background to work on // Select number of images in swap chain, we prefer one buffer in the background to work on in triple-buffered mode.
u32 image_count = std::max(surface_capabilities.minImageCount + 1u, 2u);
// maxImageCount can be zero, in which case there isn't an upper limit on the number of buffers. // maxImageCount can be zero, in which case there isn't an upper limit on the number of buffers.
if (surface_capabilities.maxImageCount > 0) u32 image_count = std::clamp<u32>(
image_count = std::min(image_count, surface_capabilities.maxImageCount); (m_present_mode == VK_PRESENT_MODE_MAILBOX_KHR) ? 3 : 2, surface_capabilities.minImageCount,
(surface_capabilities.maxImageCount == 0) ? std::numeric_limits<u32>::max() : surface_capabilities.maxImageCount);
DEV_LOG("Creating a swap chain with {} images in present mode {}", image_count, PresentModeToString(m_present_mode));
// Determine the dimensions of the swap chain. Values of -1 indicate the size we specify here // Determine the dimensions of the swap chain. Values of -1 indicate the size we specify here
// determines window size? // determines window size?
@ -348,7 +370,7 @@ bool VKSwapChain::CreateSwapChain()
// Now we can actually create the swap chain // Now we can actually create the swap chain
VkSwapchainCreateInfoKHR swap_chain_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, nullptr, 0, m_surface, VkSwapchainCreateInfoKHR swap_chain_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, nullptr, 0, m_surface,
image_count, surface_format->format, surface_format->colorSpace, size, 1u, image_usage, image_count, surface_format->format, surface_format->colorSpace, size, 1u, image_usage,
VK_SHARING_MODE_EXCLUSIVE, 0, nullptr, transform, alpha, present_mode.value(), VK_TRUE, old_swap_chain}; VK_SHARING_MODE_EXCLUSIVE, 0, nullptr, transform, alpha, m_present_mode, VK_TRUE, old_swap_chain};
std::array<uint32_t, 2> indices = {{ std::array<uint32_t, 2> indices = {{
GSDeviceVK::GetInstance()->GetGraphicsQueueFamilyIndex(), GSDeviceVK::GetInstance()->GetGraphicsQueueFamilyIndex(),
GSDeviceVK::GetInstance()->GetPresentQueueFamilyIndex(), GSDeviceVK::GetInstance()->GetPresentQueueFamilyIndex(),
@ -405,7 +427,6 @@ bool VKSwapChain::CreateSwapChain()
m_window_info.surface_width = std::max(1u, size.width); m_window_info.surface_width = std::max(1u, size.width);
m_window_info.surface_height = std::max(1u, size.height); m_window_info.surface_height = std::max(1u, size.height);
m_actual_present_mode = present_mode.value();
// Get and create images. // Get and create images.
pxAssert(m_images.empty()); pxAssert(m_images.empty());
@ -550,15 +571,15 @@ bool VKSwapChain::ResizeSwapChain(u32 new_width, u32 new_height, float new_scale
return true; return true;
} }
bool VKSwapChain::SetVSyncEnabled(bool enabled) bool VKSwapChain::SetPresentMode(VkPresentModeKHR present_mode)
{ {
if (m_vsync_enabled == enabled) if (m_present_mode == present_mode)
return true; return true;
m_vsync_enabled = enabled; m_present_mode = present_mode;
// Recreate the swap chain with the new present mode. // Recreate the swap chain with the new present mode.
DevCon.WriteLn("Recreating swap chain to change present mode."); INFO_LOG("Recreating swap chain to change present mode.");
DestroySwapChainImages(); DestroySwapChainImages();
if (!CreateSwapChain()) if (!CreateSwapChain())
{ {

View File

@ -24,8 +24,11 @@ public:
static void DestroyVulkanSurface(VkInstance instance, WindowInfo* wi, VkSurfaceKHR surface); static void DestroyVulkanSurface(VkInstance instance, WindowInfo* wi, VkSurfaceKHR surface);
// Create a new swap chain from a pre-existing surface. // Create a new swap chain from a pre-existing surface.
static std::unique_ptr<VKSwapChain> Create( static std::unique_ptr<VKSwapChain> Create(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode,
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control); std::optional<bool> exclusive_fullscreen_control);
/// Returns the Vulkan present mode for a given vsync mode that is compatible with this device.
static bool SelectPresentMode(VkSurfaceKHR surface, GSVSyncMode* vsync_mode, VkPresentModeKHR* present_mode);
__fi VkSurfaceKHR GetSurface() const { return m_surface; } __fi VkSurfaceKHR GetSurface() const { return m_surface; }
__fi VkSwapchainKHR GetSwapChain() const { return m_swap_chain; } __fi VkSwapchainKHR GetSwapChain() const { return m_swap_chain; }
@ -56,12 +59,9 @@ public:
return &m_semaphores[m_current_semaphore].rendering_finished_semaphore; return &m_semaphores[m_current_semaphore].rendering_finished_semaphore;
} }
// Returns true if the current present mode is synchronizing (adaptive or hard). // Returns true if the current present mode is synchronizing.
__fi bool IsPresentModeSynchronizing() const __fi bool IsPresentModeSynchronizing() const { return (m_present_mode == VK_PRESENT_MODE_FIFO_KHR); }
{ __fi VkPresentModeKHR GetPresentMode() const { return m_present_mode; }
return (m_actual_present_mode == VK_PRESENT_MODE_FIFO_KHR ||
m_actual_present_mode == VK_PRESENT_MODE_FIFO_RELAXED_KHR);
}
VkFormat GetTextureFormat() const; VkFormat GetTextureFormat() const;
VkResult AcquireNextImage(); VkResult AcquireNextImage();
@ -71,14 +71,13 @@ public:
bool ResizeSwapChain(u32 new_width = 0, u32 new_height = 0, float new_scale = 1.0f); bool ResizeSwapChain(u32 new_width = 0, u32 new_height = 0, float new_scale = 1.0f);
// Change vsync enabled state. This may fail as it causes a swapchain recreation. // Change vsync enabled state. This may fail as it causes a swapchain recreation.
bool SetVSyncEnabled(bool enabled); bool SetPresentMode(VkPresentModeKHR present_mode);
private: private:
VKSwapChain( VKSwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode,
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control); std::optional<bool> exclusive_fullscreen_control);
static std::optional<VkSurfaceFormatKHR> SelectSurfaceFormat(VkSurfaceKHR surface); static std::optional<VkSurfaceFormatKHR> SelectSurfaceFormat(VkSurfaceKHR surface);
static std::optional<VkPresentModeKHR> SelectPresentMode(VkSurfaceKHR surface, VkPresentModeKHR requested_mode);
bool CreateSwapChain(); bool CreateSwapChain();
void DestroySwapChain(); void DestroySwapChain();
@ -102,11 +101,11 @@ private:
std::vector<std::unique_ptr<GSTextureVK>> m_images; std::vector<std::unique_ptr<GSTextureVK>> m_images;
std::vector<ImageSemaphores> m_semaphores; std::vector<ImageSemaphores> m_semaphores;
VkPresentModeKHR m_actual_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
u32 m_current_image = 0; u32 m_current_image = 0;
u32 m_current_semaphore = 0; u32 m_current_semaphore = 0;
VkPresentModeKHR m_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
std::optional<VkResult> m_image_acquire_result; std::optional<VkResult> m_image_acquire_result;
std::optional<bool> m_exclusive_fullscreen_control; std::optional<bool> m_exclusive_fullscreen_control;
bool m_vsync_enabled = false;
}; };

View File

@ -3345,9 +3345,16 @@ void FullscreenUI::DrawEmulationSettingsPage()
SetSettingsChanged(bsi); SetSettingsChanged(bsi);
} }
DrawToggleSetting(bsi, FSUI_CSTR("Scale To Host Refresh Rate"), DrawToggleSetting(bsi, FSUI_CSTR("Vertical Sync (VSync)"), FSUI_CSTR("Synchronizes frame presentation with host refresh."),
"EmuCore/GS", "VsyncEnable", false);
DrawToggleSetting(bsi, FSUI_CSTR("Sync to Host Refresh Rate"),
FSUI_CSTR("Speeds up emulation so that the guest refresh rate matches the host."), "EmuCore/GS", "SyncToHostRefreshRate", false); FSUI_CSTR("Speeds up emulation so that the guest refresh rate matches the host."), "EmuCore/GS", "SyncToHostRefreshRate", false);
DrawToggleSetting(bsi, FSUI_CSTR("Use Host VSync Timing"),
FSUI_CSTR("Disables PCSX2's internal frame timing, and uses host vsync instead."), "EmuCore/GS", "UseVSyncForTiming", false,
GetEffectiveBoolSetting(bsi, "EmuCore/GS", "VsyncEnable", false) && GetEffectiveBoolSetting(bsi, "EmuCore/GS", "SyncToHostRefreshRate", false));
EndMenuButtons(); EndMenuButtons();
} }
@ -3598,8 +3605,6 @@ void FullscreenUI::DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_ad
MenuHeading(FSUI_CSTR("Renderer")); MenuHeading(FSUI_CSTR("Renderer"));
DrawStringListSetting(bsi, FSUI_CSTR("Renderer"), FSUI_CSTR("Selects the API used to render the emulated GS."), "EmuCore/GS", DrawStringListSetting(bsi, FSUI_CSTR("Renderer"), FSUI_CSTR("Selects the API used to render the emulated GS."), "EmuCore/GS",
"Renderer", "-1", s_renderer_names, s_renderer_values, std::size(s_renderer_names), true); "Renderer", "-1", s_renderer_names, s_renderer_values, std::size(s_renderer_names), true);
DrawToggleSetting(bsi, FSUI_CSTR("Sync To Host Refresh (VSync)"), FSUI_CSTR("Synchronizes frame presentation with host refresh."),
"EmuCore/GS", "VsyncEnable", false);
MenuHeading(FSUI_CSTR("Display")); MenuHeading(FSUI_CSTR("Display"));
DrawStringListSetting(bsi, FSUI_CSTR("Aspect Ratio"), FSUI_CSTR("Selects the aspect ratio to display the game content at."), DrawStringListSetting(bsi, FSUI_CSTR("Aspect Ratio"), FSUI_CSTR("Selects the aspect ratio to display the game content at."),

View File

@ -165,7 +165,8 @@ void MTGS::ThreadEntryPoint()
// try initializing.. this could fail // try initializing.. this could fail
std::memcpy(RingBuffer.Regs, PS2MEM_GS, sizeof(PS2MEM_GS)); std::memcpy(RingBuffer.Regs, PS2MEM_GS, sizeof(PS2MEM_GS));
const bool opened = GSopen(EmuConfig.GS, EmuConfig.GS.Renderer, RingBuffer.Regs); const bool opened = GSopen(EmuConfig.GS, EmuConfig.GS.Renderer, RingBuffer.Regs,
VMManager::GetEffectiveVSyncMode(), VMManager::ShouldAllowPresentThrottle());
s_open_flag.store(opened, std::memory_order_release); s_open_flag.store(opened, std::memory_order_release);
// notify emu thread that we finished opening (or failed) // notify emu thread that we finished opening (or failed)
@ -936,7 +937,6 @@ void MTGS::ApplySettings()
RunOnGSThread([opts = EmuConfig.GS]() { RunOnGSThread([opts = EmuConfig.GS]() {
GSUpdateConfig(opts); GSUpdateConfig(opts);
GSSetVSyncEnabled(Host::IsVsyncEffectivelyEnabled());
}); });
// We need to synchronize the thread when changing any settings when the download mode // We need to synchronize the thread when changing any settings when the download mode
@ -975,19 +975,16 @@ void MTGS::UpdateDisplayWindow()
}); });
} }
void MTGS::SetVSyncEnabled(bool enabled) void MTGS::SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle)
{ {
pxAssertRel(IsOpen(), "MTGS is running"); pxAssertRel(IsOpen(), "MTGS is running");
RunOnGSThread([enabled]() { RunOnGSThread([mode, allow_present_throttle]() { GSSetVSyncMode(mode, allow_present_throttle); });
INFO_LOG("Vsync is {}", enabled ? "ON" : "OFF");
GSSetVSyncEnabled(enabled);
});
} }
void MTGS::UpdateVSyncEnabled() void MTGS::UpdateVSyncMode()
{ {
SetVSyncEnabled(Host::IsVsyncEffectivelyEnabled()); SetVSyncMode(VMManager::GetEffectiveVSyncMode(), VMManager::ShouldAllowPresentThrottle());
} }
void MTGS::SetSoftwareRendering(bool software, GSInterlaceMode interlace, bool display_message /* = true */) void MTGS::SetSoftwareRendering(bool software, GSInterlaceMode interlace, bool display_message /* = true */)

View File

@ -71,8 +71,8 @@ namespace MTGS
void ApplySettings(); void ApplySettings();
void ResizeDisplayWindow(int width, int height, float scale); void ResizeDisplayWindow(int width, int height, float scale);
void UpdateDisplayWindow(); void UpdateDisplayWindow();
void SetVSyncEnabled(bool enabled); void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle);
void UpdateVSyncEnabled(); void UpdateVSyncMode();
void SetSoftwareRendering(bool software, GSInterlaceMode interlace, bool display_message = true); void SetSoftwareRendering(bool software, GSInterlaceMode interlace, bool display_message = true);
void ToggleSoftwareRendering(); void ToggleSoftwareRendering();
bool SaveMemorySnapshot(u32 window_width, u32 window_height, bool apply_aspect, bool crop_borders, bool SaveMemorySnapshot(u32 window_width, u32 window_height, bool apply_aspect, bool crop_borders,

View File

@ -1494,6 +1494,7 @@ void Pcsx2Config::EmulationSpeedOptions::LoadSave(SettingsWrapper& wrap)
// This was in the wrong place... but we can't change it without breaking existing configs. // This was in the wrong place... but we can't change it without breaking existing configs.
//SettingsWrapBitBool(SyncToHostRefreshRate); //SettingsWrapBitBool(SyncToHostRefreshRate);
SyncToHostRefreshRate = wrap.EntryBitBool("EmuCore/GS", "SyncToHostRefreshRate", SyncToHostRefreshRate, SyncToHostRefreshRate); SyncToHostRefreshRate = wrap.EntryBitBool("EmuCore/GS", "SyncToHostRefreshRate", SyncToHostRefreshRate, SyncToHostRefreshRate);
UseVSyncForTiming = wrap.EntryBitBool("EmuCore/GS", "UseVSyncForTiming", UseVSyncForTiming, UseVSyncForTiming);
} }
bool Pcsx2Config::EmulationSpeedOptions::operator==(const EmulationSpeedOptions& right) const bool Pcsx2Config::EmulationSpeedOptions::operator==(const EmulationSpeedOptions& right) const

View File

@ -136,7 +136,6 @@ namespace VMManager
static float GetTargetSpeedForLimiterMode(LimiterModeType mode); static float GetTargetSpeedForLimiterMode(LimiterModeType mode);
static void ResetFrameLimiter(); static void ResetFrameLimiter();
static double AdjustToHostRefreshRate(float frame_rate, float target_speed);
static void SetTimerResolutionIncreased(bool enabled); static void SetTimerResolutionIncreased(bool enabled);
static void SetHardwareDependentDefaultSettings(SettingsInterface& si); static void SetHardwareDependentDefaultSettings(SettingsInterface& si);
@ -186,6 +185,7 @@ static LimiterModeType s_limiter_mode = LimiterModeType::Nominal;
static s64 s_limiter_ticks_per_frame = 0; static s64 s_limiter_ticks_per_frame = 0;
static u64 s_limiter_frame_start = 0; static u64 s_limiter_frame_start = 0;
static float s_target_speed = 0.0f; static float s_target_speed = 0.0f;
static bool s_target_speed_can_sync_to_host = false;
static bool s_target_speed_synced_to_host = false; static bool s_target_speed_synced_to_host = false;
static bool s_use_vsync_for_timing = false; static bool s_use_vsync_for_timing = false;
@ -2026,34 +2026,6 @@ float VMManager::GetTargetSpeed()
return s_target_speed; return s_target_speed;
} }
double VMManager::AdjustToHostRefreshRate(float frame_rate, float target_speed)
{
if (!EmuConfig.EmulationSpeed.SyncToHostRefreshRate || target_speed != 1.0f)
{
s_target_speed_synced_to_host = false;
s_use_vsync_for_timing = false;
return target_speed;
}
float host_refresh_rate;
if (!GSGetHostRefreshRate(&host_refresh_rate))
{
Console.Warning("Cannot sync to host refresh since the query failed.");
s_target_speed_synced_to_host = false;
s_use_vsync_for_timing = false;
return target_speed;
}
const float ratio = host_refresh_rate / frame_rate;
const bool syncing_to_host = (ratio >= 0.95f && ratio <= 1.05f);
s_target_speed_synced_to_host = syncing_to_host;
s_use_vsync_for_timing = (syncing_to_host && !EmuConfig.GS.SkipDuplicateFrames && EmuConfig.GS.VsyncEnable);
Console.WriteLn("Refresh rate: Host=%fhz Guest=%fhz Ratio=%f - %s %s", host_refresh_rate, frame_rate, ratio,
syncing_to_host ? "can sync" : "can't sync", s_use_vsync_for_timing ? "and using vsync for pacing" : "and using sleep for pacing");
return syncing_to_host ? ratio : target_speed;
}
float VMManager::GetTargetSpeedForLimiterMode(LimiterModeType mode) float VMManager::GetTargetSpeedForLimiterMode(LimiterModeType mode)
{ {
if (EmuConfig.EnableFastBootFastForward && VMManager::Internal::IsFastBootInProgress()) if (EmuConfig.EnableFastBootFastForward && VMManager::Internal::IsFastBootInProgress())
@ -2081,7 +2053,37 @@ float VMManager::GetTargetSpeedForLimiterMode(LimiterModeType mode)
void VMManager::UpdateTargetSpeed() void VMManager::UpdateTargetSpeed()
{ {
const float frame_rate = GetFrameRate(); const float frame_rate = GetFrameRate();
const float target_speed = AdjustToHostRefreshRate(frame_rate, GetTargetSpeedForLimiterMode(s_limiter_mode)); float target_speed = GetTargetSpeedForLimiterMode(s_limiter_mode);
s_target_speed_can_sync_to_host = false;
s_target_speed_synced_to_host = false;
s_use_vsync_for_timing = false;
if (EmuConfig.EmulationSpeed.SyncToHostRefreshRate)
{
// TODO: This is accessing GS thread state.. I _think_ it should be okay, but I still hate it.
// We can at least avoid the query in the first place if we're not using sync to host.
if (const std::optional<float> host_refresh_rate = GSGetHostRefreshRate(); host_refresh_rate.has_value())
{
const float host_to_guest_ratio = host_refresh_rate.value() / frame_rate;
s_target_speed_can_sync_to_host = (host_to_guest_ratio >= 0.95f && host_to_guest_ratio <= 1.05f);
s_target_speed_synced_to_host = (s_target_speed_can_sync_to_host && target_speed == 1.0f);
target_speed = s_target_speed_synced_to_host ? host_to_guest_ratio : target_speed;
s_use_vsync_for_timing = (s_target_speed_synced_to_host && !EmuConfig.GS.SkipDuplicateFrames && EmuConfig.GS.VsyncEnable &&
EmuConfig.EmulationSpeed.UseVSyncForTiming);
Console.WriteLn("Refresh rate: Host=%fhz Guest=%fhz Ratio=%f - %s %s",
host_refresh_rate.value(), frame_rate, host_to_guest_ratio,
s_target_speed_can_sync_to_host ? "can sync" : "can't sync",
s_use_vsync_for_timing ? "and using vsync for pacing" : "and using sleep for pacing");
}
else
{
ERROR_LOG("Failed to query host refresh rate.");
}
}
const float target_frame_rate = frame_rate * target_speed; const float target_frame_rate = frame_rate * target_speed;
s_limiter_ticks_per_frame = s_limiter_ticks_per_frame =
@ -2094,7 +2096,7 @@ void VMManager::UpdateTargetSpeed()
{ {
s_target_speed = target_speed; s_target_speed = target_speed;
MTGS::UpdateVSyncEnabled(); MTGS::UpdateVSyncMode();
SPU2::OnTargetSpeedChanged(); SPU2::OnTargetSpeedChanged();
ResetFrameLimiter(); ResetFrameLimiter();
} }
@ -2105,11 +2107,6 @@ bool VMManager::IsTargetSpeedAdjustedToHost()
return s_target_speed_synced_to_host; return s_target_speed_synced_to_host;
} }
bool VMManager::IsUsingVSyncForTiming()
{
return s_use_vsync_for_timing;
}
float VMManager::GetFrameRate() float VMManager::GetFrameRate()
{ {
return GetVerticalFrequency(); return GetVerticalFrequency();
@ -2595,16 +2592,30 @@ void VMManager::SetPaused(bool paused)
SetState(paused ? VMState::Paused : VMState::Running); SetState(paused ? VMState::Paused : VMState::Running);
} }
bool Host::IsVsyncEffectivelyEnabled() GSVSyncMode VMManager::GetEffectiveVSyncMode()
{ {
const bool has_vm = VMManager::GetState() != VMState::Shutdown; // Vsync off => always disabled.
if (!EmuConfig.GS.VsyncEnable)
return GSVSyncMode::Disabled;
// Force vsync off when not running at 100% speed. // If there's no VM, or we're using vsync for timing, then we always use double-buffered (blocking).
if (has_vm && (s_target_speed != 1.0f && !s_use_vsync_for_timing)) // Try to keep the same present mode whether we're running or not, since it'll avoid flicker.
return false; const VMState state = GetState();
const bool valid_vm = (state != VMState::Shutdown && state != VMState::Stopping);
if (s_target_speed_can_sync_to_host || (!valid_vm && EmuConfig.EmulationSpeed.SyncToHostRefreshRate))
return GSVSyncMode::FIFO;
// Otherwise use the config setting. // For PAL games, we always want to triple buffer, because otherwise we'll be tearing.
return EmuConfig.GS.VsyncEnable; // Or for when we aren't using sync-to-host-refresh, to avoid dropping frames.
// Allow present skipping when running outside of normal speed, if mailbox isn't supported.
return GSVSyncMode::Mailbox;
}
bool VMManager::ShouldAllowPresentThrottle()
{
const VMState state = GetState();
const bool valid_vm = (state != VMState::Shutdown && state != VMState::Stopping);
return (!valid_vm || (!s_target_speed_synced_to_host && s_target_speed != 1.0f));
} }
bool VMManager::Internal::IsFastBootInProgress() bool VMManager::Internal::IsFastBootInProgress()
@ -2788,7 +2799,7 @@ void VMManager::CheckForGSConfigChanges(const Pcsx2Config& old_config)
{ {
// Still need to update target speed, because of sync-to-host-refresh. // Still need to update target speed, because of sync-to-host-refresh.
UpdateTargetSpeed(); UpdateTargetSpeed();
MTGS::UpdateVSyncEnabled(); MTGS::UpdateVSyncMode();
} }
MTGS::ApplySettings(); MTGS::ApplySettings();
@ -3197,13 +3208,9 @@ void VMManager::UpdateInhibitScreensaver(bool inhibit)
if (s_screensaver_inhibited == inhibit) if (s_screensaver_inhibited == inhibit)
return; return;
WindowInfo wi; if (Common::InhibitScreensaver(inhibit))
auto top_level_wi = Host::GetTopLevelWindowInfo();
if (top_level_wi.has_value())
wi = top_level_wi.value();
s_screensaver_inhibited = inhibit; s_screensaver_inhibited = inhibit;
if (!WindowInfo::InhibitScreensaver(wi, inhibit) && inhibit) else if (inhibit)
Console.Warning("Failed to inhibit screen saver."); Console.Warning("Failed to inhibit screen saver.");
} }

View File

@ -159,12 +159,15 @@ namespace VMManager
/// Returns true if the target speed is being synchronized with the host's refresh rate. /// Returns true if the target speed is being synchronized with the host's refresh rate.
bool IsTargetSpeedAdjustedToHost(); bool IsTargetSpeedAdjustedToHost();
/// Returns true if host vsync is being used for frame timing/pacing, and not its internal throttler.
bool IsUsingVSyncForTiming();
/// Returns the current frame rate of the virtual machine. /// Returns the current frame rate of the virtual machine.
float GetFrameRate(); float GetFrameRate();
/// Returns the desired vsync mode, depending on the runtime environment.
GSVSyncMode GetEffectiveVSyncMode();
/// Returns true if presents can be skipped, when running outside of normal speed.
bool ShouldAllowPresentThrottle();
/// Runs the virtual machine for the specified number of video frames, and then automatically pauses. /// Runs the virtual machine for the specified number of video frames, and then automatically pauses.
void FrameAdvance(u32 num_frames = 1); void FrameAdvance(u32 num_frames = 1);