Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_fmv/
stream.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! STR video sectors: the 32-byte chunk header and frame reassembly.
3//!
4//! Every video sector's 2048 user bytes are a 32-byte header plus 2016
5//! bytes of one frame's bitstream:
6//!
7//! | Bytes  | Field |
8//! |--------|-------|
9//! | 0..2   | `0x0160` |
10//! | 2..4   | `0x8001` (MDEC video chunk) |
11//! | 4..6   | chunk index within the frame |
12//! | 6..8   | chunks in the frame |
13//! | 8..12  | frame number (1-based) |
14//! | 12..16 | frame bitstream size in bytes |
15//! | 16..18 | width |
16//! | 18..20 | height |
17//! | 20..28 | copy of the BS frame header |
18//!
19//! Interleaved XA audio sectors never reach this layer: with the drive in
20//! XA mode they go straight to the SPU and raise no data IRQ.
21
22/// Bytes of chunk header at the start of each video sector.
23pub const CHUNK_HEADER_BYTES: usize = 32;
24/// Bitstream bytes carried per video sector.
25pub const CHUNK_PAYLOAD_BYTES: usize = 2016;
26/// Most chunks per frame [`FrameAssembler`] tracks (a 32-bit mask). At
27/// 2016 bytes each that is 64 KB, far above any frame a 2x stream carries.
28pub const MAX_CHUNKS: u16 = 32;
29
30/// Parsed chunk header of one video sector.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct Chunk {
33    /// Index of this chunk within its frame.
34    pub index: u16,
35    /// Chunks in the frame.
36    pub count: u16,
37    /// Frame number.
38    pub frame: u32,
39    /// Frame bitstream size in bytes.
40    pub size: u32,
41    /// Frame width in pixels.
42    pub width: u16,
43    /// Frame height in pixels.
44    pub height: u16,
45}
46
47impl Chunk {
48    /// Parse a sector's user data; `None` if it is not an STR video chunk.
49    pub fn parse(sector: &[u8]) -> Option<Chunk> {
50        if sector.len() < CHUNK_HEADER_BYTES {
51            return None;
52        }
53        let h = |i: usize| u16::from_le_bytes([sector[i], sector[i + 1]]);
54        let w =
55            |i: usize| u32::from_le_bytes([sector[i], sector[i + 1], sector[i + 2], sector[i + 3]]);
56        if h(0) != 0x0160 || h(2) != 0x8001 {
57            return None;
58        }
59        let chunk = Chunk {
60            index: h(4),
61            count: h(6),
62            frame: w(8),
63            size: w(12),
64            width: h(16),
65            height: h(18),
66        };
67        if chunk.count == 0 || chunk.count > MAX_CHUNKS || chunk.index >= chunk.count {
68            return None;
69        }
70        Some(chunk)
71    }
72}
73
74/// A frame whose every chunk has arrived.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub struct Frame {
77    /// Frame number from the chunk headers.
78    pub number: u32,
79    /// Bitstream bytes.
80    pub size: u32,
81    /// Width in pixels.
82    pub width: u16,
83    /// Height in pixels.
84    pub height: u16,
85}
86
87/// Collects one frame's chunks into a caller-owned buffer.
88///
89/// Chunks may arrive in any order. A chunk from a new frame while the
90/// current one is incomplete abandons it (a lost sector) and counts it in
91/// [`dropped`](Self::dropped).
92#[derive(Default)]
93pub struct FrameAssembler {
94    frame: u32,
95    mask: u32,
96    count: u16,
97    /// Frames abandoned with chunks missing.
98    pub dropped: u32,
99}
100
101impl FrameAssembler {
102    /// Empty assembler.
103    pub const fn new() -> Self {
104        FrameAssembler {
105            frame: 0,
106            mask: 0,
107            count: 0,
108            dropped: 0,
109        }
110    }
111
112    /// Add one sector (2048 user bytes). `buf` is the frame being filled
113    /// and must hold `count * 2016` bytes. Returns the finished frame when
114    /// this chunk completes it; the caller then hands `buf` to the decoder
115    /// and passes a different buffer from now on.
116    pub fn add(&mut self, sector: &[u8], buf: &mut [u8]) -> Option<Frame> {
117        let chunk = Chunk::parse(sector)?;
118        if chunk.frame != self.frame || chunk.count != self.count {
119            if self.mask != 0 {
120                self.dropped += 1;
121            }
122            self.frame = chunk.frame;
123            self.count = chunk.count;
124            self.mask = 0;
125        }
126        let at = chunk.index as usize * CHUNK_PAYLOAD_BYTES;
127        let end = at + CHUNK_PAYLOAD_BYTES;
128        if end > buf.len() || sector.len() < CHUNK_HEADER_BYTES + CHUNK_PAYLOAD_BYTES {
129            return None;
130        }
131        buf[at..end]
132            .copy_from_slice(&sector[CHUNK_HEADER_BYTES..CHUNK_HEADER_BYTES + CHUNK_PAYLOAD_BYTES]);
133        self.mask |= 1 << chunk.index;
134        let full = if chunk.count == 32 {
135            u32::MAX
136        } else {
137            (1u32 << chunk.count) - 1
138        };
139        if self.mask != full {
140            return None;
141        }
142        self.mask = 0;
143        self.frame = 0;
144        Some(Frame {
145            number: chunk.frame,
146            size: chunk.size,
147            width: chunk.width,
148            height: chunk.height,
149        })
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    fn sector(index: u16, count: u16, frame: u32, fill: u8) -> [u8; 2048] {
158        let mut s = [fill; 2048];
159        s[0..2].copy_from_slice(&0x0160u16.to_le_bytes());
160        s[2..4].copy_from_slice(&0x8001u16.to_le_bytes());
161        s[4..6].copy_from_slice(&index.to_le_bytes());
162        s[6..8].copy_from_slice(&count.to_le_bytes());
163        s[8..12].copy_from_slice(&frame.to_le_bytes());
164        s[12..16].copy_from_slice(&5000u32.to_le_bytes());
165        s[16..18].copy_from_slice(&320u16.to_le_bytes());
166        s[18..20].copy_from_slice(&240u16.to_le_bytes());
167        s
168    }
169
170    #[test]
171    fn assembles_out_of_order_and_counts_drops() {
172        let mut a = FrameAssembler::new();
173        let mut buf = [0u8; 3 * CHUNK_PAYLOAD_BYTES];
174        assert_eq!(a.add(&sector(2, 3, 1, 0xC2), &mut buf), None);
175        assert_eq!(a.add(&sector(0, 3, 1, 0xC0), &mut buf), None);
176        let f = a.add(&sector(1, 3, 1, 0xC1), &mut buf).unwrap();
177        assert_eq!((f.number, f.size, f.width, f.height), (1, 5000, 320, 240));
178        assert_eq!(buf[0], 0xC0);
179        assert_eq!(buf[CHUNK_PAYLOAD_BYTES], 0xC1);
180        assert_eq!(buf[2 * CHUNK_PAYLOAD_BYTES + 5], 0xC2);
181        // Frame 2 loses a chunk; frame 3 completes and 2 counts as dropped.
182        assert_eq!(a.add(&sector(0, 3, 2, 0), &mut buf), None);
183        assert_eq!(a.add(&sector(0, 3, 3, 0), &mut buf), None);
184        assert_eq!(a.add(&sector(1, 3, 3, 0), &mut buf), None);
185        assert!(a.add(&sector(2, 3, 3, 0), &mut buf).is_some());
186        assert_eq!(a.dropped, 1);
187    }
188
189    #[test]
190    fn ignores_non_video_sectors() {
191        let mut a = FrameAssembler::new();
192        let mut buf = [0u8; CHUNK_PAYLOAD_BYTES];
193        assert_eq!(a.add(&[0u8; 2048], &mut buf), None);
194        assert_eq!(Chunk::parse(&sector(3, 3, 1, 0)), None);
195    }
196}