Merge remote-tracking branch 'origin/fix/windows-sleeper-timers' into local/base-fixes

This commit is contained in:
Shokunin
2026-10-22 12:45:53 -07:00
3 changed files with 101 additions and 19 deletions
+4
View File
@@ -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)
+40 -19
View File
@@ -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;
}
}
+57
View File
@@ -0,0 +1,57 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#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;
}