mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
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:
1 parent
258877057f
commit
5c30a403be
2 files changed
+225
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user