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);
void DestroyMetalLayer(WindowInfo* wi);
std::optional<float> GetViewRefreshRate(const WindowInfo& wi);
/// Add a handler to be run when macOS changes between dark and light themes
void AddThemeChangeHandler(void* ctx, void(handler)(void* ctx));
/// Remove a handler previously added using AddThemeChangeHandler with the given context

View File

@ -64,6 +64,27 @@ void CocoaTools::DestroyMetalLayer(WindowInfo* wi)
[view setWantsLayer:NO];
}
std::optional<float> CocoaTools::GetViewRefreshRate(const WindowInfo& wi)
{
if (![NSThread isMainThread])
{
std::optional<float> ret;
dispatch_sync(dispatch_get_main_queue(), [&ret, wi]{ ret = GetViewRefreshRate(wi); });
return ret;
}
std::optional<float> ret;
NSView* const view = (__bridge NSView*)wi.window_handle;
const u32 did = [[[[[view window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
{
ret = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
}
return ret;
}
// MARK: - Theme Change Handlers
@interface PCSX2KVOHelper : NSObject

View File

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

View File

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

View File

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

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+
#include "WindowInfo.h"
#include "Console.h"
#include "Error.h"
#include "HeapArray.h"
#if defined(_WIN32)
#include "RedtapeWindows.h"
#include <dwmapi.h>
static bool GetRefreshRateFromDWM(HWND hwnd, float* refresh_rate)
static std::optional<float> GetRefreshRateFromDisplayConfig(HWND hwnd)
{
// Partially based on Chromium ui/display/win/display_config_helper.cc.
const HMONITOR monitor = MonitorFromWindow(hwnd, 0);
if (!monitor) [[unlikely]]
{
ERROR_LOG("{}() failed: {}", "MonitorFromWindow", Error::CreateWin32(GetLastError()).GetDescription());
return std::nullopt;
}
MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi);
if (!GetMonitorInfoW(monitor, &mi))
{
ERROR_LOG("{}() failed: {}", "GetMonitorInfoW", Error::CreateWin32(GetLastError()).GetDescription());
return std::nullopt;
}
DynamicHeapArray<DISPLAYCONFIG_PATH_INFO> path_info;
DynamicHeapArray<DISPLAYCONFIG_MODE_INFO> mode_info;
// I guess this could fail if it changes inbetween two calls... unlikely.
for (;;)
{
UINT32 path_size = 0, mode_size = 0;
LONG res = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_size, &mode_size);
if (res != ERROR_SUCCESS)
{
ERROR_LOG("{}() failed: {}", "GetDisplayConfigBufferSizes", Error::CreateWin32(res).GetDescription());
return std::nullopt;
}
path_info.resize(path_size);
mode_info.resize(mode_size);
res =
QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_size, path_info.data(), &mode_size, mode_info.data(), nullptr);
if (res == ERROR_SUCCESS)
break;
if (res != ERROR_INSUFFICIENT_BUFFER)
{
ERROR_LOG("{}() failed: {}", "QueryDisplayConfig", Error::CreateWin32(res).GetDescription());
return std::nullopt;
}
}
for (const DISPLAYCONFIG_PATH_INFO& pi : path_info)
{
DISPLAYCONFIG_SOURCE_DEVICE_NAME sdn = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME),
.adapterId = pi.sourceInfo.adapterId,
.id = pi.sourceInfo.id}};
LONG res = DisplayConfigGetDeviceInfo(&sdn.header);
if (res != ERROR_SUCCESS)
{
ERROR_LOG("{}() failed: {}", "DisplayConfigGetDeviceInfo", Error::CreateWin32(res).GetDescription());
continue;
}
if (std::wcscmp(sdn.viewGdiDeviceName, mi.szDevice) == 0)
{
// Found the monitor!
return static_cast<float>(static_cast<double>(pi.targetInfo.refreshRate.Numerator) /
static_cast<double>(pi.targetInfo.refreshRate.Denominator));
}
}
return std::nullopt;
}
static std::optional<float> GetRefreshRateFromDWM(HWND hwnd)
{
BOOL composition_enabled;
if (FAILED(DwmIsCompositionEnabled(&composition_enabled)))
return false;
return std::nullopt;
DWM_TIMING_INFO ti = {};
ti.cbSize = sizeof(ti);
@ -21,20 +92,19 @@ static bool GetRefreshRateFromDWM(HWND hwnd, float* refresh_rate)
if (SUCCEEDED(hr))
{
if (ti.rateRefresh.uiNumerator == 0 || ti.rateRefresh.uiDenominator == 0)
return false;
return std::nullopt;
*refresh_rate = static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
return true;
return static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
}
return false;
return std::nullopt;
}
static bool GetRefreshRateFromMonitor(HWND hwnd, float* refresh_rate)
static std::optional<float> GetRefreshRateFromMonitor(HWND hwnd)
{
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (!mon)
return false;
return std::nullopt;
MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi);
@ -45,23 +115,41 @@ static bool GetRefreshRateFromMonitor(HWND hwnd, float* refresh_rate)
// 0/1 are reserved for "defaults".
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
{
*refresh_rate = static_cast<float>(dm.dmDisplayFrequency);
return true;
}
return static_cast<float>(dm.dmDisplayFrequency);
}
return false;
return std::nullopt;
}
bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate)
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
std::optional<float> ret;
if (wi.type != Type::Win32 || !wi.window_handle)
return false;
return ret;
// Try DWM first, then fall back to integer values.
const HWND hwnd = static_cast<HWND>(wi.window_handle);
return GetRefreshRateFromDWM(hwnd, refresh_rate) || GetRefreshRateFromMonitor(hwnd, refresh_rate);
ret = GetRefreshRateFromDisplayConfig(hwnd);
if (!ret.has_value())
{
ret = GetRefreshRateFromDWM(hwnd);
if (!ret.has_value())
ret = GetRefreshRateFromMonitor(hwnd);
}
return ret;
}
#elif defined(__APPLE__)
#include "common/CocoaTools.h"
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
if (wi.type == WindowInfo::Type::MacOS)
return CocoaTools::GetViewRefreshRate(wi);
return std::nullopt;
}
#else
@ -72,18 +160,18 @@ bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_
#include <X11/extensions/Xrandr.h>
#include <X11/Xlib.h>
static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
static std::optional<float> GetRefreshRateFromXRandR(const WindowInfo& wi)
{
Display* display = static_cast<Display*>(wi.display_connection);
Window window = static_cast<Window>(reinterpret_cast<uintptr_t>(wi.window_handle));
if (!display || !window)
return false;
return std::nullopt;
XRRScreenResources* res = XRRGetScreenResources(display, window);
if (!res)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetScreenResources() failed");
return false;
return std::nullopt;
}
ScopedGuard res_guard([res]() { XRRFreeScreenResources(res); });
@ -93,7 +181,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (num_monitors < 0)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetMonitors() failed");
return false;
return std::nullopt;
}
else if (num_monitors > 1)
{
@ -104,7 +192,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (mi->noutput <= 0)
{
Console.Error("(GetRefreshRateFromXRandR) Monitor has no outputs");
return false;
return std::nullopt;
}
else if (mi->noutput > 1)
{
@ -115,7 +203,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!oi)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetOutputInfo() failed");
return false;
return std::nullopt;
}
ScopedGuard oi_guard([oi]() { XRRFreeOutputInfo(oi); });
@ -124,7 +212,7 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!ci)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetCrtcInfo() failed");
return false;
return std::nullopt;
}
ScopedGuard ci_guard([ci]() { XRRFreeCrtcInfo(ci); });
@ -141,30 +229,29 @@ static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
if (!mode)
{
Console.Error("(GetRefreshRateFromXRandR) Failed to look up mode %d (of %d)", static_cast<int>(ci->mode), res->nmode);
return false;
return std::nullopt;
}
if (mode->dotClock == 0 || mode->hTotal == 0 || mode->vTotal == 0)
{
Console.Error("(GetRefreshRateFromXRandR) Modeline is invalid: %ld/%d/%d", mode->dotClock, mode->hTotal, mode->vTotal);
return false;
return std::nullopt;
}
*refresh_rate =
static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal));
return true;
return static_cast<float>(
static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal)));
}
#endif // X11_API
bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate)
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
#if defined(X11_API)
if (wi.type == WindowInfo::Type::X11)
return GetRefreshRateFromXRandR(wi, refresh_rate);
return GetRefreshRateFromXRandR(wi);
#endif
return false;
return std::nullopt;
}
#endif

