Skip to main content

moq_net/model/
frame.rs

1//! Frames are the leaf of the model: a sized, timestamped payload within a group.
2//!
3//! A group is a single ordered stream, so at most one frame is ever in flight.
4//! Completed frames are plain data ([`Frame`]); the in-flight frame is written
5//! through [`Producer`], which borrows its parent [`group::Producer`] exclusively so
6//! the borrow checker enforces that only one frame is open at a time. A [`Consumer`]
7//! reads one frame, sharing the group's channel rather than a per-frame one.
8use std::sync::Arc;
9use std::sync::OnceLock;
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::task::{Poll, ready};
12
13use arrayvec::ArrayVec;
14use bytes::Bytes;
15
16use crate::group::{self, GroupState};
17use crate::{Error, IntoBytes, Result, Timestamp, stats};
18
19/// A chunk of data with an upfront size and a presentation timestamp.
20///
21/// This is just the header; the payload is carried separately (as a completed
22/// [`Frame`] or streamed via [`Producer`] / [`Consumer`]).
23#[derive(Clone, Copy, Debug)]
24pub struct Info {
25	/// Total payload size in bytes. Declared up front so consumers can preallocate.
26	pub size: u64,
27	/// Presentation timestamp.
28	///
29	/// [`group::Producer::create_frame`] converts it into the parent track's
30	/// timescale, so the scale you build it with doesn't have to match the track.
31	/// For data without a presentation time, pass [`Timestamp::now`] explicitly.
32	pub timestamp: Timestamp,
33}
34
35/// A completed frame: a timestamp and its full, contiguous payload.
36///
37/// This is the stored form of every finished frame in a group. The payload is a
38/// single [`Bytes`], so a consumer gets it with one zero-copy slice.
39#[derive(Clone, Debug)]
40pub struct Frame {
41	/// Presentation timestamp, at the parent track's timescale.
42	pub timestamp: Timestamp,
43	/// The full frame payload.
44	pub payload: Bytes,
45}
46
47/// A reusable batch of frames, filled by [`group::Consumer::read_frames`] and drained
48/// by [`group::Producer::write_frames`].
49///
50/// A fixed-capacity inline buffer: `N` frames of stack storage, never a heap
51/// allocation and never a spill. Allocate one per task and reuse it for the life of
52/// the group rather than one per read.
53///
54/// `N` defaults to 8. Most reads are not big batches: at the live edge a frame arrives
55/// at a time, so the capacity past the first frame or two is idle stack, and a
56/// publisher holds one buffer per in-flight group. 8 costs 384 bytes and still reads
57/// ~5x faster than a frame at a time (`benches/group.rs`). Ask for a larger `N` when
58/// you know you are draining a backlog: 32 is ~8x, for 1.5 KB.
59///
60/// A default on a const parameter only applies in type position, so name the type to
61/// get it: `let buf: frame::Buffer = Buffer::new()`.
62///
63/// A fill stamps the group's cache access once for the whole batch, which bounds
64/// frames rather than elapsed time. A reader that may take longer than the track's
65/// `latency_max` to work through one batch calls
66/// [`group::Consumer::keep_alive`] between frames, or the rest of the group is
67/// expired out from under it.
68#[derive(Debug, Default)]
69pub struct Buffer<const N: usize = 8>(ArrayVec<Frame, N>);
70
71impl<const N: usize> Buffer<N> {
72	/// An empty buffer with room for `N` frames.
73	pub fn new() -> Self {
74		Self(ArrayVec::new())
75	}
76
77	/// How many frames a single fill can hold.
78	pub const fn capacity(&self) -> usize {
79		N
80	}
81
82	/// How many frames the buffer currently holds.
83	pub fn len(&self) -> usize {
84		self.0.len()
85	}
86
87	/// Whether the buffer holds no frames.
88	pub fn is_empty(&self) -> bool {
89		self.0.is_empty()
90	}
91
92	/// Whether the buffer is at capacity, so [`Self::push`] would refuse.
93	pub fn is_full(&self) -> bool {
94		self.0.is_full()
95	}
96
97	/// The frames from the most recent fill, in order.
98	pub fn filled(&self) -> &[Frame] {
99		&self.0
100	}
101
102	/// The frames from the most recent fill, mutably (to take payloads out, say).
103	pub fn filled_mut(&mut self) -> &mut [Frame] {
104		&mut self.0
105	}
106
107	/// Append a frame, handing it back if the buffer is already full.
108	///
109	/// Fill a buffer this way to hand a whole batch to
110	/// [`group::Producer::write_frames`].
111	pub fn push(&mut self, frame: Frame) -> std::result::Result<(), Frame> {
112		self.0.try_push(frame).map_err(|err| err.element())
113	}
114
115	/// Move every frame out, leaving the buffer empty.
116	///
117	/// Frames left in the iterator when it drops are dropped with it, so the buffer
118	/// ends up empty either way.
119	pub fn drain(&mut self) -> impl ExactSizeIterator<Item = Frame> + '_ {
120		self.0.drain(..)
121	}
122
123	/// Drop the current batch, leaving the buffer empty.
124	pub fn clear(&mut self) {
125		self.0.clear();
126	}
127
128	/// Replace the contents with up to `N` frames, returning how many were written.
129	/// Must be cleared first.
130	pub(crate) fn fill(&mut self, frames: impl Iterator<Item = Frame>) -> usize {
131		debug_assert!(self.is_empty(), "fill on a non-empty buffer would drop frames");
132		self.0.extend(frames.take(N));
133		self.len()
134	}
135}
136
137/// Payload storage for the single in-flight frame, shared between the writing
138/// [`Producer`] and any streaming [`Consumer`]s.
139///
140/// A whole-frame [`Bytes`] write is stored directly. Chunked writes fall back to one
141/// mutable heap allocation sized to the declared frame. The producer writes through
142/// the raw pointer (sole writer, guaranteed by the exclusive borrow of the parent
143/// group); `written` provides happens-before for cross-thread reads. Implements
144/// [AsRef]<[u8]> so it can back a [`Bytes::from_owner`].
145#[derive(Clone)]
146pub(crate) struct FrameBuf(Arc<FrameBufInner>);
147
148struct FrameBufInner {
149	capacity: usize,
150	written: AtomicUsize,
151	storage: OnceLock<FrameStorage>,
152}
153
154enum FrameStorage {
155	Shared(Bytes),
156	Mutable(MutableFrameBuf),
157}
158
159struct MutableFrameBuf {
160	// Owned heap allocation of `capacity` bytes (zero-initialized).
161	data: *mut u8,
162	capacity: usize,
163}
164
165// Safety: `data` is owned (Box-allocated, freed in Drop). The producer is the sole
166// writer and consumers only read bytes `< written`.
167unsafe impl Send for MutableFrameBuf {}
168unsafe impl Sync for MutableFrameBuf {}
169
170impl Drop for MutableFrameBuf {
171	fn drop(&mut self) {
172		// Safety: data was obtained from `Box::into_raw` of a `Box<[u8]>` of length
173		// `capacity` and is not aliased at drop (Arc refcount hit 0).
174		unsafe {
175			let slice = std::ptr::slice_from_raw_parts_mut(self.data, self.capacity);
176			drop(Box::from_raw(slice));
177		}
178	}
179}
180
181impl MutableFrameBuf {
182	fn new(size: usize) -> Self {
183		let boxed: Box<[u8]> = vec![0u8; size].into_boxed_slice();
184		let capacity = boxed.len();
185		let data = Box::into_raw(boxed) as *mut u8;
186		Self { data, capacity }
187	}
188}
189
190impl FrameBuf {
191	/// Allocate a buffer for a frame of `size` bytes.
192	///
193	/// The oversized-frame guard lives in [`group::Producer`], which rejects a declared
194	/// size larger than the group's byte budget before calling this.
195	pub(crate) fn new(size: usize) -> Self {
196		Self(Arc::new(FrameBufInner {
197			capacity: size,
198			written: AtomicUsize::new(0),
199			storage: OnceLock::new(),
200		}))
201	}
202
203	pub(crate) fn capacity(&self) -> usize {
204		self.0.capacity
205	}
206
207	pub(crate) fn written(&self, ord: Ordering) -> usize {
208		self.0.written.load(ord)
209	}
210
211	fn try_set_bytes(&self, bytes: Bytes) -> std::result::Result<(), Bytes> {
212		if bytes.len() != self.capacity() || self.written(Ordering::Acquire) != 0 {
213			return Err(bytes);
214		}
215		self.0
216			.storage
217			.set(FrameStorage::Shared(bytes))
218			.map_err(|storage| match storage {
219				FrameStorage::Shared(bytes) => bytes,
220				FrameStorage::Mutable(_) => unreachable!("try_set_bytes only installs shared storage"),
221			})
222	}
223
224	/// The mutable buffer for multi-chunk writes, lazily allocated.
225	///
226	/// Returns `None` once a whole-frame write has installed shared storage.
227	fn mutable(&self) -> Option<&MutableFrameBuf> {
228		match self
229			.0
230			.storage
231			.get_or_init(|| FrameStorage::Mutable(MutableFrameBuf::new(self.capacity())))
232		{
233			FrameStorage::Shared(_) => None,
234			FrameStorage::Mutable(buf) => Some(buf),
235		}
236	}
237
238	/// Safety: caller must be the sole producer and `new_written` must be `<= capacity`.
239	unsafe fn store_written(&self, new_written: usize) {
240		// Release pairs with consumers' Acquire load to publish prior writes.
241		self.0.written.store(new_written, Ordering::Release);
242	}
243
244	/// Append `src` at the current write offset and publish it.
245	///
246	/// Safety relies on the single-producer invariant: only one [`Producer`] exists for
247	/// a frame (it holds the exclusive borrow of the parent group), so this is the sole
248	/// writer even though it takes `&self`.
249	fn append(&self, src: &[u8]) {
250		if src.is_empty() {
251			return;
252		}
253		let prev = self.written(Ordering::Relaxed);
254		let Some(buf) = self.mutable() else {
255			// Only reachable if the frame is already complete via shared storage, which
256			// `Producer::write` rejects for a non-empty chunk. Nothing to copy.
257			return;
258		};
259		// Safety: sole writer; the caller bounds-checked `src` against the remaining
260		// capacity, and consumers only read `[..written]`.
261		unsafe {
262			std::ptr::copy_nonoverlapping(src.as_ptr(), buf.data.add(prev), src.len());
263			self.store_written(prev + src.len());
264		}
265	}
266
267	/// Freeze the buffer into the completed payload (`size` bytes).
268	///
269	/// Returns the shared [`Bytes`] directly for a whole-frame write (zero-copy), or
270	/// wraps the mutable allocation otherwise.
271	fn freeze(&self, size: usize) -> Bytes {
272		match self.0.storage.get() {
273			Some(FrameStorage::Shared(bytes)) => bytes.clone(),
274			_ => self.slice(0, size),
275		}
276	}
277
278	/// A zero-copy slice of the initialized region `[start..end]`.
279	fn slice(&self, start: usize, end: usize) -> Bytes {
280		Bytes::from_owner(self.clone()).slice(start..end)
281	}
282}
283
284impl AsRef<[u8]> for FrameBuf {
285	fn as_ref(&self) -> &[u8] {
286		// Snapshot the initialized region (bytes the producer has written so far).
287		// Acquire pairs with the producer's Release on `written`.
288		let written = self.0.written.load(Ordering::Acquire);
289		match self.0.storage.get() {
290			Some(FrameStorage::Shared(bytes)) => &bytes[..written],
291			Some(FrameStorage::Mutable(buf)) => {
292				// Safety: data..data+written is initialized (zero-init at alloc + producer
293				// writes up to `written`). The Arc keeps the allocation alive while any
294				// reference to the slice lives.
295				unsafe { std::slice::from_raw_parts(buf.data, written) }
296			}
297			None => &[],
298		}
299	}
300}
301
302/// Writes the payload of the single in-flight frame in one or more chunks.
303///
304/// Borrows the parent [`group::Producer`] exclusively, so no other frame can be
305/// opened while this one is live. The total bytes written must exactly match
306/// [`Info::size`]; call [`Self::finish`] to commit the frame (or [`Self::abort`] to
307/// fail it). Dropping without either aborts the group, since an unfinished frame
308/// leaves the group's stream broken.
309///
310/// A single whole-frame [`write`](Self::write) keeps the caller's allocation
311/// (zero-copy); chunked writes copy into one buffer sized to the declared frame.
312pub struct Producer<'a> {
313	group: &'a mut group::Producer,
314	buf: FrameBuf,
315	info: Info,
316	// Set once the frame is committed (finished) or aborted, so Drop is a no-op.
317	done: bool,
318	// Ingress payload meter, inherited from the parent group. Counts each written
319	// chunk's bytes. Empty (no-op) for an untagged group.
320	stats: stats::Meter,
321}
322
323impl std::ops::Deref for Producer<'_> {
324	type Target = Info;
325
326	fn deref(&self) -> &Self::Target {
327		&self.info
328	}
329}
330
331impl<'a> Producer<'a> {
332	pub(crate) fn new(group: &'a mut group::Producer, buf: FrameBuf, info: Info) -> Self {
333		Self {
334			group,
335			buf,
336			info,
337			done: false,
338			stats: stats::Meter::default(),
339		}
340	}
341
342	/// Attach the parent group's ingress meter, so written chunks bump `bytes`.
343	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
344		self.stats = meter;
345		self
346	}
347
348	/// The parent group this frame belongs to.
349	pub fn group(&self) -> group::Info {
350		self.group.info()
351	}
352
353	/// Bytes still needed to complete the frame.
354	pub fn remaining(&self) -> usize {
355		self.buf.capacity() - self.buf.written(Ordering::Acquire)
356	}
357
358	/// Write a chunk of data to the frame.
359	///
360	/// Returns [`Error::WrongSize`] if the chunk would exceed the remaining bytes.
361	pub fn write<B: IntoBytes>(&mut self, chunk: B) -> Result<()> {
362		let len = chunk.as_ref().len();
363		if len > self.remaining() {
364			return Err(Error::WrongSize);
365		}
366		// Ingress payload: count the chunk's bytes as they're written.
367		self.stats.bytes(len as u64);
368		// Fast path: a single whole-frame write keeps the caller's allocation.
369		if len == self.buf.capacity() && self.buf.written(Ordering::Acquire) == 0 {
370			match self.buf.try_set_bytes(chunk.into_bytes()) {
371				Ok(()) => {
372					let cap = self.buf.capacity();
373					// Safety: `try_set_bytes` checked the buffer exactly matches the declared
374					// size, so publishing all bytes is in bounds.
375					unsafe { self.buf.store_written(cap) };
376				}
377				Err(chunk) => self.buf.append(&chunk),
378			}
379		} else {
380			self.buf.append(chunk.as_ref());
381		}
382		self.group.frame_notify();
383		Ok(())
384	}
385
386	/// Commit the frame, verifying that all bytes were written.
387	///
388	/// Returns [`Error::WrongSize`] if the bytes written don't match [`Info::size`].
389	pub fn finish(mut self) -> Result<()> {
390		if self.buf.written(Ordering::Acquire) != self.buf.capacity() {
391			return Err(Error::WrongSize);
392		}
393		let payload = self.buf.freeze(self.buf.capacity());
394		self.group.frame_commit(Frame {
395			timestamp: self.info.timestamp,
396			payload,
397		})?;
398		self.done = true;
399		Ok(())
400	}
401
402	/// Abort the frame (and its group) with the given error.
403	pub fn abort(mut self, err: Error) -> Result<()> {
404		self.group.frame_abort(err);
405		self.done = true;
406		Ok(())
407	}
408}
409
410impl Drop for Producer<'_> {
411	fn drop(&mut self) {
412		if !self.done {
413			// An unfinished frame leaves the group stream broken; fail the group so
414			// consumers surface an error instead of hanging on the partial forever.
415			tracing::warn!(
416				group = self.group.info().sequence,
417				"frame::Producer dropped before writing all bytes"
418			);
419			self.group.frame_abort(Error::Dropped);
420		}
421	}
422}
423
424/// The source of a [`Consumer`]'s payload: a finished frame (whole) or the in-flight
425/// tail (streamed).
426#[derive(Clone)]
427pub(crate) enum Source {
428	Complete(Bytes),
429	Partial(FrameBuf),
430}
431
432/// Reads one frame's payload, streaming as bytes arrive for the in-flight tail.
433///
434/// Owns a handle to the parent group's channel (not a per-frame one), so a group with
435/// many frames doesn't allocate a channel per frame. Cloning yields an independent
436/// reader of the same frame.
437#[derive(Clone)]
438pub struct Consumer {
439	// The group's channel, used to park while a partial frame fills.
440	state: kio::Consumer<GroupState>,
441	info: Info,
442	source: Source,
443	// Byte offset consumed so far.
444	read_idx: usize,
445	// Egress payload meter, so chunks bump `bytes` exactly once as they're read out.
446	// Empty (no-op) for an untagged group.
447	stats: stats::Meter,
448}
449
450impl std::ops::Deref for Consumer {
451	type Target = Info;
452
453	fn deref(&self) -> &Self::Target {
454		&self.info
455	}
456}
457
458impl Consumer {
459	pub(crate) fn new(state: kio::Consumer<GroupState>, info: Info, source: Source) -> Self {
460		Self {
461			state,
462			info,
463			source,
464			read_idx: 0,
465			stats: stats::Meter::default(),
466		}
467	}
468
469	/// Attach an egress meter so read-out chunks bump `bytes`. Used only for frames
470	/// read directly from the group (whose bytes weren't counted at a batch fill).
471	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
472		self.stats = meter;
473		self
474	}
475
476	/// Poll for the next chunk of bytes since the last read.
477	///
478	/// Returns `None` once the frame is finished and all bytes have been consumed.
479	pub fn poll_read_chunk(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Bytes>>> {
480		match &self.source {
481			Source::Complete(bytes) => {
482				if self.read_idx >= bytes.len() {
483					return Poll::Ready(Ok(None));
484				}
485				let out = bytes.slice(self.read_idx..);
486				self.read_idx = bytes.len();
487				self.stats.bytes(out.len() as u64);
488				Poll::Ready(Ok(Some(out)))
489			}
490			Source::Partial(buf) => {
491				let buf = buf.clone();
492				let size = self.info.size as usize;
493				loop {
494					let written = buf.written(Ordering::Acquire);
495					if written > self.read_idx {
496						let out = buf.slice(self.read_idx, written);
497						self.read_idx = written;
498						self.stats.bytes(out.len() as u64);
499						return Poll::Ready(Ok(Some(out)));
500					}
501					if written >= size {
502						return Poll::Ready(Ok(None));
503					}
504					let read_idx = self.read_idx;
505					// Park on the group's channel; the producer notifies it on each write and
506					// on abort. Re-check the atomic on wake.
507					ready!(poll_state(&self.state, waiter, |state| {
508						if let Some(err) = &state.abort {
509							return Poll::Ready(Err(err.clone()));
510						}
511						let w = buf.written(Ordering::Acquire);
512						if w > read_idx || w >= size {
513							Poll::Ready(Ok(()))
514						} else {
515							Poll::Pending
516						}
517					})?);
518				}
519			}
520		}
521	}
522
523	/// Return the next chunk of bytes since the last read.
524	pub async fn read_chunk(&mut self) -> Result<Option<Bytes>> {
525		kio::wait(|waiter| self.poll_read_chunk(waiter)).await
526	}
527
528	/// Poll for all remaining bytes, resolving once the frame is finished.
529	pub fn poll_read_all(&mut self, waiter: &kio::Waiter) -> Poll<Result<Bytes>> {
530		match &self.source {
531			Source::Complete(bytes) => {
532				let out = bytes.slice(self.read_idx..);
533				self.read_idx = bytes.len();
534				self.stats.bytes(out.len() as u64);
535				Poll::Ready(Ok(out))
536			}
537			Source::Partial(buf) => {
538				let buf = buf.clone();
539				let size = self.info.size as usize;
540				let read_idx = self.read_idx;
541				ready!(poll_state(&self.state, waiter, |state| {
542					if let Some(err) = &state.abort {
543						return Poll::Ready(Err(err.clone()));
544					}
545					if buf.written(Ordering::Acquire) >= size {
546						Poll::Ready(Ok(()))
547					} else {
548						Poll::Pending
549					}
550				})?);
551				let out = buf.slice(read_idx, size);
552				self.read_idx = size;
553				self.stats.bytes(out.len() as u64);
554				Poll::Ready(Ok(out))
555			}
556		}
557	}
558
559	/// Return all remaining bytes, blocking until the frame is finished.
560	pub async fn read_all(&mut self) -> Result<Bytes> {
561		kio::wait(|waiter| self.poll_read_all(waiter)).await
562	}
563}
564
565/// Poll the group channel, mapping a terminal close without an error to
566/// [`Error::Dropped`]. Mirrors [`group::Consumer`]'s internal helper.
567fn poll_state<F, R>(state: &kio::Consumer<GroupState>, waiter: &kio::Waiter, f: F) -> Poll<Result<R>>
568where
569	F: Fn(&kio::Ref<'_, GroupState>) -> Poll<Result<R>>,
570{
571	Poll::Ready(match ready!(state.poll(waiter, f)) {
572		Ok(res) => res,
573		Err(state) => Err(state.abort.clone().unwrap_or(Error::Dropped)),
574	})
575}