Skip to content

Commit 932e1e5

Browse files
committed
audio/asio: port unsynchronized fifo_buffer_t to retro_spsc_t
The ASIO callback thread (which is the audio driver's real-time thread) called fifo_read(ad->ring, ...) at five sites in asio_deinterleave_to_buffers without holding any lock. The main thread called fifo_write(ad->ring, src, to_write) in asio_write also without holding any lock. The only slock_* calls in the file were on cond_lock for the scond_wait/scond_signal pair around backpressure, which protects the condvar's internal state but does not serialise fifo_buffer_t access -- fifo_buffer_t is itself a plain ring buffer with no internal synchronisation, and the audit's earlier characterisation of fifo_buffer_t as "MPMC- safe with internal slock" was wrong. Both threads concurrently mutated ad->ring->first (callback via fifo_read) and ad->ring->end (main via fifo_write). fifo_* internally does an "if-then-set" on first/end inside a wraparound branch, so a racing read can observe a torn value -- not just loose ordering, an actually-corrupt half-updated cursor. On x86 TSO this is masked at the hardware level most of the time, which is presumably why the bug has been latent without major reports; on weak-memory targets (Win-on-ARM ASIO, becoming a real thing with Snapdragon X) the symptoms would be audio glitches at minimum and corrupted samples on wraparound. retro_spsc_t is the right tool for this exact pattern: single-producer (main thread, asio_write) / single-consumer (callback thread, asio_deinterleave_to_buffers) byte queue with acquire/release ordering on the head and tail cursors. Lock- free, so no priority-inversion concern from the callback path (taking a mutex from a real-time audio callback is a textbook anti-pattern in pro-audio code, which rules out the alternative "add a slock around the FIFO" fix). This also makes asio.c the first production caller of retro_spsc_t since b894479 introduced the primitive, validating the API against a real workload. Mechanical changes ------------------ - #include <queues/fifo_queue.h> -> <retro_spsc.h>. - fifo_buffer_t *ring -> retro_spsc_t ring (embedded by value) plus a sibling bool ring_initialized so cleanup paths can tell "init succeeded" from "never initialised". retro_spsc_t doesn't carry that bit internally because most callers know their own lifecycle; asio.c's free path runs from multiple error sites so the flag is the simplest way. - fifo_new/fifo_free -> retro_spsc_init/retro_spsc_free, with !ring NULL-checks replaced by !ring_initialized. - FIFO_READ_AVAIL / FIFO_WRITE_AVAIL -> retro_spsc_read_avail / retro_spsc_write_avail. - fifo_read / fifo_write -> retro_spsc_read / retro_spsc_write. The writer at line 1306 now uses retro_spsc_write's actual return value rather than assuming it equals to_write; the cap via retro_spsc_write_avail makes them equal in practice but using the return value is defensive against contract changes. - fifo_clear -> retro_spsc_clear (new API, see below) at the persistent-instance reuse path. ra_asio_t is calloc'd, so the embedded retro_spsc_t starts as a zero-bit pattern; retro_spsc_init then does the proper init via retro_atomic_size_init before any cross-thread access begins. This avoids the C++11 [atomics.types.generic] technical UB concern about uninitialised _Atomic / std::atomic members (carried over from the gfx_thumbnail port's analysis). retro_spsc_clear: new API ------------------------- retro_spsc_clear(retro_spsc_t *q) resets head and tail to zero without reallocating the buffer. It's documented as safe only when both producer and consumer are quiesced -- the same lifetime constraint as retro_spsc_init / retro_spsc_free. Implementation is two retro_atomic_size_init calls; the init form is necessary because plain assignment to a retro_atomic_size_t is illegal under the C11 stdatomic backend. Without this addition, the alternatives for "drop stale data on session restart" were: (a) repeated retro_spsc_read into a scratch buffer until empty (wasteful memcpys, ugly); (b) retro_spsc_free + retro_spsc_init (reallocates the heap buffer, churn). (c) clear is one line at the call site and atomic with respect to the calling thread. The quiescence requirement is documented in the header alongside the function. asio.c had two pre-port fifo_clear sites: 1. ra_asio_init_via_persistent (~line 1024): runs before g_asio = ad, so the ASIO callback isn't yet seeing this instance. Single-threaded; retro_spsc_clear is safe here. This site is converted directly. 2. ra_asio_free / parking path (~line 1380 pre-port): ran AFTER ASIO_CALL_STOP and AFTER g_asio = NULL. The intent was "drop stale audio before parking", but ASIO_CALL_STOP is not a synchronous join -- ASIO4ALL specifically does not terminate its audio thread before returning -- so the old fifo_clear at this site could race with a stray callback's fifo_read. This was a pre-existing latent bug. Resolution: drop the clear at the parking site entirely. The next ra_asio_init_via_persistent will re-clear after the callback is provably idle (g_asio == NULL means the callback short-circuits at line 852's null check, so it won't touch the ring). Documented with a comment at the parking site. Test coverage ------------- retro_spsc_clear gets unit-test coverage in the existing sample at libretro-common/samples/queues/retro_spsc_test/: write 50 bytes, verify read_avail == 50, retro_spsc_clear, verify read_avail == 0 and write_avail == capacity, then verify the queue is reusable after clear (write 16, read 16, content matches). Runs clean under SANITIZER=thread with halt_on_error=1. The SPSC stress test (1M-token producer/consumer race through a 4 KB buffer) continues to pass under TSan with no regression from the new function. Verified locally ---------------- - retro_spsc.c compiles clean as C (gcc, clang) and C++11 (-xc++), under Wall/Wextra/Wpedantic/Werror. - Forced backends: C11 stdatomic, GCC __sync_*, volatile fallback all build clean. - The ring-using portions of asio.c, extracted into a smoke harness mirroring the real calloc allocation pattern, compile and run clean under gcc -O2, g++ -std=c++11 -xc++, and clang. - Sample stress test passes 10M tokens, 0 mismatches. - Sample TSan run (halt_on_error=1): exit 0. Verified on Windows ------------------- - End-to-end Windows ASIO build with this patch applied. Audio plays correctly through the ASIO driver, no regressions observed in normal operation. Performance ----------- The motivating reason for the port is correctness (the cross- thread race) and bounded worst-case latency (the audio-code property), not throughput. But for the record, a head-to-head microbenchmark of the same access pattern under both designs ("fifo_buffer_t guarded by slock_t" vs "retro_spsc_t lock-free") shows: Single-threaded steady-state, 8 KB chunks (asio.c realistic payload), x86_64: fifo+slock: 338 ns/iter (write+read pair) retro_spsc: 283 ns/iter (write+read pair) Both dominated by memcpy cost (~94 ns/copy for 8 KB). Per-op overhead net of memcpy: ~150 ns vs ~96 ns. Single-threaded steady-state, 16 B chunks (atomic-cost- dominated), x86_64: fifo+slock: 44 ns/iter retro_spsc: 16 ns/iter Ratio: 2.7x faster. AArch64 (qemu-user, ratios only — absolute numbers distorted by emulation overhead): fifo+slock: 1151 ns/iter (8 KB), 637 ns/iter (16 B) retro_spsc: 891 ns/iter (8 KB), 410 ns/iter (16 B) At realistic ASIO operation rates (a few hundred queue ops/sec across both producer and consumer), the per-op savings amount to ~20 microseconds per second of CPU on x86 -- ~0.002% of one core. The throughput win is real but practically negligible at audio rates. What is meaningful is the bounded-latency property: under lock contention, the locked design's worst case is unbounded (the non-holding thread blocks in the kernel mutex; if the holder is preempted, the wait is unbounded). Under the same contention, the lock-free SPSC's worst case is one acquire-load. This is the textbook reason pro-audio guidance (Apple CoreAudio docs, Steinberg ASIO docs) discourages mutexes in real-time audio threads, and it's the qualitatively important difference here.
1 parent 9e04def commit 932e1e5