View File

@ -4,6 +4,8 @@
#pragma once
#include "Pcsx2Defs.h"
#include <optional>
/// Contains the information required to create a graphics context in a window.
struct WindowInfo
{
@ -41,8 +43,5 @@ struct WindowInfo
float surface_refresh_rate = 0.0f;
/// Returns the host's refresh rate for the given window, if available.
static bool QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate);
/// Enables or disables the screen saver from starting.
static bool InhibitScreensaver(const WindowInfo& wi, bool inhibit);
static std::optional<float> QueryRefreshRateForWindow(const WindowInfo& wi);
};

View File

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

View File

@ -302,6 +302,21 @@ namespace QtUtils
wi.surface_width = static_cast<u32>(static_cast<qreal>(widget->width()) * dpr);
wi.surface_height = static_cast<u32>(static_cast<qreal>(widget->height()) * dpr);
wi.surface_scale = static_cast<float>(dpr);
// Query refresh rate, we need it for sync.
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;
}

View File

@ -31,8 +31,12 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget
initializeSpeedCombo(m_ui.slowMotionSpeed, "Framerate", "SlomoScalar", 0.5f);
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.useVSyncForTiming, "EmuCore/GS", "UseVSyncForTiming", false);
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());
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"),
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."));
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 "
"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 "
"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();
updateUseVSyncForTimingEnabled();
}
EmulationSettingsWidget::~EmulationSettingsWidget() = default;
@ -274,3 +285,10 @@ void EmulationSettingsWidget::updateOptimalFramePacing()
m_ui.maxFrameLatency->setMinimum(optimal ? 0 : 1);
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 handleSpeedComboChange(QComboBox* cb, const char* section, const char* key);
void updateOptimalFramePacing();
void updateUseVSyncForTimingEnabled();
SettingsWindow* m_dialog;

