From 04d558cf2e8b51dd62be58d59d645f543a90010c Mon Sep 17 00:00:00 2001 From: Philip Rebohle Date: Thu, 29 Aug 2024 14:19:56 +0200 Subject: [PATCH] [util] Make frame rate limiter enablement heuristic more robust Allow for more buffering to happen in order to not enable the limiter too eagerly. --- src/d3d11/d3d11_swapchain.cpp | 4 +-- src/d3d9/d3d9_swapchain.cpp | 2 +- src/dxvk/dxvk_presenter.cpp | 4 +-- src/dxvk/dxvk_presenter.h | 2 +- src/util/util_fps_limiter.cpp | 64 +++++++++++++++++++++-------------- src/util/util_fps_limiter.h | 11 +++--- 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/d3d11/d3d11_swapchain.cpp b/src/d3d11/d3d11_swapchain.cpp index ce6faed53..0c85ddbed 100644 --- a/src/d3d11/d3d11_swapchain.cpp +++ b/src/d3d11/d3d11_swapchain.cpp @@ -353,7 +353,7 @@ namespace dxvk { m_targetFrameRate = FrameRate; if (m_presenter != nullptr) - m_presenter->setFrameRateLimit(m_targetFrameRate); + m_presenter->setFrameRateLimit(m_targetFrameRate, GetActualFrameLatency()); } @@ -506,7 +506,7 @@ namespace dxvk { presenterDesc.fullScreenExclusive = PickFullscreenMode(); m_presenter = new Presenter(m_device, m_frameLatencySignal, presenterDesc); - m_presenter->setFrameRateLimit(m_targetFrameRate); + m_presenter->setFrameRateLimit(m_targetFrameRate, GetActualFrameLatency()); } diff --git a/src/d3d9/d3d9_swapchain.cpp b/src/d3d9/d3d9_swapchain.cpp index 712a5b5e9..3fba731cf 100644 --- a/src/d3d9/d3d9_swapchain.cpp +++ b/src/d3d9/d3d9_swapchain.cpp @@ -1120,7 +1120,7 @@ namespace dxvk { if (SyncInterval && frameRateOption == 0.0) frameRate = -m_displayRefreshRate / double(SyncInterval); - m_wctx->presenter->setFrameRateLimit(frameRate); + m_wctx->presenter->setFrameRateLimit(frameRate, GetActualFrameLatency()); } diff --git a/src/dxvk/dxvk_presenter.cpp b/src/dxvk/dxvk_presenter.cpp index 17a406e51..5bfb0536b 100644 --- a/src/dxvk/dxvk_presenter.cpp +++ b/src/dxvk/dxvk_presenter.cpp @@ -415,8 +415,8 @@ namespace dxvk { } - void Presenter::setFrameRateLimit(double frameRate) { - m_fpsLimiter.setTargetFrameRate(frameRate); + void Presenter::setFrameRateLimit(double frameRate, uint32_t maxLatency) { + m_fpsLimiter.setTargetFrameRate(frameRate, maxLatency); } diff --git a/src/dxvk/dxvk_presenter.h b/src/dxvk/dxvk_presenter.h index 30247796c..8937f67c6 100644 --- a/src/dxvk/dxvk_presenter.h +++ b/src/dxvk/dxvk_presenter.h @@ -195,7 +195,7 @@ namespace dxvk { * \param [in] frameRate Target frame rate. Set * to 0 in order to disable the limiter. */ - void setFrameRateLimit(double frameRate); + void setFrameRateLimit(double frameRate, uint32_t maxLatency); /** * \brief Checks whether a Vulkan swap chain exists diff --git a/src/util/util_fps_limiter.cpp b/src/util/util_fps_limiter.cpp index 4186a5405..22780c1b9 100644 --- a/src/util/util_fps_limiter.cpp +++ b/src/util/util_fps_limiter.cpp @@ -17,7 +17,7 @@ namespace dxvk { if (!env.empty()) { try { - setTargetFrameRate(std::stod(env)); + setTargetFrameRate(std::stod(env), 0); m_envOverride = true; } catch (const std::invalid_argument&) { // no-op @@ -31,7 +31,7 @@ namespace dxvk { } - void FpsLimiter::setTargetFrameRate(double frameRate) { + void FpsLimiter::setTargetFrameRate(double frameRate, uint32_t maxLatency) { std::lock_guard lock(m_mutex); if (!m_envOverride) { @@ -42,8 +42,11 @@ namespace dxvk { if (m_targetInterval != interval) { m_targetInterval = interval; + m_heuristicFrameTime = TimePoint(); m_heuristicFrameCount = 0; m_heuristicEnable = false; + + m_maxLatency = maxLatency; } } } @@ -52,6 +55,7 @@ namespace dxvk { void FpsLimiter::delay() { std::unique_lock lock(m_mutex); auto interval = m_targetInterval; + auto latency = m_maxLatency; if (interval == TimerDuration::zero()) { m_nextFrame = TimePoint(); @@ -63,7 +67,7 @@ namespace dxvk { if (interval < TimerDuration::zero()) { interval = -interval; - if (!testRefreshHeuristic(interval, t1)) + if (!testRefreshHeuristic(interval, t1, latency)) return; } @@ -80,38 +84,48 @@ namespace dxvk { } - bool FpsLimiter::testRefreshHeuristic(TimerDuration interval, TimePoint now) { + bool FpsLimiter::testRefreshHeuristic(TimerDuration interval, TimePoint now, uint32_t maxLatency) { 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); + constexpr static uint32_t MinWindowSize = 8; + constexpr static uint32_t MaxWindowSize = 128; - m_heuristicFrameTimes[m_heuristicFrameCount % heuristicWindow] = now; - m_heuristicFrameCount += 1; + if (m_heuristicFrameCount >= MinWindowSize) { + TimerDuration windowTotalTime = now - m_heuristicFrameTime; + TimerDuration windowExpectedTime = m_heuristicFrameCount * interval; - // 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; + uint32_t minFrameCount = m_heuristicFrameCount - 1; + uint32_t maxFrameCount = m_heuristicFrameCount + maxLatency; - // 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; + // Enable frame rate limiter if frames have been delivered faster than + // the desired refresh rate even accounting for swap chain buffering. + if ((maxFrameCount * windowTotalTime) < (m_heuristicFrameCount * windowExpectedTime)) { + double got = (double(m_heuristicFrameCount) * double(TimerDuration::period::den)) + / (double(windowTotalTime.count()) * double(TimerDuration::period::num)); + double refresh = double(TimerDuration::period::den) / (double(TimerDuration::period::num) * double(interval.count())); - 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.")); - 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.")); + m_heuristicEnable = true; + return true; + } + + // Reset heuristics if frames have been delivered slower than the refresh rate. + if (((minFrameCount * windowTotalTime) > (m_heuristicFrameCount * windowExpectedTime)) + || (m_heuristicFrameCount >= MaxWindowSize)) { + m_heuristicFrameCount = 1; + m_heuristicFrameTime = now; + return false; + } } - return m_heuristicEnable; + if (!m_heuristicFrameCount) + m_heuristicFrameTime = now; + + m_heuristicFrameCount += 1; + return false; } } diff --git a/src/util/util_fps_limiter.h b/src/util/util_fps_limiter.h index 87f8bcf9b..8cde74bb2 100644 --- a/src/util/util_fps_limiter.h +++ b/src/util/util_fps_limiter.h @@ -28,7 +28,7 @@ namespace dxvk { * \brief Sets target frame rate * \param [in] frameRate Target frame rate */ - void setTargetFrameRate(double frameRate); + void setTargetFrameRate(double frameRate, uint32_t maxLatency); /** * \brief Stalls calling thread as necessary @@ -48,14 +48,15 @@ namespace dxvk { TimerDuration m_targetInterval = TimerDuration::zero(); TimePoint m_nextFrame = TimePoint(); + uint32_t m_maxLatency = 0; bool m_envOverride = false; - uint32_t m_heuristicFrameCount = 0; - std::array m_heuristicFrameTimes = { }; - bool m_heuristicEnable = false; + uint32_t m_heuristicFrameCount = 0; + TimePoint m_heuristicFrameTime = TimePoint(); + bool m_heuristicEnable = false; - bool testRefreshHeuristic(TimerDuration interval, TimePoint now); + bool testRefreshHeuristic(TimerDuration interval, TimePoint now, uint32_t maxLatency); };