From e82fdc778c0ac274244ff102d7ecea9aaf2ca9b6 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 8 Apr 2025 22:23:25 -0500 Subject: [PATCH] CoreTiming/VideoCommon: Add "Sync to Host Refresh Rate" setting. --- Source/Core/Core/Config/MainSettings.cpp | 1 + Source/Core/Core/Config/MainSettings.h | 1 + Source/Core/Core/CoreTiming.cpp | 25 +++++++++++++++++++ Source/Core/Core/CoreTiming.h | 12 +++++++++ .../Core/DolphinQt/Settings/AdvancedPane.cpp | 13 ++++++++++ Source/Core/VideoBackends/Vulkan/VKGfx.cpp | 9 +++++-- Source/Core/VideoCommon/VideoBackendBase.cpp | 7 +++++- 7 files changed, 65 insertions(+), 3 deletions(-) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 37f2a6054e..aab976aaf9 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -324,6 +324,7 @@ const Info MAIN_ISO_PATH_COUNT{{System::Main, "General", "ISOPaths"}, 0}; const Info MAIN_SKYLANDERS_PATH{{System::Main, "General", "SkylandersCollectionPath"}, ""}; const Info MAIN_TIME_TRACKING{{System::Main, "General", "EnablePlayTimeTracking"}, true}; +const Info MAIN_SYNC_REFRESH_RATE{{System::Main, "General", "SyncToHostRefreshRate"}, true}; static Info MakeISOPathConfigInfo(size_t idx) { diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 51aa7ec8aa..0140470c97 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -190,6 +190,7 @@ extern const Info MAIN_RENDER_WINDOW_AUTOSIZE; extern const Info MAIN_KEEP_WINDOW_ON_TOP; extern const Info MAIN_DISABLE_SCREENSAVER; extern const Info MAIN_TIME_TRACKING; +extern const Info MAIN_SYNC_REFRESH_RATE; // Main.General diff --git a/Source/Core/Core/CoreTiming.cpp b/Source/Core/Core/CoreTiming.cpp index 82d0db9f7c..01365d8663 100644 --- a/Source/Core/Core/CoreTiming.cpp +++ b/Source/Core/Core/CoreTiming.cpp @@ -28,6 +28,7 @@ #include "VideoCommon/PerformanceMetrics.h" #include "VideoCommon/VideoBackendBase.h" #include "VideoCommon/VideoConfig.h" +#include "VideoCommon/VideoEvents.h" namespace CoreTiming { @@ -103,6 +104,19 @@ void CoreTimingManager::Init() m_event_fifo_id = 0; m_ev_lost = RegisterEvent("_lost_event", &EmptyTimedCallback); + + m_after_frame_hook = AfterPresentEvent::Register( + [this](const PresentInfo& info) { + const bool sync_to_host_active = + Config::Get(Config::MAIN_SYNC_REFRESH_RATE) && g_ActiveConfig.bVSyncActive; + + const auto presentation_time = PresentationTime{.ticks = info.emulated_timestamp, + .time = Clock::now(), + .sync_to_host_active = sync_to_host_active}; + + m_last_presentation.Store(std::make_unique(presentation_time)); + }, + "CoreTiming AfterPresentEvent"); } void CoreTimingManager::Shutdown() @@ -112,6 +126,8 @@ void CoreTimingManager::Shutdown() ClearPendingEvents(); UnregisterAllEvents(); CPUThreadConfigCallback::RemoveConfigChangedCallback(m_registered_config_callback_id); + m_after_frame_hook.reset(); + m_last_presentation = nullptr; } void CoreTimingManager::RefreshConfig() @@ -201,6 +217,7 @@ void CoreTimingManager::DoState(PointerWrap& p) // The stave state has changed the time, so our previous Throttle targets are invalid. // Especially when global_time goes down; So we create a fake throttle update. ResetThrottle(m_globals.global_timer); + m_last_presentation = nullptr; } } @@ -399,6 +416,14 @@ void CoreTimingManager::SleepUntil(TimePoint time_point) void CoreTimingManager::Throttle(const s64 target_cycle) { + // Adjust throttle based on last presentation if "Sync to Host Refresh Rate" was active. + const auto last_presentation = m_last_presentation.Exchange(nullptr); + if (last_presentation && last_presentation->sync_to_host_active) + { + m_throttle_last_cycle = last_presentation->ticks; + m_throttle_deadline = last_presentation->time; + } + // Based on number of cycles and emulation speed, increase the target deadline const s64 cycles = target_cycle - m_throttle_last_cycle; m_throttle_last_cycle = target_cycle; diff --git a/Source/Core/Core/CoreTiming.h b/Source/Core/Core/CoreTiming.h index f0ed02ea64..2019ffa893 100644 --- a/Source/Core/Core/CoreTiming.h +++ b/Source/Core/Core/CoreTiming.h @@ -22,7 +22,9 @@ #include #include +#include "Common/AtomicUniquePtr.h" #include "Common/CommonTypes.h" +#include "Common/HookableEvent.h" #include "Common/SPSCQueue.h" #include "Common/Timer.h" #include "Core/CPUThreadConfigCallback.h" @@ -219,6 +221,16 @@ private: std::atomic_bool m_use_precision_timer = false; Common::PrecisionTimer m_precision_cpu_timer; Common::PrecisionTimer m_precision_gpu_timer; + + struct PresentationTime + { + u64 ticks; + Clock::time_point time; + bool sync_to_host_active; + }; + Common::AtomicUniquePtr m_last_presentation; + + Common::EventHook m_after_frame_hook; }; } // namespace CoreTiming diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp index e15b8f2325..82762b382d 100644 --- a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp +++ b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp @@ -88,6 +88,19 @@ void AdvancedPane::CreateLayout() "needed.

If unsure, leave this unchecked.")); cpu_options_group_layout->addWidget(m_accurate_cpu_cache_checkbox); + auto* const timing_group = new QGroupBox(tr("Timing")); + main_layout->addWidget(timing_group); + auto* timing_group_layout = new QVBoxLayout{timing_group}; + auto* const sync_to_host_refresh = + new ConfigBool{tr("Sync to Host Refresh Rate"), Config::MAIN_SYNC_REFRESH_RATE}; + sync_to_host_refresh->SetDescription( + tr("Adjusts emulation speed to match host refresh rate when V-Sync is enabled." + "
This can make 59.94 FPS games run at 60 FPS." + "

Not needed or recommended for users with variable refresh rate displays." + "

Has no effect when Immediate XFB is in use." + "

If unsure, leave this unchecked.")); + timing_group_layout->addWidget(sync_to_host_refresh); + auto* clock_override = new QGroupBox(tr("Clock Override")); auto* clock_override_layout = new QVBoxLayout(); clock_override->setLayout(clock_override_layout); diff --git a/Source/Core/VideoBackends/Vulkan/VKGfx.cpp b/Source/Core/VideoBackends/Vulkan/VKGfx.cpp index b30dd3894e..8ef6ba68d7 100644 --- a/Source/Core/VideoBackends/Vulkan/VKGfx.cpp +++ b/Source/Core/VideoBackends/Vulkan/VKGfx.cpp @@ -16,6 +16,7 @@ #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" +#include "Core/Config/MainSettings.h" #include "VideoBackends/Vulkan/CommandBufferManager.h" #include "VideoBackends/Vulkan/ObjectCache.h" #include "VideoBackends/Vulkan/StateTracker.h" @@ -316,6 +317,9 @@ void VKGfx::PresentBackbuffer() // End drawing to backbuffer StateTracker::GetInstance()->EndRenderPass(); + const bool wait_for_completion = + Config::Get(Config::MAIN_SYNC_REFRESH_RATE) && g_ActiveConfig.bVSyncActive; + if (m_swap_chain->IsCurrentImageValid()) { // Transition the backbuffer to PRESENT_SRC to ensure all commands drawing @@ -327,12 +331,13 @@ void VKGfx::PresentBackbuffer() // Because this final command buffer is rendering to the swap chain, we need to wait for // the available semaphore to be signaled before executing the buffer. This final submission // can happen off-thread in the background while we're preparing the next frame. - g_command_buffer_mgr->SubmitCommandBuffer(true, false, true, m_swap_chain->GetSwapChain(), + g_command_buffer_mgr->SubmitCommandBuffer(true, wait_for_completion, true, + m_swap_chain->GetSwapChain(), m_swap_chain->GetCurrentImageIndex()); } else { - g_command_buffer_mgr->SubmitCommandBuffer(true, false, true); + g_command_buffer_mgr->SubmitCommandBuffer(true, wait_for_completion, true); } // New cmdbuffer, so invalidate state. diff --git a/Source/Core/VideoCommon/VideoBackendBase.cpp b/Source/Core/VideoCommon/VideoBackendBase.cpp index 73035e0e1b..4a9d3bde46 100644 --- a/Source/Core/VideoCommon/VideoBackendBase.cpp +++ b/Source/Core/VideoCommon/VideoBackendBase.cpp @@ -97,7 +97,12 @@ void VideoBackendBase::Video_OutputXFB(u32 xfb_addr, u32 fb_width, u32 fb_stride auto& system = Core::System::GetInstance(); system.GetFifo().SyncGPU(Fifo::SyncGPUReason::Swap); - const TimePoint presentation_time = system.GetCoreTiming().GetTargetHostTime(ticks); + const bool sync_to_host_refresh = + Config::Get(Config::MAIN_SYNC_REFRESH_RATE) && g_ActiveConfig.bVSyncActive; + + const TimePoint presentation_time = + sync_to_host_refresh ? Clock::now() : system.GetCoreTiming().GetTargetHostTime(ticks); + AsyncRequests::GetInstance()->PushEvent([=] { g_presenter->ViSwap(xfb_addr, fb_width, fb_stride, fb_height, ticks, presentation_time); });