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