mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
os: implement win10 high-resolution timer as replacement for Sleep() / sleep_for() (#682)
## Link to GitHub Issue or related Pull Request, if one exists Fixes #681 ## Description of change `Sleep` and `sleep_for()` can be very inaccurate and varies depending on what the OS gives us... ### `timeBeginPeriod(1)` On boot, we are now calling `timeBeginPeriod(1)`, which affects the whole process but makes `Sleep` more accurate. There is some risk here if any game was relying on doing things like `Sleep(1)` and expecting it to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the game engine, though not the whole time, so I'm hoping that this is not too impactful. ### Opt out of Win11 power throttling Ensure that timer resolution change above is respected even when the window is occluded / minimized by opting out of throttling via `PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`. ### Use Win10 high resolution timer instead of Sleep On Win10 1803 and above, there is a new OS-level API for high resolution timers; if this is available, use it (`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE doesn't support this currently. If not, fall back to `Sleep`, which is significantly better than `sleep_for()` in my experiments. Callers of Sleep / sleep_for were replaced with this new timer. Most of them anyway; calls to Sleep() with more than 100ms+ was left alone. ### Add an option as a chicken bit To opt out I'm adding a new option called `Use Legacy Timers` which will revert to behavior before this PR. The code paths that switched from `sleep_for` to `Sleep` will remain in place though, not affected by the option. ## Expected changes In some I/O emulation modules, poll threads may run more frequently, resulting in lower latency. It also means that spice overall may use more CPU resources and power. If you don't like this, you can always enable the option to opt out; e.g., if you're on old arcade cab PC. ## Testing DDR p4io - ok drs touch hook - ok IIDX camera hook - ok CCJ trackball - ok
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
#define _WIN32_WINNT 0x0602
|
||||
|
||||
#include "precise_timer.h"
|
||||
|
||||
#define WIN32_NO_STATUS
|
||||
#include <windows.h>
|
||||
#undef WIN32_NO_STATUS
|
||||
|
||||
#include <timeapi.h>
|
||||
|
||||
#if !SPICE_XP
|
||||
#include <processthreadsapi.h>
|
||||
#endif
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "util/logging.h"
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
#endif
|
||||
|
||||
#ifdef max
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
#define PERIOD 1
|
||||
#define TOLERANCE 0.02
|
||||
|
||||
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
|
||||
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x4
|
||||
#endif
|
||||
|
||||
namespace timeutils {
|
||||
|
||||
bool TIMER_HACKS_DISABLE = false;
|
||||
|
||||
static TimerHandle get_precise_sleep_timer();
|
||||
static void destroy_precise_sleep_timer(TimerHandle timer);
|
||||
static void precise_sleep(TimerHandle timer, double seconds);
|
||||
|
||||
void set_timer_resolution() {
|
||||
if (TIMER_HACKS_DISABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
// make a call to opt out of power throttling
|
||||
// (requested timer resolution being ignored when window is occluded or minimized)
|
||||
// SetProcessInformation is win8+
|
||||
const auto kernel32 = GetModuleHandleA("kernel32.dll");
|
||||
if (kernel32) {
|
||||
const auto SetProcessInformation_addr =
|
||||
reinterpret_cast<decltype(SetProcessInformation) *>(
|
||||
GetProcAddress(kernel32, "SetProcessInformation"));
|
||||
|
||||
if (SetProcessInformation_addr) {
|
||||
PROCESS_POWER_THROTTLING_STATE state {};
|
||||
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
|
||||
state.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
|
||||
state.StateMask = 0;
|
||||
const auto ret = SetProcessInformation_addr(
|
||||
GetCurrentProcess(),
|
||||
ProcessPowerThrottling,
|
||||
&state,
|
||||
sizeof(state));
|
||||
|
||||
log_info(
|
||||
"timeutils",
|
||||
"SetProcessInformation called to disable timer resolution throttling, returned {}",
|
||||
ret);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
const auto ret = timeBeginPeriod(PERIOD);
|
||||
log_info(
|
||||
"timeutils",
|
||||
"timeBeginPeriod({}) called, returned {}",
|
||||
PERIOD,
|
||||
ret);
|
||||
// and then we never call timeEndPeriod();
|
||||
// the game will also call this but the OS will honor the lowest value (1 above)
|
||||
}
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
// timerSleep from https://blog.bearcats.nl/perfect-sleep-function/
|
||||
static void timerSleep(HANDLE timer, double seconds) {
|
||||
using namespace std::chrono;
|
||||
|
||||
auto t = high_resolution_clock::now();
|
||||
const auto target = t + nanoseconds(int64_t(seconds * 1e9));
|
||||
const int64_t maxTicks = PERIOD * 9'500;
|
||||
for (;;) {
|
||||
const int64_t remaining = (target - t).count();
|
||||
int64_t ticks = (remaining - TOLERANCE) / 100;
|
||||
if (ticks <= 0) {
|
||||
break;
|
||||
}
|
||||
if (ticks > maxTicks) {
|
||||
ticks = maxTicks;
|
||||
}
|
||||
|
||||
LARGE_INTEGER due;
|
||||
due.QuadPart = -ticks;
|
||||
SetWaitableTimerEx(timer, &due, 0, NULL, NULL, NULL, 0);
|
||||
WaitForSingleObject(timer, INFINITE);
|
||||
t = high_resolution_clock::now();
|
||||
}
|
||||
|
||||
while (high_resolution_clock::now() < target) {
|
||||
YieldProcessor();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static TimerHandle get_precise_sleep_timer() {
|
||||
TimerHandle timer = nullptr;
|
||||
if (TIMER_HACKS_DISABLE) {
|
||||
return timer;
|
||||
}
|
||||
|
||||
// CreateWaitableTimerExW is Vista+
|
||||
// CREATE_WAITABLE_TIMER_HIGH_RESOLUTION win10 1803+
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
timer = CreateWaitableTimerExW(
|
||||
NULL,
|
||||
NULL,
|
||||
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
|
||||
TIMER_ALL_ACCESS);
|
||||
|
||||
#endif
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
static void destroy_precise_sleep_timer(TimerHandle timer) {
|
||||
if (timer) {
|
||||
CloseHandle(timer);
|
||||
}
|
||||
}
|
||||
|
||||
static void precise_sleep(TimerHandle timer, double seconds) {
|
||||
if (!std::isfinite(seconds) || seconds <= 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if !SPICE_XP
|
||||
if (timer) {
|
||||
timerSleep(timer, seconds);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// robustSleep is too CPU heavy so we will stick to Sleep() with
|
||||
// timeBeginPeriod, which has an error at most 1ms
|
||||
const auto milliseconds = std::ceil(seconds * 1000.0);
|
||||
if (milliseconds >= std::numeric_limits<DWORD>::max()) {
|
||||
Sleep(std::numeric_limits<DWORD>::max());
|
||||
return;
|
||||
}
|
||||
|
||||
Sleep(static_cast<DWORD>(milliseconds));
|
||||
}
|
||||
|
||||
// RAII wrapper
|
||||
|
||||
PreciseSleepTimer::PreciseSleepTimer() : timer(get_precise_sleep_timer()) {}
|
||||
|
||||
PreciseSleepTimer::~PreciseSleepTimer() {
|
||||
destroy_precise_sleep_timer(timer);
|
||||
}
|
||||
|
||||
void PreciseSleepTimer::sleep(uint32_t ms) const {
|
||||
sleep(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
void PreciseSleepTimer::sleep_seconds(double seconds) const {
|
||||
precise_sleep(timer, seconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace timeutils {
|
||||
|
||||
extern bool TIMER_HACKS_DISABLE;
|
||||
|
||||
void set_timer_resolution();
|
||||
|
||||
using TimerHandle = void *;
|
||||
|
||||
class PreciseSleepTimer {
|
||||
public:
|
||||
PreciseSleepTimer();
|
||||
~PreciseSleepTimer();
|
||||
|
||||
PreciseSleepTimer(const PreciseSleepTimer &) = delete;
|
||||
PreciseSleepTimer &operator=(const PreciseSleepTimer &) = delete;
|
||||
PreciseSleepTimer(PreciseSleepTimer &&) = delete;
|
||||
PreciseSleepTimer &operator=(PreciseSleepTimer &&) = delete;
|
||||
|
||||
void sleep(uint32_t ms) const;
|
||||
|
||||
template <typename Rep, typename Period>
|
||||
void sleep(std::chrono::duration<Rep, Period> duration) const {
|
||||
sleep_seconds(std::chrono::duration<double>(duration).count());
|
||||
}
|
||||
|
||||
private:
|
||||
void sleep_seconds(double seconds) const;
|
||||
|
||||
TimerHandle timer;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user