runloop: name the frame pacing sources

The loop is held to a rate by four mechanisms, each of which decides
on its own whether to block, and none of which knows about the others:

  video_vsync          present blocks in the video driver
  audio_sync           audio_driver_write() blocks on the buffer
  video_scanline_sync  after_frame waits on the vblank-locked target
  frame limit          a timer sleep at the end of runloop_iterate()

They are not exclusive and they are not prioritised. Any combination
can be live on the same frame, which is why VSync together with
audio_sync stutters - two hardware clocks holding one loop - and why
the RUNLOOP_STATE_MENU throttle has a comment special-casing audio
backpressure against the timer. Each overlap that has been found so
far was handled where it was found.

This does not change which of them fires. It records the composition
for the current frame in runloop_state_t::pace as a bitmask, computed
once from the same conditions each mechanism already tests, and gates
the timer sleep on its bit. The timer's condition is copied verbatim,
so the gate is boolean-equivalent to the inline test it replaces; the
other three still block inside their own subsystems and are only
recorded here.

A bitmask rather than an enum deliberately. A single value would
assert that the loop chooses one source, and it does not. Making that
choice - hardware clocks over the timer, one hardware clock at a time
- is a behaviour change with a configuration-dependent blast radius,
and it belongs in its own commit argued on its own. This gives it one
place to be made.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 04:54:27 +02:00
1 parent 6869b9df65
commit 542c5abdbc
2 files changed
+51 -1

No files matched your search

+20 -1
View File
@@ -8263,9 +8263,21 @@ end:
runloop_set_frame_limit(&video_st->av_info, 1.0f);
}
/* if there's a fast forward limit, inject sleeps to keep from going too fast. */
/* Record which sources hold the pace this frame. The other three
* block inside their own subsystems; only the timer is decided
* here. See enum runloop_pace_source. */
runloop_st->pace = RUNLOOP_PACE_NONE;
if (settings->bools.video_vsync)
runloop_st->pace |= RUNLOOP_PACE_VSYNC;
if ( settings->bools.audio_sync
&& (AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_ACTIVE))
runloop_st->pace |= RUNLOOP_PACE_AUDIO;
if ( settings->bools.video_scanline_sync
&& video_st->scanline[SCANLINE_NEXT])
runloop_st->pace |= RUNLOOP_PACE_SCANLINE;
{
retro_time_t frame_limit_min = runloop_st->frame_limit_minimum_time;
/* Identical to the condition the sleep below used inline. */
if ( (frame_limit_min)
&& ( (vrr_runloop_enable)
|| (runloop_st->flags & RUNLOOP_FLAG_FASTMOTION)
@@ -8275,6 +8287,13 @@ end:
|| !(runloop_st->flags & RUNLOOP_FLAG_FOCUSED)))
#endif
|| (runloop_st->flags & RUNLOOP_FLAG_PAUSED)))
runloop_st->pace |= RUNLOOP_PACE_TIMER;
}
/* if there's a fast forward limit, inject sleeps to keep from going too fast. */
{
retro_time_t frame_limit_min = runloop_st->frame_limit_minimum_time;
if (runloop_st->pace & RUNLOOP_PACE_TIMER)
{
const retro_time_t end_frame_time = cpu_features_get_time_usec();
const retro_time_t to_sleep_us = (
+31
View File
@@ -181,6 +181,7 @@ struct runloop
retro_time_t core_run_time;
retro_time_t frame_limit_minimum_time;
retro_time_t frame_limit_last_time;
unsigned pace; /* enum runloop_pace_source bits */
retro_usec_t frame_time_last; /* int64_t alignment */
/* Per-frame scalar state. Kept adjacent to the timing block above so the
@@ -342,6 +343,36 @@ struct runloop
bool content_closing;
};
/* Frame pacing sources.
*
* The loop is held to a rate by whichever of these block on a given
* frame. They are not exclusive and they are not prioritised: VSync
* blocks in the video driver's present, audio backpressure blocks in
* audio_driver_write(), Scanline Sync waits in
* video_driver_scanline_after_frame(), and the frame-limit timer sleeps
* at the end of runloop_iterate(). Any combination can be live on the
* same frame, which is why VSync together with audio_sync stutters -
* two hardware clocks holding the same loop - and why the comment at
* the RUNLOOP_STATE_MENU throttle special-cases audio backpressure
* against the timer.
*
* runloop_state_t::pace records the composition for the current
* frame, computed once in runloop_pace_compute() from the same
* conditions each mechanism already tests. It does not yet choose
* between them: this is the composition made explicit, so that a
* priority can be decided in one place later rather than by adding a
* fourth special case in a third file. A bitmask rather than an enum
* for exactly that reason - a single value would assert a choice the
* loop does not currently make. */
enum runloop_pace_source
{
RUNLOOP_PACE_NONE = 0,
RUNLOOP_PACE_VSYNC = (1 << 0), /* display: present blocks */
RUNLOOP_PACE_AUDIO = (1 << 1), /* audio crystal: write blocks */
RUNLOOP_PACE_SCANLINE = (1 << 2), /* display: vblank-locked wait */
RUNLOOP_PACE_TIMER = (1 << 3) /* CPU counter: frame-limit sleep */
};
typedef struct runloop runloop_state_t;
RETRO_BEGIN_DECLS