View File

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

View File

@ -74,7 +74,6 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* dialog, QWidget*
// Global Settings
//////////////////////////////////////////////////////////////////////////
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.spinGPUDuringReadbacks, "EmuCore/GS", "HWSpinGPUForReadbacks", 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"),
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"),
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."));

View File

@ -224,6 +224,75 @@
</item>
</widget>
</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">
<widget class="QLabel" name="label_24">
<property name="text">
@ -325,13 +394,6 @@
</item>
<item row="8" column="0" colspan="2">
<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">
<widget class="QCheckBox" name="integerScaling">
<property name="text">
@ -339,20 +401,6 @@
</property>
</widget>
</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">
<widget class="QCheckBox" name="widescreenPatches">
<property name="text">
@ -384,72 +432,17 @@
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_21">
<item row="2" column="0">
<widget class="QCheckBox" name="PCRTCOffsets">
<property name="text">
<string>Screenshot Size:</string>
<string>Screen Offsets</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>
<item row="3" column="0">
<widget class="QCheckBox" name="PCRTCOverscan">
<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>
<string>Show Overscan</string>
</property>
</widget>
</item>

View File

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

View File

@ -56,8 +56,6 @@ Pcsx2Config::GSOptions GSConfig;
static GSRendererType GSCurrentRenderer;
static u64 s_next_manual_present_time;
GSRendererType GSGetCurrentRenderer()
{
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);
switch (new_api)
@ -133,7 +132,7 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool
return false;
}
bool okay = g_gs_device->Create();
bool okay = g_gs_device->Create(vsync_mode, allow_present_throttle);
if (okay)
{
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.
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);
if (!OpenGSDevice(new_renderer, false, recreate_window))
if (!OpenGSDevice(new_renderer, false, recreate_window, vsync_mode, allow_present_throttle))
{
Host::AddKeyedOSDMessage("GSReopenFailed",
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())
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");
Host::ReleaseRenderWindow();
@ -309,14 +310,15 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, GSRendererType new_r
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;
if (renderer == GSRendererType::Auto)
renderer = GSUtil::GetPreferredRenderer();
bool res = OpenGSDevice(renderer, true, false);
bool res = OpenGSDevice(renderer, true, false, vsync_mode, allow_present_throttle);
if (res)
{
res = OpenGSRenderer(renderer, basemem);
@ -471,29 +473,13 @@ void GSPresentCurrentFrame()
void GSThrottlePresentation()
{
if (g_gs_device->IsVSyncEnabled())
if (g_gs_device->GetVSyncMode() == GSVSyncMode::FIFO)
{
// Let vsync take care of throttling.
return;
}
// 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 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);
g_gs_device->ThrottlePresentation();
}
void GSGameChanged()
@ -528,9 +514,16 @@ void GSUpdateDisplayWindow()
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()
@ -543,12 +536,16 @@ bool GSWantsExclusiveFullscreen()
return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate);
}
bool GSGetHostRefreshRate(float* refresh_rate)
std::optional<float> GSGetHostRefreshRate()
{
if (!g_gs_device)
return false;
return std::nullopt;
return g_gs_device->GetHostRefreshRate(refresh_rate);
const float surface_refresh_rate = g_gs_device->GetWindowInfo().surface_refresh_rate;
if (surface_refresh_rate == 0.0f)
return std::nullopt;
else
return surface_refresh_rate;
}
void GSGetAdaptersAndFullscreenModes(

View File

@ -56,7 +56,8 @@ s16 GSLookupGetSkipCountFunctionId(const std::string_view name);
s16 GSLookupBeforeDrawFunctionId(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,
std::optional<const Pcsx2Config::GSOptions*> old_config);
void GSreset(bool hardware_reset);
@ -84,12 +85,12 @@ void GSSetDisplayAlignment(GSDisplayAlignment alignment);
bool GSHasDisplayWindow();
void GSResizeDisplayWindow(int width, int height, float scale);
void GSUpdateDisplayWindow();
void GSSetVSyncEnabled(bool enabled);
void GSSetVSyncMode(GSVSyncMode mode, bool allow_present_throttle);
GSRendererType GSGetCurrentRenderer();
bool GSIsHardwareRenderer();
bool GSWantsExclusiveFullscreen();
bool GSGetHostRefreshRate(float* refresh_rate);
std::optional<float> GSGetHostRefreshRate();
void GSGetAdaptersAndFullscreenModes(
GSRendererType renderer, std::vector<std::string>* adapters, std::vector<std::string>* fullscreen_modes);
GSVideoMode GSgetDisplayMode();
@ -126,9 +127,6 @@ namespace Host
/// Alters fullscreen state of hosting application.
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.
void OnCaptureStarted(const std::string& filename);
void OnCaptureStopped();

View File

@ -971,7 +971,7 @@ void GSCapture::StopEncoderThread(std::unique_lock<std::mutex>& lock)
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 int source_width = static_cast<int>(pf.tex->GetWidth());
const int source_height = static_cast<int>(pf.tex->GetHeight());

View File

@ -9,9 +9,11 @@
#include "common/Console.h"
#include "common/BitUtils.h"
#include "common/FileSystem.h"
#include "common/HostSys.h"
#include "common/Path.h"
#include "common/SmallString.h"
#include "common/StringUtil.h"
#include "common/Threading.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);
}
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;
}
@ -337,15 +340,42 @@ bool GSDevice::AcquireWindow(bool recreate_window)
return true;
}
bool GSDevice::GetHostRefreshRate(float* refresh_rate)
bool GSDevice::ShouldSkipPresentingFrame()
{
if (m_window_info.surface_refresh_rate > 0.0f)
{
*refresh_rate = m_window_info.surface_refresh_rate;
// Only needed with FIFO.
if (!m_allow_present_throttle || m_vsync_mode != GSVSyncMode::FIFO)
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;
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)

View File

@ -786,6 +786,20 @@ public:
};
// 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:
std::array<FastList<GSTexture*>, 2> m_pool; // [texture, target]
u64 m_pool_memory_usage = 0;
@ -803,6 +817,9 @@ protected:
static constexpr u32 EXPAND_BUFFER_SIZE = sizeof(u16) * 16383 * 6;
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;
@ -814,19 +831,6 @@ protected:
GSTexture* m_current = 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);
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 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 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; }
@ -886,7 +891,7 @@ public:
/// Recreates the font, call when the window scaling changes.
bool UpdateImGuiFontTexture();
virtual bool Create();
virtual bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle);
virtual void Destroy();
/// Returns the graphics API used by this device.
@ -915,10 +920,7 @@ public:
virtual void EndPresent() = 0;
/// Changes vsync mode for this display.
virtual void SetVSyncEnabled(bool enabled) = 0;
/// Returns the effective refresh rate of this display.
virtual bool GetHostRefreshRate(float* refresh_rate);
virtual void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) = 0;
/// Returns a string of information about the graphics driver being used.
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.
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 ClearDepth(GSTexture* t, float d);
void InvalidateRenderTarget(GSTexture* t);
@ -980,8 +988,6 @@ public:
bool ResizeRenderTarget(GSTexture** t, int w, int h, bool preserve_contents, bool recycle);
bool IsRBSwapped() { return m_rbswapped; }
void AgePool();
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_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))
EndPresentFrame();
PerformanceMetrics::Update(registers_written, fb_sprite_frame, true);
PerformanceMetrics::Update(registers_written, fb_sprite_frame, skip_frame);
return;
}