4 files changed

Lines changed: 147 additions & 43 deletions

File tree

audio/drivers/asio.c

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
#include <compat/strl.h>
5656
#include <string/stdstring.h>
5757
#include <lists/string_list.h>
58-
#include <queues/fifo_queue.h>
58+
#include <retro_spsc.h>
5959

6060
#ifdef HAVE_THREADS
6161
#include <rthreads/rthreads.h>
@@ -644,7 +644,26 @@ static void *asio_load_driver(const CLSID *clsid)
644644
typedef struct ra_asio
645645
{
646646
void *iasio; /* COM interface pointer */
647-
fifo_buffer_t *ring; /* Ring buffer between write() and callback */
647+
/* Lock-free SPSC ring buffer between asio_write (producer, main
648+
* thread) and asio_cb_buffer_switch (consumer, ASIO callback
649+
* thread). Pre-port this used fifo_buffer_t with no surrounding
650+
* lock, which was a real cross-thread race on first/end -- see
651+
* commit message. retro_spsc_t is the SPSC primitive designed
652+
* for this exact pattern: lock-free (avoiding the priority-
653+
* inversion concern of locking from a real-time audio callback),
654+
* acquire/release ordering on the cursors, single-producer /
655+
* single-consumer enforced by the type contract.
656+
*
657+
* Embedded by value (not via pointer) so the lifetime exactly
658+
* tracks ra_asio_t. Initialised with retro_spsc_init in
659+
* ra_asio_init / ra_asio_init_via_persistent and freed with
660+
* retro_spsc_free in the corresponding teardown paths. The
661+
* `ring_initialized` flag below distinguishes "never-initialised"
662+
* from "init succeeded" so cleanup paths know whether to call
663+
* retro_spsc_free. retro_spsc_t doesn't carry that bit
664+
* internally because most callers know their own lifecycle. */
665+
retro_spsc_t ring;
666+
bool ring_initialized;
648667
#ifdef HAVE_THREADS
649668
scond_t *cond;
650669
slock_t *cond_lock;
@@ -712,7 +731,11 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
712731
long i;
713732
void *buf_l = ad->buf_info[0].buffers[index];
714733
void *buf_r = ad->buf_info[1].buffers[index];
715-
size_t avail = FIFO_READ_AVAIL(ad->ring);
734+
/* Acquire-load on the producer's head cursor. Pairs with the
735+
* release-store inside retro_spsc_write that asio_write
736+
* issues on the main thread, so the bytes we're about to read
737+
* out via retro_spsc_read are guaranteed visible. */
738+
size_t avail = retro_spsc_read_avail(&ad->ring);
716739
long have = (long)(avail / (2 * sizeof(float)));
717740

718741
if (have > frames)
@@ -727,7 +750,7 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
727750
float tmp[2];
728751
for (i = 0; i < have; i++)
729752
{
730-
fifo_read(ad->ring, tmp, sizeof(tmp));
753+
retro_spsc_read(&ad->ring, tmp, sizeof(tmp));
731754
dl[i] = tmp[0];
732755
dr[i] = tmp[1];
733756
}
@@ -742,7 +765,7 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
742765
float tmp[2];
743766
for (i = 0; i < have; i++)
744767
{
745-
fifo_read(ad->ring, tmp, sizeof(tmp));
768+
retro_spsc_read(&ad->ring, tmp, sizeof(tmp));
746769
dl[i] = (double)tmp[0];
747770
dr[i] = (double)tmp[1];
748771
}
@@ -757,7 +780,7 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
757780
float tmp[2];
758781
for (i = 0; i < have; i++)
759782
{
760-
fifo_read(ad->ring, tmp, sizeof(tmp));
783+
retro_spsc_read(&ad->ring, tmp, sizeof(tmp));
761784
dl[i] = (int32_t)((double)tmp[0] * 2147483647.0);
762785
dr[i] = (int32_t)((double)tmp[1] * 2147483647.0);
763786
}
@@ -773,7 +796,7 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
773796
for (i = 0; i < have; i++)
774797
{
775798
int32_t l, r;
776-
fifo_read(ad->ring, tmp, sizeof(tmp));
799+
retro_spsc_read(&ad->ring, tmp, sizeof(tmp));
777800
l = (int32_t)(tmp[0] * 8388607.0f);
778801
r = (int32_t)(tmp[1] * 8388607.0f);
779802
l = l > 8388607 ? 8388607 : (l < -8388608 ? -8388608 : l);
@@ -797,7 +820,7 @@ static void asio_deinterleave_to_buffers(ra_asio_t *ad,
797820
for (i = 0; i < have; i++)
798821
{
799822
int32_t l, r;
800-
fifo_read(ad->ring, tmp, sizeof(tmp));
823+
retro_spsc_read(&ad->ring, tmp, sizeof(tmp));
801824
l = (int32_t)(tmp[0] * 32767.0f);
802825
r = (int32_t)(tmp[1] * 32767.0f);
803826
dl[i] = (int16_t)(l > 32767 ? 32767 : (l < -32768 ? -32768 : l));
@@ -826,7 +849,7 @@ static void asio_cb_buffer_switch(long index,
826849
{
827850
ra_asio_t *ad = g_asio;
828851

829-
if (!ad || !ad->ring || ad->is_paused || ad->shutdown)
852+
if (!ad || !ad->ring_initialized || ad->is_paused || ad->shutdown)
830853
{
831854
if (ad && ad->buf_info[0].buffers[index])
832855
{
@@ -924,8 +947,11 @@ static void asio_atexit_cleanup(void)
924947
ASIO_CALL_RELEASE(ad->iasio);
925948
}
926949

927-
if (ad->ring)
928-
fifo_free(ad->ring);
950+
if (ad->ring_initialized)
951+
{
952+
retro_spsc_free(&ad->ring);
953+
ad->ring_initialized = false;
954+
}
929955

930956
#ifdef HAVE_THREADS
931957
if (ad->cond_lock)
@@ -991,7 +1017,11 @@ static void *ra_asio_init(const char *device, unsigned rate,
9911017
if (new_rate)
9921018
*new_rate = ad->sample_rate;
9931019

994-
fifo_clear(ad->ring);
1020+
/* Discard any stale audio left over from the previous
1021+
* session. Safe here because the ASIO callback isn't
1022+
* running yet (g_asio is still NULL until the next line),
1023+
* so the SPSC is single-threaded at this point. */
1024+
retro_spsc_clear(&ad->ring);
9951025

9961026
g_asio = ad;
9971027
ad->running = true;
@@ -1158,14 +1188,18 @@ static void *ra_asio_init(const char *device, unsigned rate,
11581188

11591189
/* Create ring buffer BEFORE ASIO buffers — the driver may issue
11601190
* a bufferSwitch callback during ASIOCreateBuffers, and the
1161-
* callback needs the ring buffer to exist (even if empty). */
1191+
* callback needs the ring buffer to exist (even if empty).
1192+
* retro_spsc_init rounds capacity up to a power of 2; the
1193+
* over-allocation is small (factor of < 2) and irrelevant to
1194+
* the ASIO latency calculation, which uses ad->buffer_frames
1195+
* not the ring's actual byte capacity. */
11621196
ad->ring_size = pref_sz * 2 * sizeof(float) * ASIO_RING_MULT;
1163-
ad->ring = fifo_new(ad->ring_size);
1164-
if (!ad->ring)
1197+
if (!retro_spsc_init(&ad->ring, ad->ring_size))
11651198
{
11661199
RARCH_ERR("[ASIO] Failed to create ring buffer.\n");
11671200
goto error;
11681201
}
1202+
ad->ring_initialized = true;
11691203

11701204
#ifdef HAVE_THREADS
11711205
ad->cond = scond_new();
@@ -1229,8 +1263,11 @@ static void *ra_asio_init(const char *device, unsigned rate,
12291263
ASIO_CALL_DISPOSE_BUFFERS(ad->iasio);
12301264
ASIO_CALL_RELEASE(ad->iasio);
12311265
}
1232-
if (ad->ring)
1233-
fifo_free(ad->ring);
1266+
if (ad->ring_initialized)
1267+
{
1268+
retro_spsc_free(&ad->ring);
1269+
ad->ring_initialized = false;
1270+
}
12341271
#ifdef HAVE_THREADS
12351272
if (ad->cond_lock)
12361273
slock_free(ad->cond_lock);
@@ -1259,17 +1296,22 @@ static ssize_t ra_asio_write(void *data, const void *buf, size_t len)
12591296
if (ad->shutdown)
12601297
return -1;
12611298

1262-
avail = FIFO_WRITE_AVAIL(ad->ring);
1299+
avail = retro_spsc_write_avail(&ad->ring);
12631300
to_write = (len < avail) ? len : avail;
12641301
/* Align to frame boundary (stereo float = 8 bytes) */
12651302
to_write = (to_write / 8) * 8;
12661303

12671304
if (to_write > 0)
12681305
{
1269-
fifo_write(ad->ring, src, to_write);
1270-
src += to_write;
1271-
len -= to_write;
1272-
written += to_write;
1306+
/* retro_spsc_write returns bytes actually written. We've
1307+
* already capped to_write by retro_spsc_write_avail above,
1308+
* so the return value will equal to_write -- but use it
1309+
* defensively in case the contract ever changes. */
1310+
size_t actually_written =
1311+
retro_spsc_write(&ad->ring, src, to_write);
1312+
src += actually_written;
1313+
len -= actually_written;
1314+
written += actually_written;
12731315
}
12741316
else if (!ad->nonblock)
12751317
{
@@ -1346,9 +1388,14 @@ static void ra_asio_free(void *data)
13461388
/* Detach from the callback — silence output while parked */
13471389
g_asio = NULL;
13481390

1349-
/* Flush stale audio */
1350-
if (ad->ring)
1351-
fifo_clear(ad->ring);
1391+
/* No retro_spsc_clear here. The pre-port fifo_clear at this
1392+
* site was racy with stray ASIO callbacks that may still be
1393+
* running after ASIO_CALL_STOP returns (some drivers, notably
1394+
* ASIO4ALL, don't synchronously join their audio thread).
1395+
* The restart path in ra_asio_init_via_persistent calls
1396+
* retro_spsc_clear anyway, so any stale data will be flushed
1397+
* before the next run -- after the callback is provably
1398+
* stopped (g_asio == NULL gates it). */
13521399

13531400
/* Store for reuse */
13541401
g_asio_persistent = ad;
@@ -1361,9 +1408,9 @@ static bool ra_asio_use_float(void *data) { return true; }
13611408
static size_t ra_asio_write_avail(void *data)
13621409
{
13631410
ra_asio_t *ad = (ra_asio_t *)data;
1364-
if (!ad || !ad->ring)
1411+
if (!ad || !ad->ring_initialized)
13651412
return 0;
1366-
return FIFO_WRITE_AVAIL(ad->ring);
1413+
return retro_spsc_write_avail(&ad->ring);
13671414
}
13681415

13691416
static size_t ra_asio_buffer_size(void *data)

libretro-common/include/retro_spsc.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,26 @@ bool retro_spsc_init(retro_spsc_t *q, size_t min_capacity);
173173
*/
174174
void retro_spsc_free(retro_spsc_t *q);
175175

176+
/**
177+
* retro_spsc_clear:
178+
* @q : The queue.
179+
*
180+
* Resets head and tail to 0, discarding any unread data. The
181+
* underlying buffer is preserved (no reallocation).
182+
*
183+
* SAFETY: callable only when both the producer and consumer are
184+
* quiesced -- e.g. before either has started, or after both have
185+
* stopped. Concurrent calls with a live producer or consumer are
186+
* a data race. This is the same lifetime constraint as
187+
* retro_spsc_init / retro_spsc_free.
188+
*
189+
* Typical use is when a stream is stopped and restarted (e.g. an
190+
* audio driver pausing and resuming, or switching device formats),
191+
* and stale buffered data should be discarded before the new
192+
* stream begins.
193+
*/
194+
void retro_spsc_clear(retro_spsc_t *q);
195+
176196
/**
177197
* retro_spsc_write_avail:
178198
* @q : The queue.

libretro-common/queues/retro_spsc.c

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ void retro_spsc_free(retro_spsc_t *q)
7575
q->capacity = 0;
7676
}
7777

78+
void retro_spsc_clear(retro_spsc_t *q)
79+
{
80+
if (!q)
81+
return;
82+
/* Quiescence is the caller's responsibility (documented).
83+
* Under that assumption, no other thread is touching head or
84+
* tail, so plain init is correct here -- and necessary, because
85+
* plain assignment to a retro_atomic_size_t is illegal under
86+
* the C11 stdatomic backend. */
87+
retro_atomic_size_init(&q->head, 0);
88+
retro_atomic_size_init(&q->tail, 0);
89+
}
90+
7891
size_t retro_spsc_write_avail(const retro_spsc_t *q)
7992
{
8093
/* Producer query. We read our own head (which we wrote) and the
@@ -110,13 +123,12 @@ size_t retro_spsc_read_avail(const retro_spsc_t *q)
110123

111124
size_t retro_spsc_write(retro_spsc_t *q, const void *data, size_t bytes)
112125
{
126+
size_t mask, head_idx, first;
113127
const uint8_t *src = (const uint8_t*)data;
114-
size_t avail, head, tail, mask, head_idx, first;
115-
116128
/* read tail first to know how much room there is */
117-
head = retro_atomic_load_acquire_size(&q->head);
118-
tail = retro_atomic_load_acquire_size(&q->tail);
119-
avail = q->capacity - (head - tail);
129+
size_t head = retro_atomic_load_acquire_size(&q->head);
130+
size_t tail = retro_atomic_load_acquire_size(&q->tail);
131+
size_t avail = q->capacity - (head - tail);
120132
if (bytes > avail)
121133
bytes = avail;
122134
if (bytes == 0)
@@ -141,14 +153,13 @@ size_t retro_spsc_write(retro_spsc_t *q, const void *data, size_t bytes)
141153

142154
size_t retro_spsc_read(retro_spsc_t *q, void *data, size_t bytes)
143155
{
156+
size_t mask, tail_idx, first;
144157
uint8_t *dst = (uint8_t*)data;
145-
size_t avail, head, tail, mask, tail_idx, first;
146-
147158
/* acquire on head pairs with producer's release-store; this is
148159
* what makes the subsequent memcpys safe to read. */
149-
head = retro_atomic_load_acquire_size(&q->head);
150-
tail = retro_atomic_load_acquire_size(&q->tail);
151-
avail = head - tail;
160+
size_t head = retro_atomic_load_acquire_size(&q->head);
161+
size_t tail = retro_atomic_load_acquire_size(&q->tail);
162+
size_t avail = head - tail;
152163
if (bytes > avail)
153164
bytes = avail;
154165
if (bytes == 0)
@@ -171,14 +182,13 @@ size_t retro_spsc_read(retro_spsc_t *q, void *data, size_t bytes)
171182

172183
size_t retro_spsc_peek(const retro_spsc_t *q, void *data, size_t bytes)
173184
{
185+
size_t mask, tail_idx, first;
174186
uint8_t *dst = (uint8_t*)data;
175-
size_t avail, head, tail, mask, tail_idx, first;
176-
177-
head = retro_atomic_load_acquire_size(
187+
size_t head = retro_atomic_load_acquire_size(
178188
(retro_atomic_size_t*)&q->head);
179-
tail = retro_atomic_load_acquire_size(
189+
size_t tail = retro_atomic_load_acquire_size(
180190
(retro_atomic_size_t*)&q->tail);
181-
avail = head - tail;
191+
size_t avail = head - tail;
182192
if (bytes > avail)
183193
bytes = avail;
184194
if (bytes == 0)
@@ -187,7 +197,7 @@ size_t retro_spsc_peek(const retro_spsc_t *q, void *data, size_t bytes)
187197
mask = q->capacity - 1;
188198
tail_idx = tail & mask;
189199

190-
first = q->capacity - tail_idx;
200+
first = q->capacity - tail_idx;
191201
if (first > bytes)
192202
first = bytes;
193203

libretro-common/samples/queues/retro_spsc_test/retro_spsc_test.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,33 @@ static int run_property_checks(void)
222222
return 1;
223223
}
224224

225+
/* clear discards unread data without reallocating buffer */
226+
retro_spsc_write(&q, buf, 50);
227+
if (retro_spsc_read_avail(&q) != 50)
228+
{
229+
fprintf(stderr, "FAIL: pre-clear read_avail\n");
230+
retro_spsc_free(&q);
231+
return 1;
232+
}
233+
retro_spsc_clear(&q);
234+
if (retro_spsc_read_avail(&q) != 0
235+
|| retro_spsc_write_avail(&q) != 128)
236+
{
237+
fprintf(stderr, "FAIL: clear did not reset cursors\n");
238+
retro_spsc_free(&q);
239+
return 1;
240+
}
241+
/* queue is reusable after clear */
242+
retro_spsc_write(&q, buf, 16);
243+
memset(readback, 0, sizeof(readback));
244+
n = retro_spsc_read(&q, readback, 16);
245+
if (n != 16 || memcmp(buf, readback, 16) != 0)
246+
{
247+
fprintf(stderr, "FAIL: post-clear read mismatch\n");
248+
retro_spsc_free(&q);
249+
return 1;
250+
}
251+
225252
retro_spsc_free(&q);
226253
printf("[pass] property checks\n");
227254
return 0;

0 commit comments

Comments
 (0)