audio: the pipeline consumer waits for device room before it takes from the ring

audio_driver_pipeline_consume() sizes the chunk from what the ring
holds, waits for the device to have room for it, and only then reads
it out and tells the producer the ring has drained. A device that
makes no room gets nothing taken on its behalf: the ring stays full,
the producer's one bounded wait finds it so, pipe_stalled sticks, and
frames run at full rate dropping audio until a pass completes. Taking
the chunk first and then finding no room threw it away while reporting
the ring drained and the stall cleared, so the producer waited again
next frame - the frontend paced at one frame per failed pass, around
two a second, for as long as the device was stuck. While paused the
chunk is still taken and dropped, so the ring keeps flowing.

Measured against a JACK server frozen with SIGSTOP mid-run, with a
core producing audio: the stall is reported once, pipe_gen holds, and
the frame rate is unaffected; before, the main thread sat in the
producer's one-second wait every frame. Clean under ASan/UBSan/LSan
and TSan across jack, pulse and sdl2, pipeline off and on. C89
checked.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-02 18:31:00 +02:00
1 parent abcdc03a0b
commit 8f13068b79
1 file changed
+34 -16
+34 -16
View File
@@ -2497,6 +2497,40 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
if (cap >= 64 && have > cap)
have = cap;
}
snap = retro_atomic_load_acquire_int(&audio_st->runloop_snapshot);
if ( (snap & AUDIO_SNAP_PAUSED)
|| !(AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_ACTIVE)
|| !audio_st->output_samples_buf)
{
/* Nothing is going to the device while paused; the chunk is
* taken and dropped so the ring keeps flowing for the producer. */
retro_spsc_read(&audio_st->pipe_ring, audio_st->pipe_scratch,
have * sizeof(int16_t));
slock_lock(audio_st->pipe_lock);
audio_st->pipe_gen++;
scond_signal(audio_st->pipe_cond);
slock_unlock(audio_st->pipe_lock);
return;
}
/* Second wait: room for this chunk, so the write inside flush()
* returns at once and the fill rate control reads before it is the
* device's real fill, as it is for the inline path. The chunk is
* one publish's worth resampled, so a little more than have*ratio;
* wait for that with a small margin.
*
* Waited for before the chunk leaves the ring. A device that makes
* no room gets nothing taken on its behalf: the ring stays full, the
* producer's one bounded wait finds it so, and it drops at full
* frame rate until a pass completes. Taking the chunk first and then
* finding no room threw it away while telling the producer the ring
* had drained, and paced the frontend at one frame per failed pass. */
out_bytes = (size_t)((double)(have >> 1) * audio_st->src_ratio_curr + 16.0)
* frame_bytes;
if (!audio->wait_writable(audio_st->context_audio_data, out_bytes))
return;
retro_spsc_read(&audio_st->pipe_ring, audio_st->pipe_scratch,
have * sizeof(int16_t));
@@ -2508,22 +2542,6 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
scond_signal(audio_st->pipe_cond);
slock_unlock(audio_st->pipe_lock);
snap = retro_atomic_load_acquire_int(&audio_st->runloop_snapshot);
if ( (snap & AUDIO_SNAP_PAUSED)
|| !(AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_ACTIVE)
|| !audio_st->output_samples_buf)
return;
/* Second wait: room for this chunk, so the write inside flush()
* returns at once and the fill rate control reads before it is the
* device's real fill, as it is for the inline path. The chunk is
* one publish's worth resampled, so a little more than have*ratio;
* wait for that with a small margin. */
out_bytes = (size_t)((double)(have >> 1) * audio_st->src_ratio_curr + 16.0)
* frame_bytes;
if (!audio->wait_writable(audio_st->context_audio_data, out_bytes))
return;
audio_driver_state_lock();
audio_driver_flush(audio_st,
config_get_ptr()->floats.slowmotion_ratio,