mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-09 03:29:43 -10:00
gfx/video_driver: recover the frame delay target's sub-millisecond remainder
The Frame Delay target is derived from the frame time and is therefore
rarely a whole number of milliseconds: 3/4 of a 60 Hz frame is 12.5 ms,
and 100% of a 144 Hz frame is 6.944 ms. It was being truncated into a
uint8_t millisecond value, so up to 999 us was discarded on every single
frame - 6% of a frame at 60 Hz, and 14% at 144 Hz. For a feature whose
entire purpose is shaving milliseconds off input latency, that is a
large constant giveaway.
Add retro_sleep_us() to libretro-common and use it here.
libretro-common/include/retro_timers.h:
retro_sleep_us() mirrors the retro_sleep() platform chain condition for
condition, in the same order, so it resolves on exactly the same set of
platforms. Most targets already have a microsecond- or nanosecond-native
primitive and honour the request as given (Vita, 3DS, Xenon, PS3, Wii,
PSL1GHT, QNX, and the nanosleep default). Targets that can only express
whole milliseconds - WinRT, Xbox, Wii U, Emscripten - round to nearest,
so a sub-500 us request degenerates to a yield rather than being rounded
up into an unwanted full millisecond of added latency.
libretro-common/time/rtime.c:
On desktop Windows retro_sleep_us() becomes the primitive and
retro_sleep() is expressed in terms of it. The waitable timer added in
5c30a403 takes 100 ns units natively, so this needs no new mechanism -
only the multiplier changes, from usec*10 rather than msec*10000. The
Sleep() fallback rounds to nearest, which for a whole-millisecond
request is exact, so the fallback path is unchanged in behaviour. The
millisecond wrapper guards the *1000 conversion rather than wrapping
around, so a bogus value cannot turn into a short sleep.
gfx/video_driver.c:
The auto tuner works in whole millisecond steps (delay_decrease is 1, 2
or 3) and frame_delay_effective is displayed in the menu as an integer,
so both are deliberately left alone. Only the truncated fraction is
recovered: it is derived from the same float expression that produces
the integer target, which is left character for character unchanged, so
the target itself is bit for bit identical.
Computing the target directly in microseconds instead was tried first
and rejected - reordering the float operations changes the rounding, and
90% at 60 Hz came out as 14 ms rather than 15 ms, silently shifting the
value fed to the tuner and shown in the menu.
The remainder is only added while the delay is still at the target. If
the auto tuner has reduced the delay it did so because frames were being
missed, and time must not be handed back.
Verified: integer target identical to the previous code across
{59.94, 60, 50, 75, 144, 30} Hz x {20, 25, 50, 66, 75, 90, 100}% and the
auto 3/4 rule, remainder always within [0, 999]. C89 clean
(-std=c89 -pedantic) for rtime.c 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; POSIX path unchanged; import
table still carries nothing newer than Windows 95. The retro_sleep and
retro_sleep_us preprocessor chains were diffed branch by branch to
confirm they resolve identically.
Not verified: no Windows host available here, so the actual sleep
timings are untested and no sanitizer sweep was run on the Windows-only
code. Both need doing before merge.
This commit is contained in:
1 parent
5c30a403be
commit
4f3513ee34
3 files changed
+103
-12
No files matched your search
+32
-3
@@ -6007,6 +6007,8 @@ void video_frame_delay(video_driver_state_t *video_st,
|
||||
float refresh_rate = settings->floats.video_refresh_rate;
|
||||
uint8_t video_frame_delay = settings->uints.video_frame_delay;
|
||||
uint8_t video_frame_delay_effective = video_st->frame_delay_effective;
|
||||
float video_frame_delay_ms = 0.0f;
|
||||
uint16_t video_frame_delay_remainder= 0;
|
||||
uint8_t video_swap_interval = runloop_get_video_swap_interval(settings->uints.video_swap_interval);
|
||||
uint8_t video_bfi = settings->uints.video_black_frame_insertion;
|
||||
uint8_t shader_subframes = settings->uints.video_shader_subframes;
|
||||
@@ -6015,12 +6017,29 @@ void video_frame_delay(video_driver_state_t *video_st,
|
||||
|| (runloop_st->flags & RUNLOOP_FLAG_FASTMOTION)
|
||||
|| state_manager_frame_is_reversed();
|
||||
|
||||
/* The delay target is derived from the frame time and is therefore
|
||||
* rarely a whole number of milliseconds - 3/4 of a 60 Hz frame is
|
||||
* 12.5 ms. The auto tuner below works in whole millisecond steps, so
|
||||
* the target is still carried as milliseconds, but the sub-millisecond
|
||||
* remainder is kept here and added back when actually sleeping instead
|
||||
* of being truncated away. At 60 Hz that recovers up to 6% of a frame. */
|
||||
|
||||
/* Treat values 20+ as frame time percentage */
|
||||
if (video_frame_delay >= 20)
|
||||
video_frame_delay = 1 / refresh_rate * 1000 * (video_frame_delay / 100.0f);
|
||||
video_frame_delay_ms = 1 / refresh_rate * 1000 * (video_frame_delay / 100.0f);
|
||||
/* Set 0 (Auto) delay target as 3/4 frame time */
|
||||
else if (video_frame_delay == 0 && settings->bools.video_frame_delay_auto)
|
||||
video_frame_delay = 1 / refresh_rate * 1000 * 0.75f;
|
||||
video_frame_delay_ms = 1 / refresh_rate * 1000 * 0.75f;
|
||||
|
||||
if (video_frame_delay_ms > 0.0f)
|
||||
{
|
||||
video_frame_delay = (uint8_t)video_frame_delay_ms;
|
||||
/* Keep the truncated fraction so it can be added back when
|
||||
* sleeping. Derived from the same value that produced the integer
|
||||
* target above, so the target itself is bit for bit unchanged. */
|
||||
video_frame_delay_remainder = (uint16_t)
|
||||
((video_frame_delay_ms - (float)video_frame_delay) * 1000.0f);
|
||||
}
|
||||
|
||||
/* Black frame insertion + swap interval multiplier */
|
||||
refresh_rate = (refresh_rate / (video_bfi + 1.0f) / video_swap_interval / shader_subframes);
|
||||
@@ -6115,7 +6134,17 @@ void video_frame_delay(video_driver_state_t *video_st,
|
||||
|
||||
/* Never apply frame delay when slow/fastmotion is active */
|
||||
if (video_frame_delay_effective > 0 && !skip_delay)
|
||||
retro_sleep(video_frame_delay_effective);
|
||||
{
|
||||
unsigned delay_us = (unsigned)video_frame_delay_effective * 1000;
|
||||
|
||||
/* Only add the sub-millisecond remainder while the delay is still
|
||||
* at the target. If the auto tuner has reduced it, frames were
|
||||
* being missed, and time must not be handed back. */
|
||||
if (video_frame_delay_effective == video_st->frame_delay_target)
|
||||
delay_us += video_frame_delay_remainder;
|
||||
|
||||
retro_sleep_us(delay_us);
|
||||
}
|
||||
}
|
||||
|
||||
void video_driver_scanline_init(void)
|
||||
|
||||
@@ -137,4 +137,53 @@ static INLINE void retro_sleep(unsigned msec)
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Briefly suspends the running thread, with microsecond resolution.
|
||||
*
|
||||
* Platforms whose sleep primitive is microsecond- or nanosecond-native
|
||||
* honour the request as given. Platforms that can only express whole
|
||||
* milliseconds round to nearest, so requests below 500 us degenerate to
|
||||
* a yield rather than an unwanted full millisecond of latency.
|
||||
*
|
||||
* @param usec The time to sleep for, in microseconds.
|
||||
**/
|
||||
#if defined(VITA)
|
||||
#define retro_sleep_us(usec) (sceKernelDelayThread((usec)))
|
||||
#elif defined(_3DS)
|
||||
#define retro_sleep_us(usec) (svcSleepThread(1000 * (s64)(usec)))
|
||||
#elif defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP
|
||||
#define retro_sleep_us(usec) (SleepEx(((usec) + 500) / 1000, FALSE))
|
||||
#elif defined(_WIN32) && !defined(_XBOX)
|
||||
/* Desktop Windows: see the note on retro_sleep() above. Implemented in
|
||||
* libretro-common/time/rtime.c, where it is the primitive that
|
||||
* retro_sleep() itself is expressed in terms of. */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
void retro_sleep_us(unsigned usec);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#elif defined(_WIN32)
|
||||
#define retro_sleep_us(usec) (Sleep(((usec) + 500) / 1000))
|
||||
#elif defined(XENON)
|
||||
#define retro_sleep_us(usec) (udelay((usec)))
|
||||
#elif !defined(__PSL1GHT__) && defined(__PS3__)
|
||||
#define retro_sleep_us(usec) (sys_timer_usleep((usec)))
|
||||
#elif defined(GEKKO) || defined(__PSL1GHT__) || defined(__QNX__)
|
||||
#define retro_sleep_us(usec) (usleep((usec)))
|
||||
#elif defined(WIIU)
|
||||
#define retro_sleep_us(usec) (OSSleepTicks(ms_to_ticks(((usec) + 500) / 1000)))
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
#define retro_sleep_us(usec) (retro_sleep(((usec) + 500) / 1000))
|
||||
#else
|
||||
static INLINE void retro_sleep_us(unsigned usec)
|
||||
{
|
||||
struct timespec tv;
|
||||
tv.tv_sec = usec / 1000000;
|
||||
tv.tv_nsec = (usec % 1000000) * 1000;
|
||||
nanosleep(&tv, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -159,15 +159,15 @@ static HANDLE rtime_sleep_timer_get(void)
|
||||
return timer;
|
||||
}
|
||||
|
||||
void retro_sleep(unsigned msec)
|
||||
void retro_sleep_us(unsigned usec)
|
||||
{
|
||||
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)
|
||||
/* A zero duration means "yield the rest of this time slice", which
|
||||
* must not be emulated with a timer. */
|
||||
if (!usec)
|
||||
{
|
||||
Sleep(0);
|
||||
return;
|
||||
@@ -180,7 +180,7 @@ void retro_sleep(unsigned msec)
|
||||
* spinning on the result. */
|
||||
if (InterlockedCompareExchange(&rtime_sleep_init_ran, 1, 0) != 0)
|
||||
{
|
||||
Sleep(msec);
|
||||
Sleep((usec + 500) / 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,26 +190,39 @@ void retro_sleep(unsigned msec)
|
||||
if ( rtime_sleep_state != RTIME_SLEEP_HIGHRES
|
||||
|| !(timer = rtime_sleep_timer_get()))
|
||||
{
|
||||
Sleep(msec);
|
||||
Sleep((usec + 500) / 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Negative due time == relative, in 100 ns units */
|
||||
due.QuadPart = -((LONGLONG)msec * 10000);
|
||||
due.QuadPart = -((LONGLONG)usec * 10);
|
||||
|
||||
if (!rtime_set_waitable_timer(timer, &due, 0, NULL, NULL, FALSE))
|
||||
{
|
||||
Sleep(msec);
|
||||
Sleep((usec + 500) / 1000);
|
||||
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;
|
||||
timeout = (usec / 1000) + 100;
|
||||
WaitForSingleObject(timer, timeout);
|
||||
}
|
||||
|
||||
void retro_sleep(unsigned msec)
|
||||
{
|
||||
/* Guard the conversion rather than wrapping around. Nothing sleeps
|
||||
* for 49 days, but a bogus value must not turn into a short sleep. */
|
||||
if (msec > 0xFFFFFFFFU / 1000)
|
||||
{
|
||||
Sleep(msec);
|
||||
return;
|
||||
}
|
||||
|
||||
retro_sleep_us(msec * 1000);
|
||||
}
|
||||
|
||||
/* 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
|
||||
|
||||
Reference in new issue
Block a user