View File

@ -61,7 +61,7 @@ bool GSTexture::Save(const std::string& fn)
}
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)

View File

@ -80,9 +80,9 @@ RenderAPI GSDevice11::GetRenderAPI() const
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;
UINT create_flags = 0;
@ -609,28 +609,38 @@ bool GSDevice11::HasSurface() const
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;
if (SUCCEEDED(m_swap_chain->GetDesc(&desc)) && desc.BufferDesc.RefreshRate.Numerator > 0 &&
desc.BufferDesc.RefreshRate.Denominator > 0)
WARNING_LOG("Using FIFO instead of Mailbox vsync due to exclusive fullscreen.");
mode = GSVSyncMode::FIFO;
}
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(
"using fs rr: %u %u", desc.BufferDesc.RefreshRate.Numerator, desc.BufferDesc.RefreshRate.Denominator);
*refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) /
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
return true;
DestroySwapChain();
if (!CreateSwapChain())
pxFailRel("Failed to recreate swap chain after vsync change.");
}
}
return GSDevice::GetHostRefreshRate(refresh_rate);
}
void GSDevice11::SetVSyncEnabled(bool enabled)
u32 GSDevice11::GetSwapChainBufferCount() const
{
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()
@ -655,6 +665,13 @@ bool GSDevice11::CreateSwapChain()
D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width,
fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode,
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
{
@ -668,7 +685,7 @@ bool GSDevice11::CreateSwapChain()
swap_chain_desc.Height = static_cast<u32>(client_rc.bottom - client_rc.top);
swap_chain_desc.Format = swap_chain_format;
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.SwapEffect =
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) /
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
}
else
{
m_window_info.surface_refresh_rate = 0.0f;
}
}
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
// 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.
if (m_vsync_enabled && m_gpu_timing_enabled)
if (m_vsync_mode == GSVSyncMode::FIFO && m_gpu_timing_enabled)
PopTimestampQuery();
m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), s_present_clear_color.data());
@ -957,13 +970,12 @@ void GSDevice11::EndPresent()
RenderImGui();
// 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();
if (!m_vsync_enabled && m_using_allow_tearing)
m_swap_chain->Present(0, DXGI_PRESENT_ALLOW_TEARING);
else
m_swap_chain->Present(static_cast<UINT>(m_vsync_enabled), 0);
const UINT sync_interval = static_cast<UINT>(m_vsync_mode == GSVSyncMode::FIFO);
const UINT flags = (m_vsync_mode == GSVSyncMode::Disabled && m_using_allow_tearing) ? DXGI_PRESENT_ALLOW_TEARING : 0;
m_swap_chain->Present(sync_interval, flags);
if (m_gpu_timing_enabled)
KickTimestampQuery();

