From a078950d145e0cfeed52207eb57db1f222c23fcc 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 | 2 ++ Source/Core/Core/Config/MainSettings.h | 1 + Source/Core/Core/CoreTiming.cpp | 18 +++++++++++----- Source/Core/Core/CoreTiming.h | 10 ++++++--- .../Core/DolphinQt/Settings/AdvancedPane.cpp | 11 ++++++++++ Source/Core/VideoBackends/Vulkan/VKGfx.cpp | 11 ++++++++-- Source/Core/VideoCommon/Present.cpp | 21 ++++++++++++++++++- Source/Core/VideoCommon/Present.h | 4 ++++ 8 files changed, 67 insertions(+), 11 deletions(-) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index e42796e864..da1fd35d68 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -339,6 +339,8 @@ 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_TO_HOST_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 b6a7094c93..5790767078 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -197,6 +197,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_TO_HOST_REFRESH_RATE; // Main.General diff --git a/Source/Core/Core/CoreTiming.cpp b/Source/Core/Core/CoreTiming.cpp index a692e8db6f..afbad2ede3 100644 --- a/Source/Core/Core/CoreTiming.cpp +++ b/Source/Core/Core/CoreTiming.cpp @@ -164,7 +164,7 @@ void CoreTimingManager::RefreshConfig() OSD::AddMessage("Minimum speed is 100% in Hardcore Mode"); } - UpdateSpeedLimit(GetTicks(), Config::Get(Config::MAIN_EMULATION_SPEED)); + UpdateSpeedLimit(GetTicks()); m_use_precision_timer = Config::Get(Config::MAIN_PRECISION_FRAME_TIMING); } @@ -433,8 +433,16 @@ void CoreTimingManager::SleepUntil(TimePoint time_point) } } +void CoreTimingManager::AdjustThrottleReferenceTime(DT adjustment) +{ + m_throttle_reference_time_adjustment.fetch_add(adjustment.count()); +} + void CoreTimingManager::Throttle(const s64 target_cycle) { + m_throttle_reference_time += + DT{m_throttle_reference_time_adjustment.exchange(0, std::memory_order_relaxed)}; + const TimePoint time = Clock::now(); const bool already_throttled = @@ -494,10 +502,9 @@ void CoreTimingManager::Throttle(const s64 target_cycle) SleepUntil(target_time); } -void CoreTimingManager::UpdateSpeedLimit(s64 cycle, double new_speed) +void CoreTimingManager::UpdateSpeedLimit(s64 cycle) { - m_emulation_speed = new_speed; - + const auto new_speed = Config::Get(Config::MAIN_EMULATION_SPEED); const u32 new_clock_per_sec = std::lround(m_system.GetSystemTimers().GetTicksPerSecond() * new_speed); @@ -517,6 +524,7 @@ void CoreTimingManager::ResetThrottle(s64 cycle) { m_throttle_reference_cycle = cycle; m_throttle_reference_time = Clock::now(); + m_throttle_reference_time_adjustment.store(0, std::memory_order_relaxed); } void CoreTimingManager::UpdateVISkip(TimePoint current_time, TimePoint target_time) @@ -560,7 +568,7 @@ void CoreTimingManager::AdjustEventQueueTimes(u32 new_ppc_clock, u32 old_ppc_clo { const s64 ticks = m_globals.global_timer; - UpdateSpeedLimit(ticks, m_emulation_speed); + UpdateSpeedLimit(ticks); g_perf_metrics.AdjustClockSpeed(ticks, new_ppc_clock, old_ppc_clock); diff --git a/Source/Core/Core/CoreTiming.h b/Source/Core/Core/CoreTiming.h index 68d1fd4fd2..23b0e5ea4f 100644 --- a/Source/Core/Core/CoreTiming.h +++ b/Source/Core/Core/CoreTiming.h @@ -157,8 +157,11 @@ public: // Directly accessed by the JIT. Globals& GetGlobals() { return m_globals; } + // Callable from any thread. Takes effect on the next Throttle call. + void AdjustThrottleReferenceTime(DT adjustment); + // Throttle the CPU to the specified target cycle. - void Throttle(const s64 target_cycle); + void Throttle(s64 target_cycle); // May be used from CPU or GPU thread. void SleepUntil(TimePoint time_point); @@ -211,16 +214,17 @@ private: s64 m_throttle_reference_cycle = 0; TimePoint m_throttle_reference_time = Clock::now(); + std::atomic m_throttle_reference_time_adjustment{}; + u32 m_throttle_adj_clock_per_sec = 0; bool m_throttle_disable_vi_int = false; DT m_max_fallback = {}; DT m_max_variance = {}; bool m_correct_time_drift = false; - double m_emulation_speed = 1.0; bool IsSpeedUnlimited() const; - void UpdateSpeedLimit(s64 cycle, double new_speed); + void UpdateSpeedLimit(s64 cycle); void ResetThrottle(s64 cycle); TimePoint CalculateTargetHostTimeInternal(s64 target_cycle); void UpdateVISkip(TimePoint current_time, TimePoint target_time); diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp index b96d2b24ea..c67602d8f1 100644 --- a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp +++ b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp @@ -138,6 +138,17 @@ void AdvancedPane::CreateLayout() "

