CoreTiming/VideoCommon: Add "Sync to Host Refresh Rate" setting.

This commit is contained in:
Jordan Woyak 2025-04-08 22:23:25 -05:00
parent 7a0bceecb0
commit a078950d14
8 changed files with 67 additions and 11 deletions

View File

@ -339,6 +339,8 @@ const Info<int> MAIN_ISO_PATH_COUNT{{System::Main, "General", "ISOPaths"}, 0};
const Info<std::string> MAIN_SKYLANDERS_PATH{{System::Main, "General", "SkylandersCollectionPath"},
""};
const Info<bool> MAIN_TIME_TRACKING{{System::Main, "General", "EnablePlayTimeTracking"}, true};
const Info<bool> MAIN_SYNC_TO_HOST_REFRESH_RATE{{System::Main, "General", "SyncToHostRefreshRate"},
true};
static Info<std::string> MakeISOPathConfigInfo(size_t idx)
{

View File

@ -197,6 +197,7 @@ extern const Info<bool> MAIN_RENDER_WINDOW_AUTOSIZE;
extern const Info<bool> MAIN_KEEP_WINDOW_ON_TOP;
extern const Info<bool> MAIN_DISABLE_SCREENSAVER;
extern const Info<bool> MAIN_TIME_TRACKING;
extern const Info<bool> MAIN_SYNC_TO_HOST_REFRESH_RATE;
// Main.General

View File

@ -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);

View File

@ -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<DT::rep> 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);

View File

@ -138,6 +138,17 @@ void AdvancedPane::CreateLayout()
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>"));
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."
"<br>This can make 59.94 FPS games run at 60 FPS."
"<br><br>Not needed or recommended for users with variable refresh rate displays."
"<br><br>For best results, turn off Immediately Present XFB"
" and Skip Presenting Duplicate Frames."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>"));
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();

View File

@ -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.

View File

@ -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.

View File

@ -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