microphone: the outgoing sample queue is a retro_spsc ring; the flush runs unlocked

The queue from the capture worker to the core's retro_microphone_read()
was a fifo_buffer_t under fifo_lock, and the worker held that lock
across the whole of microphone_driver_flush() - the device read, the
dual-mono up-channel, the resampler and the write - so the core's
read, which needs the same lock to take its frames, queued behind a
slice's resampling every time the two met.  On the frame-synchronous
path (no worker) the same thread flushes and reads, so the lock was
never the issue there.

One producer, one consumer, either way: replace it with a
retro_spsc_t.  The worker's flush runs without the lock and its room
check is a producer-side query; the core's read takes what is there
without the lock.  fifo_lock and fifo_cond stay for the two bounded
waits and the signals, as they were.  retro_spsc rounds capacity up
to a power of two; outgoing_size keeps the size asked for, the
flush's room is measured against it, and the "asked for more than
the queue holds" check compares to it.

audio_driver.o compiles through the real build rule; the eight
audio samples that #include audio_driver.c build and pass under
ASan+UBSan.  There is no microphone harness and no capture device
here, so the mic path itself is not run.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-11 04:38:29 +02:00
1 parent f79caf9168
commit afacc8c8ee
2 files changed
+56 -34

No files matched your search

+43 -30
View File
@@ -6352,10 +6352,10 @@ static void mic_driver_microphone_handle_free(retro_microphone_t *microphone, bo
microphone->microphone_context = NULL;
}
if (microphone->outgoing_samples)
if (microphone->outgoing_init)
{
fifo_free(microphone->outgoing_samples);
microphone->outgoing_samples = NULL;
retro_spsc_free(&microphone->outgoing_samples);
microphone->outgoing_init = false;
}
if (microphone->resampler && microphone->resampler->free && microphone->resampler_data)
@@ -6537,8 +6537,10 @@ static bool mic_driver_open_mic_internal(retro_microphone_t* microphone)
if (!mic_driver_allocate_frames(mic_st))
goto error;
microphone->outgoing_samples = fifo_new(max_samples * sizeof(int16_t));
if (!microphone->outgoing_samples)
microphone->outgoing_size = max_samples * sizeof(int16_t);
microphone->outgoing_init = retro_spsc_init(&microphone->outgoing_samples,
microphone->outgoing_size);
if (!microphone->outgoing_init)
goto error;
microphone->microphone_context = mic_driver->open_mic(driver_context,
@@ -6783,6 +6785,15 @@ bool microphone_driver_get_mic_state(const retro_microphone_t *microphone)
* @param[out] frames The buffer in which the core will receive microphone samples.
* @param num_frames The size of \c frames, in samples.
*/
/* Room in the outgoing ring, in bytes, against the size asked for:
* retro_spsc rounds capacity up to a power of two. */
static size_t microphone_outgoing_room(const retro_microphone_t *microphone)
{
size_t room = retro_spsc_write_avail(&microphone->outgoing_samples);
size_t excess = microphone->outgoing_samples.capacity - microphone->outgoing_size;
return room > excess ? room - excess : 0;
}
static size_t microphone_driver_flush(
microphone_driver_state_t *mic_st,
retro_microphone_t *microphone,
@@ -6840,16 +6851,16 @@ static size_t microphone_driver_flush(
{ /* If the mic's native rate is practically the same as the requested one... */
/* ...then skip the resampler, since it'll produce (more or less) identical results. */
frames_to_enqueue = MIN(FIFO_WRITE_AVAIL(microphone->outgoing_samples) / sizeof(int16_t), resampler_data.input_frames);
frames_to_enqueue = MIN(microphone_outgoing_room(microphone) / sizeof(int16_t), resampler_data.input_frames);
/* If this mic provides floating-point samples... */
if (sample_size == sizeof(float))
{
convert_float_to_s16(mic_st->final_frames, (const float*)mic_st->input_frames, resampler_data.input_frames);
fifo_write(microphone->outgoing_samples, mic_st->final_frames, frames_to_enqueue * sizeof(int16_t));
retro_spsc_write(&microphone->outgoing_samples, mic_st->final_frames, frames_to_enqueue * sizeof(int16_t));
}
else
fifo_write(microphone->outgoing_samples, mic_st->input_frames, frames_to_enqueue * sizeof(int16_t));
retro_spsc_write(&microphone->outgoing_samples, mic_st->input_frames, frames_to_enqueue * sizeof(int16_t));
return frames_to_enqueue;
}
@@ -6896,9 +6907,9 @@ static size_t microphone_driver_flush(
mic_st->final_frames[n] = mic_st->resampled_frames_int16[n << 1];
frames_to_enqueue = MIN(
FIFO_WRITE_AVAIL(microphone->outgoing_samples) / sizeof(int16_t),
microphone_outgoing_room(microphone) / sizeof(int16_t),
s16.output_frames);
fifo_write(microphone->outgoing_samples, mic_st->final_frames,
retro_spsc_write(&microphone->outgoing_samples, mic_st->final_frames,
frames_to_enqueue * sizeof(int16_t));
return frames_to_enqueue;
}
@@ -6935,8 +6946,8 @@ static size_t microphone_driver_flush(
/* Finally, we convert the audio back to 16-bit ints, as the mic interface requires. */
convert_float_to_s16(mic_st->final_frames, mic_st->resampled_mono_frames, resampler_data.output_frames);
frames_to_enqueue = MIN(FIFO_WRITE_AVAIL(microphone->outgoing_samples) / sizeof(int16_t), resampler_data.output_frames);
fifo_write(microphone->outgoing_samples, mic_st->final_frames, frames_to_enqueue * sizeof(int16_t));
frames_to_enqueue = MIN(microphone_outgoing_room(microphone) / sizeof(int16_t), resampler_data.output_frames);
retro_spsc_write(&microphone->outgoing_samples, mic_st->final_frames, frames_to_enqueue * sizeof(int16_t));
return frames_to_enqueue;
}
@@ -6965,15 +6976,13 @@ static void microphone_driver_capture_thread(void *data)
if ( !mic_st->driver
|| !mic_st->driver->wait_readable
|| !microphone->outgoing_samples)
|| !microphone->outgoing_init)
break;
/* Do not read more than the fifo can take, or the flush would
/* Do not read more than the ring can take, or the flush would
* discard what it could not enqueue and the device would run
* ahead of the core. */
slock_lock(microphone->fifo_lock);
room = FIFO_WRITE_AVAIL(microphone->outgoing_samples);
slock_unlock(microphone->fifo_lock);
* ahead of the core. Producer-side query, no lock needed. */
room = microphone_outgoing_room(microphone);
sample_size = microphone->worker_sample_size;
if (room < slice * sizeof(int16_t))
@@ -6990,8 +6999,11 @@ static void microphone_driver_capture_thread(void *data)
microphone->microphone_context, slice * sample_size))
continue;
slock_lock(microphone->fifo_lock);
/* The flush - device read, up-channel, resample, ring write -
* runs without the lock: SPSC, this thread is the producer. The
* core's read no longer waits behind the resampler. */
microphone_driver_flush(mic_st, microphone, slice);
slock_lock(microphone->fifo_lock);
scond_signal(microphone->fifo_cond);
slock_unlock(microphone->fifo_lock);
}
@@ -7066,8 +7078,8 @@ int microphone_driver_read(retro_microphone_t *microphone, int16_t* frames, size
return -1;
}
/* If the core asked for more frames than the FIFO can hold... */
if (num_frames * sizeof(int16_t) > microphone->outgoing_samples->size - 1)
/* If the core asked for more frames than the ring can hold... */
if (num_frames * sizeof(int16_t) > microphone->outgoing_size)
return -1;
retro_assert(mic_st->input_frames != NULL);
@@ -7083,15 +7095,16 @@ int microphone_driver_read(retro_microphone_t *microphone, int16_t* frames, size
size_t want = num_frames * sizeof(int16_t);
size_t got;
slock_lock(microphone->fifo_lock);
if (FIFO_READ_AVAIL(microphone->outgoing_samples) < want)
if (retro_spsc_read_avail(&microphone->outgoing_samples) < want)
{
slock_lock(microphone->fifo_lock);
scond_wait_timeout(microphone->fifo_cond, microphone->fifo_lock,
10000);
got = FIFO_READ_AVAIL(microphone->outgoing_samples);
if (got > want)
got = want;
if (got)
fifo_read(microphone->outgoing_samples, frames, got);
slock_unlock(microphone->fifo_lock);
}
/* Consumer side, no lock: SPSC. */
got = retro_spsc_read(&microphone->outgoing_samples, frames, want);
slock_lock(microphone->fifo_lock);
scond_signal(microphone->fifo_cond);
slock_unlock(microphone->fifo_lock);
@@ -7103,7 +7116,7 @@ int microphone_driver_read(retro_microphone_t *microphone, int16_t* frames, size
{
unsigned stall_count = 0;
while (FIFO_READ_AVAIL(microphone->outgoing_samples) < num_frames * sizeof(int16_t))
while (retro_spsc_read_avail(&microphone->outgoing_samples) < num_frames * sizeof(int16_t))
{ /* Until we can give the core the frames it asked for... */
size_t frames_to_read = MIN(AUDIO_CHUNK_SIZE_NONBLOCKING, frames_remaining);
size_t frames_read = microphone_driver_flush(
@@ -7122,7 +7135,7 @@ int microphone_driver_read(retro_microphone_t *microphone, int16_t* frames, size
} /* If the queue already has enough samples to give, the loop will be skipped */
}
fifo_read(microphone->outgoing_samples, frames, num_frames * sizeof(int16_t));
retro_spsc_read(&microphone->outgoing_samples, frames, num_frames * sizeof(int16_t));
return (int)num_frames;
}
+13 -4
View File
@@ -26,7 +26,7 @@
#include <libretro.h>
#include <audio/audio_resampler.h>
#include <audio/sinc_resampler_int16.h>
#include <queues/fifo_queue.h>
#include <retro_spsc.h>
/**
* Flags that indicate the current state of the microphone driver.
@@ -148,9 +148,18 @@ struct retro_microphone
int flags;
/**
* Samples that will be sent to the core.
* Samples that will be sent to the core. One producer (the capture
* worker, or the core's own read on the frame-synchronous path) and
* one consumer (retro_microphone_read), so a lock-free retro_spsc
* ring; with the worker, fifo_lock covers only the waits, no longer
* the flush - the read, up-channel and resample of a slice - which
* the core's read used to queue behind. retro_spsc rounds capacity
* up to a power of two; outgoing_size is the size asked for and the
* producer never fills past it.
*/
fifo_buffer_t *outgoing_samples;
retro_spsc_t outgoing_samples;
size_t outgoing_size;
bool outgoing_init;
/**
* The requested microphone parameters,
@@ -206,7 +215,7 @@ struct retro_microphone
/* Threaded capture. When the driver offers wait_readable() and the
* Threaded Pipeline setting is on, a worker owns the blocking read,
* the dual-mono up-channel and the resampler, and the core's
* retro_microphone_read() becomes a fifo read that never touches the
* retro_microphone_read() becomes a ring read that never touches the
* device. Without it, all three happen inside the core's call, on the
* frame - which is what the threaded pipeline removed from the
* playback path for the same reasons.