From c1ab09a048f503cbdc2ce5ced3ea4461a040e8f6 Mon Sep 17 00:00:00 2001 From: Philip Rebohle Date: Thu, 15 Sep 2022 13:41:03 +0200 Subject: [PATCH] [util] Move platform-specific sleep code to dedicated helper class --- src/util/meson.build | 1 + src/util/util_fps_limiter.cpp | 94 +------------------------ src/util/util_fps_limiter.h | 20 ------ src/util/util_sleep.cpp | 125 ++++++++++++++++++++++++++++++++++ src/util/util_sleep.h | 78 +++++++++++++++++++++ 5 files changed, 206 insertions(+), 112 deletions(-) create mode 100644 src/util/util_sleep.cpp create mode 100644 src/util/util_sleep.h diff --git a/src/util/meson.build b/src/util/meson.build index 50044a00b..55f4482a4 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -6,6 +6,7 @@ util_src = files([ 'util_luid.cpp', 'util_matrix.cpp', 'util_shared_res.cpp', + 'util_sleep.cpp', 'thread.cpp', diff --git a/src/util/util_fps_limiter.cpp b/src/util/util_fps_limiter.cpp index 1bddcd410..8bec7b523 100644 --- a/src/util/util_fps_limiter.cpp +++ b/src/util/util_fps_limiter.cpp @@ -3,6 +3,7 @@ #include "thread.h" #include "util_env.h" #include "util_fps_limiter.h" +#include "util_sleep.h" #include "util_string.h" #include "./log/log.h" @@ -63,7 +64,7 @@ namespace dxvk { // 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; - t1 = sleep(t1, sleepDuration); + 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 @@ -77,100 +78,9 @@ namespace dxvk { } - FpsLimiter::TimePoint FpsLimiter::sleep(TimePoint t0, TimerDuration duration) { - if (duration <= TimerDuration::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. - TimerDuration sleepThreshold = m_sleepThreshold; - - if (m_sleepGranularity != TimerDuration::zero()) - sleepThreshold += duration / 6; - - TimerDuration remaining = duration; - TimePoint t1 = t0; - - while (remaining > sleepThreshold) { - TimerDuration sleepDuration = remaining - sleepThreshold; - - performSleep(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 > TimerDuration::zero()) { - t1 = dxvk::high_resolution_clock::now(); - remaining -= std::chrono::duration_cast(t1 - t0); - t0 = t1; - } - - return t1; - } - - void FpsLimiter::initialize() { - updateSleepGranularity(); - m_sleepThreshold = 4 * m_sleepGranularity; m_lastFrame = dxvk::high_resolution_clock::now(); m_initialized = true; } - - void FpsLimiter::updateSleepGranularity() { -#ifdef _WIN32 - 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 = TimerDuration(cur); - - if (NtSetTimerResolution && !NtSetTimerResolution(max, TRUE, &cur)) { - Logger::info(str::format("Setting timer interval to ", (double(max) / 10.0), " us")); - m_sleepGranularity = TimerDuration(max); - } - } - } else { - // Assume 1ms sleep granularity by default - m_sleepGranularity = TimerDuration(1ms); - } -#else - // Assume 0.5ms sleep granularity by default - m_sleepGranularity = TimerDuration(500us); -#endif - } - - - void FpsLimiter::performSleep(TimerDuration sleepDuration) { -#ifdef _WIN32 - if (NtDelayExecution) { - LARGE_INTEGER ticks; - ticks.QuadPart = -sleepDuration.count(); - - NtDelayExecution(FALSE, &ticks); - } else { - std::this_thread::sleep_for(sleepDuration); - } -#else - std::this_thread::sleep_for(sleepDuration); -#endif - } - } diff --git a/src/util/util_fps_limiter.h b/src/util/util_fps_limiter.h index 0d2d63258..e75cbc706 100644 --- a/src/util/util_fps_limiter.h +++ b/src/util/util_fps_limiter.h @@ -49,18 +49,7 @@ namespace dxvk { private: using TimePoint = dxvk::high_resolution_clock::time_point; - -#ifdef _WIN32 - // On Windows, we use NtDelayExecution which has units of 100ns. - using TimerDuration = std::chrono::duration>; - using NtQueryTimerResolutionProc = UINT (WINAPI *) (ULONG*, ULONG*, ULONG*); - using NtSetTimerResolutionProc = UINT (WINAPI *) (ULONG, BOOL, ULONG*); - using NtDelayExecutionProc = UINT (WINAPI *) (BOOL, LARGE_INTEGER*); - NtDelayExecutionProc NtDelayExecution = nullptr; -#else - // On other platforms, we use the std library, which calls through to nanosleep -- which is ns. using TimerDuration = std::chrono::nanoseconds; -#endif dxvk::mutex m_mutex; @@ -71,17 +60,8 @@ namespace dxvk { bool m_initialized = false; bool m_envOverride = false; - TimerDuration m_sleepGranularity = TimerDuration::zero(); - TimerDuration m_sleepThreshold = TimerDuration::zero(); - - TimePoint sleep(TimePoint t0, TimerDuration duration); - void initialize(); - void updateSleepGranularity(); - - void performSleep(TimerDuration sleepDuration); - }; } diff --git a/src/util/util_sleep.cpp b/src/util/util_sleep.cpp new file mode 100644 index 000000000..b50b65e65 --- /dev/null +++ b/src/util/util_sleep.cpp @@ -0,0 +1,125 @@ +#include "util_sleep.h" +#include "util_string.h" + +#include "./log/log.h" + +using namespace std::chrono_literals; + +namespace dxvk { + + Sleep Sleep::s_instance; + + + Sleep::Sleep() { + + } + + + Sleep::~Sleep() { + + } + + + void Sleep::initialize() { + std::lock_guard lock(m_mutex); + + if (m_initialized.load()) + return; + + initializePlatformSpecifics(); + m_sleepThreshold = 4 * m_sleepGranularity; + + m_initialized.store(true, std::memory_order_release); + } + + + void Sleep::initializePlatformSpecifics() { +#ifdef _WIN32 + 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 = TimerDuration(cur); + + if (NtSetTimerResolution && !NtSetTimerResolution(max, TRUE, &cur)) { + Logger::info(str::format("Setting timer interval to ", (double(max) / 10.0), " us")); + m_sleepGranularity = TimerDuration(max); + } + } + } else { + // Assume 1ms sleep granularity by default + m_sleepGranularity = TimerDuration(1ms); + } +#else + // Assume 0.5ms sleep granularity by default + m_sleepGranularity = TimerDuration(500us); +#endif + } + + + Sleep::TimePoint Sleep::sleep(TimePoint t0, TimerDuration duration) { + if (duration <= TimerDuration::zero()) + return t0; + + // If necessary, initialize function pointers and some values + if (!m_initialized.load(std::memory_order_acquire)) + initialize(); + + // Busy-wait for the last couple of milliseconds since sleeping + // on Windows is highly inaccurate and inconsistent. + TimerDuration sleepThreshold = m_sleepThreshold; + + if (m_sleepGranularity != TimerDuration::zero()) + sleepThreshold += duration / 6; + + TimerDuration remaining = duration; + TimePoint t1 = t0; + + while (remaining > sleepThreshold) { + TimerDuration sleepDuration = remaining - sleepThreshold; + + systemSleep(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 > TimerDuration::zero()) { + t1 = dxvk::high_resolution_clock::now(); + remaining -= std::chrono::duration_cast(t1 - t0); + t0 = t1; + } + + return t1; + } + + + void Sleep::systemSleep(TimerDuration duration) { +#ifdef _WIN32 + if (NtDelayExecution) { + LARGE_INTEGER ticks; + ticks.QuadPart = -duration.count(); + + NtDelayExecution(FALSE, &ticks); + } else { + std::this_thread::sleep_for(duration); + } +#else + std::this_thread::sleep_for(duration); +#endif + } + +} diff --git a/src/util/util_sleep.h b/src/util/util_sleep.h new file mode 100644 index 000000000..dc7fe0fca --- /dev/null +++ b/src/util/util_sleep.h @@ -0,0 +1,78 @@ +#pragma once + +#include "thread.h" +#include "util_time.h" + +namespace dxvk { + + /** + * \brief Utility class for accurate sleeping + */ + class Sleep { + + public: + + using TimePoint = dxvk::high_resolution_clock::time_point; + + ~Sleep(); + + /** + * \brief Sleeps for a given period of time + * + * \param [in] t0 Current time + * \param [in] duration Sleep duration + * \returns Time after sleep has finished + */ + template + static TimePoint sleepFor(TimePoint t0, std::chrono::duration duration) { + return s_instance.sleep(t0, std::chrono::duration_cast(duration)); + } + + /** + * \brief Sleeps until a given time point + * + * Convenience function that sleeps for the + * time difference between t1 and t0. + * \param [in] t0 Current time + * \param [in] t1 Target time + * \returns Time after sleep has finished + */ + static TimePoint sleepUntil(TimePoint t0, TimePoint t1) { + return sleepFor(t0, t1 - t0); + } + + private: + + static Sleep s_instance; + + std::mutex m_mutex; + std::atomic m_initialized = { false }; + +#ifdef _WIN32 + // On Windows, we use NtDelayExecution which has units of 100ns. + using TimerDuration = std::chrono::duration>; + using NtQueryTimerResolutionProc = UINT (WINAPI *) (ULONG*, ULONG*, ULONG*); + using NtSetTimerResolutionProc = UINT (WINAPI *) (ULONG, BOOL, ULONG*); + using NtDelayExecutionProc = UINT (WINAPI *) (BOOL, LARGE_INTEGER*); + NtDelayExecutionProc NtDelayExecution = nullptr; +#else + // On other platforms, we use the std library, which calls through to nanosleep -- which is ns. + using TimerDuration = std::chrono::nanoseconds; +#endif + + TimerDuration m_sleepGranularity = TimerDuration::zero(); + TimerDuration m_sleepThreshold = TimerDuration::zero(); + + Sleep(); + + void initialize(); + + void initializePlatformSpecifics(); + + TimePoint sleep(TimePoint t0, TimerDuration duration); + + void systemSleep(TimerDuration duration); + + }; + +}