From a16c861358cd4a6531b13318865011c2ca3162ae Mon Sep 17 00:00:00 2001 From: Philip Rebohle Date: Wed, 9 Jun 2021 03:43:19 +0200 Subject: [PATCH] [util] Implement frame rate limiter This tries to be sophisticated and disables itself when it notices that the frame rate is going to be limited by presentation anyway. --- src/util/meson.build | 1 + src/util/util_fps_limiter.cpp | 159 ++++++++++++++++++++++++++++++++++ src/util/util_fps_limiter.h | 89 +++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 src/util/util_fps_limiter.cpp create mode 100644 src/util/util_fps_limiter.h diff --git a/src/util/meson.build b/src/util/meson.build index 80a424f9..072ad2bc 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -1,6 +1,7 @@ util_src = files([ 'util_env.cpp', 'util_string.cpp', + 'util_fps_limiter.cpp', 'util_gdi.cpp', 'util_luid.cpp', 'util_matrix.cpp', diff --git a/src/util/util_fps_limiter.cpp b/src/util/util_fps_limiter.cpp new file mode 100644 index 00000000..b7a263e1 --- /dev/null +++ b/src/util/util_fps_limiter.cpp @@ -0,0 +1,159 @@ +#include + +#include "thread.h" +#include "util_fps_limiter.h" +#include "util_string.h" + +#include "./log/log.h" + +namespace dxvk { + + FpsLimiter::FpsLimiter() { + + } + + + FpsLimiter::~FpsLimiter() { + + } + + + void FpsLimiter::setTargetFrameRate(double frameRate) { + std::lock_guard lock(m_mutex); + + m_targetInterval = frameRate > 0.0 + ? NtTimerDuration(int64_t(double(NtTimerDuration::period::den) / frameRate)) + : NtTimerDuration::zero(); + + if (isEnabled() && !m_initialized) + initialize(); + } + + + void FpsLimiter::setDisplayRefreshRate(double refreshRate) { + std::lock_guard lock(m_mutex); + + m_refreshInterval = refreshRate > 0.0 + ? NtTimerDuration(int64_t(double(NtTimerDuration::period::den) / refreshRate)) + : NtTimerDuration::zero(); + } + + + void FpsLimiter::delay(bool vsyncEnabled) { + std::lock_guard lock(m_mutex); + + if (!isEnabled()) + return; + + // If the swap chain is known to have vsync enabled and the + // refresh rate is similar to the target frame rate, disable + // the limiter so it does not screw up frame times + if (vsyncEnabled && m_refreshInterval * 100 > m_targetInterval * 97) + return; + + auto t0 = m_lastFrame; + auto t1 = dxvk::high_resolution_clock::now(); + + auto frameTime = std::chrono::duration_cast(t1 - t0); + + if (frameTime * 100 > m_targetInterval * 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 = NtTimerDuration::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 + NtTimerDuration sleepDuration = m_targetInterval - m_deviation - frameTime; + t1 = sleep(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_lastFrame = t1; + } + + + FpsLimiter::TimePoint FpsLimiter::sleep(TimePoint t0, NtTimerDuration duration) { + if (duration <= NtTimerDuration::zero()) + return t0; + + // On wine, we can rely on NtDelayExecution waiting for more or + // less exactly the desired amount of time, and we want to avoid + // spamming QueryPerformanceCounter for performance reasons. + // On Windows, we busy-wait for the last couple of milliseconds + // since sleeping is highly inaccurate and inconsistent. + NtTimerDuration sleepThreshold = m_sleepThreshold; + + if (m_sleepGranularity != NtTimerDuration::zero()) + sleepThreshold += duration / 6; + + NtTimerDuration remaining = duration; + TimePoint t1 = t0; + + while (remaining > sleepThreshold) { + NtTimerDuration sleepDuration = remaining - sleepThreshold; + + if (NtDelayExecution) { + LARGE_INTEGER ticks; + ticks.QuadPart = -sleepDuration.count(); + + NtDelayExecution(FALSE, &ticks); + } else { + std::this_thread::sleep_for(sleepDuration); + } + + t1 = dxvk::high_resolution_clock::now(); + remaining -= std::chrono::duration_cast(t1 - t0); + t0 = t1; + } + + // Busy-wait until we have slept long enough + while (remaining > NtTimerDuration::zero()) { + t1 = dxvk::high_resolution_clock::now(); + remaining -= std::chrono::duration_cast(t1 - t0); + t0 = t1; + } + + return t1; + } + + + void FpsLimiter::initialize() { + HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll"); + + if (ntdll) { + NtDelayExecution = reinterpret_cast( + ::GetProcAddress(ntdll, "NtDelayExecution")); + auto NtQueryTimerResolution = reinterpret_cast( + ::GetProcAddress(ntdll, "NtQueryTimerResolution")); + auto NtSetTimerResolution = reinterpret_cast( + ::GetProcAddress(ntdll, "NtSetTimerResolution")); + + ULONG min, max, cur; + + // Wine's implementation of these functions is a stub as of 6.10, which is fine + // since it uses select() in NtDelayExecution. This is only relevant for Windows. + if (NtQueryTimerResolution && !NtQueryTimerResolution(&min, &max, &cur)) { + m_sleepGranularity = NtTimerDuration(cur); + + if (NtSetTimerResolution && !NtSetTimerResolution(max, TRUE, &cur)) { + Logger::info(str::format("Setting timer interval to ", (double(max) / 10.0), " us")); + m_sleepGranularity = NtTimerDuration(max); + } + } + } else { + // Assume 1ms sleep granularity by default + m_sleepGranularity = NtTimerDuration(10000); + } + + m_sleepThreshold = 4 * m_sleepGranularity; + 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 new file mode 100644 index 00000000..f90af7d4 --- /dev/null +++ b/src/util/util_fps_limiter.h @@ -0,0 +1,89 @@ +#pragma once + +#include + +#include "util_time.h" + +namespace dxvk { + + /** + * \brief Frame rate limiter + * + * Provides functionality to stall an application + * thread in order to maintain a given frame rate. + */ + class FpsLimiter { + + public: + + /** + * \brief Creates frame rate limiter + */ + FpsLimiter(); + + ~FpsLimiter(); + + /** + * \brief Sets target frame rate + * \param [in] frameRate Target frame rate + */ + void setTargetFrameRate(double frameRate); + + /** + * \brief Sets display refresh rate + * + * This information is used to decide whether or not + * the limiter should be active in the first place in + * case vertical synchronization is enabled. + * \param [in] refreshRate Current refresh rate + */ + void setDisplayRefreshRate(double refreshRate); + + /** + * \brief Stalls calling thread as necessary + * + * Blocks the calling thread if the limiter is enabled + * and the time since the last call to \ref delay is + * shorter than the target interval. + * \param [in] vsyncEnabled \c true if vsync is enabled + */ + void delay(bool vsyncEnabled); + + /** + * \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 != NtTimerDuration::zero(); + } + + private: + + using TimePoint = dxvk::high_resolution_clock::time_point; + + using NtTimerDuration = std::chrono::duration>; + using NtQueryTimerResolutionProc = UINT (WINAPI *) (ULONG*, ULONG*, ULONG*); + using NtSetTimerResolutionProc = UINT (WINAPI *) (ULONG, BOOL, ULONG*); + using NtDelayExecutionProc = UINT (WINAPI *) (BOOL, LARGE_INTEGER*); + + std::mutex m_mutex; + + NtTimerDuration m_targetInterval = NtTimerDuration::zero(); + NtTimerDuration m_refreshInterval = NtTimerDuration::zero(); + NtTimerDuration m_deviation = NtTimerDuration::zero(); + TimePoint m_lastFrame; + + bool m_initialized = false; + + NtTimerDuration m_sleepGranularity = NtTimerDuration::zero(); + NtTimerDuration m_sleepThreshold = NtTimerDuration::zero(); + + NtDelayExecutionProc NtDelayExecution = nullptr; + + TimePoint sleep(TimePoint t0, NtTimerDuration duration); + + void initialize(); + + }; + +}