runloop: do not run the frame-limit timer under a live hardware clock

First policy on the pace composition from 542c5abdbc. If vsync, audio
backpressure or Scanline Sync is holding the loop this frame, the
timer sleep does not also run. A loop that is already paced by
hardware gains nothing from a second pace off the CPU counter.

Two bits had to be tightened first for this to be safe, both found by
walking the cases:

  RUNLOOP_PACE_SCANLINE now mirrors the gate at the after_frame call
  site, which skips the wait under fast-forward. Without that the bit
  claimed a live wait during fast-forward and this policy would have
  dropped the limiter there.

  RUNLOOP_PACE_AUDIO now also requires runloop_is_libretro_running().
  In the menu with the core paused the driver stays in blocking mode
  but nothing calls audio_driver_write(), so nothing is held; the bit
  would have suppressed the timer that paces the menu at refresh rate
  and let it spin. This is the predicate the RUNLOOP_STATE_MENU
  throttle already uses for the same question.

With the bits honest, the cases:

  VRR, audio blocking        AUDIO     timer was redundant   -> skipped
  VRR, scanline sync         SCANLINE  timer was redundant   -> skipped
  VRR, audio non-blocking    none      timer is the pace     -> fires
  fast-forward               none      timer is the limiter  -> fires
  menu, vsync off, paused    none      timer paces the menu  -> fires
  vsync on, focused          VSYNC     timer never fired     -> unchanged

Only the first two change, and in both the phase accumulator already
brought to_sleep to zero or below in steady state, so the sleep was
not happening. The one observable difference is at content start under
VRR with audio_sync: before the audio buffer first fills the timer used
to pace those frames and now audio backpressure takes over a few
frames later. That is how audio_sync already behaves without VRR, where
the timer never ran, so VRR now matches it.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 05:05:25 +02:00
1 parent d4af93f8ad
commit a05198f9dc
1 file changed
+27 -4
+27 -4
View File
@@ -8281,10 +8281,22 @@ end:
* audio_sync stays true, and during those frames audio is not
* holding anything. */
if ( (AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_ACTIVE)
&& !(AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_NONBLOCK))
&& !(AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_NONBLOCK)
/* ...and the core is actually writing samples this frame. In
* the menu with the core paused the driver is still in
* blocking mode but nothing calls audio_driver_write(), so
* nothing is held. Same predicate the RUNLOOP_STATE_MENU
* throttle uses for this question. */
&& runloop_is_libretro_running(runloop_st,
settings->bools.menu_pause_libretro))
runloop_st->pace |= RUNLOOP_PACE_AUDIO;
/* Mirrors the gate at the video_driver_scanline_after_frame() call
* site, which also skips the wait under fast-forward. A failed
* calibration zeroes SCANLINE_NEXT, so the target test already
* covers the unlocked case. */
if ( settings->bools.video_scanline_sync
&& video_st->scanline[SCANLINE_NEXT])
&& video_st->scanline[SCANLINE_NEXT]
&& !(input_st->flags & INP_FLAG_NONBLOCKING))
runloop_st->pace |= RUNLOOP_PACE_SCANLINE;
{
retro_time_t frame_limit_min = runloop_st->frame_limit_minimum_time;
@@ -8301,10 +8313,21 @@ end:
runloop_st->pace |= RUNLOOP_PACE_TIMER;
}
/* if there's a fast forward limit, inject sleeps to keep from going too fast. */
/* if there's a fast forward limit, inject sleeps to keep from going too fast.
*
* Only when no hardware clock is holding the loop. If vsync, audio
* backpressure or Scanline Sync is live this frame, the rate is
* already set by hardware and the timer would be pacing a loop
* that is already paced. The three hardware bits read the drivers'
* actual blocking state rather than the settings, so fast-forward -
* which puts both drivers into non-blocking mode with the settings
* unchanged - clears them and the timer remains the limiter there. */
{
retro_time_t frame_limit_min = runloop_st->frame_limit_minimum_time;
if (runloop_st->pace & RUNLOOP_PACE_TIMER)
if ( (runloop_st->pace & RUNLOOP_PACE_TIMER)
&& !(runloop_st->pace & (RUNLOOP_PACE_VSYNC
| RUNLOOP_PACE_AUDIO
| RUNLOOP_PACE_SCANLINE)))
{
const retro_time_t end_frame_time = cpu_features_get_time_usec();
const retro_time_t to_sleep_us = (