diff --git a/src/util/util_fps_limiter.cpp b/src/util/util_fps_limiter.cpp index ff9ec40e0..6b3e13cd2 100644 --- a/src/util/util_fps_limiter.cpp +++ b/src/util/util_fps_limiter.cpp @@ -35,49 +35,103 @@ namespace dxvk { std::lock_guard lock(m_mutex); if (!m_envOverride) { - m_targetInterval = frameRate != 0.0 + TimerDuration interval = frameRate != 0.0 ? TimerDuration(int64_t(double(TimerDuration::period::den) / frameRate)) : TimerDuration::zero(); - if (isEnabled() && !m_initialized) - initialize(); + if (m_targetInterval != interval) { + m_targetInterval = interval; + + m_heuristicFrameCount = 0; + m_heuristicEnable = false; + + if (m_targetInterval != TimerDuration::zero() && !m_initialized) + initialize(); + } } } void FpsLimiter::delay() { - std::lock_guard lock(m_mutex); + std::unique_lock lock(m_mutex); + auto interval = m_targetInterval; - if (!isEnabled()) + if (interval == TimerDuration::zero()) return; auto t0 = m_lastFrame; auto t1 = dxvk::high_resolution_clock::now(); + if (interval < TimerDuration::zero()) { + interval = -interval; + + if (!testRefreshHeuristic(interval, t1)) + return; + } + + // Subsequent code must not access any class members + // that can be written by setTargetFrameRate + lock.unlock(); + auto frameTime = std::chrono::duration_cast(t1 - t0); - if (frameTime * 100 > m_targetInterval * 103 - m_deviation * 100) { + if (frameTime * 100 > interval * 103 - m_deviation * 100) { // If we have a slow frame, reset the deviation since we // do not want to compensate for low performance later on m_deviation = TimerDuration::zero(); } else { // Don't call sleep if the amount of time to sleep is shorter // than the time the function calls are likely going to take - TimerDuration sleepDuration = m_targetInterval - m_deviation - frameTime; + TimerDuration sleepDuration = interval - m_deviation - frameTime; t1 = Sleep::sleepFor(t1, sleepDuration); // Compensate for any sleep inaccuracies in the next frame, and // limit cumulative deviation in order to avoid stutter in case we // have a number of slow frames immediately followed by a fast one. frameTime = std::chrono::duration_cast(t1 - t0); - m_deviation += frameTime - m_targetInterval; - m_deviation = std::min(m_deviation, m_targetInterval / 16); + m_deviation += frameTime - interval; + m_deviation = std::min(m_deviation, interval / 16); } m_lastFrame = t1; } + bool FpsLimiter::testRefreshHeuristic(TimerDuration interval, TimePoint now) { + if (m_heuristicEnable) + return true; + + // Use a sliding window to determine whether the current + // frame rate is higher than the targeted refresh rate + uint32_t heuristicWindow = m_heuristicFrameTimes.size(); + auto windowStart = m_heuristicFrameTimes[m_heuristicFrameCount % heuristicWindow]; + auto windowDuration = std::chrono::duration_cast(now - windowStart); + + m_heuristicFrameTimes[m_heuristicFrameCount % heuristicWindow] = now; + m_heuristicFrameCount += 1; + + // The first window of frames may contain faster frames as the + // internal swap chain queue fills up, so we should ignore it. + if (m_heuristicFrameCount < 2 * heuristicWindow) + return false; + + // Test whether we should engage the frame rate limiter. It will + // stay enabled until the refresh rate or vsync enablement change. + m_heuristicEnable = (103 * windowDuration) < (100 * heuristicWindow) * interval; + + if (m_heuristicEnable) { + double got = (double(heuristicWindow) * double(TimerDuration::period::den)) + / (double(windowDuration.count()) * double(TimerDuration::period::num)); + double refresh = double(TimerDuration::period::den) / (double(TimerDuration::period::num) * double(interval.count())); + + Logger::info(str::format("Detected frame rate (~", uint32_t(got), ") higher than selected refresh rate of ~", + uint32_t(refresh), " Hz.\n", "Engaging frame rate limiter.")); + } + + return m_heuristicEnable; + } + + void FpsLimiter::initialize() { m_lastFrame = dxvk::high_resolution_clock::now(); m_initialized = true; diff --git a/src/util/util_fps_limiter.h b/src/util/util_fps_limiter.h index fe863cfc8..a30e37258 100644 --- a/src/util/util_fps_limiter.h +++ b/src/util/util_fps_limiter.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "thread.h" #include "util_time.h" @@ -37,14 +39,6 @@ namespace dxvk { */ void delay(); - /** - * \brief Checks whether the frame rate limiter is enabled - * \returns \c true if the target frame rate is non-zero. - */ - bool isEnabled() const { - return m_targetInterval != TimerDuration::zero(); - } - private: using TimePoint = dxvk::high_resolution_clock::time_point; @@ -54,11 +48,17 @@ namespace dxvk { TimerDuration m_targetInterval = TimerDuration::zero(); TimerDuration m_deviation = TimerDuration::zero(); - TimePoint m_lastFrame; + TimePoint m_lastFrame = TimePoint(); bool m_initialized = false; bool m_envOverride = false; + uint32_t m_heuristicFrameCount = 0; + std::array m_heuristicFrameTimes = { }; + bool m_heuristicEnable = false; + + bool testRefreshHeuristic(TimerDuration interval, TimePoint now); + void initialize(); };