Skip to main content

g2g_core/
wire.rs

1//! Wire serialization of a [`PipelinePacket`] (M551, the distributed-graph
2//! primitive).
3//!
4//! A hand-rolled, versioned, little-endian binary codec that turns any
5//! [`PipelinePacket`] into a self-contained byte buffer and back. This is the
6//! target-agnostic core of the "remote" transport pair (`RemoteSink` /
7//! `RemoteSrc` in `g2g-plugins`): serialize a packet here, ship the bytes over
8//! any byte transport (TCP, WebSocket, ...), and reconstruct the identical
9//! packet on the far side. Cutting an edge in a graph and re-linking the two
10//! halves across a network boundary is then just a `RemoteSink` on the near
11//! side and a `RemoteSrc` on the far side, with the whole `PipelinePacket`
12//! stream (leading `CapsChanged`, `Segment`, `DataFrame`s, mid-stream caps
13//! refinement, `Flush`, `Eos`) flowing over the wire.
14//!
15//! `no_std + alloc`, no external dependency: the codec is pure computation
16//! (bytes in, bytes out), so it compiles on every target the core does,
17//! including `wasm32` (a browser client can speak the same wire format as a
18//! native peer, generalizing the bespoke M549 detect-server shim into a first
19//! class primitive).
20//!
21//! # What crosses the boundary
22//!
23//! Only CPU memory serializes. [`MemoryDomain::System`] frames carry their bytes
24//! verbatim; [`MemoryDomain::SystemView`] frames are materialized to dense
25//! row-major bytes (the one copy a strided chain pays when it must leave the
26//! process). A device-resident domain (CUDA, D3D11, wgpu, DMABUF, ...) is a
27//! bare pointer into another process's GPU and cannot be shipped, so
28//! [`encode_packet`] returns [`WireError::UnsupportedDomain`]: put an explicit
29//! download element (e.g. `CudaDownload`) before a `RemoteSink` to reach the
30//! wire, exactly as the pipeline already requires to reach a CPU sink.
31//!
32//! Per-frame metadata (the `metadata` feature) is carried when both peers build
33//! with it: the two concrete meta types, `AnalyticsMeta` (the detection graph)
34//! and `BlobMeta` (opaque tagged side-data), round-trip in band, so a detection
35//! computed on one machine arrives attached to its frame on another. Metadata
36//! is the last field of a `DataFrame` body, so a `metadata`-off receiver simply
37//! ignores a `metadata`-on sender's meta payload rather than mis-parsing the
38//! stream (a mixed-feature deployment degrades to no metadata, never to
39//! corruption).
40
41use alloc::string::String;
42use alloc::vec::Vec;
43
44use crate::caps::{
45    AudioFormat, ByteStreamEncoding, Caps, ClosedCaptionFormat, Dim, Interlace, Rate,
46    RawVideoFormat, SubPictureFormat, TensorDType, TensorLayout, TensorShape, TextFormat,
47    VideoCodec,
48};
49use crate::frame::{Frame, FrameTiming, PipelinePacket};
50use crate::memory::{MemoryDomain, SystemSlice};
51use crate::meta::FrameMetaSet;
52use crate::segment::Segment;
53use crate::tensor::MAX_TENSOR_RANK;
54
55/// Wire format version, the first byte of every encoded packet. Bumped on any
56/// incompatible layout change so a decoder rejects a mismatched peer up front
57/// rather than mis-parsing.
58pub const WIRE_VERSION: u8 = 1;
59
60/// Failure decoding (or encoding) a [`PipelinePacket`].
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum WireError {
63    /// The buffer ended mid-field (a truncated or corrupt message).
64    Truncated,
65    /// An unknown version byte, packet tag, enum discriminant, or invalid UTF-8
66    /// in a string field. Also reported for a packet with no wire tag at all (a
67    /// runner-internal [`PipelinePacket::Tick`]).
68    BadTag,
69    /// A device-resident / foreign memory domain that cannot be serialized over
70    /// a byte transport (only [`MemoryDomain::System`] / `SystemView` can).
71    UnsupportedDomain,
72}
73
74// ---- packet / domain / meta tags ----
75
76const PKT_CAPS_CHANGED: u8 = 0;
77const PKT_DATA_FRAME: u8 = 1;
78const PKT_EOS: u8 = 2;
79const PKT_FLUSH: u8 = 3;
80const PKT_SEGMENT: u8 = 4;
81
82const DOMAIN_SYSTEM: u8 = 0;
83
84#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
85const META_ANALYTICS: u8 = 0;
86#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
87const META_BLOB: u8 = 1;
88#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
89const META_CAPTION: u8 = 2;
90#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
91const META_HDR_STATIC: u8 = 3;
92#[cfg_attr(not(feature = "metadata"), allow(dead_code))]
93const META_TIMECODE: u8 = 4;
94
95// ---- primitive writer ----
96
97struct Writer {
98    buf: Vec<u8>,
99}
100
101impl Writer {
102    fn new() -> Self {
103        Writer { buf: Vec::new() }
104    }
105    fn u8(&mut self, v: u8) {
106        self.buf.push(v);
107    }
108    fn u32(&mut self, v: u32) {
109        self.buf.extend_from_slice(&v.to_le_bytes());
110    }
111    fn u64(&mut self, v: u64) {
112        self.buf.extend_from_slice(&v.to_le_bytes());
113    }
114    fn bool(&mut self, v: bool) {
115        self.u8(v as u8);
116    }
117    /// Only the metadata path (`AnalyticsMeta` boxes / confidences) writes f32s.
118    #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
119    fn f32(&mut self, v: f32) {
120        self.u32(v.to_bits());
121    }
122    fn f64(&mut self, v: f64) {
123        self.u64(v.to_bits());
124    }
125    /// A length-prefixed byte slice (`u32` length then the bytes).
126    fn bytes(&mut self, b: &[u8]) {
127        self.u32(b.len() as u32);
128        self.buf.extend_from_slice(b);
129    }
130    /// Only the metadata path (`BlobMeta` headers) writes strings.
131    #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
132    fn str(&mut self, s: &str) {
133        self.bytes(s.as_bytes());
134    }
135}
136
137// ---- primitive reader ----
138
139struct Reader<'a> {
140    buf: &'a [u8],
141    pos: usize,
142}
143
144impl<'a> Reader<'a> {
145    fn new(buf: &'a [u8]) -> Self {
146        Reader { buf, pos: 0 }
147    }
148    fn take(&mut self, n: usize) -> Result<&'a [u8], WireError> {
149        let end = self.pos.checked_add(n).ok_or(WireError::Truncated)?;
150        let slice = self.buf.get(self.pos..end).ok_or(WireError::Truncated)?;
151        self.pos = end;
152        Ok(slice)
153    }
154    fn u8(&mut self) -> Result<u8, WireError> {
155        Ok(self.take(1)?[0])
156    }
157    fn u32(&mut self) -> Result<u32, WireError> {
158        let b = self.take(4)?;
159        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
160    }
161    fn u64(&mut self) -> Result<u64, WireError> {
162        let b = self.take(8)?;
163        Ok(u64::from_le_bytes([
164            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
165        ]))
166    }
167    fn bool(&mut self) -> Result<bool, WireError> {
168        Ok(self.u8()? != 0)
169    }
170    /// Only the metadata path reads f32s.
171    #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
172    fn f32(&mut self) -> Result<f32, WireError> {
173        Ok(f32::from_bits(self.u32()?))
174    }
175    fn f64(&mut self) -> Result<f64, WireError> {
176        Ok(f64::from_bits(self.u64()?))
177    }
178    fn bytes(&mut self) -> Result<Vec<u8>, WireError> {
179        let len = self.u32()? as usize;
180        Ok(self.take(len)?.to_vec())
181    }
182    /// Only the metadata path reads strings.
183    #[cfg_attr(not(feature = "metadata"), allow(dead_code))]
184    fn str(&mut self) -> Result<String, WireError> {
185        String::from_utf8(self.bytes()?).map_err(|_| WireError::BadTag)
186    }
187}
188
189// ---- enum <-> u8 (exhaustive matches so a new variant is a compile error here) ----
190
191fn video_codec_to_u8(c: VideoCodec) -> u8 {
192    match c {
193        VideoCodec::H264 => 0,
194        VideoCodec::H265 => 1,
195        VideoCodec::Av1 => 2,
196        VideoCodec::Vp8 => 3,
197        VideoCodec::Vp9 => 4,
198        VideoCodec::Mjpeg => 5,
199        VideoCodec::Mpeg4Part2 => 6,
200        VideoCodec::JpegXs => 7,
201        VideoCodec::SorensonH263 => 8,
202        VideoCodec::Vp6 { alpha: false } => 9,
203        VideoCodec::Vp6 { alpha: true } => 10,
204        VideoCodec::Mpeg2 => 11,
205        VideoCodec::Png => 12,
206        VideoCodec::WebP => 13,
207        VideoCodec::Vc1 => 14,
208        VideoCodec::Pnm => 15,
209    }
210}
211fn video_codec_from_u8(v: u8) -> Result<VideoCodec, WireError> {
212    Ok(match v {
213        0 => VideoCodec::H264,
214        1 => VideoCodec::H265,
215        2 => VideoCodec::Av1,
216        3 => VideoCodec::Vp8,
217        4 => VideoCodec::Vp9,
218        5 => VideoCodec::Mjpeg,
219        6 => VideoCodec::Mpeg4Part2,
220        7 => VideoCodec::JpegXs,
221        8 => VideoCodec::SorensonH263,
222        9 => VideoCodec::Vp6 { alpha: false },
223        10 => VideoCodec::Vp6 { alpha: true },
224        11 => VideoCodec::Mpeg2,
225        12 => VideoCodec::Png,
226        13 => VideoCodec::WebP,
227        14 => VideoCodec::Vc1,
228        15 => VideoCodec::Pnm,
229        _ => return Err(WireError::BadTag),
230    })
231}
232
233/// Map a [`RawVideoFormat`] to its stable wire byte. Public so out-of-crate
234/// transports (e.g. the local DMABUF socket) reuse the one canonical numbering
235/// instead of duplicating it.
236pub fn raw_format_to_u8(f: RawVideoFormat) -> u8 {
237    match f {
238        RawVideoFormat::Nv12 => 0,
239        RawVideoFormat::I420 => 1,
240        RawVideoFormat::Rgba8 => 2,
241        RawVideoFormat::Bgra8 => 3,
242        RawVideoFormat::Yuyv => 4,
243        RawVideoFormat::I420p10 => 5,
244        RawVideoFormat::I420p12 => 6,
245        RawVideoFormat::I422 => 7,
246        RawVideoFormat::I422p10 => 8,
247        RawVideoFormat::I422p12 => 9,
248        RawVideoFormat::I444 => 10,
249        RawVideoFormat::I444p10 => 11,
250        RawVideoFormat::I444p12 => 12,
251        RawVideoFormat::P010 => 13,
252        RawVideoFormat::Rgb8 => 14,
253    }
254}
255/// Inverse of [`raw_format_to_u8`]; errors on an unknown byte (never trust the
256/// transport).
257pub fn raw_format_from_u8(v: u8) -> Result<RawVideoFormat, WireError> {
258    Ok(match v {
259        0 => RawVideoFormat::Nv12,
260        1 => RawVideoFormat::I420,
261        2 => RawVideoFormat::Rgba8,
262        3 => RawVideoFormat::Bgra8,
263        4 => RawVideoFormat::Yuyv,
264        5 => RawVideoFormat::I420p10,
265        6 => RawVideoFormat::I420p12,
266        7 => RawVideoFormat::I422,
267        8 => RawVideoFormat::I422p10,
268        9 => RawVideoFormat::I422p12,
269        10 => RawVideoFormat::I444,
270        11 => RawVideoFormat::I444p10,
271        12 => RawVideoFormat::I444p12,
272        13 => RawVideoFormat::P010,
273        14 => RawVideoFormat::Rgb8,
274        _ => return Err(WireError::BadTag),
275    })
276}
277
278fn audio_format_to_u8(f: AudioFormat) -> u8 {
279    match f {
280        AudioFormat::Aac => 0,
281        AudioFormat::Opus => 1,
282        AudioFormat::PcmS16Le => 2,
283        AudioFormat::PcmF32Le => 3,
284        AudioFormat::PcmS24Le => 4,
285        AudioFormat::Mulaw => 5,
286        AudioFormat::Alaw => 6,
287        AudioFormat::ImaAdpcm => 7,
288        AudioFormat::Mp2 => 8,
289        AudioFormat::Ac3 => 9,
290        AudioFormat::Flac => 10,
291        AudioFormat::Vorbis => 11,
292        AudioFormat::Mp3 => 12,
293        AudioFormat::Speex => 13,
294        AudioFormat::PcmS32Le => 14,
295        AudioFormat::PcmU8 => 15,
296    }
297}
298fn audio_format_from_u8(v: u8) -> Result<AudioFormat, WireError> {
299    Ok(match v {
300        0 => AudioFormat::Aac,
301        1 => AudioFormat::Opus,
302        2 => AudioFormat::PcmS16Le,
303        3 => AudioFormat::PcmF32Le,
304        4 => AudioFormat::PcmS24Le,
305        5 => AudioFormat::Mulaw,
306        6 => AudioFormat::Alaw,
307        7 => AudioFormat::ImaAdpcm,
308        8 => AudioFormat::Mp2,
309        9 => AudioFormat::Ac3,
310        10 => AudioFormat::Flac,
311        11 => AudioFormat::Vorbis,
312        12 => AudioFormat::Mp3,
313        13 => AudioFormat::Speex,
314        14 => AudioFormat::PcmS32Le,
315        15 => AudioFormat::PcmU8,
316        _ => return Err(WireError::BadTag),
317    })
318}
319
320fn bytestream_to_u8(e: ByteStreamEncoding) -> u8 {
321    match e {
322        ByteStreamEncoding::MpegTs => 0,
323        ByteStreamEncoding::Matroska => 1,
324        ByteStreamEncoding::Ogg => 2,
325        ByteStreamEncoding::Flv => 3,
326        ByteStreamEncoding::IsoBmff => 4,
327        ByteStreamEncoding::Mp4 => 5,
328        ByteStreamEncoding::Ivf => 6,
329        ByteStreamEncoding::MpegPs => 7,
330        ByteStreamEncoding::Wav => 8,
331        ByteStreamEncoding::Aiff => 18,
332        ByteStreamEncoding::Au => 19,
333        ByteStreamEncoding::Avi => 9,
334        ByteStreamEncoding::Y4m => 10,
335        ByteStreamEncoding::Multipart => 11,
336        ByteStreamEncoding::Raw => 12,
337        ByteStreamEncoding::Rtp => 13,
338        ByteStreamEncoding::Srtp => 14,
339        ByteStreamEncoding::Rtcp => 15,
340        ByteStreamEncoding::Srtcp => 16,
341        ByteStreamEncoding::Dtls => 17,
342    }
343}
344fn bytestream_from_u8(v: u8) -> Result<ByteStreamEncoding, WireError> {
345    Ok(match v {
346        0 => ByteStreamEncoding::MpegTs,
347        1 => ByteStreamEncoding::Matroska,
348        2 => ByteStreamEncoding::Ogg,
349        3 => ByteStreamEncoding::Flv,
350        4 => ByteStreamEncoding::IsoBmff,
351        5 => ByteStreamEncoding::Mp4,
352        6 => ByteStreamEncoding::Ivf,
353        7 => ByteStreamEncoding::MpegPs,
354        8 => ByteStreamEncoding::Wav,
355        9 => ByteStreamEncoding::Avi,
356        18 => ByteStreamEncoding::Aiff,
357        19 => ByteStreamEncoding::Au,
358        10 => ByteStreamEncoding::Y4m,
359        11 => ByteStreamEncoding::Multipart,
360        12 => ByteStreamEncoding::Raw,
361        13 => ByteStreamEncoding::Rtp,
362        14 => ByteStreamEncoding::Srtp,
363        15 => ByteStreamEncoding::Rtcp,
364        16 => ByteStreamEncoding::Srtcp,
365        17 => ByteStreamEncoding::Dtls,
366        _ => return Err(WireError::BadTag),
367    })
368}
369
370fn text_format_to_u8(f: TextFormat) -> u8 {
371    match f {
372        TextFormat::Utf8 => 0,
373        TextFormat::PangoMarkup => 1,
374        TextFormat::Srt => 2,
375        TextFormat::WebVtt => 3,
376        TextFormat::Ssa => 4,
377        TextFormat::Ttml => 5,
378        TextFormat::Teletext => 6,
379    }
380}
381fn text_format_from_u8(v: u8) -> Result<TextFormat, WireError> {
382    Ok(match v {
383        0 => TextFormat::Utf8,
384        1 => TextFormat::PangoMarkup,
385        2 => TextFormat::Srt,
386        3 => TextFormat::WebVtt,
387        4 => TextFormat::Ssa,
388        5 => TextFormat::Ttml,
389        6 => TextFormat::Teletext,
390        _ => return Err(WireError::BadTag),
391    })
392}
393
394fn cc_format_to_u8(f: ClosedCaptionFormat) -> u8 {
395    match f {
396        ClosedCaptionFormat::Cea608 => 0,
397        ClosedCaptionFormat::Cea708 => 1,
398        ClosedCaptionFormat::Cea608Raw => 2,
399        ClosedCaptionFormat::Cea608S334 => 3,
400        ClosedCaptionFormat::Cea708Cdp => 4,
401    }
402}
403fn cc_format_from_u8(v: u8) -> Result<ClosedCaptionFormat, WireError> {
404    Ok(match v {
405        0 => ClosedCaptionFormat::Cea608,
406        1 => ClosedCaptionFormat::Cea708,
407        2 => ClosedCaptionFormat::Cea608Raw,
408        3 => ClosedCaptionFormat::Cea608S334,
409        4 => ClosedCaptionFormat::Cea708Cdp,
410        _ => return Err(WireError::BadTag),
411    })
412}
413
414fn subpicture_format_to_u8(f: SubPictureFormat) -> u8 {
415    match f {
416        SubPictureFormat::VobSub => 0,
417        SubPictureFormat::DvbSub => 1,
418        SubPictureFormat::Pgs => 2,
419    }
420}
421fn subpicture_format_from_u8(v: u8) -> Result<SubPictureFormat, WireError> {
422    Ok(match v {
423        0 => SubPictureFormat::VobSub,
424        1 => SubPictureFormat::DvbSub,
425        2 => SubPictureFormat::Pgs,
426        _ => return Err(WireError::BadTag),
427    })
428}
429
430fn dtype_to_u8(d: TensorDType) -> u8 {
431    match d {
432        TensorDType::F16 => 0,
433        TensorDType::F32 => 1,
434        TensorDType::I8 => 2,
435        TensorDType::U8 => 3,
436    }
437}
438fn dtype_from_u8(v: u8) -> Result<TensorDType, WireError> {
439    Ok(match v {
440        0 => TensorDType::F16,
441        1 => TensorDType::F32,
442        2 => TensorDType::I8,
443        3 => TensorDType::U8,
444        _ => return Err(WireError::BadTag),
445    })
446}
447
448fn layout_to_u8(l: TensorLayout) -> u8 {
449    match l {
450        TensorLayout::Nchw => 0,
451        TensorLayout::Nhwc => 1,
452    }
453}
454fn layout_from_u8(v: u8) -> Result<TensorLayout, WireError> {
455    Ok(match v {
456        0 => TensorLayout::Nchw,
457        1 => TensorLayout::Nhwc,
458        _ => return Err(WireError::BadTag),
459    })
460}
461
462// ---- Dim / Rate ----
463
464fn put_dim(w: &mut Writer, d: &Dim) {
465    match d {
466        Dim::Any => w.u8(0),
467        Dim::Range { min, max } => {
468            w.u8(1);
469            w.u32(*min);
470            w.u32(*max);
471        }
472        Dim::Fixed(v) => {
473            w.u8(2);
474            w.u32(*v);
475        }
476    }
477}
478fn get_dim(r: &mut Reader) -> Result<Dim, WireError> {
479    Ok(match r.u8()? {
480        0 => Dim::Any,
481        1 => Dim::Range {
482            min: r.u32()?,
483            max: r.u32()?,
484        },
485        2 => Dim::Fixed(r.u32()?),
486        _ => return Err(WireError::BadTag),
487    })
488}
489
490fn put_rate(w: &mut Writer, rt: &Rate) {
491    match rt {
492        Rate::Any => w.u8(0),
493        Rate::Range { min_q16, max_q16 } => {
494            w.u8(1);
495            w.u32(*min_q16);
496            w.u32(*max_q16);
497        }
498        Rate::Fixed(v) => {
499            w.u8(2);
500            w.u32(*v);
501        }
502    }
503}
504fn get_rate(r: &mut Reader) -> Result<Rate, WireError> {
505    Ok(match r.u8()? {
506        0 => Rate::Any,
507        1 => Rate::Range {
508            min_q16: r.u32()?,
509            max_q16: r.u32()?,
510        },
511        2 => Rate::Fixed(r.u32()?),
512        _ => return Err(WireError::BadTag),
513    })
514}
515
516fn interlace_to_u8(i: Interlace) -> u8 {
517    match i {
518        Interlace::Any => 0,
519        Interlace::Progressive => 1,
520        Interlace::Interleaved => 2,
521    }
522}
523fn interlace_from_u8(v: u8) -> Result<Interlace, WireError> {
524    Ok(match v {
525        0 => Interlace::Any,
526        1 => Interlace::Progressive,
527        2 => Interlace::Interleaved,
528        _ => return Err(WireError::BadTag),
529    })
530}
531
532// ---- Caps ----
533
534fn put_caps(w: &mut Writer, c: &Caps) {
535    match c {
536        Caps::CompressedVideo {
537            codec,
538            width,
539            height,
540            framerate,
541        } => {
542            w.u8(0);
543            w.u8(video_codec_to_u8(*codec));
544            put_dim(w, width);
545            put_dim(w, height);
546            put_rate(w, framerate);
547        }
548        Caps::RawVideo {
549            format,
550            width,
551            height,
552            framerate,
553            interlace,
554        } => {
555            w.u8(1);
556            w.u8(raw_format_to_u8(*format));
557            put_dim(w, width);
558            put_dim(w, height);
559            put_rate(w, framerate);
560            w.u8(interlace_to_u8(*interlace));
561        }
562        Caps::Audio {
563            format,
564            channels,
565            sample_rate,
566        } => {
567            w.u8(2);
568            w.u8(audio_format_to_u8(*format));
569            w.u8(*channels);
570            w.u32(*sample_rate);
571        }
572        Caps::Tensor {
573            dtype,
574            shape,
575            layout,
576        } => {
577            w.u8(3);
578            w.u8(dtype_to_u8(*dtype));
579            w.u32(shape.dims().len() as u32);
580            for d in shape.dims() {
581                w.u32(*d);
582            }
583            w.u8(layout_to_u8(*layout));
584        }
585        Caps::ByteStream { encoding } => {
586            w.u8(4);
587            w.u8(bytestream_to_u8(*encoding));
588        }
589        Caps::Text { format } => {
590            w.u8(5);
591            w.u8(text_format_to_u8(*format));
592        }
593        Caps::Klv => w.u8(6),
594        Caps::ClosedCaption { format } => {
595            w.u8(7);
596            w.u8(cc_format_to_u8(*format));
597        }
598        Caps::SubPicture { format } => {
599            w.u8(8);
600            w.u8(subpicture_format_to_u8(*format));
601        }
602    }
603}
604
605fn get_caps(r: &mut Reader) -> Result<Caps, WireError> {
606    Ok(match r.u8()? {
607        0 => Caps::CompressedVideo {
608            codec: video_codec_from_u8(r.u8()?)?,
609            width: get_dim(r)?,
610            height: get_dim(r)?,
611            framerate: get_rate(r)?,
612        },
613        1 => Caps::RawVideo {
614            format: raw_format_from_u8(r.u8()?)?,
615            width: get_dim(r)?,
616            height: get_dim(r)?,
617            framerate: get_rate(r)?,
618            interlace: interlace_from_u8(r.u8()?)?,
619        },
620        2 => Caps::Audio {
621            format: audio_format_from_u8(r.u8()?)?,
622            channels: r.u8()?,
623            sample_rate: r.u32()?,
624        },
625        3 => {
626            let dtype = dtype_from_u8(r.u8()?)?;
627            // The rank is attacker-controlled; a fixed-rank TensorShape can
628            // only carry 1..=MAX_TENSOR_RANK dims, so reject anything else
629            // before reading (which also bounds the read loop).
630            let n = r.u32()? as usize;
631            let mut dims = [0u32; MAX_TENSOR_RANK];
632            let slots = dims.get_mut(..n).ok_or(WireError::BadTag)?;
633            for d in slots.iter_mut() {
634                *d = r.u32()?;
635            }
636            let layout = layout_from_u8(r.u8()?)?;
637            let shape = TensorShape::from_slice(&dims[..n]).ok_or(WireError::BadTag)?;
638            Caps::Tensor {
639                dtype,
640                shape,
641                layout,
642            }
643        }
644        4 => Caps::ByteStream {
645            encoding: bytestream_from_u8(r.u8()?)?,
646        },
647        5 => Caps::Text {
648            format: text_format_from_u8(r.u8()?)?,
649        },
650        6 => Caps::Klv,
651        7 => Caps::ClosedCaption {
652            format: cc_format_from_u8(r.u8()?)?,
653        },
654        8 => Caps::SubPicture {
655            format: subpicture_format_from_u8(r.u8()?)?,
656        },
657        _ => return Err(WireError::BadTag),
658    })
659}
660
661// ---- FrameTiming ----
662
663fn put_timing(w: &mut Writer, t: &FrameTiming) {
664    w.u64(t.pts_ns);
665    w.u64(t.dts_ns);
666    w.u64(t.duration_ns);
667    w.u64(t.capture_ns);
668    w.u64(t.arrival_ns);
669    w.bool(t.keyframe);
670}
671fn get_timing(r: &mut Reader) -> Result<FrameTiming, WireError> {
672    Ok(FrameTiming {
673        pts_ns: r.u64()?,
674        dts_ns: r.u64()?,
675        duration_ns: r.u64()?,
676        capture_ns: r.u64()?,
677        arrival_ns: r.u64()?,
678        keyframe: r.bool()?,
679    })
680}
681
682// ---- Segment ----
683
684fn put_segment(w: &mut Writer, s: &Segment) {
685    w.f64(s.rate);
686    w.f64(s.applied_rate);
687    w.u64(s.base);
688    w.u64(s.start);
689    match s.stop {
690        Some(v) => {
691            w.bool(true);
692            w.u64(v);
693        }
694        None => w.bool(false),
695    }
696    w.u64(s.time);
697    w.u64(s.position);
698    w.bool(s.key_units_only);
699}
700fn get_segment(r: &mut Reader) -> Result<Segment, WireError> {
701    let rate = r.f64()?;
702    let applied_rate = r.f64()?;
703    let base = r.u64()?;
704    let start = r.u64()?;
705    let stop = if r.bool()? { Some(r.u64()?) } else { None };
706    let time = r.u64()?;
707    let position = r.u64()?;
708    let key_units_only = r.bool()?;
709    Ok(Segment {
710        rate,
711        applied_rate,
712        base,
713        start,
714        stop,
715        time,
716        position,
717        key_units_only,
718    })
719}
720
721// ---- MemoryDomain (CPU only) ----
722
723fn put_domain(w: &mut Writer, d: &MemoryDomain) -> Result<(), WireError> {
724    match d {
725        MemoryDomain::System(s) => {
726            w.u8(DOMAIN_SYSTEM);
727            w.bytes(s.as_slice());
728            Ok(())
729        }
730        // A strided shared-CPU view is materialized to dense row-major bytes:
731        // the far side receives plain System bytes (the one copy leaving the
732        // process costs).
733        MemoryDomain::SystemView(v) => {
734            w.u8(DOMAIN_SYSTEM);
735            let dense = v.materialize();
736            w.bytes(&dense);
737            Ok(())
738        }
739        // Everything else is a device / foreign pointer that cannot be shipped.
740        _ => Err(WireError::UnsupportedDomain),
741    }
742}
743fn get_domain(r: &mut Reader) -> Result<MemoryDomain, WireError> {
744    match r.u8()? {
745        DOMAIN_SYSTEM => {
746            let bytes = r.bytes()?;
747            Ok(MemoryDomain::System(SystemSlice::from_boxed(
748                bytes.into_boxed_slice(),
749            )))
750        }
751        _ => Err(WireError::BadTag),
752    }
753}
754
755// ---- per-frame metadata (last field of a DataFrame body) ----
756
757#[cfg(feature = "metadata")]
758fn put_meta(w: &mut Writer, meta: &FrameMetaSet) {
759    use crate::meta::{
760        AnalyticsMeta, AnalyticsNode, BlobMeta, CaptionMeta, HdrStaticMeta, TimecodeMeta,
761    };
762
763    let analytics = meta.get::<AnalyticsMeta>();
764    let blob = meta.get::<BlobMeta>();
765    let caption = meta.get::<CaptionMeta>();
766    let hdr = meta.get::<HdrStaticMeta>();
767    let timecode = meta.get::<TimecodeMeta>();
768    let count = analytics.is_some() as u8
769        + blob.is_some() as u8
770        + caption.is_some() as u8
771        + hdr.is_some() as u8
772        + timecode.is_some() as u8;
773    w.u8(count);
774
775    if let Some(a) = analytics {
776        w.u8(META_ANALYTICS);
777        w.u32(a.nodes.len() as u32);
778        for node in &a.nodes {
779            match node {
780                AnalyticsNode::Detection(d) => {
781                    w.u8(0);
782                    w.f32(d.bbox.x);
783                    w.f32(d.bbox.y);
784                    w.f32(d.bbox.w);
785                    w.f32(d.bbox.h);
786                    w.u32(d.label);
787                    w.f32(d.confidence);
788                }
789                AnalyticsNode::Classification(c) => {
790                    w.u8(1);
791                    w.u32(c.label);
792                    w.f32(c.confidence);
793                }
794                AnalyticsNode::Tracking(t) => {
795                    w.u8(2);
796                    w.u64(t.object_id);
797                }
798                AnalyticsNode::Segmentation(s) => {
799                    w.u8(3);
800                    w.f32(s.bbox.x);
801                    w.f32(s.bbox.y);
802                    w.f32(s.bbox.w);
803                    w.f32(s.bbox.h);
804                    w.u32(s.label);
805                    w.f32(s.confidence);
806                    w.u32(s.mask.width());
807                    w.u32(s.mask.height());
808                    w.u32(s.mask.stride());
809                    w.bytes(s.mask.data());
810                }
811                AnalyticsNode::Roi(r) => {
812                    w.u8(4);
813                    w.f32(r.bbox.x);
814                    w.f32(r.bbox.y);
815                    w.f32(r.bbox.w);
816                    w.f32(r.bbox.h);
817                    w.u32(r.id);
818                    w.u32(r.label);
819                }
820            }
821        }
822        w.u32(a.relations.len() as u32);
823        for rel in &a.relations {
824            w.u32(rel.from as u32);
825            w.u32(rel.to as u32);
826            w.u8(relation_kind_to_u8(rel.kind));
827        }
828    }
829
830    if let Some(b) = blob {
831        w.u8(META_BLOB);
832        w.u32(b.blobs.len() as u32);
833        for blob in &b.blobs {
834            w.str(&blob.header);
835            w.bytes(&blob.payload);
836        }
837    }
838
839    if let Some(c) = caption {
840        w.u8(META_CAPTION);
841        w.u32(c.triples.len() as u32);
842        for t in &c.triples {
843            w.u8(t.cc_type);
844            w.u8(t.b0);
845            w.u8(t.b1);
846        }
847    }
848
849    if let Some(h) = hdr {
850        w.u8(META_HDR_STATIC);
851        match &h.mastering {
852            Some(m) => {
853                w.bool(true);
854                for p in &m.display_primaries {
855                    w.f32(p.x);
856                    w.f32(p.y);
857                }
858                w.f32(m.white_point.x);
859                w.f32(m.white_point.y);
860                w.f32(m.max_luminance);
861                w.f32(m.min_luminance);
862            }
863            None => w.bool(false),
864        }
865        put_opt_u16(w, h.max_content_light_level);
866        put_opt_u16(w, h.max_frame_average_light_level);
867    }
868
869    if let Some(t) = timecode {
870        w.u8(META_TIMECODE);
871        w.u8(t.hours);
872        w.u8(t.minutes);
873        w.u8(t.seconds);
874        w.u8(t.frames);
875        w.bool(t.drop_frame);
876        w.bool(t.framerate_q16.is_some());
877        w.u32(t.framerate_q16.unwrap_or(0));
878    }
879}
880
881/// An optional `u16` as a presence flag then the value (only the HDR meta needs
882/// one, so it is not a `Writer` primitive).
883#[cfg(feature = "metadata")]
884fn put_opt_u16(w: &mut Writer, v: Option<u16>) {
885    w.bool(v.is_some());
886    w.u32(v.unwrap_or(0) as u32);
887}
888
889#[cfg(feature = "metadata")]
890fn get_opt_u16(r: &mut Reader) -> Result<Option<u16>, WireError> {
891    let present = r.bool()?;
892    let v = u16::try_from(r.u32()?).map_err(|_| WireError::BadTag)?;
893    Ok(present.then_some(v))
894}
895
896#[cfg(not(feature = "metadata"))]
897fn put_meta(w: &mut Writer, _meta: &FrameMetaSet) {
898    // The baseline `FrameMetaSet` is a ZST: nothing to carry.
899    w.u8(0);
900}
901
902#[cfg(feature = "metadata")]
903fn relation_kind_to_u8(k: crate::meta::RelationKind) -> u8 {
904    use crate::meta::RelationKind;
905    match k {
906        RelationKind::Classifies => 0,
907        RelationKind::Tracks => 1,
908        RelationKind::Contains => 2,
909    }
910}
911
912#[cfg(feature = "metadata")]
913fn relation_kind_from_u8(v: u8) -> Result<crate::meta::RelationKind, WireError> {
914    use crate::meta::RelationKind;
915    Ok(match v {
916        0 => RelationKind::Classifies,
917        1 => RelationKind::Tracks,
918        2 => RelationKind::Contains,
919        _ => return Err(WireError::BadTag),
920    })
921}
922
923#[cfg(feature = "metadata")]
924fn get_meta(r: &mut Reader) -> Result<FrameMetaSet, WireError> {
925    use crate::meta::{
926        AnalyticsMeta, AnalyticsNode, BBox, Blob, BlobMeta, CaptionMeta, CaptionTriple,
927        Chromaticity, Classification, HdrStaticMeta, Mask, MasteringDisplay, ObjectDetection,
928        Relation, Roi, Segmentation, TimecodeMeta, Tracking,
929    };
930
931    let count = r.u8()?;
932    let mut set = FrameMetaSet::new();
933    for _ in 0..count {
934        match r.u8()? {
935            META_ANALYTICS => {
936                let mut a = AnalyticsMeta::new();
937                let n = r.u32()? as usize;
938                for _ in 0..n {
939                    let node = match r.u8()? {
940                        0 => AnalyticsNode::Detection(ObjectDetection {
941                            bbox: BBox {
942                                x: r.f32()?,
943                                y: r.f32()?,
944                                w: r.f32()?,
945                                h: r.f32()?,
946                            },
947                            label: r.u32()?,
948                            confidence: r.f32()?,
949                        }),
950                        1 => AnalyticsNode::Classification(Classification {
951                            label: r.u32()?,
952                            confidence: r.f32()?,
953                        }),
954                        2 => AnalyticsNode::Tracking(Tracking {
955                            object_id: r.u64()?,
956                        }),
957                        3 => {
958                            let bbox = BBox {
959                                x: r.f32()?,
960                                y: r.f32()?,
961                                w: r.f32()?,
962                                h: r.f32()?,
963                            };
964                            let label = r.u32()?;
965                            let confidence = r.f32()?;
966                            let (width, height, stride) = (r.u32()?, r.u32()?, r.u32()?);
967                            // The mask bytes are length-prefixed and bounded by
968                            // the message, and `Mask::new` rejects geometry that
969                            // does not fit them: a peer cannot make us index out
970                            // of the buffer it sent.
971                            let mask = Mask::new(width, height, stride, r.bytes()?)
972                                .ok_or(WireError::BadTag)?;
973                            AnalyticsNode::Segmentation(Segmentation {
974                                bbox,
975                                label,
976                                confidence,
977                                mask,
978                            })
979                        }
980                        4 => AnalyticsNode::Roi(Roi {
981                            bbox: BBox {
982                                x: r.f32()?,
983                                y: r.f32()?,
984                                w: r.f32()?,
985                                h: r.f32()?,
986                            },
987                            id: r.u32()?,
988                            label: r.u32()?,
989                        }),
990                        _ => return Err(WireError::BadTag),
991                    };
992                    a.nodes.push(node);
993                }
994                let m = r.u32()? as usize;
995                for _ in 0..m {
996                    a.relations.push(Relation {
997                        from: r.u32()? as usize,
998                        to: r.u32()? as usize,
999                        kind: relation_kind_from_u8(r.u8()?)?,
1000                    });
1001                }
1002                set.attach(a);
1003            }
1004            META_BLOB => {
1005                let mut b = BlobMeta::new();
1006                let n = r.u32()? as usize;
1007                for _ in 0..n {
1008                    b.blobs.push(Blob {
1009                        header: r.str()?,
1010                        payload: r.bytes()?,
1011                    });
1012                }
1013                set.attach(b);
1014            }
1015            META_CAPTION => {
1016                let mut c = CaptionMeta::new();
1017                let n = r.u32()? as usize;
1018                for _ in 0..n {
1019                    c.push(CaptionTriple {
1020                        cc_type: r.u8()?,
1021                        b0: r.u8()?,
1022                        b1: r.u8()?,
1023                    });
1024                }
1025                set.attach(c);
1026            }
1027            META_HDR_STATIC => {
1028                let mastering = if r.bool()? {
1029                    let mut primaries = [Chromaticity { x: 0.0, y: 0.0 }; 3];
1030                    for p in &mut primaries {
1031                        p.x = r.f32()?;
1032                        p.y = r.f32()?;
1033                    }
1034                    Some(MasteringDisplay {
1035                        display_primaries: primaries,
1036                        white_point: Chromaticity {
1037                            x: r.f32()?,
1038                            y: r.f32()?,
1039                        },
1040                        max_luminance: r.f32()?,
1041                        min_luminance: r.f32()?,
1042                    })
1043                } else {
1044                    None
1045                };
1046                set.attach(HdrStaticMeta {
1047                    mastering,
1048                    max_content_light_level: get_opt_u16(r)?,
1049                    max_frame_average_light_level: get_opt_u16(r)?,
1050                });
1051            }
1052            META_TIMECODE => {
1053                let tc = TimecodeMeta {
1054                    hours: r.u8()?,
1055                    minutes: r.u8()?,
1056                    seconds: r.u8()?,
1057                    frames: r.u8()?,
1058                    drop_frame: r.bool()?,
1059                    framerate_q16: {
1060                        let present = r.bool()?;
1061                        let v = r.u32()?;
1062                        present.then_some(v)
1063                    },
1064                };
1065                set.attach(tc);
1066            }
1067            _ => return Err(WireError::BadTag),
1068        }
1069    }
1070    Ok(set)
1071}
1072
1073#[cfg(not(feature = "metadata"))]
1074fn get_meta(r: &mut Reader) -> Result<FrameMetaSet, WireError> {
1075    // Metadata is the last field of a DataFrame body, so a `metadata`-off
1076    // receiver just reads the entry count and ignores the payload that follows
1077    // (a `metadata`-on peer's metas): the stream is already fully framed by the
1078    // transport, so the un-consumed tail is harmless. Degrades to no metadata,
1079    // never to a mis-parse.
1080    let _count = r.u8()?;
1081    Ok(FrameMetaSet::new())
1082}
1083
1084// ---- public API ----
1085
1086/// Serialize a [`PipelinePacket`] into a self-contained byte buffer.
1087///
1088/// Returns [`WireError::UnsupportedDomain`] for a `DataFrame` whose memory is
1089/// device-resident or foreign (only [`MemoryDomain::System`] / `SystemView`
1090/// can cross a byte transport). The transport is expected to length-frame the
1091/// returned buffer (the codec produces the body only).
1092pub fn encode_packet(packet: &PipelinePacket) -> Result<Vec<u8>, WireError> {
1093    let mut w = Writer::new();
1094    w.u8(WIRE_VERSION);
1095    match packet {
1096        PipelinePacket::CapsChanged(caps) => {
1097            w.u8(PKT_CAPS_CHANGED);
1098            put_caps(&mut w, caps);
1099        }
1100        PipelinePacket::DataFrame(frame) => {
1101            w.u8(PKT_DATA_FRAME);
1102            put_timing(&mut w, &frame.timing);
1103            w.u64(frame.sequence);
1104            put_domain(&mut w, &frame.domain)?;
1105            put_meta(&mut w, &frame.meta);
1106        }
1107        PipelinePacket::Eos => w.u8(PKT_EOS),
1108        PipelinePacket::Flush => w.u8(PKT_FLUSH),
1109        PipelinePacket::Segment(seg) => {
1110            w.u8(PKT_SEGMENT);
1111            put_segment(&mut w, seg);
1112        }
1113        // A `Tick` is runner-internal (a fan-in arm's deadline, consumed at the
1114        // arm), so it has no wire tag and cannot reach a transport. There is no
1115        // skip convention here (every packet encodes to a body), so encoding one
1116        // is a bug, reported rather than silently dropped.
1117        PipelinePacket::Tick => return Err(WireError::BadTag),
1118    }
1119    Ok(w.buf)
1120}
1121
1122/// Reconstruct a [`PipelinePacket`] from bytes produced by [`encode_packet`].
1123///
1124/// Trailing bytes after the packet are ignored (the transport frames each
1125/// message), so a `metadata`-on sender's meta payload does not trip a
1126/// `metadata`-off receiver.
1127pub fn decode_packet(bytes: &[u8]) -> Result<PipelinePacket, WireError> {
1128    let mut r = Reader::new(bytes);
1129    if r.u8()? != WIRE_VERSION {
1130        return Err(WireError::BadTag);
1131    }
1132    Ok(match r.u8()? {
1133        PKT_CAPS_CHANGED => PipelinePacket::CapsChanged(get_caps(&mut r)?),
1134        PKT_DATA_FRAME => {
1135            let timing = get_timing(&mut r)?;
1136            let sequence = r.u64()?;
1137            let domain = get_domain(&mut r)?;
1138            let meta = get_meta(&mut r)?;
1139            PipelinePacket::DataFrame(Frame {
1140                domain,
1141                timing,
1142                sequence,
1143                meta,
1144            })
1145        }
1146        PKT_EOS => PipelinePacket::Eos,
1147        PKT_FLUSH => PipelinePacket::Flush,
1148        PKT_SEGMENT => PipelinePacket::Segment(get_segment(&mut r)?),
1149        _ => return Err(WireError::BadTag),
1150    })
1151}
1152
1153// ---- framed recordings ----
1154
1155/// Width of a framed record's length prefix: a `u32-le` payload byte count
1156/// ahead of each [`encode_packet`] body.
1157pub const RECORD_LENGTH_PREFIX_BYTES: usize = 4;
1158
1159/// The length prefix that frames a `payload_len`-byte [`encode_packet`] body in
1160/// a recording, the one definition of the on-disk record framing that
1161/// `recordsink`, `replaysrc`, and the runner's flight-recorder dump share.
1162/// [`UnsupportedDomain`](WireError::UnsupportedDomain) for a payload too large
1163/// to describe in the prefix.
1164pub fn record_length_prefix(
1165    payload_len: usize,
1166) -> Result<[u8; RECORD_LENGTH_PREFIX_BYTES], WireError> {
1167    let len = u32::try_from(payload_len).map_err(|_| WireError::UnsupportedDomain)?;
1168    Ok(len.to_le_bytes())
1169}
1170
1171/// Split a recording buffer into its packets. A truncated trailing record (a
1172/// recording cut off mid-write, e.g. by the crash being investigated) is dropped
1173/// rather than failing the replay.
1174pub fn read_records(buf: &[u8]) -> Result<Vec<PipelinePacket>, WireError> {
1175    let mut out = Vec::new();
1176    let mut i = 0usize;
1177    while i + RECORD_LENGTH_PREFIX_BYTES <= buf.len() {
1178        let len = u32::from_le_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]) as usize;
1179        let start = i + RECORD_LENGTH_PREFIX_BYTES;
1180        let end = match start.checked_add(len) {
1181            Some(e) if e <= buf.len() => e,
1182            _ => break, // truncated tail
1183        };
1184        out.push(decode_packet(&buf[start..end])?);
1185        i = end;
1186    }
1187    Ok(out)
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192    use super::*;
1193    use alloc::boxed::Box;
1194
1195    fn roundtrip(p: &PipelinePacket) -> PipelinePacket {
1196        let bytes = encode_packet(p).expect("encode");
1197        decode_packet(&bytes).expect("decode")
1198    }
1199
1200    #[test]
1201    fn every_codec_tag_round_trips() {
1202        // A wrong tag would silently retarget a remote stream's codec. A shared
1203        // tag fails here too: only one variant can come back out of the byte.
1204        let video = [
1205            VideoCodec::H264,
1206            VideoCodec::H265,
1207            VideoCodec::Av1,
1208            VideoCodec::Vp8,
1209            VideoCodec::Vp9,
1210            VideoCodec::Mjpeg,
1211            VideoCodec::Mpeg4Part2,
1212            VideoCodec::JpegXs,
1213            VideoCodec::SorensonH263,
1214            VideoCodec::Vp6 { alpha: false },
1215            VideoCodec::Vp6 { alpha: true },
1216            VideoCodec::Mpeg2,
1217            VideoCodec::Png,
1218            VideoCodec::WebP,
1219            VideoCodec::Vc1,
1220            VideoCodec::Pnm,
1221        ];
1222        for c in video {
1223            assert_eq!(video_codec_from_u8(video_codec_to_u8(c)), Ok(c));
1224        }
1225        let audio = [
1226            AudioFormat::Aac,
1227            AudioFormat::Opus,
1228            AudioFormat::Mp2,
1229            AudioFormat::Mp3,
1230            AudioFormat::Speex,
1231            AudioFormat::Ac3,
1232            AudioFormat::Flac,
1233            AudioFormat::Vorbis,
1234            AudioFormat::PcmS16Le,
1235            AudioFormat::PcmF32Le,
1236            AudioFormat::PcmS24Le,
1237            AudioFormat::PcmS32Le,
1238            AudioFormat::PcmU8,
1239            AudioFormat::Mulaw,
1240            AudioFormat::Alaw,
1241            AudioFormat::ImaAdpcm,
1242        ];
1243        for f in audio {
1244            assert_eq!(audio_format_from_u8(audio_format_to_u8(f)), Ok(f));
1245        }
1246    }
1247
1248    #[test]
1249    fn packet_bytestream_tags_round_trip() {
1250        for encoding in [
1251            ByteStreamEncoding::Rtp,
1252            ByteStreamEncoding::Srtp,
1253            ByteStreamEncoding::Rtcp,
1254            ByteStreamEncoding::Srtcp,
1255            ByteStreamEncoding::Dtls,
1256            ByteStreamEncoding::Aiff,
1257            ByteStreamEncoding::Au,
1258        ] {
1259            assert_eq!(bytestream_from_u8(bytestream_to_u8(encoding)), Ok(encoding));
1260        }
1261    }
1262
1263    #[test]
1264    fn caps_changed_round_trips_every_variant() {
1265        let cases = [
1266            Caps::CompressedVideo {
1267                codec: VideoCodec::H265,
1268                width: Dim::Fixed(1920),
1269                height: Dim::Range {
1270                    min: 480,
1271                    max: 1080,
1272                },
1273                framerate: Rate::Fixed(30 << 16),
1274            },
1275            Caps::RawVideo {
1276                format: RawVideoFormat::Nv12,
1277                width: Dim::Fixed(640),
1278                height: Dim::Fixed(480),
1279                framerate: Rate::Any,
1280                interlace: crate::Interlace::Any,
1281            },
1282            Caps::Audio {
1283                format: AudioFormat::Opus,
1284                channels: 2,
1285                sample_rate: 48_000,
1286            },
1287            Caps::Tensor {
1288                dtype: TensorDType::F32,
1289                shape: TensorShape::new([1, 3, 224, 224]),
1290                layout: TensorLayout::Nchw,
1291            },
1292            Caps::ByteStream {
1293                encoding: ByteStreamEncoding::MpegTs,
1294            },
1295            Caps::Text {
1296                format: TextFormat::WebVtt,
1297            },
1298            Caps::ClosedCaption {
1299                format: ClosedCaptionFormat::Cea708,
1300            },
1301        ];
1302        for caps in cases {
1303            let p = PipelinePacket::CapsChanged(caps.clone());
1304            match roundtrip(&p) {
1305                PipelinePacket::CapsChanged(got) => assert_eq!(got, caps),
1306                other => panic!("expected CapsChanged, got {other:?}"),
1307            }
1308        }
1309    }
1310
1311    #[test]
1312    fn tensor_caps_rank_beyond_max_rejected() {
1313        // Hand-encode a tensor caps blob whose declared rank exceeds
1314        // MAX_TENSOR_RANK: the decoder must reject it up front (fixed-rank
1315        // TensorShape, M636) instead of reading an unbounded dim list.
1316        let mut w = Writer::new();
1317        w.u8(3); // Caps::Tensor tag
1318        w.u8(dtype_to_u8(TensorDType::F32));
1319        let n = (MAX_TENSOR_RANK + 1) as u32;
1320        w.u32(n);
1321        for _ in 0..n {
1322            w.u32(1);
1323        }
1324        w.u8(layout_to_u8(TensorLayout::Nchw));
1325        let mut r = Reader::new(&w.buf);
1326        assert_eq!(get_caps(&mut r), Err(WireError::BadTag));
1327    }
1328
1329    #[test]
1330    fn mpeg4_part2_codec_round_trips() {
1331        let caps = Caps::CompressedVideo {
1332            codec: VideoCodec::Mpeg4Part2,
1333            width: Dim::Fixed(720),
1334            height: Dim::Fixed(576),
1335            framerate: Rate::Fixed(25 << 16),
1336        };
1337        match roundtrip(&PipelinePacket::CapsChanged(caps.clone())) {
1338            PipelinePacket::CapsChanged(got) => assert_eq!(got, caps),
1339            other => panic!("expected CapsChanged, got {other:?}"),
1340        }
1341        // The wire tag is stable: appended after the existing codecs (Mjpeg = 5).
1342        assert_eq!(video_codec_to_u8(VideoCodec::Mpeg4Part2), 6);
1343    }
1344
1345    #[test]
1346    fn data_frame_round_trips_bytes_timing_and_sequence() {
1347        let bytes: Vec<u8> = (0u8..=200).collect();
1348        let timing = FrameTiming {
1349            pts_ns: 1_000,
1350            dts_ns: 900,
1351            duration_ns: 33,
1352            capture_ns: 7,
1353            arrival_ns: 42,
1354            keyframe: true,
1355        };
1356        let frame = Frame {
1357            domain: MemoryDomain::System(SystemSlice::from_boxed(bytes.clone().into_boxed_slice())),
1358            timing,
1359            sequence: 12_345,
1360            meta: FrameMetaSet::new(),
1361        };
1362        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1363            PipelinePacket::DataFrame(got) => {
1364                assert_eq!(got.sequence, 12_345);
1365                assert_eq!(got.timing, timing);
1366                match got.domain {
1367                    MemoryDomain::System(s) => assert_eq!(s.as_slice(), &bytes[..]),
1368                    other => panic!("expected System, got {other:?}"),
1369                }
1370            }
1371            other => panic!("expected DataFrame, got {other:?}"),
1372        }
1373    }
1374
1375    #[test]
1376    fn control_packets_round_trip() {
1377        assert!(matches!(
1378            roundtrip(&PipelinePacket::Eos),
1379            PipelinePacket::Eos
1380        ));
1381        assert!(matches!(
1382            roundtrip(&PipelinePacket::Flush),
1383            PipelinePacket::Flush
1384        ));
1385        let seg = Segment {
1386            rate: 2.0,
1387            applied_rate: 1.0,
1388            base: 5,
1389            start: 1_000,
1390            stop: Some(9_000),
1391            time: 1_000,
1392            position: 3_000,
1393            key_units_only: true,
1394        };
1395        match roundtrip(&PipelinePacket::Segment(seg)) {
1396            PipelinePacket::Segment(got) => assert_eq!(got, seg),
1397            other => panic!("expected Segment, got {other:?}"),
1398        }
1399    }
1400
1401    #[test]
1402    fn device_domain_cannot_be_serialized() {
1403        // A DMABUF is a device fd, not CPU bytes: encoding must refuse it rather
1404        // than ship a meaningless pointer. (fd -1 never opens a real resource;
1405        // its Drop close is harmless.)
1406        // SAFETY: fd -1 is never a live DMABUF; `from_raw` only stores it (no
1407        // I/O), and the Drop `close(-1)` is a harmless no-op. This exercises the
1408        // encode refusal of a device domain, not real DMABUF handling.
1409        let dmabuf = unsafe { crate::memory::OwnedDmaBuf::from_raw(-1, 0, 0) };
1410        let frame = Frame::new(MemoryDomain::DmaBuf(dmabuf), FrameTiming::default(), 0);
1411        assert_eq!(
1412            encode_packet(&PipelinePacket::DataFrame(frame)),
1413            Err(WireError::UnsupportedDomain)
1414        );
1415    }
1416
1417    #[test]
1418    fn truncated_and_bad_version_are_rejected() {
1419        assert!(matches!(decode_packet(&[]), Err(WireError::Truncated)));
1420        // Wrong version byte.
1421        assert!(matches!(
1422            decode_packet(&[WIRE_VERSION + 1, PKT_EOS]),
1423            Err(WireError::BadTag)
1424        ));
1425        // Right version, unknown packet tag.
1426        assert!(matches!(
1427            decode_packet(&[WIRE_VERSION, 250]),
1428            Err(WireError::BadTag)
1429        ));
1430        // A CapsChanged header with the caps body cut off.
1431        let mut bytes = encode_packet(&PipelinePacket::CapsChanged(Caps::Text {
1432            format: TextFormat::Utf8,
1433        }))
1434        .unwrap();
1435        bytes.pop();
1436        assert!(matches!(decode_packet(&bytes), Err(WireError::Truncated)));
1437    }
1438
1439    #[test]
1440    fn system_view_frame_materializes_to_system_bytes() {
1441        use crate::memory::SystemView;
1442        use crate::tensor::TensorView;
1443        // A contiguous 1-D view over 8 bytes: materialize is identity here, but
1444        // it proves a SystemView frame serializes as System bytes.
1445        let backing: alloc::sync::Arc<[u8]> = Box::<[u8]>::from([1u8, 2, 3, 4, 5, 6, 7, 8]).into();
1446        let view = TensorView::contiguous(TensorDType::U8, &[8]);
1447        let frame = Frame::new(
1448            MemoryDomain::SystemView(SystemView::new(backing, view)),
1449            FrameTiming::default(),
1450            1,
1451        );
1452        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1453            PipelinePacket::DataFrame(got) => match got.domain {
1454                MemoryDomain::System(s) => assert_eq!(s.as_slice(), &[1, 2, 3, 4, 5, 6, 7, 8]),
1455                other => panic!("SystemView should decode as System, got {other:?}"),
1456            },
1457            other => panic!("expected DataFrame, got {other:?}"),
1458        }
1459    }
1460
1461    #[cfg(feature = "metadata")]
1462    #[test]
1463    fn analytics_and_blob_metadata_round_trip() {
1464        use crate::meta::{
1465            AnalyticsMeta, AnalyticsNode, BBox, BlobMeta, Classification, ObjectDetection,
1466            RelationKind,
1467        };
1468        let mut analytics = AnalyticsMeta::new();
1469        let d = analytics.add_detection(ObjectDetection {
1470            bbox: BBox {
1471                x: 0.1,
1472                y: 0.2,
1473                w: 0.3,
1474                h: 0.4,
1475            },
1476            label: 7,
1477            confidence: 0.9,
1478        });
1479        let c = analytics.push(AnalyticsNode::Classification(Classification {
1480            label: 42,
1481            confidence: 0.7,
1482        }));
1483        analytics.relate(d, c, RelationKind::Classifies);
1484
1485        let mut blob = BlobMeta::new();
1486        blob.push("embedding", alloc::vec![1, 2, 3, 4]);
1487
1488        let mut meta = FrameMetaSet::new();
1489        meta.attach(analytics.clone());
1490        meta.attach(blob.clone());
1491
1492        let frame = Frame {
1493            domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([9u8; 16]))),
1494            timing: FrameTiming::default(),
1495            sequence: 0,
1496            meta,
1497        };
1498        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1499            PipelinePacket::DataFrame(got) => {
1500                let a = got.meta.get::<AnalyticsMeta>().expect("analytics survived");
1501                assert_eq!(a.nodes, analytics.nodes);
1502                assert_eq!(a.relations, analytics.relations);
1503                let b = got.meta.get::<BlobMeta>().expect("blob survived");
1504                assert_eq!(b, &blob);
1505            }
1506            other => panic!("expected DataFrame, got {other:?}"),
1507        }
1508    }
1509
1510    #[cfg(feature = "metadata")]
1511    #[test]
1512    fn segmentation_and_roi_nodes_round_trip() {
1513        use crate::meta::{AnalyticsMeta, AnalyticsNode, BBox, Mask, Roi, Segmentation};
1514        let bbox = BBox {
1515            x: 0.25,
1516            y: 0.5,
1517            w: 0.1,
1518            h: 0.2,
1519        };
1520        // A 3x2 mask with a 4-byte stride, so the padded layout has to survive.
1521        let mask = Mask::new(3, 2, 4, alloc::vec![10, 20, 30, 0, 40, 50, 60, 0])
1522            .expect("mask fits its data");
1523        let mut analytics = AnalyticsMeta::new();
1524        analytics.push(AnalyticsNode::Segmentation(Segmentation {
1525            bbox,
1526            label: 3,
1527            confidence: 0.75,
1528            mask,
1529        }));
1530        analytics.push(AnalyticsNode::Roi(Roi {
1531            bbox,
1532            id: 9,
1533            label: 4,
1534        }));
1535
1536        let mut meta = FrameMetaSet::new();
1537        meta.attach(analytics.clone());
1538        let frame = Frame {
1539            domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1540            timing: FrameTiming::default(),
1541            sequence: 0,
1542            meta,
1543        };
1544        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1545            PipelinePacket::DataFrame(got) => {
1546                let a = got.meta.get::<AnalyticsMeta>().expect("analytics survived");
1547                assert_eq!(a.nodes, analytics.nodes);
1548                let seg = a.segmentations().next().expect("segmentation node");
1549                assert_eq!(seg.mask.sample(2, 1), Some(60));
1550                assert_eq!(seg.mask.sample(3, 0), None, "outside the mask width");
1551                assert_eq!(a.rois().next().expect("roi node").id, 9);
1552            }
1553            other => panic!("expected DataFrame, got {other:?}"),
1554        }
1555    }
1556
1557    #[cfg(feature = "metadata")]
1558    #[test]
1559    fn a_mask_whose_geometry_overruns_its_bytes_is_rejected() {
1560        use crate::meta::Mask;
1561        assert!(
1562            Mask::new(4, 4, 4, alloc::vec![0; 15]).is_none(),
1563            "short data"
1564        );
1565        assert!(
1566            Mask::new(8, 2, 4, alloc::vec![0; 64]).is_none(),
1567            "stride < width"
1568        );
1569        assert!(
1570            Mask::new(u32::MAX, u32::MAX, u32::MAX, alloc::vec![0; 8]).is_none(),
1571            "the row product must not overflow into a valid-looking size"
1572        );
1573    }
1574
1575    #[cfg(feature = "metadata")]
1576    #[test]
1577    fn caption_metadata_round_trips() {
1578        use crate::meta::{CaptionMeta, CaptionTriple};
1579        let mut captions = CaptionMeta::new();
1580        captions.push(CaptionTriple {
1581            cc_type: 0,
1582            b0: 0x94,
1583            b1: 0xAE,
1584        });
1585        captions.push(CaptionTriple {
1586            cc_type: 3,
1587            b0: 0x01,
1588            b1: 0xFF,
1589        });
1590
1591        let mut meta = FrameMetaSet::new();
1592        meta.attach(captions.clone());
1593        let frame = Frame {
1594            domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1595            timing: FrameTiming::default(),
1596            sequence: 3,
1597            meta,
1598        };
1599        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1600            PipelinePacket::DataFrame(got) => {
1601                let c = got.meta.get::<CaptionMeta>().expect("captions survived");
1602                assert_eq!(c, &captions);
1603            }
1604            other => panic!("expected DataFrame, got {other:?}"),
1605        }
1606    }
1607
1608    #[cfg(feature = "metadata")]
1609    #[test]
1610    fn hdr_static_metadata_round_trips() {
1611        use crate::meta::{Chromaticity, HdrStaticMeta, MasteringDisplay};
1612        let xy = |x, y| Chromaticity { x, y };
1613        let hdr = HdrStaticMeta {
1614            mastering: Some(MasteringDisplay {
1615                display_primaries: [xy(0.708, 0.292), xy(0.170, 0.797), xy(0.131, 0.046)],
1616                white_point: xy(0.3127, 0.3290),
1617                max_luminance: 1000.0,
1618                min_luminance: 0.005,
1619            }),
1620            max_content_light_level: Some(1200),
1621            max_frame_average_light_level: Some(300),
1622        };
1623
1624        let mut meta = FrameMetaSet::new();
1625        meta.attach(hdr);
1626        let frame = Frame {
1627            domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1628            timing: FrameTiming::default(),
1629            sequence: 0,
1630            meta,
1631        };
1632        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1633            PipelinePacket::DataFrame(got) => {
1634                let h = got.meta.get::<HdrStaticMeta>().expect("hdr survived");
1635                assert_eq!(h, &hdr);
1636            }
1637            other => panic!("expected DataFrame, got {other:?}"),
1638        }
1639    }
1640
1641    #[cfg(feature = "metadata")]
1642    #[test]
1643    fn hdr_static_metadata_round_trips_without_a_mastering_display() {
1644        // A stream carrying only content_light_level_info: the absent half must
1645        // decode back as absent, not as zeroed primaries.
1646        use crate::meta::HdrStaticMeta;
1647        let hdr = HdrStaticMeta {
1648            mastering: None,
1649            max_content_light_level: Some(400),
1650            max_frame_average_light_level: None,
1651        };
1652        let mut meta = FrameMetaSet::new();
1653        meta.attach(hdr);
1654        let frame = Frame {
1655            domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1656            timing: FrameTiming::default(),
1657            sequence: 0,
1658            meta,
1659        };
1660        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1661            PipelinePacket::DataFrame(got) => {
1662                assert_eq!(got.meta.get::<HdrStaticMeta>(), Some(&hdr));
1663            }
1664            other => panic!("expected DataFrame, got {other:?}"),
1665        }
1666    }
1667
1668    #[cfg(feature = "metadata")]
1669    #[test]
1670    fn timecode_metadata_round_trips() {
1671        use crate::meta::TimecodeMeta;
1672        let tc = TimecodeMeta {
1673            hours: 10,
1674            minutes: 59,
1675            seconds: 58,
1676            frames: 29,
1677            drop_frame: true,
1678            framerate_q16: Some(1_965_691), // 29.97 fps
1679        };
1680        let mut meta = FrameMetaSet::new();
1681        meta.attach(tc);
1682        let frame = Frame {
1683            domain: MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
1684            timing: FrameTiming::default(),
1685            sequence: 0,
1686            meta,
1687        };
1688        match roundtrip(&PipelinePacket::DataFrame(frame)) {
1689            PipelinePacket::DataFrame(got) => {
1690                assert_eq!(got.meta.get::<TimecodeMeta>(), Some(&tc));
1691            }
1692            other => panic!("expected DataFrame, got {other:?}"),
1693        }
1694    }
1695}