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..4fb5f435 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,21 @@ 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); + /* 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; } } diff --git a/runtime/tests/test_host_time_threads.c b/runtime/tests/test_host_time_threads.c new file mode 100644 index 00000000..a8eae238 --- /dev/null +++ b/runtime/tests/test_host_time_threads.c @@ -0,0 +1,57 @@ +#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; +} +static int run_wave(void) { + HANDLE threads[8]; + 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); + 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) { + printf("FAIL: sleeper timer handles leaked after thread exit: before=%lu after=%lu\n", before, after); + return 1; + } + if (errors) return 1; + puts("host time concurrent sleepers: PASS"); + return 0; +}