[util] Move platform-specific sleep code to dedicated helper class

This commit is contained in:
Philip Rebohle 2022-09-15 13:41:03 +02:00
parent 000a647c56
commit c1ab09a048
5 changed files with 206 additions and 112 deletions

View File

@ -6,6 +6,7 @@ util_src = files([
'util_luid.cpp',
'util_matrix.cpp',
'util_shared_res.cpp',
'util_sleep.cpp',
'thread.cpp',

View File

@ -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<TimerDuration>(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<TimerDuration>(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<NtDelayExecutionProc>(
::GetProcAddress(ntdll, "NtDelayExecution"));
auto NtQueryTimerResolution = reinterpret_cast<NtQueryTimerResolutionProc>(
::GetProcAddress(ntdll, "NtQueryTimerResolution"));
auto NtSetTimerResolution = reinterpret_cast<NtSetTimerResolutionProc>(
::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
}
}

View File

@ -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<int64_t, std::ratio<1, 10000000>>;
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);
};
}

125
src/util/util_sleep.cpp Normal file
View File

@ -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<NtDelayExecutionProc>(
::GetProcAddress(ntdll, "NtDelayExecution"));
auto NtQueryTimerResolution = reinterpret_cast<NtQueryTimerResolutionProc>(
::GetProcAddress(ntdll, "NtQueryTimerResolution"));
auto NtSetTimerResolution = reinterpret_cast<NtSetTimerResolutionProc>(
::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<TimerDuration>(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<TimerDuration>(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
}
}

78
src/util/util_sleep.h Normal file
View File

@ -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<typename Rep, typename Period>
static TimePoint sleepFor(TimePoint t0, std::chrono::duration<Rep, Period> duration) {
return s_instance.sleep(t0, std::chrono::duration_cast<TimerDuration>(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<bool> m_initialized = { false };
#ifdef _WIN32
// On Windows, we use NtDelayExecution which has units of 100ns.
using TimerDuration = std::chrono::duration<int64_t, std::ratio<1, 10000000>>;
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);
};
}