From 71ba9b6b476085e26e80a4ecd8b8e7b9fd133044 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 8 May 2026 02:25:20 -0700 Subject: [PATCH] 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 --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/acio/mdxf/mdxf.cpp | 9 +- src/spice2x/acioemu/icca.cpp | 4 +- src/spice2x/api/modules/keypads.cpp | 6 +- src/spice2x/api/serial.cpp | 4 +- src/spice2x/games/ccj/trackball.cpp | 4 +- src/spice2x/games/ddr/p3io/p3io.cpp | 4 +- src/spice2x/games/ddr/p4io/p4io.cpp | 4 +- src/spice2x/games/drs/drs.cpp | 4 +- src/spice2x/games/iidx/bi2a.cpp | 4 +- src/spice2x/games/iidx/bi2x.cpp | 4 +- src/spice2x/games/iidx/local_camera.cpp | 4 +- src/spice2x/games/iidx/poke.cpp | 4 +- src/spice2x/games/nost/poke.cpp | 4 +- src/spice2x/games/onpara/touchpanel.cpp | 8 +- .../hooks/audio/implementations/wave_out.cpp | 4 +- src/spice2x/launcher/launcher.cpp | 8 + src/spice2x/launcher/options.cpp | 12 +- src/spice2x/launcher/options.h | 1 + src/spice2x/misc/eamuse.cpp | 15 +- src/spice2x/rawinput/rawinput.cpp | 6 +- src/spice2x/reader/reader.cpp | 14 +- src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp | 2 +- src/spice2x/util/precise_timer.cpp | 188 ++++++++++++++++++ src/spice2x/util/precise_timer.h | 36 ++++ 25 files changed, 320 insertions(+), 34 deletions(-) create mode 100644 src/spice2x/util/precise_timer.cpp create mode 100644 src/spice2x/util/precise_timer.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 0836a18..ccca346 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -636,6 +636,7 @@ set(SOURCE_FILES ${SOURCE_FILES} util/time.cpp util/cpuutils.cpp util/netutils.cpp + util/precise_timer.cpp util/sysutils.cpp util/lz77.cpp util/tapeled.cpp diff --git a/src/spice2x/acio/mdxf/mdxf.cpp b/src/spice2x/acio/mdxf/mdxf.cpp index 67648fc..655c46e 100644 --- a/src/spice2x/acio/mdxf/mdxf.cpp +++ b/src/spice2x/acio/mdxf/mdxf.cpp @@ -1,14 +1,16 @@ #include "mdxf.h" #include "mdxf_poll.h" +#include + #include "avs/game.h" #include "games/ddr/ddr.h" #include "games/ddr/io.h" #include "launcher/launcher.h" #include "rawinput/rawinput.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "util/utils.h" -#include #define MDFX_DEBUG_VERBOSE 0 @@ -109,9 +111,10 @@ static void mdxf_thread_start() { MDXF_THREAD = std::thread([] { SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); + timeutils::PreciseSleepTimer timer; while (MDXF_THREAD_RUNNING.load(std::memory_order_acquire)) { mdxf_poll(false); - std::this_thread::sleep_for(THREAD_PERIOD); + timer.sleep(THREAD_PERIOD); } }); } @@ -512,4 +515,4 @@ acio::MDXFModule::~MDXFModule() { if (IS_THREAD_NEEDED) { mdxf_thread_stop(); } -} \ No newline at end of file +} diff --git a/src/spice2x/acioemu/icca.cpp b/src/spice2x/acioemu/icca.cpp index 4454177..ff003ed 100644 --- a/src/spice2x/acioemu/icca.cpp +++ b/src/spice2x/acioemu/icca.cpp @@ -5,6 +5,7 @@ #include "games/sdvx/sdvx.h" #include "misc/eamuse.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "util/utils.h" using namespace acioemu; @@ -46,11 +47,12 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count) // keypad thread for faster polling if (keypad_thread) { this->keypad_thread = new std::thread([this]() { + timeutils::PreciseSleepTimer timer; while (this->cards) { for (int unit = 0; unit < this->node_count; unit++) { this->update_keypad(unit, false); } - Sleep(7); + timer.sleep(7); } }); } diff --git a/src/spice2x/api/modules/keypads.cpp b/src/spice2x/api/modules/keypads.cpp index 8980f03..0e20e4b 100644 --- a/src/spice2x/api/modules/keypads.cpp +++ b/src/spice2x/api/modules/keypads.cpp @@ -7,6 +7,7 @@ #include "avs/game.h" #include "external/rapidjson/document.h" #include "misc/eamuse.h" +#include "util/precise_timer.h" using namespace std::placeholders; using namespace rapidjson; @@ -58,6 +59,7 @@ namespace api::modules { // get params auto keypad = req.params[0].GetUint(); auto input = std::string(req.params[1].GetString()); + timeutils::PreciseSleepTimer timer; // process all chars for (auto c : input) { @@ -91,11 +93,11 @@ namespace api::modules { // set eamuse_set_keypad_overrides(keypad, state); - Sleep(sleep_time); + timer.sleep(sleep_time); // unset eamuse_set_keypad_overrides(keypad, 0); - Sleep(sleep_time); + timer.sleep(sleep_time); } } diff --git a/src/spice2x/api/serial.cpp b/src/spice2x/api/serial.cpp index b1beee6..70ba1b1 100644 --- a/src/spice2x/api/serial.cpp +++ b/src/spice2x/api/serial.cpp @@ -5,6 +5,7 @@ #include #include "util/logging.h" +#include "util/precise_timer.h" #include "util/utils.h" @@ -16,6 +17,7 @@ namespace api { controller->init_state(this->state); this->thread = new std::thread([this] () { log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud); + timeutils::PreciseSleepTimer timer; // read buffer uint8_t read_buffer[16*1024]; @@ -160,7 +162,7 @@ namespace api { // slow down on reconnect if (this->running) { - Sleep(retry_time); + timer.sleep(retry_time); } } }); diff --git a/src/spice2x/games/ccj/trackball.cpp b/src/spice2x/games/ccj/trackball.cpp index 5485f94..3c6a320 100644 --- a/src/spice2x/games/ccj/trackball.cpp +++ b/src/spice2x/games/ccj/trackball.cpp @@ -6,6 +6,7 @@ #include "util/logging.h" #include "rawinput/rawinput.h" #include "games/io.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "io.h" @@ -243,6 +244,7 @@ namespace games::ccj { log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE); tbThread = new std::thread([&] { + timeutils::PreciseSleepTimer timer; while (tbThreadRunning) { if (hWnd && wndProc) { wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle); @@ -251,7 +253,7 @@ namespace games::ccj { if (!tbThreadRunning) break; - std::this_thread::sleep_for(10ms); + timer.sleep(10); } }); } diff --git a/src/spice2x/games/ddr/p3io/p3io.cpp b/src/spice2x/games/ddr/p3io/p3io.cpp index 2c2f624..06ae29c 100644 --- a/src/spice2x/games/ddr/p3io/p3io.cpp +++ b/src/spice2x/games/ddr/p3io/p3io.cpp @@ -3,6 +3,7 @@ #include "cfg/api.h" #include "rawinput/rawinput.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "../ddr.h" @@ -459,7 +460,8 @@ int games::ddr::DDRP3IOHandle::device_io( if (nOutBufferSize >= 4) { // cool down - Sleep(1); + static thread_local timeutils::PreciseSleepTimer timer; + timer.sleep(1); // get controls as single variable (4 bytes) auto &controls = *(uint32_t*) lpOutBuffer; diff --git a/src/spice2x/games/ddr/p4io/p4io.cpp b/src/spice2x/games/ddr/p4io/p4io.cpp index dd13a90..4b9c2ef 100644 --- a/src/spice2x/games/ddr/p4io/p4io.cpp +++ b/src/spice2x/games/ddr/p4io/p4io.cpp @@ -2,6 +2,7 @@ #include "rawinput/rawinput.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "../io.h" @@ -255,7 +256,8 @@ namespace games::ddr { case P4IO_IOCTL_GET_INPUTS: { // Prevents this function from being called at its normal 2000-2500 kHz cadence and overloading the CPU, instead reduces it to 250 Hz - Sleep(4); + static thread_local timeutils::PreciseSleepTimer timer; + timer.sleep(4); memset(lpOutBuffer, 0, 16); auto controls = (uint32_t*) lpOutBuffer; diff --git a/src/spice2x/games/drs/drs.cpp b/src/spice2x/games/drs/drs.cpp index 585e2b1..7d2bf6a 100644 --- a/src/spice2x/games/drs/drs.cpp +++ b/src/spice2x/games/drs/drs.cpp @@ -7,6 +7,7 @@ #include "games/game.h" #include "util/detour.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "util/memutils.h" #include "rgb_cam.h" @@ -221,10 +222,11 @@ namespace games::drs { void start_touch() { std::thread t([] { log_info("drs", "starting touch input thread"); + timeutils::PreciseSleepTimer timer; // main loop while (TRUE) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + timer.sleep(1); TOUCH_EVENTS.clear(); touch_get_events(TOUCH_EVENTS); diff --git a/src/spice2x/games/iidx/bi2a.cpp b/src/spice2x/games/iidx/bi2a.cpp index fff4962..6e8531d 100644 --- a/src/spice2x/games/iidx/bi2a.cpp +++ b/src/spice2x/games/iidx/bi2a.cpp @@ -2,6 +2,7 @@ #include "misc/eamuse.h" #include "rawinput/rawinput.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "iidx.h" @@ -82,7 +83,8 @@ bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg( } // sleep - otherwise the IO thread will go too hard on the CPU - Sleep(1); + static thread_local timeutils::PreciseSleepTimer timer; + timer.sleep(1); // generate message auto msg = this->create_msg(msg_in, 0x2E); diff --git a/src/spice2x/games/iidx/bi2x.cpp b/src/spice2x/games/iidx/bi2x.cpp index b719701..71270c3 100644 --- a/src/spice2x/games/iidx/bi2x.cpp +++ b/src/spice2x/games/iidx/bi2x.cpp @@ -2,6 +2,7 @@ #include "misc/eamuse.h" #include "rawinput/rawinput.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "iidx.h" @@ -82,7 +83,8 @@ bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg( } // sleep - otherwise the IO thread will go too hard on the CPU - Sleep(1); + static thread_local timeutils::PreciseSleepTimer timer; + timer.sleep(1); // generate message auto msg = this->create_msg(msg_in, 0x2E); diff --git a/src/spice2x/games/iidx/local_camera.cpp b/src/spice2x/games/iidx/local_camera.cpp index 4e01d42..0d64239 100644 --- a/src/spice2x/games/iidx/local_camera.cpp +++ b/src/spice2x/games/iidx/local_camera.cpp @@ -3,6 +3,7 @@ #if SPICE64 && !SPICE_XP #include "util/logging.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "mf_wrappers.h" @@ -399,6 +400,7 @@ namespace games::iidx { void IIDXLocalCamera::CreateThread() { // Create thread m_drawThread = new std::thread([this]() { + timeutils::PreciseSleepTimer timer; SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); double accumulator = 0.0; @@ -412,7 +414,7 @@ namespace games::iidx { accumulator -= 1.0; floorFrameTimeMicroSec += 1; } - std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec)); + timer.sleep(std::chrono::microseconds(floorFrameTimeMicroSec)); } }); } diff --git a/src/spice2x/games/iidx/poke.cpp b/src/spice2x/games/iidx/poke.cpp index e0dc816..97a133c 100644 --- a/src/spice2x/games/iidx/poke.cpp +++ b/src/spice2x/games/iidx/poke.cpp @@ -15,6 +15,7 @@ #include "touch/touch.h" #include "util/libutils.h" #include "util/logging.h" +#include "util/precise_timer.h" #define POKE_NATIVE_TOUCH 0 @@ -200,6 +201,7 @@ namespace games::iidx::poke { // create new thread THREAD_RUNNING = true; THREAD = new std::thread([] { + timeutils::PreciseSleepTimer timer; // log log_info("poke", "enabled"); @@ -319,7 +321,7 @@ namespace games::iidx::poke { } // slow down - Sleep(50); + timer.sleep(50); } return nullptr; diff --git a/src/spice2x/games/nost/poke.cpp b/src/spice2x/games/nost/poke.cpp index 64876cc..799a6e4 100644 --- a/src/spice2x/games/nost/poke.cpp +++ b/src/spice2x/games/nost/poke.cpp @@ -9,6 +9,7 @@ #include "misc/eamuse.h" #include "touch/touch.h" #include "util/logging.h" +#include "util/precise_timer.h" namespace games::nost::poke { @@ -63,6 +64,7 @@ namespace games::nost::poke { // create new thread THREAD_RUNNING = true; THREAD = new std::thread([] { + timeutils::PreciseSleepTimer timer; const DWORD touch_id = (DWORD)(0xFFFFFFFE); const int swipe_anim_total_frames = 6; @@ -178,7 +180,7 @@ namespace games::nost::poke { } // slow down - Sleep(30); + timer.sleep(30); } return nullptr; diff --git a/src/spice2x/games/onpara/touchpanel.cpp b/src/spice2x/games/onpara/touchpanel.cpp index 0805658..a153d7f 100644 --- a/src/spice2x/games/onpara/touchpanel.cpp +++ b/src/spice2x/games/onpara/touchpanel.cpp @@ -4,6 +4,7 @@ #include "touchpanel.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "touch/touch.h" using namespace std::chrono_literals; @@ -44,6 +45,9 @@ bool games::onpara::TouchPanelHandle::open(LPCWSTR lpFileName) { } int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { + + static thread_local timeutils::PreciseSleepTimer timer; + DWORD i; auto buffer = reinterpret_cast(lpBuffer); @@ -67,7 +71,7 @@ int games::onpara::TouchPanelHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesT enqueue_packet(report); // prevent cpu bullying - std::this_thread::sleep_for(1ms); + timer.sleep(1); } // copy from output queue @@ -133,4 +137,4 @@ bool games::onpara::TouchPanelHandle::close() { log_info("touchpanel", "Closed COM1 (Touch Panel)"); return true; -} \ No newline at end of file +} diff --git a/src/spice2x/hooks/audio/implementations/wave_out.cpp b/src/spice2x/hooks/audio/implementations/wave_out.cpp index cee1697..ab58de3 100644 --- a/src/spice2x/hooks/audio/implementations/wave_out.cpp +++ b/src/spice2x/hooks/audio/implementations/wave_out.cpp @@ -3,6 +3,7 @@ #include "hooks/audio/audio.h" #include "hooks/audio/backends/wasapi/audio_client.h" #include "hooks/audio/backends/wasapi/defs.h" +#include "util/precise_timer.h" static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME; @@ -185,6 +186,7 @@ HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppDa } HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) { bool written = false; + timeutils::PreciseSleepTimer timer; // reset the dispatcher event ResetEvent(this->dispatcher_event); @@ -209,7 +211,7 @@ HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwF // avoid pegging the CPU if (!written) { - Sleep(1); + timer.sleep(1); } } diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index e3a7db1..57ca956 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -108,6 +108,7 @@ #include "util/libutils.h" #include "util/logging.h" #include "util/peb.h" +#include "util/precise_timer.h" #include "util/socd_cleaner.h" #include "util/sysutils.h" #include "util/tapeled.h" @@ -1273,6 +1274,10 @@ int main_implementation(int argc, char *argv[]) { games::loveplus::CAMERA_ENABLE = true; } + if (options[launcher::Options::DisableHighResTimer].value_bool()) { + timeutils::TIMER_HACKS_DISABLE = true; + } + // API debugging if (api_debug && !cfg::CONFIGURATOR_STANDALONE) { API_CONTROLLER = std::make_unique(api_port, api_pass, api_pretty); @@ -2227,6 +2232,9 @@ int main_implementation(int argc, char *argv[]) { hooks::lang::early_init(); } + // use high resolution timer for this process + timeutils::set_timer_resolution(); + // load DLLs avs::core::load_dll(); avs::ea3::load_dll(); diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 42944cf..43b3175 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -2769,7 +2769,17 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "Otoca D'or", .category = "Game Options (Peripherals)", - } + }, + { + // DisableHighResTimer + .title = "Use Legacy Timers", + .name = "notimerhacks", + .desc = "Disables high resolution timers, reverting to legacy behavior. " + "Recommended that you leave this OFF for optimal input latency, " + "unless you're on a resource-constrained system (e.g., old cabinet PC)", + .type = OptionType::Bool, + .category = "Development" + }, }; const std::vector &launcher::get_categories(Options::OptionsCategory category) { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 49974c5..e023cc3 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -279,6 +279,7 @@ namespace launcher { LovePlusPrinterJPGQuality, OptionConflictResolution, OtocaCamHook, + DisableHighResTimer, }; enum class OptionsCategory { diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index d1c9ec8..467d812 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -9,6 +9,7 @@ #include "rawinput/rawinput.h" #include "games/sdvx/sdvx.h" #include "util/logging.h" +#include "util/precise_timer.h" #include "util/time.h" #include "util/utils.h" #include "overlay/overlay.h" @@ -286,6 +287,7 @@ void eamuse_coin_start_thread() { COIN_INPUT_THREAD = new std::thread([]() { auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game()); static bool COIN_INPUT_KEY_STATE = false; + timeutils::PreciseSleepTimer timer; while (COIN_INPUT_THREAD_ACTIVE) { // check input key @@ -305,7 +307,7 @@ void eamuse_coin_start_thread() { } // once every two frames - Sleep(1000 / 30); + timer.sleep(1000 / 30); } }); } @@ -340,6 +342,7 @@ void eamuse_pin_macro_start_thread() { size_t pin_index[2] = {PIN_MACRO_VALUES[0].length(), PIN_MACRO_VALUES[1].length()}; std::optional active_unit = std::nullopt; + timeutils::PreciseSleepTimer timer; while (PIN_MACRO_THREAD_ACTIVE) { // wait for key press @@ -359,7 +362,7 @@ void eamuse_pin_macro_start_thread() { } if (!active_unit.has_value()) { - Sleep(20); + timer.sleep(20); continue; } } @@ -375,22 +378,22 @@ void eamuse_pin_macro_start_thread() { eamuse_set_keypad_overrides(unit, keypad_overrides[char_index]); } pin_index[unit]++; - Sleep(100); + timer.sleep(100); // clear eamuse_set_keypad_overrides(unit, 0); - Sleep(50); + timer.sleep(50); // end of PIN if (pin_index[unit] == PIN_MACRO_VALUES[unit].length()) { active_unit = std::nullopt; - Sleep(120); + timer.sleep(120); } continue; } - Sleep(200); + timer.sleep(200); } }); } diff --git a/src/spice2x/rawinput/rawinput.cpp b/src/spice2x/rawinput/rawinput.cpp index e13c9e5..7dbf27c 100644 --- a/src/spice2x/rawinput/rawinput.cpp +++ b/src/spice2x/rawinput/rawinput.cpp @@ -9,6 +9,7 @@ #include "util/logging.h" #include "external/robin_hood.h" +#include "util/precise_timer.h" #include "util/time.h" #include "util/utils.h" @@ -155,8 +156,9 @@ void rawinput::RawInputManager::input_hwnd_create() { }); // wait for window creation being done + timeutils::PreciseSleepTimer timer; while (!this->input_hwnd) { - Sleep(1); + timer.sleep(1); } } @@ -2983,4 +2985,4 @@ void rawinput::RawInputManager::remove_callback_midi(void * data, const std::fun [data, callback](MidiCallback const &cb) { return cb.data == data && cb.f.target() == callback.target(); }), this->callback_midi.end()); -} \ No newline at end of file +} diff --git a/src/spice2x/reader/reader.cpp b/src/spice2x/reader/reader.cpp index 7fbe5b0..690ad01 100644 --- a/src/spice2x/reader/reader.cpp +++ b/src/spice2x/reader/reader.cpp @@ -7,6 +7,7 @@ #include "util/logging.h" #include "misc/eamuse.h" +#include "util/precise_timer.h" #include "util/utils.h" #include "structuredmessage.h" @@ -217,6 +218,7 @@ bool Reader::set_comm_state(DWORD BaudRate) { } bool Reader::wait_for_handshake() { + timeutils::PreciseSleepTimer timer; // baud rates DWORD baud_rates[] = { CBR_57600, CBR_38400, CBR_19200, CBR_9600 }; @@ -268,7 +270,7 @@ bool Reader::wait_for_handshake() { } // sleep - Sleep(50); + timer.sleep(50); } log_warning("reader", "{}: no handshake received for {} baud", this->port, baud_rates[i]); @@ -405,6 +407,7 @@ void start_reader_thread(const std::string &port, int id) { READER_THREAD_RUNNING = true; READER_THREADS.push_back(new std::thread([port, id]() { log_info("reader", "{}: starting reader thread", port); + timeutils::PreciseSleepTimer timer; while (READER_THREAD_RUNNING) { @@ -442,21 +445,22 @@ void start_reader_thread(const std::string &port, int id) { } if (did_read_card) { - Sleep(2500); + timer.sleep(2500); } - Sleep(20); + timer.sleep(20); } } // sleep between reader connection retries if (READER_THREAD_RUNNING) - Sleep(5000); + timer.sleep(5000); } })); // wait for thread to start - Sleep(10); + timeutils::PreciseSleepTimer timer; + timer.sleep(10); } void stop_reader_thread() { diff --git a/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp b/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp index c18c0d8..f6c0441 100644 --- a/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp +++ b/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp @@ -93,6 +93,6 @@ static void worker_thread_main(std::stop_token stop_token) { } } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Sleep(1); } } diff --git a/src/spice2x/util/precise_timer.cpp b/src/spice2x/util/precise_timer.cpp new file mode 100644 index 0000000..394a607 --- /dev/null +++ b/src/spice2x/util/precise_timer.cpp @@ -0,0 +1,188 @@ +#define _WIN32_WINNT 0x0602 + +#include "precise_timer.h" + +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS + +#include + +#if !SPICE_XP +#include +#endif + +#include +#include +#include +#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( + 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::max()) { + Sleep(std::numeric_limits::max()); + return; + } + + Sleep(static_cast(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); + } +} diff --git a/src/spice2x/util/precise_timer.h b/src/spice2x/util/precise_timer.h new file mode 100644 index 0000000..788a47a --- /dev/null +++ b/src/spice2x/util/precise_timer.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +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 + void sleep(std::chrono::duration duration) const { + sleep_seconds(std::chrono::duration(duration).count()); + } + + private: + void sleep_seconds(double seconds) const; + + TimerHandle timer; + }; +}