Skip to main content

vst_audio_ring/
lib.rs

1#![forbid(unsafe_code)]
2
3//! A bounded, allocation-free shared-memory audio block ring.
4//!
5//! This is a Rust port of EqualizerAPO-XT `runtime/ipc/StreamRing.{h,cpp}` at
6//! commit `1432f61cc437c99fde8e8e84e10e648fe516d4ce`. Copyright 2026 115dkk.
7//! It is distributed under MIT with the copyright holder's permission; `NOTICE`
8//! records the source revision and license provenance.
9//!
10//! The wire image is little-endian. Every value is accessed atomically. The
11//! mapping is split into a prefix of [`AtomicU32`] words and a 128-byte suffix
12//! of [`AtomicU64`] words. This keeps f32 samples in `AtomicU32`, gives the
13//! counters their natural alignment, and never accesses one byte at two widths.
14//! A platform adapter must create those two non-overlapping slices and keep the
15//! mapping alive longer than the [`Region`].
16//!
17//! # Header layout
18//!
19//! | Offset | Bytes | Field |
20//! |---:|---:|---|
21//! | 0 | 4 | magic `SSRR` |
22//! | 4 | 4 | layout version |
23//! | 8 | 4 | total bytes, including the counter suffix |
24//! | 12 | 4 | lane count |
25//! | 16 | 4 | input channels |
26//! | 20 | 4 | output channels |
27//! | 24 | 4 | maximum frames |
28//! | 28 | 4 | sample rate, Hz |
29//! | 32 | 4 | latency bound, frames |
30//! | 36 | 4 | state |
31//! | 40 | 4 | fault code |
32//! | 44 | 4 | producer pid |
33//! | 48 | 4 | consumer pid |
34//! | 52 | 4 | sample format |
35//! | 56 | 4 | slots per lane |
36//! | 64 | 24 | lane geometry: first slot offset, slot bytes, slot count, per lane |
37//! | 128 | 8 | lane sequences (one u32 per lane; own cache line) |
38//! | 192 | 8 | lane completions (one u32 per lane; own cache line) |
39//! | 512 | variable | `slots` 64-byte-aligned sample slots per lane |
40//! | rounded slot end | 64 | heartbeat u64 and padding |
41//! | +64 | 64 | last error u64 and padding |
42
43use std::array;
44use std::fmt;
45use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
46use std::sync::{Arc, Condvar, Mutex, TryLockError};
47use std::time::{Duration, Instant};
48
49mod block;
50pub use block::{BlockError, BlockLane, Exchange};
51pub mod events;
52#[cfg(test)]
53mod serial_tests;
54use events::{CONTEXT_WORDS, EVENT_WORDS, METADATA_WORDS, PACKET_WORDS};
55pub use events::{Event, EventBuffer, EventData, MAX_EVENTS, MAX_SYSEX_BYTES, Transport};
56#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
57#[repr(u32)]
58pub enum SampleFormat {
59    #[default]
60    F32 = 0,
61    F64 = 1,
62}
63impl SampleFormat {
64    pub fn words(self) -> usize {
65        if self == Self::F64 { 2 } else { 1 }
66    }
67}
68
69pub const MAGIC: u32 = u32::from_le_bytes(*b"SSRR");
70pub const LAYOUT_VERSION: u32 = 4;
71pub const HEADER_BYTES: usize = 512;
72pub const CACHE_LINE: usize = 64;
73pub const MAX_CHANNELS: u32 = 64;
74pub const MAX_FRAMES: u32 = 65_536;
75pub const MAX_SLOTS: u32 = 16;
76pub const MAX_REGION_BYTES: usize = 128 * 1024 * 1024;
77/// Byte length of the two cache-line-aligned `AtomicU64` suffixes for the
78/// heartbeat and last-error counters.
79pub const COUNTER_BYTES: usize = 2 * CACHE_LINE;
80/// Number of `AtomicU64` words in the heartbeat and last-error suffixes.
81pub const COUNTER_WORDS: usize = COUNTER_BYTES / 8;
82/// Smallest mapping that can contain the fixed header and counter suffixes.
83pub const MIN_REGION_BYTES: usize = HEADER_BYTES + COUNTER_BYTES;
84const HEADER_WORDS: usize = HEADER_BYTES / 4;
85
86const MAGIC_I: usize = 0;
87const VERSION_I: usize = 1;
88const TOTAL_I: usize = 2;
89const LANES_I: usize = 3;
90const INPUTS_I: usize = 4;
91const OUTPUTS_I: usize = 5;
92const FRAMES_I: usize = 6;
93const RATE_I: usize = 7;
94const LATENCY_I: usize = 8;
95/// Byte offset of the latency-bound header word, the only header value mapping
96/// adapters read before they know the geometry.
97pub const LATENCY_BOUND_OFFSET: usize = LATENCY_I * 4;
98const STATE_I: usize = 9;
99const FAULT_I: usize = 10;
100const PRODUCER_PID_I: usize = 11;
101const CONSUMER_PID_I: usize = 12;
102const FORMAT_I: usize = 13;
103const SLOTS_I: usize = 14;
104const LANE_GEOMETRY_I: usize = 16;
105const SEQUENCE_I: usize = 32;
106const COMPLETED_I: usize = 48;
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109#[repr(u32)]
110pub enum State {
111    Empty = 0,
112    Announced = 1,
113    Ready = 2,
114    Closing = 3,
115    Fault = 4,
116}
117
118impl State {
119    fn from_wire(value: u32) -> Option<Self> {
120        Some(match value {
121            0 => Self::Empty,
122            1 => Self::Announced,
123            2 => Self::Ready,
124            3 => Self::Closing,
125            4 => Self::Fault,
126            _ => return None,
127        })
128    }
129}
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132#[repr(u32)]
133pub enum Fault {
134    None = 0,
135    LayoutMismatch = 1,
136    EngineFailed = 2,
137    HostShuttingDown = 3,
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
141pub struct Geometry {
142    pub sample_format: SampleFormat,
143    pub lanes: u32,
144    pub slots: u32,
145    pub input_channels: u32,
146    pub output_channels: u32,
147    pub max_frames: u32,
148    pub sample_rate: u32,
149    pub latency_bound_frames: u32,
150}
151
152impl Geometry {
153    pub fn validate(self) -> Result<Self, GeometryError> {
154        if !(1..=2).contains(&self.lanes) {
155            return Err(GeometryError::LaneCount);
156        }
157        if !matches!(self.slots, 2 | 4 | 8 | 16) {
158            return Err(GeometryError::Slots);
159        }
160        if self.input_channels > MAX_CHANNELS || self.output_channels > MAX_CHANNELS {
161            return Err(GeometryError::Channels);
162        }
163        if !(1..=MAX_FRAMES).contains(&self.max_frames) {
164            return Err(GeometryError::Frames);
165        }
166        if !(1_000..=1_000_000).contains(&self.sample_rate) {
167            return Err(GeometryError::SampleRate);
168        }
169        if layout(self).is_none() {
170            return Err(GeometryError::RegionTooLarge);
171        }
172        Ok(self)
173    }
174
175    pub fn planes(self) -> usize {
176        self.input_channels.max(self.output_channels) as usize
177    }
178
179    pub fn samples_per_slot(self) -> usize {
180        self.planes() * self.max_frames as usize
181    }
182
183    pub fn total_bytes(self) -> Result<usize, GeometryError> {
184        self.validate()?;
185        Ok(layout(self)
186            .expect("validated geometry has a layout")
187            .total_bytes)
188    }
189
190    /// The byte length of the `AtomicU32` prefix when this geometry is placed in a
191    /// mapping of `mapping_bytes` bytes: the 128-byte `AtomicU64` suffix starts
192    /// there. The returned split is always a multiple of [`CACHE_LINE`]. Fails
193    /// when the geometry does not fit the mapping.
194    pub fn mapping_split(self, mapping_bytes: usize) -> Result<usize, GeometryError> {
195        let total = self.total_bytes()?;
196        if total > mapping_bytes {
197            return Err(GeometryError::RegionTooLarge);
198        }
199        Ok(total - COUNTER_BYTES)
200    }
201}
202
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum GeometryError {
205    LaneCount,
206    Slots,
207    Channels,
208    Frames,
209    SampleRate,
210    RegionTooLarge,
211}
212
213#[derive(Clone, Copy, Debug)]
214struct Layout {
215    slot_bytes: usize,
216    first_slot_offsets: [usize; 2],
217    slots: u32,
218    prefix_bytes: usize,
219    total_bytes: usize,
220}
221
222impl Layout {
223    fn slot_offset(self, lane: usize, sequence: u32) -> usize {
224        self.first_slot_offsets[lane] + (sequence & (self.slots - 1)) as usize * self.slot_bytes
225    }
226}
227
228fn round_up(value: usize, alignment: usize) -> Option<usize> {
229    value
230        .checked_add(alignment - 1)
231        .map(|v| v / alignment * alignment)
232}
233
234fn layout(geometry: Geometry) -> Option<Layout> {
235    let audio_words = geometry
236        .samples_per_slot()
237        .checked_mul(geometry.sample_format.words())?;
238    let slot_bytes = round_up(
239        audio_words.checked_add(METADATA_WORDS)?.checked_mul(4)?,
240        CACHE_LINE,
241    )?;
242    let mut offset = HEADER_BYTES;
243    let mut first_slot_offsets = [0; 2];
244    for first_slot_offset in first_slot_offsets.iter_mut().take(geometry.lanes as usize) {
245        *first_slot_offset = offset;
246        offset = offset.checked_add(slot_bytes.checked_mul(geometry.slots as usize)?)?;
247    }
248    let prefix_bytes = round_up(offset, CACHE_LINE)?;
249    let total_bytes = prefix_bytes.checked_add(COUNTER_BYTES)?;
250    (total_bytes < MAX_REGION_BYTES).then_some(Layout {
251        slot_bytes,
252        first_slot_offsets,
253        slots: geometry.slots,
254        prefix_bytes,
255        total_bytes,
256    })
257}
258
259pub trait Signal: Send + Sync {
260    fn signal(&self);
261}
262
263impl<T: Signal + ?Sized> Signal for Arc<T> {
264    fn signal(&self) {
265        T::signal(self);
266    }
267}
268
269impl<T: Signal + ?Sized> Signal for Box<T> {
270    fn signal(&self) {
271        T::signal(self);
272    }
273}
274
275pub trait Wait: Send + Sync {
276    fn wait(&self, timeout: Duration) -> Waited;
277}
278
279#[derive(Clone, Copy, Debug, Eq, PartialEq)]
280pub enum Waited {
281    Signalled,
282    TimedOut,
283    PeerGone,
284}
285
286#[derive(Debug)]
287struct EventInner {
288    generation: AtomicU64,
289    gone: AtomicBool,
290    contended: AtomicBool,
291    observed: Mutex<u64>,
292    changed: Condvar,
293}
294
295/// Same-process event for tests and adapters. `signal` never blocks or allocates;
296/// it attempts the condition-variable mutex and uses the waiter's bounded atomic
297/// recheck when the waiter owns it.
298#[derive(Clone, Debug)]
299pub struct InProcessEvent(Arc<EventInner>);
300
301impl InProcessEvent {
302    pub fn new() -> Self {
303        Self(Arc::new(EventInner {
304            generation: AtomicU64::new(0),
305            gone: AtomicBool::new(false),
306            contended: AtomicBool::new(false),
307            observed: Mutex::new(0),
308            changed: Condvar::new(),
309        }))
310    }
311
312    pub fn peer_gone(&self) {
313        self.0.gone.store(true, Ordering::Release);
314        self.notify(false);
315    }
316
317    fn notify(&self, one: bool) {
318        // A notifier that acquires `observed` cannot race between the waiter's
319        // predicate check and parking. `signal` must remain non-blocking, so a
320        // contended mutex sets a handoff flag that makes the waiter skip parking.
321        match self.0.observed.try_lock() {
322            Ok(_guard) => {
323                if one {
324                    self.0.changed.notify_one();
325                } else {
326                    self.0.changed.notify_all();
327                }
328            }
329            Err(TryLockError::Poisoned(error)) => {
330                let _guard = error.into_inner();
331                if one {
332                    self.0.changed.notify_one();
333                } else {
334                    self.0.changed.notify_all();
335                }
336            }
337            Err(TryLockError::WouldBlock) => {
338                self.0.contended.store(true, Ordering::Release);
339                if one {
340                    self.0.changed.notify_one();
341                } else {
342                    self.0.changed.notify_all();
343                }
344            }
345        }
346    }
347}
348
349impl Default for InProcessEvent {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355impl Signal for InProcessEvent {
356    fn signal(&self) {
357        self.0.generation.fetch_add(1, Ordering::Release);
358        self.notify(true);
359    }
360}
361
362impl Wait for InProcessEvent {
363    fn wait(&self, timeout: Duration) -> Waited {
364        if self.0.gone.load(Ordering::Acquire) {
365            return Waited::PeerGone;
366        }
367        let mut observed = self.0.observed.lock().unwrap_or_else(|e| e.into_inner());
368        let current = self.0.generation.load(Ordering::Acquire);
369        if current != *observed {
370            *observed = current;
371            return Waited::Signalled;
372        }
373        if self.0.contended.swap(false, Ordering::AcqRel) {
374            let current = self.0.generation.load(Ordering::Acquire);
375            if current != *observed {
376                *observed = current;
377                return Waited::Signalled;
378            }
379            if self.0.gone.load(Ordering::Acquire) {
380                return Waited::PeerGone;
381            }
382        }
383        let deadline = Instant::now() + timeout;
384        loop {
385            let now = Instant::now();
386            if now >= deadline {
387                return Waited::TimedOut;
388            }
389            let slice = deadline
390                .saturating_duration_since(now)
391                .min(Duration::from_millis(1));
392            let (next_observed, _) = self
393                .0
394                .changed
395                .wait_timeout_while(observed, slice, |seen| {
396                    !self.0.gone.load(Ordering::Acquire)
397                        && !self.0.contended.swap(false, Ordering::AcqRel)
398                        && self.0.generation.load(Ordering::Acquire) == *seen
399                })
400                .unwrap_or_else(|e| e.into_inner());
401            observed = next_observed;
402            if self.0.gone.load(Ordering::Acquire) {
403                return Waited::PeerGone;
404            }
405            let current = self.0.generation.load(Ordering::Acquire);
406            if current != *observed {
407                *observed = current;
408                return Waited::Signalled;
409            }
410        }
411    }
412}
413
414/// Borrowed shared mapping. The u32 prefix contains the header and samples;
415/// the u64 suffix contains only heartbeat and last-error cache lines.
416#[derive(Clone, Copy, Debug)]
417pub struct Region<'a> {
418    words32: &'a [AtomicU32],
419    words64: &'a [AtomicU64],
420}
421
422impl<'a> Region<'a> {
423    pub fn new(words32: &'a [AtomicU32], words64: &'a [AtomicU64]) -> Self {
424        Self { words32, words64 }
425    }
426
427    pub fn state(self) -> Option<State> {
428        State::from_wire(self.load32(STATE_I, Ordering::Acquire))
429    }
430
431    pub fn fault_code(self) -> u32 {
432        self.load32(FAULT_I, Ordering::Acquire)
433    }
434
435    pub fn heartbeat(self) -> u64 {
436        self.words64
437            .first()
438            .map_or(0, |v| v.load(Ordering::Acquire))
439    }
440
441    pub fn last_error(self) -> u64 {
442        self.words64
443            .get(CACHE_LINE / 8)
444            .map_or(0, |v| v.load(Ordering::Acquire))
445    }
446
447    fn load32(self, index: usize, ordering: Ordering) -> u32 {
448        self.words32.get(index).map_or(0, |v| v.load(ordering))
449    }
450
451    fn store32(self, index: usize, value: u32, ordering: Ordering) {
452        if let Some(word) = self.words32.get(index) {
453            word.store(value, ordering);
454        }
455    }
456}
457
458#[derive(Debug)]
459pub struct RegionBuf {
460    words32: Box<[AtomicU32]>,
461    words64: Box<[AtomicU64]>,
462}
463
464impl RegionBuf {
465    pub fn new(geometry: Geometry) -> Result<Self, GeometryError> {
466        let geometry = geometry.validate()?;
467        let layout = layout(geometry).expect("validated geometry has a layout");
468        Ok(Self {
469            words32: (0..layout.prefix_bytes / 4)
470                .map(|_| AtomicU32::new(0))
471                .collect(),
472            words64: (0..COUNTER_WORDS).map(|_| AtomicU64::new(0)).collect(),
473        })
474    }
475
476    pub fn region(&self) -> Region<'_> {
477        Region::new(&self.words32, &self.words64)
478    }
479}
480
481/// Memory that owns a mapping and lends it as a [`Region`]: an in-process
482/// [`RegionBuf`], or a platform adapter's shared-memory view. An owner that
483/// outlives the borrow lets a [`Publisher`] travel to another thread without
484/// the [`Producer`] that announced the header.
485pub trait RegionMemory: Send + Sync {
486    fn region(&self) -> Region<'_>;
487}
488
489impl RegionMemory for RegionBuf {
490    fn region(&self) -> Region<'_> {
491        self.region()
492    }
493}
494
495impl<T: RegionMemory + ?Sized> RegionMemory for Arc<T> {
496    fn region(&self) -> Region<'_> {
497        T::region(self)
498    }
499}
500
501impl<T: RegionMemory + ?Sized> RegionMemory for Box<T> {
502    fn region(&self) -> Region<'_> {
503        T::region(self)
504    }
505}
506
507#[derive(Clone, Copy, Debug, Eq, PartialEq)]
508pub enum HeaderError {
509    TooShort,
510    Magic,
511    Version,
512    State,
513    Geometry(GeometryError),
514    TotalBytes,
515    LaneGeometry,
516}
517
518#[derive(Clone, Copy, Debug, Eq, PartialEq)]
519pub enum Publish {
520    Published,
521    Dropped,
522}
523
524/// The producer's real-time half: the announced geometry and slot layout,
525/// cached so the per-block path needs no [`Producer`] and no borrow of the
526/// mapping beyond each call. Every method takes the [`Region`] it works on and
527/// neither blocks, locks nor allocates; `work` is only signalled.
528///
529/// Each lane has exactly one producer and one consumer. The producer publishes
530/// only the next wrapping sequence and accesses a completed slot only after the
531/// completion counter's Acquire load. The consumer likewise accesses a slot
532/// only after observing its publication with Acquire, then calls
533/// [`Consumer::complete`] after processing it.
534#[derive(Clone, Copy, Debug)]
535pub struct Publisher {
536    geometry: Geometry,
537    layout: Layout,
538}
539
540impl Publisher {
541    pub fn geometry(&self) -> Geometry {
542        self.geometry
543    }
544
545    pub fn state(&self, region: Region<'_>) -> Option<State> {
546        region.state()
547    }
548
549    /// The last sequence the producer published on `lane` (0 outside the
550    /// geometry).
551    pub fn published(&self, region: Region<'_>, lane: usize) -> u32 {
552        if lane >= self.geometry.lanes as usize {
553            return 0;
554        }
555        u32::from_le(region.load32(SEQUENCE_I + lane, Ordering::Acquire))
556    }
557
558    /// The last sequence the consumer completed on `lane` (0 outside the
559    /// geometry).
560    pub fn completed(&self, region: Region<'_>, lane: usize) -> u32 {
561        if lane >= self.geometry.lanes as usize {
562            return 0;
563        }
564        u32::from_le(region.load32(COMPLETED_I + lane, Ordering::Acquire))
565    }
566
567    /// Writes the next wrapping `sequence` into its slot, publishes it, then
568    /// signals `work`. `Dropped`, touching nothing, unless the consumer is
569    /// `Ready` and has completed `sequence.wrapping_sub(slots)`, whose slot
570    /// this reuses.
571    pub fn try_publish<F>(
572        &self,
573        region: Region<'_>,
574        lane: usize,
575        sequence: u32,
576        work: Option<&dyn Signal>,
577        write: F,
578    ) -> Publish
579    where
580        F: FnOnce(&mut SlotWriter<'_>),
581    {
582        if region.state() != Some(State::Ready) || lane >= self.geometry.lanes as usize {
583            return Publish::Dropped;
584        }
585        let completed = self.completed(region, lane);
586        if !sequence_at_least(completed, sequence.wrapping_sub(self.geometry.slots)) {
587            return Publish::Dropped;
588        }
589        let offset = self.layout.slot_offset(lane, sequence) / 4;
590        let Some(samples) = region.words32.get(
591            offset..offset + self.geometry.samples_per_slot() * self.geometry.sample_format.words(),
592        ) else {
593            return Publish::Dropped;
594        };
595        let meta_offset =
596            offset + self.geometry.samples_per_slot() * self.geometry.sample_format.words();
597        let Some(metadata) = region
598            .words32
599            .get(meta_offset..meta_offset + METADATA_WORDS)
600        else {
601            return Publish::Dropped;
602        };
603        let mut writer = SlotWriter {
604            samples,
605            metadata,
606            format: self.geometry.sample_format,
607            frames: self.geometry.max_frames as usize,
608            planes: self.geometry.planes(),
609        };
610        writer.set_transport(Transport::default());
611        writer.set_events(&[]);
612        metadata[CONTEXT_WORDS + PACKET_WORDS].store(0, Ordering::Relaxed);
613        metadata[CONTEXT_WORDS + PACKET_WORDS + 1].store(0, Ordering::Relaxed);
614        write(&mut writer);
615        region.store32(SEQUENCE_I + lane, sequence.to_le(), Ordering::Release);
616        if let Some(signal) = work {
617            signal.signal();
618        }
619        Publish::Published
620    }
621
622    /// Lends `sequence`'s slot to `read` once the consumer has completed it.
623    pub fn try_read<F>(&self, region: Region<'_>, lane: usize, sequence: u32, read: F) -> bool
624    where
625        F: FnOnce(SlotReader<'_>),
626    {
627        if lane >= self.geometry.lanes as usize
628            || !sequence_at_least(self.completed(region, lane), sequence)
629        {
630            return false;
631        }
632        let published = self.published(region, lane);
633        if published.wrapping_sub(sequence) >= self.geometry.slots {
634            return false;
635        }
636        let offset = self.layout.slot_offset(lane, sequence) / 4;
637        let Some(samples) = region.words32.get(
638            offset..offset + self.geometry.samples_per_slot() * self.geometry.sample_format.words(),
639        ) else {
640            return false;
641        };
642        let meta_offset =
643            offset + self.geometry.samples_per_slot() * self.geometry.sample_format.words();
644        let Some(metadata) = region
645            .words32
646            .get(meta_offset..meta_offset + METADATA_WORDS)
647        else {
648            return false;
649        };
650        read(SlotReader {
651            samples,
652            metadata,
653            format: self.geometry.sample_format,
654            frames: self.geometry.max_frames as usize,
655            planes: self.geometry.planes(),
656        });
657        true
658    }
659}
660
661pub struct Producer<'a> {
662    region: Region<'a>,
663    geometry: Option<Geometry>,
664    layout: Option<Layout>,
665    work: [Option<&'a dyn Signal>; 2],
666    ready: &'a dyn Wait,
667}
668
669impl fmt::Debug for Producer<'_> {
670    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
671        formatter
672            .debug_struct("Producer")
673            .field("region", &self.region)
674            .field("geometry", &self.geometry)
675            .field("layout", &self.layout)
676            .finish_non_exhaustive()
677    }
678}
679
680impl<'a> Producer<'a> {
681    pub fn new(region: Region<'a>, work: [Option<&'a dyn Signal>; 2], ready: &'a dyn Wait) -> Self {
682        Self {
683            region,
684            geometry: None,
685            layout: None,
686            work,
687            ready,
688        }
689    }
690
691    pub fn announce(&mut self, geometry: Geometry, producer_pid: u32) -> Result<(), GeometryError> {
692        self.announce_at(geometry, producer_pid, 0)
693    }
694
695    /// Announces a ring whose lane sequence counters start at `origin`.
696    pub fn announce_at(
697        &mut self,
698        geometry: Geometry,
699        producer_pid: u32,
700        origin: u32,
701    ) -> Result<(), GeometryError> {
702        let geometry = geometry.validate()?;
703        let layout = layout(geometry).expect("validated geometry has a layout");
704        if self.region.words32.len() * 4 < layout.prefix_bytes
705            || self.region.words64.len() < COUNTER_WORDS
706        {
707            return Err(GeometryError::RegionTooLarge);
708        }
709        for word in self.region.words32.iter().take(HEADER_WORDS) {
710            word.store(0, Ordering::Relaxed);
711        }
712        for word in self.region.words64 {
713            word.store(0, Ordering::Relaxed);
714        }
715        let fields = [
716            (MAGIC_I, MAGIC),
717            (VERSION_I, LAYOUT_VERSION),
718            (TOTAL_I, layout.total_bytes as u32),
719            (LANES_I, geometry.lanes),
720            (INPUTS_I, geometry.input_channels),
721            (OUTPUTS_I, geometry.output_channels),
722            (FRAMES_I, geometry.max_frames),
723            (RATE_I, geometry.sample_rate),
724            (FORMAT_I, geometry.sample_format as u32),
725            (SLOTS_I, geometry.slots),
726            (LATENCY_I, geometry.latency_bound_frames),
727            (PRODUCER_PID_I, producer_pid),
728        ];
729        for (index, value) in fields {
730            self.region.store32(index, value.to_le(), Ordering::Relaxed);
731        }
732        for lane in 0..geometry.lanes as usize {
733            self.region
734                .store32(SEQUENCE_I + lane, origin.to_le(), Ordering::Relaxed);
735            self.region
736                .store32(COMPLETED_I + lane, origin.to_le(), Ordering::Relaxed);
737            let base = LANE_GEOMETRY_I + lane * 3;
738            self.region.store32(
739                base,
740                (layout.first_slot_offsets[lane] as u32).to_le(),
741                Ordering::Relaxed,
742            );
743            self.region.store32(
744                base + 1,
745                (layout.slot_bytes as u32).to_le(),
746                Ordering::Relaxed,
747            );
748            self.region
749                .store32(base + 2, geometry.slots.to_le(), Ordering::Relaxed);
750        }
751        self.region
752            .store32(FAULT_I, Fault::None as u32, Ordering::Release);
753        self.region
754            .store32(STATE_I, State::Announced as u32, Ordering::Release);
755        self.geometry = Some(geometry);
756        self.layout = Some(layout);
757        Ok(())
758    }
759
760    pub fn wait_ready(&self, timeout: Duration) -> Waited {
761        let deadline = Instant::now() + timeout;
762        loop {
763            match self.region.state() {
764                Some(State::Ready) => return Waited::Signalled,
765                Some(State::Fault | State::Closing) | None => return Waited::PeerGone,
766                _ => {}
767            }
768            let now = Instant::now();
769            if now >= deadline {
770                return Waited::TimedOut;
771            }
772            match self.ready.wait(deadline.saturating_duration_since(now)) {
773                Waited::Signalled => {}
774                other => return other,
775            }
776        }
777    }
778
779    /// The real-time half, once [`Producer::announce`] has succeeded.
780    pub fn publisher(&self) -> Option<Publisher> {
781        Some(Publisher {
782            geometry: self.geometry?,
783            layout: self.layout?,
784        })
785    }
786
787    pub fn completed(&self, lane: usize) -> u32 {
788        self.publisher()
789            .map_or(0, |publisher| publisher.completed(self.region, lane))
790    }
791
792    pub fn try_publish<F>(&self, lane: usize, sequence: u32, write: F) -> Publish
793    where
794        F: FnOnce(&mut SlotWriter<'_>),
795    {
796        let Some(publisher) = self.publisher() else {
797            return Publish::Dropped;
798        };
799        let Some(work) = self.work.get(lane).copied() else {
800            return Publish::Dropped;
801        };
802        publisher.try_publish(self.region, lane, sequence, work, write)
803    }
804
805    pub fn try_read<F>(&self, lane: usize, sequence: u32, read: F) -> bool
806    where
807        F: FnOnce(SlotReader<'_>),
808    {
809        self.publisher()
810            .is_some_and(|publisher| publisher.try_read(self.region, lane, sequence, read))
811    }
812
813    pub fn close(&self) {
814        self.region
815            .store32(STATE_I, State::Closing as u32, Ordering::Release);
816        for signal in self.work.iter().flatten() {
817            signal.signal();
818        }
819    }
820}
821
822#[derive(Debug)]
823pub struct SlotWriter<'a> {
824    metadata: &'a [AtomicU32],
825    format: SampleFormat,
826    samples: &'a [AtomicU32],
827    frames: usize,
828    planes: usize,
829}
830
831impl SlotWriter<'_> {
832    pub fn frames(&self) -> usize {
833        self.frames
834    }
835
836    pub fn planes(&self) -> usize {
837        self.planes
838    }
839
840    pub fn write(&mut self, plane: usize, frame: usize, sample: f32) {
841        self.write_f64(plane, frame, f64::from(sample));
842    }
843
844    pub fn write_f64(&mut self, plane: usize, frame: usize, sample: f64) {
845        let index = (plane * self.frames + frame) * self.format.words();
846        if self.format == SampleFormat::F64 {
847            self.samples[index].store((sample.to_bits() as u32).to_le(), Ordering::Relaxed);
848            self.samples[index + 1]
849                .store(((sample.to_bits() >> 32) as u32).to_le(), Ordering::Relaxed);
850        } else {
851            self.samples[index].store((sample as f32).to_bits().to_le(), Ordering::Relaxed);
852        }
853    }
854
855    pub fn write_plane(&mut self, plane: usize, samples: &[f32]) -> bool {
856        if plane >= self.planes || samples.len() != self.frames {
857            return false;
858        }
859        let words = self.format.words();
860        let destination =
861            &self.samples[plane * self.frames * words..(plane + 1) * self.frames * words];
862        if self.format == SampleFormat::F64 {
863            for (to, from) in destination.as_chunks::<2>().0.iter().zip(samples) {
864                let bits = f64::from(*from).to_bits();
865                to[0].store((bits as u32).to_le(), Ordering::Relaxed);
866                to[1].store(((bits >> 32) as u32).to_le(), Ordering::Relaxed);
867            }
868        } else {
869            for (to, from) in destination.iter().zip(samples) {
870                to.store(from.to_bits().to_le(), Ordering::Relaxed);
871            }
872        }
873        true
874    }
875
876    pub fn write_plane_f64(&mut self, plane: usize, samples: &[f64]) -> bool {
877        if plane >= self.planes || samples.len() != self.frames {
878            return false;
879        }
880        let words = self.format.words();
881        let destination =
882            &self.samples[plane * self.frames * words..(plane + 1) * self.frames * words];
883        if self.format == SampleFormat::F64 {
884            for (to, from) in destination.as_chunks::<2>().0.iter().zip(samples) {
885                let bits = from.to_bits();
886                to[0].store((bits as u32).to_le(), Ordering::Relaxed);
887                to[1].store(((bits >> 32) as u32).to_le(), Ordering::Relaxed);
888            }
889        } else {
890            for (to, from) in destination.iter().zip(samples) {
891                to.store((*from as f32).to_bits().to_le(), Ordering::Relaxed);
892            }
893        }
894        true
895    }
896
897    pub fn clear_plane(&mut self, plane: usize) -> bool {
898        if plane >= self.planes {
899            return false;
900        }
901        let words = self.format.words();
902        let destination =
903            &self.samples[plane * self.frames * words..(plane + 1) * self.frames * words];
904        for word in destination {
905            word.store(0, Ordering::Relaxed);
906        }
907        true
908    }
909
910    pub fn set_transport(&mut self, transport: Transport) {
911        for (to, from) in self.metadata.iter().zip(transport.words()) {
912            to.store(from.to_le(), Ordering::Relaxed);
913        }
914    }
915    pub fn set_events(&mut self, events: &[Event]) -> bool {
916        if events.len() > MAX_EVENTS || events.iter().any(|e| !e.valid(self.frames as u32)) {
917            return false;
918        }
919        store_packet(
920            &self.metadata[CONTEXT_WORDS..CONTEXT_WORDS + PACKET_WORDS],
921            events,
922            0,
923        );
924        true
925    }
926    pub fn copy_from(&mut self, samples: &[f32]) -> bool {
927        if self.format != SampleFormat::F32 || samples.len() != self.samples.len() {
928            return false;
929        }
930        for (destination, sample) in self.samples.iter().zip(samples) {
931            destination.store(sample.to_bits(), Ordering::Relaxed);
932        }
933        true
934    }
935}
936
937#[derive(Clone, Copy, Debug)]
938pub struct SlotReader<'a> {
939    metadata: &'a [AtomicU32],
940    format: SampleFormat,
941    samples: &'a [AtomicU32],
942    frames: usize,
943    planes: usize,
944}
945
946impl SlotReader<'_> {
947    pub fn frames(self) -> usize {
948        self.frames
949    }
950
951    pub fn planes(self) -> usize {
952        self.planes
953    }
954
955    pub fn read(self, plane: usize, frame: usize) -> f32 {
956        self.read_f64(plane, frame) as f32
957    }
958
959    pub fn read_f64(self, plane: usize, frame: usize) -> f64 {
960        let index = (plane * self.frames + frame) * self.format.words();
961        let lo = u32::from_le(self.samples[index].load(Ordering::Relaxed));
962        if self.format == SampleFormat::F64 {
963            let hi = u32::from_le(self.samples[index + 1].load(Ordering::Relaxed));
964            f64::from_bits(u64::from(lo) | (u64::from(hi) << 32))
965        } else {
966            f64::from(f32::from_bits(lo))
967        }
968    }
969
970    pub fn read_plane(self, plane: usize, samples: &mut [f32]) -> bool {
971        if plane >= self.planes || samples.len() != self.frames {
972            return false;
973        }
974        let words = self.format.words();
975        let source = &self.samples[plane * self.frames * words..(plane + 1) * self.frames * words];
976        if self.format == SampleFormat::F64 {
977            for (to, from) in samples.iter_mut().zip(source.as_chunks::<2>().0.iter()) {
978                let lo = u32::from_le(from[0].load(Ordering::Relaxed));
979                let hi = u32::from_le(from[1].load(Ordering::Relaxed));
980                *to = f64::from_bits(u64::from(lo) | (u64::from(hi) << 32)) as f32;
981            }
982        } else {
983            for (to, from) in samples.iter_mut().zip(source) {
984                *to = f32::from_bits(u32::from_le(from.load(Ordering::Relaxed)));
985            }
986        }
987        true
988    }
989
990    pub fn read_plane_f64(self, plane: usize, samples: &mut [f64]) -> bool {
991        if plane >= self.planes || samples.len() != self.frames {
992            return false;
993        }
994        let words = self.format.words();
995        let source = &self.samples[plane * self.frames * words..(plane + 1) * self.frames * words];
996        if self.format == SampleFormat::F64 {
997            for (to, from) in samples.iter_mut().zip(source.as_chunks::<2>().0.iter()) {
998                let lo = u32::from_le(from[0].load(Ordering::Relaxed));
999                let hi = u32::from_le(from[1].load(Ordering::Relaxed));
1000                *to = f64::from_bits(u64::from(lo) | (u64::from(hi) << 32));
1001            }
1002        } else {
1003            for (to, from) in samples.iter_mut().zip(source) {
1004                *to = f64::from(f32::from_bits(u32::from_le(from.load(Ordering::Relaxed))));
1005            }
1006        }
1007        true
1008    }
1009
1010    pub fn output_events(self, events: &mut EventBuffer) -> bool {
1011        load_packet(
1012            &self.metadata[CONTEXT_WORDS + PACKET_WORDS..],
1013            self.frames as u32,
1014            events,
1015        )
1016    }
1017    pub fn copy_to(self, samples: &mut [f32]) -> bool {
1018        if self.format != SampleFormat::F32 || samples.len() != self.samples.len() {
1019            return false;
1020        }
1021        for (destination, sample) in samples.iter_mut().zip(self.samples) {
1022            *destination = f32::from_bits(sample.load(Ordering::Relaxed));
1023        }
1024        true
1025    }
1026}
1027
1028fn store_packet(words: &[AtomicU32], events: &[Event], dropped: u32) {
1029    words[0].store((events.len() as u32).to_le(), Ordering::Relaxed);
1030    words[1].store(dropped.to_le(), Ordering::Relaxed);
1031    for (target, event) in words[2..].as_chunks::<EVENT_WORDS>().0.iter().zip(events) {
1032        for (to, from) in target.iter().zip(event.words()) {
1033            to.store(from.to_le(), Ordering::Relaxed);
1034        }
1035    }
1036}
1037fn load_packet(words: &[AtomicU32], frames: u32, events: &mut EventBuffer) -> bool {
1038    events.clear();
1039    let count = u32::from_le(words[0].load(Ordering::Relaxed)) as usize;
1040    if count > MAX_EVENTS {
1041        return false;
1042    }
1043    events.dropped = u32::from_le(words[1].load(Ordering::Relaxed));
1044    for source in words[2..].as_chunks::<EVENT_WORDS>().0.iter().take(count) {
1045        let mut values = [0; EVENT_WORDS];
1046        for (to, from) in values.iter_mut().zip(source) {
1047            *to = u32::from_le(from.load(Ordering::Relaxed));
1048        }
1049        let Some(event) = Event::from_words(&values, frames) else {
1050            events.clear();
1051            return false;
1052        };
1053        events.push(event);
1054    }
1055    true
1056}
1057
1058/// The worker-side half of the ring.
1059///
1060/// Use one consumer per lane. After [`Consumer::next_published`] observes the
1061/// next sequence with Acquire, read and process that slot through the methods
1062/// below, then call [`Consumer::complete`] for the same sequence before moving
1063/// to the next one. These ordering and ownership rules pair with [`Publisher`].
1064pub struct Consumer<'a> {
1065    region: Region<'a>,
1066    geometry: Option<Geometry>,
1067    layout: Option<Layout>,
1068    ready: &'a dyn Signal,
1069    done: [Option<&'a dyn Signal>; 2],
1070}
1071
1072impl fmt::Debug for Consumer<'_> {
1073    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1074        formatter
1075            .debug_struct("Consumer")
1076            .field("region", &self.region)
1077            .field("geometry", &self.geometry)
1078            .field("layout", &self.layout)
1079            .finish_non_exhaustive()
1080    }
1081}
1082
1083impl<'a> Consumer<'a> {
1084    pub fn new(
1085        region: Region<'a>,
1086        ready: &'a dyn Signal,
1087        done: [Option<&'a dyn Signal>; 2],
1088    ) -> Self {
1089        Self {
1090            region,
1091            geometry: None,
1092            layout: None,
1093            ready,
1094            done,
1095        }
1096    }
1097
1098    pub fn validate_and_ready(&mut self, consumer_pid: u32) -> Result<Geometry, HeaderError> {
1099        let geometry = validate_header(self.region)?;
1100        let layout = layout(geometry).expect("validated geometry has a layout");
1101        self.geometry = Some(geometry);
1102        self.layout = Some(layout);
1103        self.region
1104            .store32(CONSUMER_PID_I, consumer_pid.to_le(), Ordering::Relaxed);
1105        self.region
1106            .store32(FAULT_I, Fault::None as u32, Ordering::Relaxed);
1107        self.region
1108            .store32(STATE_I, State::Ready as u32, Ordering::Release);
1109        self.ready.signal();
1110        Ok(geometry)
1111    }
1112
1113    pub fn fault(&self, code: u32) {
1114        self.region.store32(FAULT_I, code, Ordering::Relaxed);
1115        self.region
1116            .store32(STATE_I, State::Fault as u32, Ordering::Release);
1117        self.ready.signal();
1118        for signal in self.done.iter().flatten() {
1119            signal.signal();
1120        }
1121    }
1122
1123    /// Acquires publication and returns only the next uncompleted sequence.
1124    /// Slot access for that sequence must happen after this call.
1125    pub fn next_published(&self, lane: usize) -> Option<u32> {
1126        let geometry = self.geometry?;
1127        if lane >= geometry.lanes as usize || self.region.state() != Some(State::Ready) {
1128            return None;
1129        }
1130        let published = u32::from_le(self.region.load32(SEQUENCE_I + lane, Ordering::Acquire));
1131        let completed = u32::from_le(self.region.load32(COMPLETED_I + lane, Ordering::Acquire));
1132        let next = completed.wrapping_add(1);
1133        (published != completed && sequence_at_least(published, next)).then_some(next)
1134    }
1135
1136    /// Copies one slot to `buffer`, lets the caller process ordinary f32s, and
1137    /// copies it back. No plugin ever receives shared-memory atomics.
1138    pub fn slot_mut<F>(&self, lane: usize, sequence: u32, buffer: &mut [f32], process: F) -> bool
1139    where
1140        F: FnOnce(&mut [f32]),
1141    {
1142        let Some((geometry, layout)) = self.geometry.zip(self.layout) else {
1143            return false;
1144        };
1145        if geometry.sample_format != SampleFormat::F32
1146            || lane >= geometry.lanes as usize
1147            || buffer.len() != geometry.samples_per_slot()
1148        {
1149            return false;
1150        }
1151        let offset = layout.slot_offset(lane, sequence) / 4;
1152        let samples = &self.region.words32[offset..offset + buffer.len()];
1153        for (destination, sample) in buffer.iter_mut().zip(samples) {
1154            *destination = f32::from_bits(sample.load(Ordering::Relaxed));
1155        }
1156        process(buffer);
1157        for (destination, sample) in samples.iter().zip(buffer) {
1158            destination.store(sample.to_bits(), Ordering::Relaxed);
1159        }
1160        true
1161    }
1162
1163    pub fn slot_mut_f64<F>(
1164        &self,
1165        lane: usize,
1166        sequence: u32,
1167        buffer: &mut [f64],
1168        process: F,
1169    ) -> bool
1170    where
1171        F: FnOnce(&mut [f64]),
1172    {
1173        let Some((geometry, layout)) = self.geometry.zip(self.layout) else {
1174            return false;
1175        };
1176        if geometry.sample_format != SampleFormat::F64
1177            || lane >= geometry.lanes as usize
1178            || buffer.len() != geometry.samples_per_slot()
1179        {
1180            return false;
1181        }
1182        let offset = layout.slot_offset(lane, sequence) / 4;
1183        let samples = &self.region.words32[offset..offset + buffer.len() * 2];
1184        for (to, from) in buffer.iter_mut().zip(samples.as_chunks::<2>().0.iter()) {
1185            *to = f64::from_bits(
1186                u64::from(u32::from_le(from[0].load(Ordering::Relaxed)))
1187                    | (u64::from(u32::from_le(from[1].load(Ordering::Relaxed))) << 32),
1188            );
1189        }
1190        process(buffer);
1191        for (to, from) in samples.as_chunks::<2>().0.iter().zip(buffer) {
1192            to[0].store((from.to_bits() as u32).to_le(), Ordering::Relaxed);
1193            to[1].store(((from.to_bits() >> 32) as u32).to_le(), Ordering::Relaxed);
1194        }
1195        true
1196    }
1197    fn metadata(&self, lane: usize, sequence: u32) -> Option<&[AtomicU32]> {
1198        let geometry = self.geometry?;
1199        let layout = self.layout?;
1200        if lane >= geometry.lanes as usize {
1201            return None;
1202        }
1203        let offset = layout.slot_offset(lane, sequence) / 4
1204            + geometry.samples_per_slot() * geometry.sample_format.words();
1205        self.region.words32.get(offset..offset + METADATA_WORDS)
1206    }
1207    pub fn input_packet(
1208        &self,
1209        lane: usize,
1210        sequence: u32,
1211        context: &mut Transport,
1212        events: &mut EventBuffer,
1213    ) -> bool {
1214        let Some(words) = self.metadata(lane, sequence) else {
1215            return false;
1216        };
1217        let mut raw = [0; CONTEXT_WORDS];
1218        for (to, from) in raw.iter_mut().zip(words) {
1219            *to = u32::from_le(from.load(Ordering::Relaxed));
1220        }
1221        let Some(value) = Transport::from_words(&raw) else {
1222            return false;
1223        };
1224        *context = value;
1225        load_packet(
1226            &words[CONTEXT_WORDS..CONTEXT_WORDS + PACKET_WORDS],
1227            self.geometry.expect("validated").max_frames,
1228            events,
1229        )
1230    }
1231    pub fn output_packet(&self, lane: usize, sequence: u32, events: &EventBuffer) -> bool {
1232        let Some(words) = self.metadata(lane, sequence) else {
1233            return false;
1234        };
1235        if events
1236            .as_slice()
1237            .iter()
1238            .any(|e| !e.valid(self.geometry.expect("validated").max_frames))
1239        {
1240            return false;
1241        }
1242        store_packet(
1243            &words[CONTEXT_WORDS + PACKET_WORDS..],
1244            events.as_slice(),
1245            events.dropped,
1246        );
1247        true
1248    }
1249
1250    /// Releases `sequence` after all sample and packet processing has finished,
1251    /// allowing the producer to read and eventually reuse the slot.
1252    pub fn complete(&self, lane: usize, sequence: u32) {
1253        let Some(geometry) = self.geometry else {
1254            return;
1255        };
1256        if lane >= geometry.lanes as usize {
1257            return;
1258        }
1259        self.region
1260            .store32(COMPLETED_I + lane, sequence.to_le(), Ordering::Release);
1261        if let Some(signal) = self.done[lane] {
1262            signal.signal();
1263        }
1264    }
1265
1266    pub fn beat(&self) -> u64 {
1267        self.region
1268            .words64
1269            .first()
1270            .map_or(0, |v| v.fetch_add(1, Ordering::Release).wrapping_add(1))
1271    }
1272
1273    pub fn set_last_error(&self, error: u64) {
1274        if let Some(word) = self.region.words64.get(CACHE_LINE / 8) {
1275            word.store(error, Ordering::Release);
1276        }
1277    }
1278}
1279
1280fn validate_header(region: Region<'_>) -> Result<Geometry, HeaderError> {
1281    if region.words32.len() < HEADER_WORDS || region.words64.len() < COUNTER_WORDS {
1282        return Err(HeaderError::TooShort);
1283    }
1284    if u32::from_le(region.load32(MAGIC_I, Ordering::Relaxed)) != MAGIC {
1285        return Err(HeaderError::Magic);
1286    }
1287    if u32::from_le(region.load32(VERSION_I, Ordering::Relaxed)) != LAYOUT_VERSION {
1288        return Err(HeaderError::Version);
1289    }
1290    if region.state() != Some(State::Announced) {
1291        return Err(HeaderError::State);
1292    }
1293    let geometry = Geometry {
1294        sample_format: match u32::from_le(region.load32(FORMAT_I, Ordering::Relaxed)) {
1295            0 => SampleFormat::F32,
1296            1 => SampleFormat::F64,
1297            _ => return Err(HeaderError::Version),
1298        },
1299        lanes: u32::from_le(region.load32(LANES_I, Ordering::Relaxed)),
1300        slots: u32::from_le(region.load32(SLOTS_I, Ordering::Relaxed)),
1301        input_channels: u32::from_le(region.load32(INPUTS_I, Ordering::Relaxed)),
1302        output_channels: u32::from_le(region.load32(OUTPUTS_I, Ordering::Relaxed)),
1303        max_frames: u32::from_le(region.load32(FRAMES_I, Ordering::Relaxed)),
1304        sample_rate: u32::from_le(region.load32(RATE_I, Ordering::Relaxed)),
1305        latency_bound_frames: u32::from_le(region.load32(LATENCY_I, Ordering::Relaxed)),
1306    }
1307    .validate()
1308    .map_err(HeaderError::Geometry)?;
1309    let expected = layout(geometry).expect("validated geometry has a layout");
1310    if u32::from_le(region.load32(TOTAL_I, Ordering::Relaxed)) as usize != expected.total_bytes
1311        || region.words32.len() * 4 < expected.prefix_bytes
1312    {
1313        return Err(HeaderError::TotalBytes);
1314    }
1315    for lane in 0..geometry.lanes as usize {
1316        let base = LANE_GEOMETRY_I + lane * 3;
1317        let actual = [
1318            u32::from_le(region.load32(base, Ordering::Relaxed)) as usize,
1319            u32::from_le(region.load32(base + 1, Ordering::Relaxed)) as usize,
1320            u32::from_le(region.load32(base + 2, Ordering::Relaxed)) as usize,
1321        ];
1322        if actual
1323            != [
1324                expected.first_slot_offsets[lane],
1325                expected.slot_bytes,
1326                expected.slots as usize,
1327            ]
1328        {
1329            return Err(HeaderError::LaneGeometry);
1330        }
1331    }
1332    Ok(geometry)
1333}
1334
1335/// Compares modulo-2^32 serial numbers using their signed forward distance.
1336///
1337/// Ring users maintain `0 <= D(published, completed) <= slots <= 16 < 2^31`,
1338/// so every sequence pair compared here is unambiguous across wraparound.
1339pub fn sequence_at_least(value: u32, reference: u32) -> bool {
1340    value.wrapping_sub(reference) as i32 >= 0
1341}
1342
1343#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1344struct PipelineEntry {
1345    sequence: u32,
1346    app_block: u64,
1347    generation: u64,
1348    valid: bool,
1349}
1350
1351/// Metadata enforcing a fixed-depth pipeline.
1352///
1353/// An application that publishes block `k` and then reads must use
1354/// `depth <= slots - 1`; read-then-publish order allows `depth <= slots`.
1355#[derive(Debug)]
1356pub struct Pipeline {
1357    entries: [PipelineEntry; MAX_SLOTS as usize],
1358    depth: u32,
1359}
1360
1361impl Default for Pipeline {
1362    fn default() -> Self {
1363        Self::with_depth(1)
1364    }
1365}
1366
1367impl Pipeline {
1368    pub fn new() -> Self {
1369        Self::default()
1370    }
1371
1372    pub fn with_depth(depth: u32) -> Self {
1373        assert!((1..=MAX_SLOTS).contains(&depth));
1374        Self {
1375            entries: array::from_fn(|_| PipelineEntry::default()),
1376            depth,
1377        }
1378    }
1379
1380    pub fn depth(&self) -> u32 {
1381        self.depth
1382    }
1383
1384    pub fn set_depth(&mut self, depth: u32) {
1385        assert!((1..=MAX_SLOTS).contains(&depth));
1386        self.depth = depth;
1387        self.clear();
1388    }
1389
1390    pub fn published(&mut self, sequence: u32, app_block: u64, generation: u64) {
1391        self.entries[sequence as usize % MAX_SLOTS as usize] = PipelineEntry {
1392            sequence,
1393            app_block,
1394            generation,
1395            valid: true,
1396        };
1397    }
1398
1399    /// Returns the sequence for `app_block - depth` only when that exact block
1400    /// was published, completed, and belongs to the current generation.
1401    pub fn output_sequence(
1402        &mut self,
1403        app_block: u64,
1404        generation: u64,
1405        completed: u32,
1406    ) -> Option<u32> {
1407        let wanted = app_block.checked_sub(u64::from(self.depth))?;
1408        let found = self.entries.iter_mut().find(|entry| {
1409            entry.valid && entry.app_block == wanted && entry.generation == generation
1410        })?;
1411        if sequence_at_least(completed, found.sequence) {
1412            found.valid = false;
1413            Some(found.sequence)
1414        } else {
1415            // Once its output instant passes, a late result must never be used.
1416            found.valid = false;
1417            None
1418        }
1419    }
1420
1421    pub fn clear(&mut self) {
1422        self.entries = array::from_fn(|_| PipelineEntry::default());
1423    }
1424}