If unsure, leave this unchecked.")); timing_group_layout->addWidget(smooth_early_presentation); + auto* const sync_to_host_refresh = + new ConfigBool{tr("Sync to Host Refresh Rate"), Config::MAIN_SYNC_TO_HOST_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." + "

For best results, turn off Immediately Present XFB" + " and Skip Presenting Duplicate Frames." + "

If unsure, leave this unchecked.")); + timing_group_layout->addWidget(sync_to_host_refresh); + // Make all labels the same width, so that the sliders are aligned. const QFontMetrics font_metrics{font()}; const int label_width = font_metrics.boundingRect(QStringLiteral(" 500% (000.00 VPS)")).width(); diff --git a/Source/Core/VideoBackends/Vulkan/VKGfx.cpp b/Source/Core/VideoBackends/Vulkan/VKGfx.cpp index c071d99357..61cda48bd4 100644 --- a/Source/Core/VideoBackends/Vulkan/VKGfx.cpp +++ b/Source/Core/VideoBackends/Vulkan/VKGfx.cpp @@ -11,6 +11,8 @@ #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" @@ -311,6 +313,10 @@ void VKGfx::PresentBackbuffer() // End drawing to backbuffer StateTracker::GetInstance()->EndRenderPass(); + // If "Sync to Host Refresh Rate" is active, wait for completion for a usable timestamp. + const bool wait_for_completion = + g_ActiveConfig.bVSyncActive && Config::Get(Config::MAIN_SYNC_TO_HOST_REFRESH_RATE); + if (m_swap_chain->IsCurrentImageValid()) { // Transition the backbuffer to PRESENT_SRC to ensure all commands drawing @@ -322,12 +328,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/Present.cpp b/Source/Core/VideoCommon/Present.cpp index ea7f2d6d74..3e324c3c26 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -902,9 +902,22 @@ void Presenter::Present(PresentInfo* present_info) if (present_info != nullptr) { + auto& core_timing = Core::System::GetInstance().GetCoreTiming(); + const auto present_time = GetUpdatedPresentationTime(present_info->intended_present_time); - Core::System::GetInstance().GetCoreTiming().SleepUntil(present_time); + // "Sync to Host Refresh Rate" throttle adjustment. + if (Config::Get(Config::MAIN_SYNC_TO_HOST_REFRESH_RATE) && Config::Get(Config::GFX_VSYNC)) + { + constexpr DT MAX_ADJUSTMENT = std::chrono::microseconds{100}; + const auto adjustment = + std::clamp(m_ideal_present_time - present_time, -MAX_ADJUSTMENT, MAX_ADJUSTMENT); + + DEBUG_LOG_FMT(VIDEO, "Adjusting throttle by {:.2f} ms.", DT_ms(adjustment).count()); + core_timing.AdjustThrottleReferenceTime(adjustment); + } + + core_timing.SleepUntil(present_time); // Perhaps in the future a more accurate time can be acquired from the various backends. present_info->actual_present_time = Clock::now(); @@ -914,6 +927,12 @@ void Presenter::Present(PresentInfo* present_info) g_gfx->PresentBackbuffer(); } + // "Sync to Host Refresh Rate" targets the timing immediately between the return from two swaps. + // Maybe there is a better way to determine a proper vsync deadline ? + const auto now = Clock::now(); + const auto time_since_last_present = now - std::exchange(m_last_after_present_time, now); + m_ideal_present_time = now + time_since_last_present / 2; + if (m_xfb_entry) { // Update the window size based on the frame that was just rendered. diff --git a/Source/Core/VideoCommon/Present.h b/Source/Core/VideoCommon/Present.h index 7e5ce651e3..6b26aaaf19 100644 --- a/Source/Core/VideoCommon/Present.h +++ b/Source/Core/VideoCommon/Present.h @@ -184,6 +184,10 @@ private: TimePoint m_next_swap_estimated_time{Clock::now()}; std::atomic_bool m_immediate_swap_happened_this_field{}; + + // Used for "Sync to Host Refresh Rate". + TimePoint m_last_after_present_time{}; + TimePoint m_ideal_present_time{}; }; } // namespace VideoCommon