Skip to main content

oxideav_bitstream/
vc1.rs

1//! VC-1 (SMPTE 421M) sequence-header / entry-point / picture-header
2//! parser. Advanced profile (Annex G start codes) is the primary
3//! target — that is what `VdpPictureInfoVC1` and
4//! `VAPictureParameterBufferVC1` consume in any modern HW pipeline.
5//!
6//! # Scope
7//!
8//! Like the H.264 / HEVC / VP9 / VP8 / MPEG-2 siblings this is **not**
9//! a full VC-1 decoder. Only the parameter-buffer-shaped header bits
10//! the HW backends need to populate `VdpPictureInfoVC1` /
11//! `VAPictureParameterBufferVC1`.
12//!
13//! - Advanced-profile sequence header (§6.1) — full parse.
14//! - Entry-point header (§6.2) — full parse, including HRD fullness
15//!   (skipped past) and optional coded-size override.
16//! - Picture header (§7.1.1) — minimal: FCM + picture-type only
17//!   (enough to identify intra frames).
18//! - Simple/Main profiles are **not** supported (no Annex G start
19//!   codes — they require the codec_data byte from the container).
20//!
21//! # Bit ordering
22//!
23//! Unlike VP8/VP9, VC-1 reads bits MSB-first inside each byte —
24//! identical to H.264 / HEVC / MPEG-2. We can re-use the shared
25//! [`crate::bit_reader::BitReader`].
26//!
27//! # Spec references
28//!
29//! SMPTE ST 421:2006 ("VC-1 Compressed Video Bitstream Format and
30//! Decoding Process") sections 6.1 (sequence header), 6.2
31//! (entry-point header), 7.1.1 (picture header for advanced profile).
32//!
33//! # Annex G start codes (advanced profile only)
34//!
35//! VC-1 BDU start codes are 4 bytes: `00 00 01 XX`. The XX byte
36//! gives the BDU type:
37//! - `0x0F`: sequence header
38//! - `0x0E`: entry-point header
39//! - `0x0D`: frame (picture) BDU
40//! - `0x0C`: field (interlaced field picture) BDU
41//! - `0x0B`: slice BDU (advanced profile only, when slices used)
42//! - `0x1D`: user data, sequence level
43//! - `0x1E`: user data, entry-point level
44//! - `0x1F`: end-of-sequence
45
46use crate::bit_reader::BitReader;
47use crate::BitstreamError;
48
49// ─────────────────────────── BDU type bytes ─────────────────────────────────
50
51pub const BDU_SEQUENCE_HEADER: u8 = 0x0F;
52pub const BDU_ENTRY_POINT: u8 = 0x0E;
53pub const BDU_FRAME: u8 = 0x0D;
54pub const BDU_FIELD: u8 = 0x0C;
55pub const BDU_SLICE: u8 = 0x0B;
56pub const BDU_END_OF_SEQUENCE: u8 = 0x1F;
57
58// ─────────────────────────── Profile ID values ──────────────────────────────
59
60pub const PROFILE_SIMPLE: u8 = 0;
61pub const PROFILE_MAIN: u8 = 1;
62pub const PROFILE_ADVANCED: u8 = 3;
63
64// ─────────────────────────── BDU framing ────────────────────────────────────
65
66#[derive(Debug, Clone, Copy)]
67pub struct VcBdu<'a> {
68    pub bdu_type: u8,
69    pub payload: &'a [u8],
70}
71
72/// Walk the input stream and return the list of BDUs (start code
73/// stripped — `payload` points at the first body byte after
74/// `00 00 01 XX`). Empty BDUs (zero-byte payload) are returned too.
75pub fn split_bdus(stream: &[u8]) -> Vec<VcBdu<'_>> {
76    let mut out = Vec::new();
77    let mut i = 0;
78    let n = stream.len();
79    let mut current: Option<(u8, usize)> = None; // (bdu_type, body_start)
80    while i + 3 < n {
81        if stream[i] == 0 && stream[i + 1] == 0 && stream[i + 2] == 1 {
82            if let Some((t, body_start)) = current.take() {
83                out.push(VcBdu {
84                    bdu_type: t,
85                    payload: &stream[body_start..i],
86                });
87            }
88            let bdu_type = stream[i + 3];
89            i += 4;
90            current = Some((bdu_type, i));
91            continue;
92        }
93        i += 1;
94    }
95    if let Some((t, body_start)) = current.take() {
96        out.push(VcBdu {
97            bdu_type: t,
98            payload: &stream[body_start..n],
99        });
100    }
101    out
102}
103
104// ─────────────────────────── Output structs ─────────────────────────────────
105
106/// Sequence header (§6.1).
107#[derive(Debug, Clone, Default, PartialEq, Eq)]
108pub struct Vc1SequenceHeader {
109    /// 0=Simple, 1=Main, 3=Advanced. Only Advanced is parsed by this
110    /// module — the parser errors on the others.
111    pub profile: u8,
112    pub level: u8,
113    /// 1 = 4:2:0. Other values reserved.
114    pub colordiff_format: u8,
115    pub frmrtq_postproc: u8,
116    pub bitrtq_postproc: u8,
117    pub postprocflag: bool,
118    /// `MAX_CODED_WIDTH+1` — the encoded coded width in luma samples
119    /// (already +1).
120    pub max_coded_width: u32,
121    /// `MAX_CODED_HEIGHT+1` — encoded coded height (already +1).
122    pub max_coded_height: u32,
123    pub pulldown: bool,
124    pub interlace: bool,
125    pub tfcntrflag: bool,
126    pub finterpflag: bool,
127    /// "Progressive segmented frame" flag — see §6.1.13.
128    pub psf: bool,
129    /// Whether the display-extension block (display size, aspect
130    /// ratio, framerate, color description) is present.
131    pub display_ext: bool,
132    /// `DISP_HORIZ_SIZE+1` if `display_ext`, else 0.
133    pub display_horiz_size: u32,
134    /// `DISP_VERT_SIZE+1` if `display_ext`, else 0.
135    pub display_vert_size: u32,
136    pub aspect_ratio_flag: bool,
137    pub aspect_ratio: u8,
138    pub aspect_horiz_size: u8,
139    pub aspect_vert_size: u8,
140    pub framerate_flag: bool,
141    pub framerateind: u8,
142    pub frameratenr: u32,
143    pub frameratedr: u8,
144    pub color_format_flag: bool,
145    pub color_primaries: u8,
146    pub transfer_char: u8,
147    pub matrix_coef: u8,
148    pub hrd_param_flag: bool,
149    pub hrd_num_leaky_buckets: u8,
150    pub hrd_bit_rate_exponent: u8,
151    pub hrd_buffer_size_exponent: u8,
152}
153
154/// Entry-point header (§6.2).
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
156pub struct Vc1EntryPointHeader {
157    pub broken_link: bool,
158    pub closed_entry: bool,
159    pub panscan_flag: bool,
160    pub refdist_flag: bool,
161    pub loopfilter: bool,
162    pub fastuvmc: bool,
163    pub extended_mv: bool,
164    pub dquant: u8,
165    pub vstransform: bool,
166    pub overlap: bool,
167    pub quantizer: u8,
168    pub coded_size_flag: bool,
169    /// `CODED_WIDTH+1` if `coded_size_flag`, else 0. The HW backend
170    /// should fall back to the sequence header's `max_coded_width`
171    /// when `coded_size_flag=0`.
172    pub coded_width: u32,
173    /// `CODED_HEIGHT+1` if `coded_size_flag`, else 0.
174    pub coded_height: u32,
175    pub extended_dmv: bool,
176    pub range_mapy_flag: bool,
177    pub range_mapy: u8,
178    pub range_mapuv_flag: bool,
179    pub range_mapuv: u8,
180}
181
182/// Minimal picture header (§7.1.1). Picture-type decode in VC-1 is
183/// VLC-coded and depends on the entry-point's `interlace` flag, the
184/// sequence header's `finterpflag`, etc. We surface only the FCM
185/// (frame coding mode) and the picture type, which is what
186/// `VdpPictureInfoVC1::picture_type` ultimately consumes.
187#[derive(Debug, Clone, Default, PartialEq, Eq)]
188pub struct Vc1PictureHeader {
189    /// FCM — frame coding mode (§7.1.1.15). 0 = progressive frame,
190    /// 2 = interlaced field, 3 = interlaced frame.
191    pub fcm: u8,
192    /// Picture type: 0=I, 1=P, 2=B, 3=BI, 4=skipped (for progressive
193    /// pictures). For interlaced field pictures this is per-field
194    /// and uses different VLC; the parser falls back to a 3-bit
195    /// fixed-length read for those.
196    pub picture_type: u8,
197}
198
199/// Convenience structure returned by [`parse_first_picture`].
200#[derive(Debug)]
201pub struct Vc1FirstPictureParse<'a> {
202    pub sequence_header: Vc1SequenceHeader,
203    pub entry_point: Vc1EntryPointHeader,
204    pub picture_header: Vc1PictureHeader,
205    pub frame_payload: &'a [u8],
206}
207
208// ─────────────────────────── Sequence header parser ─────────────────────────
209
210/// Parse an Advanced-profile sequence header. `bytes` is the buffer
211/// starting **after** the `00 00 01 0F` start code (i.e. at the
212/// PROFILE field). The parser errors if PROFILE is not 3.
213pub fn parse_sequence_header_advanced(bytes: &[u8]) -> Result<Vc1SequenceHeader, BitstreamError> {
214    if bytes.len() < 6 {
215        return Err(BitstreamError::unexpected_end(
216            "VC-1 sequence header shorter than 6 bytes",
217        ));
218    }
219    let mut r = BitReader::new(bytes);
220    let mut s = Vc1SequenceHeader {
221        profile: r.u(2) as u8,
222        ..Vc1SequenceHeader::default()
223    };
224    if s.profile != PROFILE_ADVANCED {
225        return Err(BitstreamError::unsupported(format!(
226            "VC-1 profile {} not supported (only Advanced/3)",
227            s.profile
228        )));
229    }
230    s.level = r.u(3) as u8;
231    s.colordiff_format = r.u(2) as u8;
232    s.frmrtq_postproc = r.u(3) as u8;
233    s.bitrtq_postproc = r.u(5) as u8;
234    s.postprocflag = r.u(1) != 0;
235    s.max_coded_width = r.u(12) + 1;
236    s.max_coded_height = r.u(12) + 1;
237    s.pulldown = r.u(1) != 0;
238    s.interlace = r.u(1) != 0;
239    s.tfcntrflag = r.u(1) != 0;
240    s.finterpflag = r.u(1) != 0;
241    let _reserved = r.u(1);
242    s.psf = r.u(1) != 0;
243    s.display_ext = r.u(1) != 0;
244    if s.display_ext {
245        s.display_horiz_size = r.u(14) + 1;
246        s.display_vert_size = r.u(14) + 1;
247        s.aspect_ratio_flag = r.u(1) != 0;
248        if s.aspect_ratio_flag {
249            s.aspect_ratio = r.u(4) as u8;
250            if s.aspect_ratio == 15 {
251                s.aspect_horiz_size = r.u(8) as u8;
252                s.aspect_vert_size = r.u(8) as u8;
253            }
254        }
255        s.framerate_flag = r.u(1) != 0;
256        if s.framerate_flag {
257            s.framerateind = r.u(1) as u8;
258            if s.framerateind == 0 {
259                s.frameratenr = r.u(8);
260                s.frameratedr = r.u(4) as u8;
261            } else {
262                s.frameratenr = r.u(16);
263            }
264        }
265        s.color_format_flag = r.u(1) != 0;
266        if s.color_format_flag {
267            s.color_primaries = r.u(8) as u8;
268            s.transfer_char = r.u(8) as u8;
269            s.matrix_coef = r.u(8) as u8;
270        }
271    }
272    s.hrd_param_flag = r.u(1) != 0;
273    if s.hrd_param_flag {
274        s.hrd_num_leaky_buckets = r.u(5) as u8;
275        s.hrd_bit_rate_exponent = r.u(4) as u8;
276        s.hrd_buffer_size_exponent = r.u(4) as u8;
277        // For each leaky bucket, 16-bit HRD_RATE + 16-bit HRD_BUFFER.
278        // We don't surface the rate/buffer arrays; just consume the bits.
279        for _ in 0..s.hrd_num_leaky_buckets {
280            let _hrd_rate = r.u(16);
281            let _hrd_buffer = r.u(16);
282        }
283    }
284    Ok(s)
285}
286
287// ─────────────────────────── Entry-point header parser ──────────────────────
288
289/// Parse an entry-point header. The parser needs the previously
290/// decoded sequence header so it can know how many HRD_FULL bytes
291/// to skip, plus whether `extended_mv` was already signalled.
292pub fn parse_entry_point_header(
293    bytes: &[u8],
294    seq: &Vc1SequenceHeader,
295) -> Result<Vc1EntryPointHeader, BitstreamError> {
296    if bytes.is_empty() {
297        return Err(BitstreamError::unexpected_end(
298            "VC-1 entry-point header empty",
299        ));
300    }
301    let mut r = BitReader::new(bytes);
302    let mut e = Vc1EntryPointHeader {
303        broken_link: r.u(1) != 0,
304        closed_entry: r.u(1) != 0,
305        panscan_flag: r.u(1) != 0,
306        refdist_flag: r.u(1) != 0,
307        loopfilter: r.u(1) != 0,
308        fastuvmc: r.u(1) != 0,
309        extended_mv: r.u(1) != 0,
310        dquant: r.u(2) as u8,
311        vstransform: r.u(1) != 0,
312        overlap: r.u(1) != 0,
313        quantizer: r.u(2) as u8,
314        ..Vc1EntryPointHeader::default()
315    };
316    // HRD fullness: 8 bits per leaky bucket if HRD was signalled in
317    // the sequence header.
318    if seq.hrd_param_flag {
319        for _ in 0..seq.hrd_num_leaky_buckets {
320            let _hrd_full = r.u(8);
321        }
322    }
323    e.coded_size_flag = r.u(1) != 0;
324    if e.coded_size_flag {
325        e.coded_width = r.u(12) + 1;
326        e.coded_height = r.u(12) + 1;
327    }
328    if e.extended_mv {
329        e.extended_dmv = r.u(1) != 0;
330    }
331    e.range_mapy_flag = r.u(1) != 0;
332    if e.range_mapy_flag {
333        e.range_mapy = r.u(3) as u8;
334    }
335    e.range_mapuv_flag = r.u(1) != 0;
336    if e.range_mapuv_flag {
337        e.range_mapuv = r.u(3) as u8;
338    }
339    Ok(e)
340}
341
342// ─────────────────────────── Picture header parser ──────────────────────────
343
344/// Parse a minimal frame-BDU picture header (§7.1.1). Only the FCM
345/// (frame coding mode) and the picture type are extracted — that
346/// is enough for the HW backends to populate
347/// `VdpPictureInfoVC1::picture_type` / `frame_coding_mode`.
348pub fn parse_picture_header(
349    bytes: &[u8],
350    seq: &Vc1SequenceHeader,
351) -> Result<Vc1PictureHeader, BitstreamError> {
352    if bytes.is_empty() {
353        return Err(BitstreamError::unexpected_end("VC-1 picture header empty"));
354    }
355    let mut r = BitReader::new(bytes);
356    let mut h = Vc1PictureHeader::default();
357    // FCM: only present when interlace=1. VLC-coded:
358    //   0  → progressive frame (FCM=0)
359    //   10 → interlaced field (FCM=2)
360    //   11 → interlaced frame (FCM=3)
361    if seq.interlace {
362        let fcm0 = r.u(1);
363        if fcm0 == 0 {
364            h.fcm = 0;
365        } else {
366            let fcm1 = r.u(1);
367            h.fcm = if fcm1 == 0 { 2 } else { 3 };
368        }
369    } else {
370        h.fcm = 0;
371    }
372    // Picture type. The exact VLC depends on FCM and on
373    // `MAXBFRAMES` (signalled at sequence/entry-point level —
374    // omitted from this minimal parser). For FCM=0 the table is:
375    //   110  → I
376    //   0    → P
377    //   10   → B
378    //   1110 → BI
379    //   1111 → skipped
380    // For FCM=3 (interlaced frame) it's a 3-bit fixed length:
381    //   000=I, 001=P, 010=B, 011=BI, 100=skipped (per §7.1.1.4).
382    // For FCM=2 (interlaced field) it's a 3-bit fixed length code
383    // identifying a pair of field types (see SMPTE 421M Table 88).
384    //
385    // For the workspace's IRAP-only HW path we only need to
386    // distinguish I from non-I. We surface the raw VLC value so the
387    // HW backend / caller can interpret it according to the FCM it
388    // sees.
389    if h.fcm == 0 {
390        // Progressive: consume the variable-length code.
391        let b0 = r.u(1);
392        if b0 == 0 {
393            h.picture_type = 1; // P
394        } else {
395            let b1 = r.u(1);
396            if b1 == 0 {
397                h.picture_type = 2; // B
398            } else {
399                let b2 = r.u(1);
400                if b2 == 0 {
401                    h.picture_type = 0; // I (110)
402                } else {
403                    let b3 = r.u(1);
404                    h.picture_type = if b3 == 0 { 3 } else { 4 }; // BI / skipped
405                }
406            }
407        }
408    } else {
409        // Interlaced field/frame: 3-bit fixed length.
410        h.picture_type = r.u(3) as u8;
411    }
412    Ok(h)
413}
414
415// ─────────────────────────── End-to-end walker ──────────────────────────────
416
417/// Walk an Annex-G start-code stream, find the first sequence
418/// header, the first entry-point header that follows it, and the
419/// first frame BDU after that — parse all three and return them
420/// together with a slice covering the frame BDU's payload.
421pub fn parse_first_picture(stream: &[u8]) -> Result<Vc1FirstPictureParse<'_>, BitstreamError> {
422    let bdus = split_bdus(stream);
423    let seq = bdus
424        .iter()
425        .find(|b| b.bdu_type == BDU_SEQUENCE_HEADER)
426        .ok_or_else(|| BitstreamError::invalid("VC-1: no sequence header BDU in stream"))?;
427    let sequence_header = parse_sequence_header_advanced(seq.payload)?;
428    let entry = bdus
429        .iter()
430        .find(|b| b.bdu_type == BDU_ENTRY_POINT)
431        .ok_or_else(|| BitstreamError::invalid("VC-1: no entry-point BDU in stream"))?;
432    let entry_point = parse_entry_point_header(entry.payload, &sequence_header)?;
433    let frame = bdus
434        .iter()
435        .find(|b| b.bdu_type == BDU_FRAME)
436        .ok_or_else(|| BitstreamError::invalid("VC-1: no frame BDU in stream"))?;
437    let picture_header = parse_picture_header(frame.payload, &sequence_header)?;
438    Ok(Vc1FirstPictureParse {
439        sequence_header,
440        entry_point,
441        picture_header,
442        frame_payload: frame.payload,
443    })
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn split_bdus_finds_seq_entry_frame() {
452        let stream = [
453            0,
454            0,
455            1,
456            BDU_SEQUENCE_HEADER,
457            0xaa,
458            0,
459            0,
460            1,
461            BDU_ENTRY_POINT,
462            0xbb,
463            0,
464            0,
465            1,
466            BDU_FRAME,
467            0xcc,
468            0xdd,
469        ];
470        let bdus = split_bdus(&stream);
471        assert_eq!(bdus.len(), 3);
472        assert_eq!(bdus[0].bdu_type, BDU_SEQUENCE_HEADER);
473        assert_eq!(bdus[0].payload, &[0xaa]);
474        assert_eq!(bdus[1].bdu_type, BDU_ENTRY_POINT);
475        assert_eq!(bdus[1].payload, &[0xbb]);
476        assert_eq!(bdus[2].bdu_type, BDU_FRAME);
477        assert_eq!(bdus[2].payload, &[0xcc, 0xdd]);
478    }
479
480    #[test]
481    fn parse_sequence_header_rejects_simple_profile() {
482        // A 6-byte buffer where the top 2 bits = 0 (PROFILE = SIMPLE).
483        let buf = [0u8; 6];
484        assert!(matches!(
485            parse_sequence_header_advanced(&buf),
486            Err(BitstreamError::Unsupported(_))
487        ));
488    }
489
490    #[test]
491    fn parse_sequence_header_rejects_short_input() {
492        assert!(matches!(
493            parse_sequence_header_advanced(&[0u8; 2]),
494            Err(BitstreamError::UnexpectedEnd(_))
495        ));
496    }
497}