From ec43661664af498e43eff1eadef3c18f626d9fba Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 6 Feb 2022 22:17:48 +1000 Subject: [PATCH] GS: Add sync to host refresh rate option --- pcsx2-qt/EmuThread.cpp | 4 ++ pcsx2/Config.h | 1 + pcsx2/Counters.cpp | 40 ++++++++++++- pcsx2/GS/GS.cpp | 1 + pcsx2/GS/Renderers/Common/GSRenderer.cpp | 3 +- pcsx2/Pcsx2Config.cpp | 2 + pcsx2/SPU2/SndOut.cpp | 49 +++++---------- pcsx2/SPU2/SndOut.h | 3 +- pcsx2/SPU2/spu2.cpp | 76 ++++++++++++++++++------ pcsx2/SPU2/spu2.h | 1 + pcsx2/gui/AppConfig.cpp | 1 + pcsx2/gui/Panels/ConfigurationPanels.h | 1 + pcsx2/gui/Panels/GSWindowPanel.cpp | 4 ++ 13 files changed, 128 insertions(+), 58 deletions(-) diff --git a/pcsx2-qt/EmuThread.cpp b/pcsx2-qt/EmuThread.cpp index 0ed8f39bd8..1e126df475 100644 --- a/pcsx2-qt/EmuThread.cpp +++ b/pcsx2-qt/EmuThread.cpp @@ -27,6 +27,7 @@ #include "common/StringUtil.h" #include "pcsx2/CDVD/CDVD.h" +#include "pcsx2/Counters.h" #include "pcsx2/Frontend/InputManager.h" #include "pcsx2/Frontend/ImGuiManager.h" #include "pcsx2/GS.h" @@ -361,6 +362,9 @@ void EmuThread::setFullscreen(bool fullscreen) m_is_fullscreen = fullscreen; GetMTGS().UpdateDisplayWindow(); GetMTGS().WaitGS(); + + // If we're using exclusive fullscreen, the refresh rate may have changed. + UpdateVSyncRate(); } void EmuThread::setSurfaceless(bool surfaceless) diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 8ef2908393..293c5488d1 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -435,6 +435,7 @@ struct Pcsx2Config PCRTCOffsets : 1, IntegerScaling : 1, LinearPresent : 1, + SyncToHostRefreshRate : 1, UseDebugDevice : 1, UseBlitSwapChain : 1, DisableShaderCache : 1, diff --git a/pcsx2/Counters.cpp b/pcsx2/Counters.cpp index 2339490941..c003741fd3 100644 --- a/pcsx2/Counters.cpp +++ b/pcsx2/Counters.cpp @@ -31,6 +31,8 @@ #include "ps2/HwInternal.h" #include "Sio.h" +#include "HostDisplay.h" +#include "SPU2/spu2.h" #ifndef PCSX2_CORE #include "gui/App.h" @@ -46,6 +48,7 @@ using namespace Threading; extern u8 psxhblankgate; static const uint EECNT_FUTURE_TARGET = 0x10000000; static int gates = 0; +static bool s_use_vsync_for_timing = false; uint g_FrameCount = 0; @@ -346,6 +349,39 @@ double GetVerticalFrequency() } } +static double AdjustToHostRefreshRate(double vertical_frequency, double frame_limit) +{ + if (!EmuConfig.GS.SyncToHostRefreshRate || EmuConfig.GS.LimitScalar != 1.0) + { + SPU2SetDeviceSampleRateMultiplier(1.0); + s_use_vsync_for_timing = false; + return frame_limit; + } + + float host_refresh_rate; + if (!Host::GetHostDisplay()->GetHostRefreshRate(&host_refresh_rate)) + { + Console.Warning("Cannot sync to host refresh since the query failed."); + SPU2SetDeviceSampleRateMultiplier(1.0); + s_use_vsync_for_timing = false; + return frame_limit; + } + + const double ratio = host_refresh_rate / vertical_frequency; + const bool syncing_to_host = (ratio >= 0.95f && ratio <= 1.05f); + s_use_vsync_for_timing = (syncing_to_host && !EmuConfig.GS.SkipDuplicateFrames && EmuConfig.GS.VsyncEnable != VsyncMode::Off); + Console.WriteLn("Refresh rate: Host=%fhz Guest=%fhz Ratio=%f - %s %s", host_refresh_rate, + vertical_frequency, ratio, syncing_to_host ? "can sync" : "can't sync", + s_use_vsync_for_timing ? "and using vsync for pacing" : "and using sleep for pacing"); + + if (!syncing_to_host) + return frame_limit; + + frame_limit *= ratio; + SPU2SetDeviceSampleRateMultiplier(ratio); + return frame_limit; +} + u32 UpdateVSyncRate() { // Notice: (and I probably repeat this elsewhere, but it's worth repeating) @@ -357,7 +393,7 @@ u32 UpdateVSyncRate() const double vertical_frequency = GetVerticalFrequency(); const double frames_per_second = vertical_frequency / 2.0; - const double frame_limit = frames_per_second * EmuConfig.GS.LimitScalar; + const double frame_limit = AdjustToHostRefreshRate(vertical_frequency, frames_per_second * EmuConfig.GS.LimitScalar); const double tick_rate = GetTickFrequency() / 2.0; const s64 ticks = static_cast(tick_rate / std::max(frame_limit, 1.0)); @@ -515,7 +551,7 @@ static __fi void frameLimitUpdateCore() static __fi void frameLimit() { // Framelimiter off in settings? Framelimiter go brrr. - if (EmuConfig.GS.LimitScalar == 0.0) + if (EmuConfig.GS.LimitScalar == 0.0 || s_use_vsync_for_timing) { frameLimitUpdateCore(); return; diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index aeeb8c86a2..5841a2773a 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -1371,6 +1371,7 @@ void GSApp::Init() m_default_configuration["extrathreads_height"] = "4"; m_default_configuration["filter"] = std::to_string(static_cast(BiFiltering::PS2)); m_default_configuration["FMVSoftwareRendererSwitch"] = "0"; + m_default_configuration["FullscreenMode"] = ""; m_default_configuration["fxaa"] = "0"; m_default_configuration["GSDumpCompression"] = "0"; m_default_configuration["HWDisableReadbacks"] = "0"; diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp index 6fed0eb8ad..2bbe8a42ca 100644 --- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp +++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp @@ -502,7 +502,6 @@ void GSRenderer::VSync(u32 field, bool registers_written) const int fb_sprite_blits = g_perfmon.GetDisplayFramebufferSpriteBlits(); const bool fb_sprite_frame = (fb_sprite_blits > 0); - PerformanceMetrics::Update(registers_written, fb_sprite_frame); bool skip_frame = m_frameskip; if (GSConfig.SkipDuplicateFrames) @@ -540,6 +539,7 @@ void GSRenderer::VSync(u32 field, bool registers_written) if (Host::BeginPresentFrame(true)) Host::EndPresentFrame(); g_gs_device->RestoreAPIState(); + PerformanceMetrics::Update(registers_written, fb_sprite_frame); return; } @@ -572,6 +572,7 @@ void GSRenderer::VSync(u32 field, bool registers_written) PerformanceMetrics::OnGPUPresent(Host::GetHostDisplay()->GetAndResetAccumulatedGPUTime()); } g_gs_device->RestoreAPIState(); + PerformanceMetrics::Update(registers_written, fb_sprite_frame); // snapshot // wx is dumb and call this from the UI thread... diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index bfc4e6957c..fd1121a2cd 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -301,6 +301,7 @@ Pcsx2Config::GSOptions::GSOptions() PCRTCOffsets = false; IntegerScaling = false; LinearPresent = true; + SyncToHostRefreshRate = false; UseDebugDevice = false; UseBlitSwapChain = false; DisableShaderCache = false; @@ -464,6 +465,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) #ifdef PCSX2_CORE // These are loaded from GSWindow in wx. + SettingsWrapBitBool(SyncToHostRefreshRate); SettingsWrapEnumEx(AspectRatio, "AspectRatio", AspectRatioNames); SettingsWrapEnumEx(FMVAspectRatioSwitch, "FMVAspectRatioSwitch", FMVAspectRatioSwitchNames); diff --git a/pcsx2/SPU2/SndOut.cpp b/pcsx2/SPU2/SndOut.cpp index 3b919f979a..f504219068 100644 --- a/pcsx2/SPU2/SndOut.cpp +++ b/pcsx2/SPU2/SndOut.cpp @@ -149,14 +149,6 @@ bool SndBuffer::CheckUnderrunStatus(int& nSamples, int& quietSampleCount) return true; } -void SndBuffer::_InitFail() -{ - // If a failure occurs, just initialize the NoSound driver. This'll allow - // the game to emulate properly (hopefully), albeit without sound. - OutputModule = FindOutputModuleById(NullOut->GetIdent()); - mods[OutputModule]->Init(); -} - int SndBuffer::_GetApproximateDataInBuffer() { // WARNING: not necessarily 100% up to date by the time it's used, but it will have to do. @@ -357,13 +349,10 @@ void SndBuffer::_WriteSamples(StereoOut32* bData, int nSamples) _WriteSamples_Safe(bData, nSamples); } -void SndBuffer::Init() +bool SndBuffer::Init() { - if (mods[OutputModule] == nullptr) - { - _InitFail(); - return; - } + if (!mods[OutputModule]) + return false; // initialize sound buffer // Buffer actually attempts to run ~50%, so allocate near double what @@ -372,33 +361,25 @@ void SndBuffer::Init() m_rpos = 0; m_wpos = 0; - try - { - const float latencyMS = SndOutLatencyMS * 16; - m_size = GetAlignedBufferSize((int)(latencyMS * SampleRate / 1000.0f)); - printf("%d SampleRate: \n", SampleRate); - m_buffer = new StereoOut32[m_size]; - m_underrun_freeze = false; - - sndTempBuffer = new StereoOut32[SndOutPacketSize]; - sndTempBuffer16 = new StereoOut16[SndOutPacketSize * 2]; // in case of leftovers. - } - catch (std::bad_alloc&) - { - // out of memory exception (most likely) - - SysMessage("Out of memory error occurred while initializing SPU2."); - _InitFail(); - return; - } + const float latencyMS = SndOutLatencyMS * 16; + m_size = GetAlignedBufferSize((int)(latencyMS * SampleRate / 1000.0f)); + m_buffer = new StereoOut32[m_size]; + m_underrun_freeze = false; + sndTempBuffer = new StereoOut32[SndOutPacketSize]; + sndTempBuffer16 = new StereoOut16[SndOutPacketSize * 2]; // in case of leftovers. sndTempProgress = 0; soundtouchInit(); // initializes the timestretching // initialize module if (!mods[OutputModule]->Init()) - _InitFail(); + { + Cleanup(); + return false; + } + + return true; } void SndBuffer::Cleanup() diff --git a/pcsx2/SPU2/SndOut.h b/pcsx2/SPU2/SndOut.h index 00cc0363eb..b3e0488917 100644 --- a/pcsx2/SPU2/SndOut.h +++ b/pcsx2/SPU2/SndOut.h @@ -586,7 +586,6 @@ private: static float eTempo; static int ssFreeze; - static void _InitFail(); static bool CheckUnderrunStatus(int& nSamples, int& quietSampleCount); static void soundtouchInit(); @@ -614,7 +613,7 @@ private: public: static void UpdateTempoChangeAsyncMixing(); - static void Init(); + static bool Init(); static void Cleanup(); static void Write(const StereoOut32& Sample); static void ClearContents(); diff --git a/pcsx2/SPU2/spu2.cpp b/pcsx2/SPU2/spu2.cpp index 7ffa7fa4d8..2c003422ed 100644 --- a/pcsx2/SPU2/spu2.cpp +++ b/pcsx2/SPU2/spu2.cpp @@ -33,8 +33,11 @@ using namespace Threading; std::recursive_mutex mtx_SPU2Status; +static int ConsoleSampleRate = 48000; int SampleRate = 48000; +static double DeviceSampleRateMultiplier = 1.0; + static bool IsOpened = false; static bool IsInitialized = false; @@ -120,9 +123,44 @@ void SPU2writeDMA7Mem(u16* pMem, u32 size) Cores[1].DoDMAwrite(pMem, size); } -s32 SPU2reset(PS2Modes isRunningPSXMode) +static void SPU2InitSndBuffer() { - int requiredSampleRate = (isRunningPSXMode == PS2Modes::PSX) ? 44100 : 48000; + Console.WriteLn("Initializing SndBuffer at sample rate of %u...", SampleRate); + if (SndBuffer::Init()) + return; + + if (SampleRate != ConsoleSampleRate) + { + // TODO: Resample on our side... + const int original_sample_rate = SampleRate; + Console.Error("Failed to init SPU2 at adjusted sample rate %u, trying console rate.", SampleRate); + SampleRate = ConsoleSampleRate; + if (SndBuffer::Init()) + return; + + SampleRate = original_sample_rate; + } + + // just use nullout + OutputModule = FindOutputModuleById(NullOut->GetIdent()); + if (!SndBuffer::Init()) + pxFailRel("Failed to initialize nullout."); +} + +static void SPU2UpdateSampleRate() +{ + const int new_sample_rate = static_cast(std::round(static_cast(ConsoleSampleRate) * DeviceSampleRateMultiplier)); + if (SampleRate == new_sample_rate) + return; + + SndBuffer::Cleanup(); + SampleRate = new_sample_rate; + SPU2InitSndBuffer(); +} + +static void SPU2InternalReset(PS2Modes isRunningPSXMode) +{ + ConsoleSampleRate = (isRunningPSXMode == PS2Modes::PSX) ? 44100 : 48000; if (isRunningPSXMode == PS2Modes::PS2) { @@ -136,25 +174,24 @@ s32 SPU2reset(PS2Modes isRunningPSXMode) Cores[0].Init(0); Cores[1].Init(1); } +} - if (SampleRate != requiredSampleRate) - { - SampleRate = requiredSampleRate; - SndBuffer::Cleanup(); - try - { - SndBuffer::Init(); - } - catch (std::exception& ex) - { - fprintf(stderr, "SPU2 Error: Could not initialize device, or something.\nReason: %s", ex.what()); - SPU2close(); - return -1; - } - } +s32 SPU2reset(PS2Modes isRunningPSXMode) +{ + SPU2InternalReset(isRunningPSXMode); + SPU2UpdateSampleRate(); return 0; } +void SPU2SetDeviceSampleRateMultiplier(double multiplier) +{ + if (DeviceSampleRateMultiplier == multiplier) + return; + + DeviceSampleRateMultiplier = multiplier; + SPU2UpdateSampleRate(); +} + s32 SPU2init() { assert(regtable[0x400] == nullptr); @@ -207,7 +244,7 @@ s32 SPU2init() } } - SPU2reset(PS2Modes::PS2); + SPU2InternalReset(PS2Modes::PS2); DMALogOpen(); InitADSR(); @@ -289,7 +326,8 @@ s32 SPU2open() try { - SndBuffer::Init(); + SampleRate = static_cast(std::round(static_cast(ConsoleSampleRate) * DeviceSampleRateMultiplier)); + SPU2InitSndBuffer(); #if defined(_WIN32) && !defined(PCSX2_CORE) DspLoadLibrary(dspPlugin, dspPluginModule); diff --git a/pcsx2/SPU2/spu2.h b/pcsx2/SPU2/spu2.h index b9ffbe9e2f..758d1185dc 100644 --- a/pcsx2/SPU2/spu2.h +++ b/pcsx2/SPU2/spu2.h @@ -33,6 +33,7 @@ s32 SPU2open(); void SPU2close(); void SPU2shutdown(); void SPU2SetOutputPaused(bool paused); +void SPU2SetDeviceSampleRateMultiplier(double multiplier); void SPU2write(u32 mem, u16 value); u16 SPU2read(u32 mem); diff --git a/pcsx2/gui/AppConfig.cpp b/pcsx2/gui/AppConfig.cpp index 507348e733..6d2b1df4e0 100644 --- a/pcsx2/gui/AppConfig.cpp +++ b/pcsx2/gui/AppConfig.cpp @@ -886,6 +886,7 @@ void AppConfig::GSWindowOptions::LoadSave(IniInterface& ini) // WARNING: array must be NULL terminated to compute it size NULL}; + g_Conf->EmuOptions.GS.SyncToHostRefreshRate = ini.EntryBitBool(L"SyncToHostRefreshRate", g_Conf->EmuOptions.GS.SyncToHostRefreshRate, g_Conf->EmuOptions.GS.SyncToHostRefreshRate); ini.EnumEntry(L"AspectRatio", g_Conf->EmuOptions.GS.AspectRatio, AspectRatioNames, g_Conf->EmuOptions.GS.AspectRatio); if (ini.IsLoading()) EmuConfig.CurrentAspectRatio = g_Conf->EmuOptions.GS.AspectRatio; diff --git a/pcsx2/gui/Panels/ConfigurationPanels.h b/pcsx2/gui/Panels/ConfigurationPanels.h index 730d1b1561..1af9675a01 100644 --- a/pcsx2/gui/Panels/ConfigurationPanels.h +++ b/pcsx2/gui/Panels/ConfigurationPanels.h @@ -285,6 +285,7 @@ namespace Panels pxCheckBox* m_check_HideMouse; pxCheckBox* m_check_DclickFullscreen; + pxCheckBox* m_check_SyncToHostRefreshRate; wxTextCtrl* m_text_WindowWidth; wxTextCtrl* m_text_WindowHeight; diff --git a/pcsx2/gui/Panels/GSWindowPanel.cpp b/pcsx2/gui/Panels/GSWindowPanel.cpp index 4fe472c329..6761a70e8c 100644 --- a/pcsx2/gui/Panels/GSWindowPanel.cpp +++ b/pcsx2/gui/Panels/GSWindowPanel.cpp @@ -74,6 +74,7 @@ Panels::GSWindowSettingsPanel::GSWindowSettingsPanel(wxWindow* parent) // Implement custom hotkeys (Alt + Enter) with translatable string intact + not blank in GUI. m_check_Fullscreen = new pxCheckBox(this, _("Start in fullscreen mode by default") + wxString(" (") + wxGetApp().GlobalAccels->findKeycodeWithCommandId("FullscreenToggle").toTitleizedString() + wxString(")")); m_check_DclickFullscreen = new pxCheckBox(this, _("Double-click toggles fullscreen mode")); + m_check_SyncToHostRefreshRate = new pxCheckBox(this, _("Sync To Host Refresh Rate")); m_combo_FMVAspectRatioSwitch->SetToolTip(pxEt(L"Off: Disables temporary aspect ratio switch. (It will use the above setting from Aspect Ratio instead of FMV Aspect Ratio Override.)\n\n" L"Auto 4:3/3:2: Temporarily switch to a 4:3 aspect ratio while an FMV plays to correctly display a 4:3 FMV. Will use 3:2 is the resolution is 480P\n\n" @@ -136,6 +137,7 @@ Panels::GSWindowSettingsPanel::GSWindowSettingsPanel(wxWindow* parent) *this += m_check_Fullscreen; *this += m_check_DclickFullscreen; + *this += m_check_SyncToHostRefreshRate; *this += new wxStaticLine(this) | StdExpand(); *this += s_vsync | StdExpand(); @@ -169,6 +171,7 @@ void Panels::GSWindowSettingsPanel::ApplyConfigToGui(AppConfig& configToApply, i m_text_Zoom->ChangeValue(wxString::FromDouble(gsconf.Zoom, 2)); m_check_DclickFullscreen->SetValue(conf.IsToggleFullscreenOnDoubleClick); + m_check_SyncToHostRefreshRate->SetValue(gsconf.SyncToHostRefreshRate); m_text_WindowWidth->ChangeValue(wxsFormat(L"%d", conf.WindowSize.GetWidth())); m_text_WindowHeight->ChangeValue(wxsFormat(L"%d", conf.WindowSize.GetHeight())); @@ -199,6 +202,7 @@ void Panels::GSWindowSettingsPanel::Apply() gsconf.VsyncEnable = static_cast(m_combo_vsync->GetSelection()); appconf.IsToggleFullscreenOnDoubleClick = m_check_DclickFullscreen->GetValue(); + gsconf.SyncToHostRefreshRate = m_check_SyncToHostRefreshRate->GetValue(); long xr, yr = 1;