View File

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

View File

@ -676,9 +676,9 @@ bool GSDevice12::HasSurface() const
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;
if (!CreateDevice())
@ -755,28 +755,38 @@ void GSDevice12::Destroy()
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;
if (SUCCEEDED(m_swap_chain->GetDesc(&desc)) && desc.BufferDesc.RefreshRate.Numerator > 0 &&
desc.BufferDesc.RefreshRate.Denominator > 0)
WARNING_LOG("Using FIFO instead of Mailbox vsync due to exclusive fullscreen.");
mode = GSVSyncMode::FIFO;
}
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(
"using fs rr: %u %u", desc.BufferDesc.RefreshRate.Numerator, desc.BufferDesc.RefreshRate.Denominator);
*refresh_rate = static_cast<float>(desc.BufferDesc.RefreshRate.Numerator) /
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
return true;
DestroySwapChain();
if (!CreateSwapChain())
pxFailRel("Failed to recreate swap chain after vsync change.");
}
}
return GSDevice::GetHostRefreshRate(refresh_rate);
}
void GSDevice12::SetVSyncEnabled(bool enabled)
u32 GSDevice12::GetSwapChainBufferCount() const
{
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()
@ -801,6 +811,13 @@ bool GSDevice12::CreateSwapChain()
D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width,
fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode,
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
{
@ -812,7 +829,7 @@ bool GSDevice12::CreateSwapChain()
swap_chain_desc.Height = static_cast<u32>(client_rc.bottom - client_rc.top);
swap_chain_desc.Format = swap_chain_format;
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.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) /
static_cast<float>(desc.BufferDesc.RefreshRate.Denominator);
}
else
{
m_window_info.surface_refresh_rate = 0.0f;
}
}
m_current_swap_chain_buffer = 0;
@ -1109,10 +1122,9 @@ void GSDevice12::EndPresent()
return;
}
if (!m_vsync_enabled && m_using_allow_tearing)
m_swap_chain->Present(0, DXGI_PRESENT_ALLOW_TEARING);
else
m_swap_chain->Present(static_cast<UINT>(m_vsync_enabled), 0);
const UINT sync_interval = static_cast<UINT>(m_vsync_mode == GSVSyncMode::FIFO);
const UINT flags = (m_vsync_mode == GSVSyncMode::Disabled && m_using_allow_tearing) ? DXGI_PRESENT_ALLOW_TEARING : 0;
m_swap_chain->Present(sync_interval, flags);
InvalidateCachedState();
}

View File

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

View File

@ -373,7 +373,7 @@ public:
MRCOwned<id<MTLFunction>> LoadShader(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);
bool Create() override;
bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override;
void AttachSurfaceOnMainThread();
@ -392,9 +392,7 @@ public:
PresentResult BeginPresent(bool frame_skip) override;
void EndPresent() override;
void SetVSyncEnabled(bool enabled) override;
bool GetHostRefreshRate(float* refresh_rate) override;
void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle) override;
bool SetGPUTimingEnabled(bool enabled) override;
float GetAndResetAccumulatedGPUTime() override;

View File

