libretro-common: high resolution retro_sleep() on desktop Windows

Sleep() rounds up to the global timer period, which is ~15.6 ms by
default - longer than one frame at any common refresh rate. Frame
Delay (video_frame_delay(), gfx/video_driver.c) and Scanline Sync
(video_driver_scanline_after_frame(), same file) both call
retro_sleep() with values as low as 1 ms and are therefore entirely
dependent on that period being lowered.

Nothing in the tree lowers it. The only timeBeginPeriod(1) call is in
rthreads.c, guarded on _WIN32_WINNT < 0x0500 and so compiled out of
every modern build. What has actually been lowering it is the WinMM
MIDI driver: MIDI_DEFAULT_DRIVER is MIDI_WINMM on Windows and
DEFAULT_MIDI_OUTPUT is "Microsoft GS Wavetable Synth", so
winmm_midi_init() opens and starts a MIDI stream on every startup
whether or not a core ever emits a MIDI byte, and a running WinMM
stream holds the process at a 1 ms period.

That is an accident, and it means anything that stops the stream from
being opened - a null MIDI driver, lazy MIDI init - silently disables
both features. It also explains the pre-existing FIXME in
midi_driver_set_all_sounds_off(): DirectSound and XAudio2 sit on
stacks that lower the period as a side effect, WASAPI does not, so on
WASAPI the MIDI stream was the only thing holding it down.

Break the dependency without reintroducing winmm. Rather than request
a global period via timeBeginPeriod(), which is only refcounted in
winmmbase user-mode over a single per-process kernel slot and can
therefore be cleared out from under us by an unrelated timeEndPeriod()
anywhere in the process, use a high resolution waitable timer. It
affects only our own waits and cannot be cancelled by third-party code.

Gating:

- CreateWaitableTimerExW() and SetWaitableTimer() are both resolved
  through GetProcAddress(), so the object carries no import newer than
  Windows 95 and builds against SDKs predating either.
- CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS and
  TLS_OUT_OF_INDEXES are defined locally when absent.
- Availability is probed at runtime by attempting the create, not by
  version check: pre-1803 kernels reject the flag with
  ERROR_INVALID_PARAMETER rather than ignoring it. Any failure at any
  stage falls back to plain Sleep(), which is exactly the current
  behaviour.
- A plain waitable timer is deliberately not used as an intermediate
  fallback; it is no more accurate than Sleep().
- _XBOX and __WINRT__ keep their existing paths untouched.

The timer object is per thread via TlsAlloc(), because
SetWaitableTimer() on a handle another thread is already waiting on
reschedules that thread's wait. The TLS index and the calling thread's
handle are released in rtime_deinit(), so repeated core load/unload
cannot exhaust TLS indices.

retro_sleep(0) keeps Sleep(0) semantics (yield the remainder of the
time slice) rather than being turned into a zero-length timer wait.

Implemented in rtime.c rather than inline in retro_timers.h because of
the per-thread state; rtime.c is already in the unconditional object
list and in griffin.c, so no build system changes are needed.

Verified: C89 clean (-std=c89 -pedantic) under i686-w64-mingw32 and
x86_64-w64-mingw32 with _WIN32_WINNT unset and at 0x0400, 0x0501,
0x0600 and 0x0A00, with and without HAVE_THREADS, and the POSIX path
of rtime.c unchanged. Import table checked to confirm no static
CreateWaitableTimerEx/SetWaitableTimer/winmm dependency. Sanitizers
were not run: the added code is Windows-only and unreachable on the
Linux build - it needs an ASan/TSan sweep on a Windows host before
merge.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-08-09 05:21:15 +02:00
1 parent 258877057f
commit 5c30a403be
2 files changed
+225

No files matched your search

