diff --git a/Source/Core/Common/AtomicUniquePtr.h b/Source/Core/Common/AtomicUniquePtr.h new file mode 100644 index 0000000000..a959fe69a4 --- /dev/null +++ b/Source/Core/Common/AtomicUniquePtr.h @@ -0,0 +1,51 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +// atomic> is heavy and has limited compiler availability. +// atomic> isn't feasible. + +// This class provides something similar to a would-be atomic unique_ptr. + +#include +#include + +namespace Common +{ + +template +class AtomicUniquePtr +{ +public: + using UniquePtr = std::unique_ptr; + using RawPtr = T*; + + AtomicUniquePtr() = default; + ~AtomicUniquePtr() { Store(nullptr); } + + AtomicUniquePtr(const AtomicUniquePtr&) = delete; + void operator=(const AtomicUniquePtr&) = delete; + + explicit AtomicUniquePtr(std::nullptr_t) {} + explicit AtomicUniquePtr(UniquePtr ptr) { Store(std::move(ptr)); } + + void operator=(std::nullptr_t) { Store(nullptr); } + void operator=(UniquePtr ptr) { Store(std::move(ptr)); } + + void Store(UniquePtr desired) + { + // A unique_ptr is returned and appropriately destructed here. + Exchange(std::move(desired)); + } + + UniquePtr Exchange(UniquePtr desired) + { + return UniquePtr{m_ptr.exchange(desired.release(), std::memory_order_acq_rel)}; + } + +private: + std::atomic m_ptr = nullptr; +}; + +} // namespace Common diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 608e2f270a..c975a0c1f6 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(common Assembler/GekkoParser.cpp Assembler/GekkoParser.h Assert.h + AtomicUniquePtr.h BitField.h BitSet.h BitUtils.h diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 9f4d1cce17..0dc001a3a4 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 28a044d253..4e75147556 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 3ffa5d897f..ac46f79a36 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/DolphinLib.props b/Source/Core/DolphinLib.props index 67e3a15f4c..b4fdbb9c9f 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -22,6 +22,7 @@ + diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp index 917e7e898c..8591281b05 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); });