@ -822,9 +822,9 @@ static MRCOwned<id<MTLSamplerState>> CreateSampler(id<MTLDevice> dev, GSHWDrawCo
return ret;
}
bool GSDeviceMTL::Create()
bool GSDeviceMTL::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
{ @autoreleasepool {
if (!GSDevice::Create())
if (!GSDevice::Create(vsync_mode, allow_present_throttle))
return false;
NSString* ns_adapter_name = [NSString stringWithUTF8String:GSConfig.Adapter.c_str()];
@ -877,7 +877,10 @@ bool GSDeviceMTL::Create()
{
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
{
@ -1305,7 +1308,7 @@ void GSDeviceMTL::EndPresent()
if (m_current_drawable)
{
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)
[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;
[m_layer setDisplaySyncEnabled:enabled];
m_vsync_enabled = enabled;
}
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;
m_vsync_mode = (mode == GSVSyncMode::Mailbox) ? GSVSyncMode::FIFO : mode;
[m_layer setDisplaySyncEnabled:m_vsync_mode == GSVSyncMode::FIFO];
}
bool GSDeviceMTL::SetGPUTimingEnabled(bool enabled)

View File

@ -146,18 +146,20 @@ bool GSDeviceOGL::HasSurface() const
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;
m_vsync_enabled = enabled;
m_vsync_mode = mode;
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;
// 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)
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.
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;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &current_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

View File

@ -283,7 +283,7 @@ public:
RenderAPI GetRenderAPI() const override;
bool HasSurface() const override;
bool Create() override;
bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override;
bool UpdateWindow() override;
@ -292,7 +292,7 @@ public:
void DestroySurface() 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;
void EndPresent() override;

View File

@ -2088,9 +2088,9 @@ bool GSDeviceVK::HasSurface() const
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;
if (!CreateDeviceAndSwapChain())
@ -2222,9 +2222,10 @@ bool GSDeviceVK::UpdateWindow()
return false;
}
m_swap_chain = VKSwapChain::Create(m_window_info, surface, m_vsync_enabled,
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl));
if (!m_swap_chain)
VkPresentModeKHR present_mode;
if (!VKSwapChain::SelectPresentMode(surface, &m_vsync_mode, &present_mode) ||
!(m_swap_chain = VKSwapChain::Create(m_window_info, surface, present_mode,
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl))))
{
Console.Error("Failed to create swap chain");
VKSwapChain::DestroyVulkanSurface(m_instance, &m_window_info, surface);
@ -2297,27 +2298,36 @@ std::string GSDeviceVK::GetDriverInfo() const
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;
}
// This swap chain should not be used by the current buffer, thus safe to destroy.
WaitForGPUIdle();
if (!m_swap_chain->SetVSyncEnabled(enabled))
VkPresentModeKHR present_mode;
if (!VKSwapChain::SelectPresentMode(m_swap_chain->GetSurface(), &mode, &present_mode))
{
// Try switching back to the old mode..
if (!m_swap_chain->SetVSyncEnabled(m_vsync_enabled))
{
pxFailRel("Failed to reset old vsync mode after failure");
m_swap_chain.reset();
}
ERROR_LOG("Ignoring vsync mode change.");
return;
}
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)
@ -2616,11 +2626,12 @@ bool GSDeviceVK::CreateDeviceAndSwapChain()
if (surface != VK_NULL_HANDLE)
{
m_swap_chain = VKSwapChain::Create(m_window_info, surface, m_vsync_enabled,
Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl));
if (!m_swap_chain)
VkPresentModeKHR present_mode;
if (!VKSwapChain::SelectPresentMode(surface, &m_vsync_mode, &present_mode) ||
!(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;
}

View File

@ -515,7 +515,7 @@ public:
RenderAPI GetRenderAPI() const override;
bool HasSurface() const override;
bool Create() override;
bool Create(GSVSyncMode vsync_mode, bool allow_present_throttle) override;
void Destroy() override;
bool UpdateWindow() override;
@ -524,7 +524,7 @@ public:
void DestroySurface() 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;
void EndPresent() override;

View File

@ -18,12 +18,12 @@
#include <X11/Xlib.h>
#endif
VKSwapChain::VKSwapChain(
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control)
VKSwapChain::VKSwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode,
std::optional<bool> exclusive_fullscreen_control)
: m_window_info(wi)
, m_surface(surface)
, m_present_mode(present_mode)
, m_exclusive_fullscreen_control(exclusive_fullscreen_control)
, m_vsync_enabled(vsync)
{
}
@ -135,11 +135,11 @@ void VKSwapChain::DestroyVulkanSurface(VkInstance instance, WindowInfo* wi, VkSu
#endif
}
std::unique_ptr<VKSwapChain> VKSwapChain::Create(
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control)
std::unique_ptr<VKSwapChain> VKSwapChain::Create(const WindowInfo& wi, VkSurfaceKHR surface,
VkPresentModeKHR present_mode, std::optional<bool> exclusive_fullscreen_control)
{
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())
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;
u32 mode_count;
@ -236,7 +236,7 @@ std::optional<VkPresentModeKHR> VKSwapChain::SelectPresentMode(VkSurfaceKHR surf
if (res != VK_SUCCESS || mode_count == 0)
{
LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceSurfaceFormatsKHR failed: ");
return std::nullopt;
return false;
}
std::vector<VkPresentModeKHR> present_modes(mode_count);
@ -245,48 +245,70 @@ std::optional<VkPresentModeKHR> VKSwapChain::SelectPresentMode(VkSurfaceKHR surf
pxAssert(res == VK_SUCCESS);
// 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(),
[check_mode](VkPresentModeKHR mode) { return check_mode == mode; });
return it != present_modes.end();
};
// Use preferred mode if available.
VkPresentModeKHR selected_mode;
if (CheckForMode(requested_mode))
switch (*vsync_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.
selected_mode = VK_PRESENT_MODE_MAILBOX_KHR;
WARNING_LOG("Immediate not supported for vsync-disabled, using mailbox.");
*present_mode = VK_PRESENT_MODE_MAILBOX_KHR;
*vsync_mode = GSVSyncMode::Mailbox;
}
else
{
// Fallback to FIFO if we we can't use mailbox. This should never fail, FIFO is mandated.
selected_mode = VK_PRESENT_MODE_FIFO_KHR;
WARNING_LOG("Mailbox not supported for vsync-disabled, using FIFO.");
*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),
PresentModeToString(selected_mode));
return selected_mode;
return true;
}
bool VKSwapChain::CreateSwapChain()
{
// Select swap chain format and present mode
// Select swap chain format
std::optional<VkSurfaceFormatKHR> surface_format = SelectSurfaceFormat(m_surface);
// 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())
if (!surface_format.has_value())
return false;
// Look up surface properties to determine image count and dimensions
@ -299,12 +321,12 @@ bool VKSwapChain::CreateSwapChain()
return false;
}
// Select number of images in swap chain, we prefer one buffer in the background to work on
u32 image_count = std::max(surface_capabilities.minImageCount + 1u, 2u);
// Select number of images in swap chain, we prefer one buffer in the background to work on in triple-buffered mode.
// maxImageCount can be zero, in which case there isn't an upper limit on the number of buffers.
if (surface_capabilities.maxImageCount > 0)
image_count = std::min(image_count, surface_capabilities.maxImageCount);
u32 image_count = std::clamp<u32>(
(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
// determines window size?
@ -348,7 +370,7 @@ bool VKSwapChain::CreateSwapChain()
// Now we can actually create the swap chain
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,
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 = {{
GSDeviceVK::GetInstance()->GetGraphicsQueueFamilyIndex(),
GSDeviceVK::GetInstance()->GetPresentQueueFamilyIndex(),
@ -405,7 +427,6 @@ bool VKSwapChain::CreateSwapChain()
m_window_info.surface_width = std::max(1u, size.width);
m_window_info.surface_height = std::max(1u, size.height);
m_actual_present_mode = present_mode.value();
// Get and create images.
pxAssert(m_images.empty());
@ -550,15 +571,15 @@ bool VKSwapChain::ResizeSwapChain(u32 new_width, u32 new_height, float new_scale
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;
m_vsync_enabled = enabled;
m_present_mode = 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();
if (!CreateSwapChain())
{

View File

@ -24,8 +24,11 @@ public:
static void DestroyVulkanSurface(VkInstance instance, WindowInfo* wi, VkSurfaceKHR surface);
// Create a new swap chain from a pre-existing surface.
static std::unique_ptr<VKSwapChain> Create(
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control);
static std::unique_ptr<VKSwapChain> Create(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode,
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 VkSwapchainKHR GetSwapChain() const { return m_swap_chain; }
@ -56,12 +59,9 @@ public:
return &m_semaphores[m_current_semaphore].rendering_finished_semaphore;
}
// Returns true if the current present mode is synchronizing (adaptive or hard).
__fi bool IsPresentModeSynchronizing() const
{
return (m_actual_present_mode == VK_PRESENT_MODE_FIFO_KHR ||
m_actual_present_mode == VK_PRESENT_MODE_FIFO_RELAXED_KHR);
}
// Returns true if the current present mode is synchronizing.
__fi bool IsPresentModeSynchronizing() const { return (m_present_mode == VK_PRESENT_MODE_FIFO_KHR); }
__fi VkPresentModeKHR GetPresentMode() const { return m_present_mode; }
VkFormat GetTextureFormat() const;
VkResult AcquireNextImage();
@ -71,14 +71,13 @@ public:
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.
bool SetVSyncEnabled(bool enabled);
bool SetPresentMode(VkPresentModeKHR present_mode);
private:
VKSwapChain(
const WindowInfo& wi, VkSurfaceKHR surface, bool vsync, std::optional<bool> exclusive_fullscreen_control);
VKSwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode,
std::optional<bool> exclusive_fullscreen_control);
static std::optional<VkSurfaceFormatKHR> SelectSurfaceFormat(VkSurfaceKHR surface);
static std::optional<VkPresentModeKHR> SelectPresentMode(VkSurfaceKHR surface, VkPresentModeKHR requested_mode);
bool CreateSwapChain();
void DestroySwapChain();
@ -102,11 +101,11 @@ private:
std::vector<std::unique_ptr<GSTextureVK>> m_images;
std::vector<ImageSemaphores> m_semaphores;
VkPresentModeKHR m_actual_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
u32 m_current_image = 0;
u32 m_current_semaphore = 0;
VkPresentModeKHR m_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
std::optional<VkResult> m_image_acquire_result;
std::optional<bool> m_exclusive_fullscreen_control;
bool m_vsync_enabled = false;
};

View File

@ -3345,9 +3345,16 @@ void FullscreenUI::DrawEmulationSettingsPage()
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);
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();
}
@ -3598,8 +3605,6 @@ void FullscreenUI::DrawGraphicsSettingsPage(SettingsInterface* bsi, bool show_ad
MenuHeading(FSUI_CSTR("Renderer"));
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);
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"));
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
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);
// notify emu thread that we finished opening (or failed)
@ -936,7 +937,6 @@ void MTGS::ApplySettings()
RunOnGSThread([opts = EmuConfig.GS]() {
GSUpdateConfig(opts);
GSSetVSyncEnabled(Host::IsVsyncEffectivelyEnabled());
});
// 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");
RunOnGSThread([enabled]() {
INFO_LOG("Vsync is {}", enabled ? "ON" : "OFF");
GSSetVSyncEnabled(enabled);
});
RunOnGSThread([mode, allow_present_throttle]() { GSSetVSyncMode(mode, allow_present_throttle); });
}
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 */)