+20
View File
@@ -88,6 +88,26 @@ static int nanosleepDOS(const struct timespec *rqtp, struct timespec *rmtp)
#define retro_sleep(msec) (svcSleepThread(1000000 * (s64)(msec)))
#elif defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP
#define retro_sleep(msec) (SleepEx((msec), FALSE))
#elif defined(_WIN32) && !defined(_XBOX)
/* Desktop Windows.
*
* Sleep() resolves at the granularity of the global timer period,
* which defaults to ~15.6 ms - longer than a single frame at any
* common refresh rate. Frame Delay and Scanline Sync both depend on
* sub-frame sleeps, so this is implemented out of line (in
* libretro-common/time/rtime.c) on top of a high resolution waitable
* timer where the running system provides one, falling back to plain
* Sleep() everywhere else.
*
* NOTE: implemented in rtime.c rather than inline here because the
* backing timer object must be per thread; see the comments there. */
#ifdef __cplusplus
extern "C" {
#endif
void retro_sleep(unsigned msec);
#ifdef __cplusplus
}
#endif
#elif defined(_WIN32)
#define retro_sleep(msec) (Sleep((msec)))
#elif defined(XENON)
+205
View File
@@ -33,6 +33,208 @@
slock_t *rtime_localtime_lock = NULL;
#endif
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
/* Desktop Windows implementation of retro_sleep().
*
* Sleep() rounds up to the global timer period, which is ~15.6 ms by
* default - longer than one frame at any common refresh rate. Until
* now RetroArch got a 1 ms period by accident, because the WinMM MIDI
* driver is the Windows default and a running MIDI stream holds the
* period down for the whole process; anything that stops that stream
* from being opened silently broke Frame Delay and Scanline Sync.
*
* Rather than reintroduce that dependency by requesting a global
* period with timeBeginPeriod(), use a high resolution waitable
* timer. This affects only this process' own waits, needs no winmm,
* and cannot be cancelled out from under us by an unrelated
* timeEndPeriod() elsewhere in the process.
*
* Availability:
* - CREATE_WAITABLE_TIMER_HIGH_RESOLUTION requires Windows 10 1803.
* - CreateWaitableTimerExW() itself requires Windows Vista.
* Both are resolved at runtime and the flag is probed rather than
* version-checked, so this builds and runs unchanged all the way back
* to Windows 95 (and against SDKs predating either); anything that
* cannot satisfy the above simply keeps the old Sleep() behaviour. */
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
#endif
#ifndef TIMER_ALL_ACCESS
#define TIMER_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x3)
#endif
#ifndef TLS_OUT_OF_INDEXES
#define TLS_OUT_OF_INDEXES ((DWORD)0xFFFFFFFF)
#endif
typedef HANDLE (WINAPI *CreateWaitableTimerExW_t)(
LPSECURITY_ATTRIBUTES, LPCWSTR, DWORD, DWORD);
/* The APC routine and completion argument are always NULL here, so
* they are typed as LPVOID to avoid depending on PTIMERAPCROUTINE
* being present in the SDK being built against. */
typedef BOOL (WINAPI *SetWaitableTimer_t)(
HANDLE, const LARGE_INTEGER*, LONG, LPVOID, LPVOID, BOOL);
enum rtime_sleep_state
{
RTIME_SLEEP_UNKNOWN = 0,
RTIME_SLEEP_FALLBACK,
RTIME_SLEEP_HIGHRES
};
static CreateWaitableTimerExW_t rtime_create_waitable_timer_ex = NULL;
static SetWaitableTimer_t rtime_set_waitable_timer = NULL;
static volatile LONG rtime_sleep_state = RTIME_SLEEP_UNKNOWN;
static volatile LONG rtime_sleep_init_ran = 0;
static DWORD rtime_sleep_tls = TLS_OUT_OF_INDEXES;
static void rtime_sleep_init(void)
{
HMODULE kernel32;
HANDLE probe = NULL;
LONG state = RTIME_SLEEP_FALLBACK;
DWORD tls = TLS_OUT_OF_INDEXES;
if ((kernel32 = GetModuleHandleA("kernel32.dll")))
{
rtime_create_waitable_timer_ex = (CreateWaitableTimerExW_t)
GetProcAddress(kernel32, "CreateWaitableTimerExW");
rtime_set_waitable_timer = (SetWaitableTimer_t)
GetProcAddress(kernel32, "SetWaitableTimer");
}
/* Pre-1803 kernels reject the high resolution flag outright with
* ERROR_INVALID_PARAMETER instead of silently ignoring it, so a
* successful create is the capability test. Note that a plain
* (non high resolution) waitable timer is deliberately not used as
* a fallback: it is no more accurate than Sleep(), and would only
* add a wait object to the path for nothing. */
if (rtime_create_waitable_timer_ex && rtime_set_waitable_timer)
{
if ((probe = rtime_create_waitable_timer_ex(NULL, NULL,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
TIMER_ALL_ACCESS)))
{
CloseHandle(probe);
if ((tls = TlsAlloc()) != TLS_OUT_OF_INDEXES)
{
rtime_sleep_tls = tls;
state = RTIME_SLEEP_HIGHRES;
}
}
}
InterlockedExchange(&rtime_sleep_state, state);
}
/* The timer object must not be shared between threads: SetWaitableTimer()
* on a handle that another thread is already waiting on reschedules that
* thread's wait. One handle per thread, created on first use. */
static HANDLE rtime_sleep_timer_get(void)
{
HANDLE timer = (HANDLE)TlsGetValue(rtime_sleep_tls);
if (!timer)
{
if (!(timer = rtime_create_waitable_timer_ex(NULL, NULL,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
TIMER_ALL_ACCESS)))
return NULL;
if (!TlsSetValue(rtime_sleep_tls, timer))
{
CloseHandle(timer);
return NULL;
}
}
return timer;
}
void retro_sleep(unsigned msec)
{
HANDLE timer;
LARGE_INTEGER due;
DWORD timeout;
/* Sleep(0) means "yield the rest of this time slice", which is not
* a duration and must not be emulated with a timer. */
if (!msec)
{
Sleep(0);
return;
}
if (rtime_sleep_state == RTIME_SLEEP_UNKNOWN)
{
/* First caller performs the probe; any thread that races it
* takes the plain Sleep() path for this one call rather than
* spinning on the result. */
if (InterlockedCompareExchange(&rtime_sleep_init_ran, 1, 0) != 0)
{
Sleep(msec);
return;
}
rtime_sleep_init();
}
if ( rtime_sleep_state != RTIME_SLEEP_HIGHRES
|| !(timer = rtime_sleep_timer_get()))
{
Sleep(msec);
return;
}
/* Negative due time == relative, in 100 ns units */
due.QuadPart = -((LONGLONG)msec * 10000);
if (!rtime_set_waitable_timer(timer, &due, 0, NULL, NULL, FALSE))
{
Sleep(msec);
return;
}
/* Bounded rather than INFINITE. A timer that was successfully set
* is guaranteed to signal, but there is no reason for this to be
* an unbounded wait if it somehow does not. */
timeout = (msec < 0xFFFFFF00) ? (DWORD)(msec + 100) : INFINITE;
WaitForSingleObject(timer, timeout);
}
/* Called from rtime_deinit(), i.e. main thread only, at program or
* core termination - by which point no other thread may still be
* calling retro_sleep(). Timer handles belonging to threads that have
* already exited are reclaimed by the OS. */
static void rtime_sleep_deinit(void)
{
HANDLE timer;
if (rtime_sleep_tls == TLS_OUT_OF_INDEXES)
return;
if ((timer = (HANDLE)TlsGetValue(rtime_sleep_tls)))
{
TlsSetValue(rtime_sleep_tls, NULL);
CloseHandle(timer);
}
TlsFree(rtime_sleep_tls);
rtime_sleep_tls = TLS_OUT_OF_INDEXES;
InterlockedExchange(&rtime_sleep_state, RTIME_SLEEP_UNKNOWN);
InterlockedExchange(&rtime_sleep_init_ran, 0);
}
#endif
/* Must be called before using rtime_localtime() */
void rtime_init(void)
{
@@ -46,6 +248,9 @@ void rtime_init(void)
/* Must be called upon program termination */
void rtime_deinit(void)
{
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
rtime_sleep_deinit();
#endif
#ifdef HAVE_THREADS
if (rtime_localtime_lock)
{