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 1/3] 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; +} From 7d06facf1846622b1f7c7e4c4c973b0c8b7509ce Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:58:36 -0700 Subject: [PATCH 2/3] Check sleeper timer cleanup across repeated thread lifetimes --- runtime/tests/test_host_time_threads.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/runtime/tests/test_host_time_threads.c b/runtime/tests/test_host_time_threads.c index 1197fc69..a8eae238 100644 --- a/runtime/tests/test_host_time_threads.c +++ b/runtime/tests/test_host_time_threads.c @@ -17,10 +17,8 @@ static DWORD WINAPI sleeper(void *arg) { } return 0; } -int main(void) { +static int run_wave(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) { @@ -35,8 +33,22 @@ int main(void) { } for (unsigned i=0; i<8; ++i) CloseHandle(threads[i]); CloseHandle(start_event); + return errors ? 1 : 0; +} +int main(void) { + /* The cold wave still tests concurrent InitOnce initialization. Measure + * handle growth only after process/CRT/Windows lazy initialization, then + * repeat whole thread lifetimes: a leaked caller timer grows every wave. */ + int result = run_wave(); + if (result) return result; + DWORD before, after; + if (!GetProcessHandleCount(GetCurrentProcess(), &before)) return 2; + for (int wave = 0; wave < 3; ++wave) { + result = run_wave(); + if (result) return result; + } if (!GetProcessHandleCount(GetCurrentProcess(), &after) || after > before) { - puts("FAIL: sleeper timer handles leaked after thread exit"); + printf("FAIL: sleeper timer handles leaked after thread exit: before=%lu after=%lu\n", before, after); return 1; } if (errors) return 1; From 33dfa2d1347e9ed4dc05fbde909fe0c5ca9dac8f Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:01:16 -0700 Subject: [PATCH 3/3] host_time: bound the Windows timer wait Even with per-thread timers, an INFINITE wait turns any missed signal into an indefinitely parked thread. Wait at most the requested sleep plus 2 ms. Co-Authored-By: Claude Opus 5.5 --- runtime/src/host_time.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtime/src/host_time.c b/runtime/src/host_time.c index cbd5664d..4fb5f435 100644 --- a/runtime/src/host_time.c +++ b/runtime/src/host_time.c @@ -86,8 +86,10 @@ void psx_host_sleep_micros(unsigned usec) if (hundred_ns >= 0) hundred_ns = -1LL; due.QuadPart = hundred_ns; - if (SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE) && - WaitForSingleObject(timer, INFINITE) == WAIT_OBJECT_0) { + /* Bounded: a missed signal costs at most the sleep plus 2 ms, never + * an indefinitely parked thread. */ + if (SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE)) { + (void)WaitForSingleObject(timer, (DWORD)(usec / 1000u) + 2u); return; } }