1
0
mirror of https://github.com/EduApps-CDG/OpenDX synced 2024-12-30 09:45:37 +01:00

[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.
This commit is contained in:
Philip Rebohle 2021-06-09 03:43:19 +02:00 committed by Philip Rebohle
parent 6f468ec5e0
commit a16c861358
3 changed files with 249 additions and 0 deletions

View File

@ -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',

View File

@ -0,0 +1,159 @@
#include <thread>
#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<std::mutex> 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<std::mutex> 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<std::mutex> 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<NtTimerDuration>(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<NtTimerDuration>(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<NtTimerDuration>(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<NtTimerDuration>(t1 - t0);
t0 = t1;
}
return t1;
}
void FpsLimiter::initialize() {
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 = 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;
}
}

View File

@ -0,0 +1,89 @@
#pragma once
#include <mutex>
#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<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*);
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();
};
}