From 149cb6d89e6c907cf96c28fc7be8cd661fc03a4b Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:56:54 -0700 Subject: [PATCH] Give concurrent Windows sleepers independent timers --- runtime/CMakeLists.txt | 4 ++ runtime/src/host_time.c | 57 +++++++++++++++++--------- runtime/tests/test_host_time_threads.c | 45 ++++++++++++++++++++ 3 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 runtime/tests/test_host_time_threads.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index f1c45888..5e6cc7c1 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -26,6 +26,10 @@ if(BUILD_TESTING) # Windows UTF-8 text boundary (issue #371): the helpers legacy Windows # relies on, and — with the shipped manifest — a UTF-8 process code page. if(WIN32) + add_executable(host_time_threads_test tests/test_host_time_threads.c src/host_time.c) + target_include_directories(host_time_threads_test PRIVATE include) + add_test(NAME host_time_threads_test COMMAND host_time_threads_test) + set_tests_properties(host_time_threads_test PROPERTIES TIMEOUT 15) add_executable(win_utf8_helpers_test tests/test_win_utf8.cpp) target_include_directories(win_utf8_helpers_test PRIVATE include) add_test(NAME win_utf8_helpers_test COMMAND win_utf8_helpers_test) diff --git a/runtime/src/host_time.c b/runtime/src/host_time.c index c70cd94b..cbd5664d 100644 --- a/runtime/src/host_time.c +++ b/runtime/src/host_time.c @@ -21,31 +21,49 @@ #endif static LARGE_INTEGER s_qpc_freq; -static int s_qpc_freq_init; -static HANDLE s_sleep_timer; -static int s_sleep_timer_init; +static INIT_ONCE s_time_once = INIT_ONCE_STATIC_INIT; +static DWORD s_sleep_timer_slot = FLS_OUT_OF_INDEXES; -static void sleep_timer_ensure(void) +static void CALLBACK sleep_timer_close(void *timer) { - if (s_sleep_timer_init) - return; - s_sleep_timer_init = 1; - s_sleep_timer = + if (timer) CloseHandle((HANDLE)timer); +} + +static BOOL CALLBACK time_initialize(PINIT_ONCE once, PVOID parameter, PVOID *context) +{ + (void)once; (void)parameter; (void)context; + if (!QueryPerformanceFrequency(&s_qpc_freq)) s_qpc_freq.QuadPart = 0; + /* An auto-reset timer has one pending deadline and wakes one waiter. + * Sharing it lets a render/present worker cancel the guest pacer's wait. + * FLS also releases the handle when its thread/guest fiber is destroyed. */ + s_sleep_timer_slot = FlsAlloc(sleep_timer_close); + return TRUE; +} + +static HANDLE sleep_timer_get(void) +{ + HANDLE timer; + if (!InitOnceExecuteOnce(&s_time_once, time_initialize, NULL, NULL) || + s_sleep_timer_slot == FLS_OUT_OF_INDEXES) + return NULL; + timer = (HANDLE)FlsGetValue(s_sleep_timer_slot); + if (timer) return timer; + timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); - if (!s_sleep_timer) - s_sleep_timer = CreateWaitableTimerW(NULL, FALSE, NULL); + if (!timer) timer = CreateWaitableTimerW(NULL, FALSE, NULL); + if (timer && !FlsSetValue(s_sleep_timer_slot, timer)) { + CloseHandle(timer); + return NULL; + } + return timer; } uint64_t psx_host_mono_ms(void) { LARGE_INTEGER counter; - if (!s_qpc_freq_init) { - if (!QueryPerformanceFrequency(&s_qpc_freq)) - s_qpc_freq.QuadPart = 0; - s_qpc_freq_init = 1; - } + InitOnceExecuteOnce(&s_time_once, time_initialize, NULL, NULL); if (s_qpc_freq.QuadPart == 0) return (uint64_t)GetTickCount64(); if (!QueryPerformanceCounter(&counter)) @@ -57,18 +75,19 @@ void psx_host_sleep_micros(unsigned usec) { LARGE_INTEGER due; LONGLONG hundred_ns; + HANDLE timer; if (usec == 0u) return; - sleep_timer_ensure(); - if (s_sleep_timer) { + timer = sleep_timer_get(); + if (timer) { hundred_ns = -((LONGLONG)usec * 10LL); if (hundred_ns >= 0) hundred_ns = -1LL; due.QuadPart = hundred_ns; - if (SetWaitableTimer(s_sleep_timer, &due, 0, NULL, NULL, FALSE)) { - (void)WaitForSingleObject(s_sleep_timer, INFINITE); + if (SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE) && + WaitForSingleObject(timer, INFINITE) == WAIT_OBJECT_0) { return; } } diff --git a/runtime/tests/test_host_time_threads.c b/runtime/tests/test_host_time_threads.c new file mode 100644 index 00000000..1197fc69 --- /dev/null +++ b/runtime/tests/test_host_time_threads.c @@ -0,0 +1,45 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "host_time.h" + +static HANDLE start_event; +static volatile LONG errors; +static DWORD WINAPI sleeper(void *arg) { + (void)arg; + WaitForSingleObject(start_event, INFINITE); + uint64_t previous = psx_host_mono_ms(); + for (unsigned i=0; i<250; ++i) { + psx_host_sleep_micros(1000); + uint64_t now = psx_host_mono_ms(); + if (now < previous) InterlockedIncrement(&errors); + previous = now; + } + return 0; +} +int main(void) { + HANDLE threads[8]; + DWORD before, after; + if (!GetProcessHandleCount(GetCurrentProcess(), &before)) return 2; + start_event = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!start_event) return 2; + for (unsigned i=0; i<8; ++i) { + threads[i] = CreateThread(NULL, 0, sleeper, NULL, 0, NULL); + if (!threads[i]) return 2; + } + SetEvent(start_event); + DWORD result = WaitForMultipleObjects(8, threads, TRUE, 10000); + if (result != WAIT_OBJECT_0) { + puts("FAIL: concurrent sleepers lost a wakeup"); + return 1; + } + for (unsigned i=0; i<8; ++i) CloseHandle(threads[i]); + CloseHandle(start_event); + if (!GetProcessHandleCount(GetCurrentProcess(), &after) || after > before) { + puts("FAIL: sleeper timer handles leaked after thread exit"); + return 1; + } + if (errors) return 1; + puts("host time concurrent sleepers: PASS"); + return 0; +}