View File

@ -71,8 +71,8 @@ namespace MTGS
void ApplySettings();
void ResizeDisplayWindow(int width, int height, float scale);
void UpdateDisplayWindow();
void SetVSyncEnabled(bool enabled);
void UpdateVSyncEnabled();
void SetVSyncMode(GSVSyncMode mode, bool allow_present_throttle);
void UpdateVSyncMode();
void SetSoftwareRendering(bool software, GSInterlaceMode interlace, bool display_message = true);
void ToggleSoftwareRendering();
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.
//SettingsWrapBitBool(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

View File

@ -136,7 +136,6 @@ namespace VMManager
static float GetTargetSpeedForLimiterMode(LimiterModeType mode);
static void ResetFrameLimiter();
static double AdjustToHostRefreshRate(float frame_rate, float target_speed);
static void SetTimerResolutionIncreased(bool enabled);
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 u64 s_limiter_frame_start = 0;
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_use_vsync_for_timing = false;
@ -2026,34 +2026,6 @@ float VMManager::GetTargetSpeed()
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)
{
if (EmuConfig.EnableFastBootFastForward && VMManager::Internal::IsFastBootInProgress())
@ -2081,7 +2053,37 @@ float VMManager::GetTargetSpeedForLimiterMode(LimiterModeType mode)
void VMManager::UpdateTargetSpeed()
{
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;
s_limiter_ticks_per_frame =
@ -2094,7 +2096,7 @@ void VMManager::UpdateTargetSpeed()
{
s_target_speed = target_speed;
MTGS::UpdateVSyncEnabled();
MTGS::UpdateVSyncMode();
SPU2::OnTargetSpeedChanged();
ResetFrameLimiter();
}
@ -2105,11 +2107,6 @@ bool VMManager::IsTargetSpeedAdjustedToHost()
return s_target_speed_synced_to_host;
}
bool VMManager::IsUsingVSyncForTiming()
{
return s_use_vsync_for_timing;
}
float VMManager::GetFrameRate()
{
return GetVerticalFrequency();
@ -2595,16 +2592,30 @@ void VMManager::SetPaused(bool paused)
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 (has_vm && (s_target_speed != 1.0f && !s_use_vsync_for_timing))
return false;
// If there's no VM, or we're using vsync for timing, then we always use double-buffered (blocking).
// Try to keep the same present mode whether we're running or not, since it'll avoid flicker.
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.
return EmuConfig.GS.VsyncEnable;
// For PAL games, we always want to triple buffer, because otherwise we'll be tearing.
// 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()
@ -2788,7 +2799,7 @@ void VMManager::CheckForGSConfigChanges(const Pcsx2Config& old_config)
{
// Still need to update target speed, because of sync-to-host-refresh.
UpdateTargetSpeed();
MTGS::UpdateVSyncEnabled();
MTGS::UpdateVSyncMode();
}
MTGS::ApplySettings();
@ -3197,13 +3208,9 @@ void VMManager::UpdateInhibitScreensaver(bool inhibit)
if (s_screensaver_inhibited == inhibit)
return;
WindowInfo wi;
auto top_level_wi = Host::GetTopLevelWindowInfo();
if (top_level_wi.has_value())
wi = top_level_wi.value();
if (Common::InhibitScreensaver(inhibit))
s_screensaver_inhibited = inhibit;
if (!WindowInfo::InhibitScreensaver(wi, inhibit) && inhibit)
else if (inhibit)
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.
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.
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.
void FrameAdvance(u32 num_frames = 1);