Skip to main content

codec/
pixel_format.rs

1//! Pixel-format detection from codec sequence headers.
2//!
3//! Given raw bitstream samples (the same Vec<Vec<u8>> our decoders
4//! consume), parse just enough of the first sequence header to
5//! extract chroma subsampling + luma bit depth, then map to our
6//! PixelFormat enum.
7//!
8//! Why not use the full decoder: our CPU decoders (H.264 openh264,
9//! HEVC Rust, VP9 Rust, rav1d AV1) each have their own parser
10//! entry points, but none of them expose a "just probe the format"
11//! API. NVDEC's sequence_callback tells us, but only after decode
12//! starts. This module gives the pipeline a fast, codec-agnostic
13//! probe path that runs before decoder construction.
14
15use crate::frame::PixelFormat;
16
17/// Detect pixel format from the first sequence header in `samples`.
18/// Falls back to Yuv420p on any parse failure — that matches the
19/// previous hard-coded behavior so a bad probe doesn't block the
20/// transcode, just the probe payload accuracy.
21pub fn detect(codec: &str, samples: &[Vec<u8>]) -> PixelFormat {
22    if samples.is_empty() {
23        return PixelFormat::Yuv420p;
24    }
25
26    let result = match codec.to_lowercase().as_str() {
27        "h264" | "avc1" | "avc" => detect_h264(&samples[0]),
28        "h265" | "hevc" | "hvc1" | "hev1" => detect_hevc(&samples[0]),
29        "vp9" | "vp09" => detect_vp9(&samples[0]),
30        "av1" | "av01" => detect_av1(&samples[0]),
31        _ => None,
32    };
33
34    result.unwrap_or(PixelFormat::Yuv420p)
35}
36
37// ─── Bit reader ────────────────────────────────────────────────────
38struct BitReader<'a> {
39    data: &'a [u8],
40    pos: usize,
41}
42
43impl<'a> BitReader<'a> {
44    fn new(data: &'a [u8]) -> Self {
45        Self { data, pos: 0 }
46    }
47
48    fn read_bits(&mut self, n: usize) -> Option<u32> {
49        let mut val = 0u32;
50        for _ in 0..n {
51            let byte_idx = self.pos / 8;
52            let bit_idx = 7 - (self.pos % 8);
53            if byte_idx >= self.data.len() {
54                return None;
55            }
56            val = (val << 1) | (((self.data[byte_idx] >> bit_idx) & 1) as u32);
57            self.pos += 1;
58        }
59        Some(val)
60    }
61
62    /// Exp-Golomb unsigned — used by H.264 and HEVC SPS fields.
63    fn read_ue(&mut self) -> Option<u32> {
64        let mut zeros = 0;
65        while self.read_bits(1)? == 0 {
66            zeros += 1;
67            if zeros > 31 {
68                // Cap before `1u32 << 32` would panic. 31 zeros already
69                // allow any value up to u32::MAX; any SPS field we care
70                // about fits within ~10 zeros.
71                return None;
72            }
73        }
74        if zeros == 0 {
75            return Some(0);
76        }
77        let suffix = self.read_bits(zeros)?;
78        Some((1u32 << zeros) - 1 + suffix)
79    }
80
81    /// Signed Exp-Golomb (se(v)). H.264 §9.1.1: `codeNum` from `read_ue`,
82    /// then `(-1)^(codeNum+1) * ceil(codeNum/2)` — odd codeNums map to
83    /// positive values, even to negative (or zero for codeNum=0).
84    /// Used by H.264 SPS `scaling_list` deltas and `pic_order_cnt_type==1`
85    /// offsets.
86    fn read_se(&mut self) -> Option<i32> {
87        let code = self.read_ue()? as i64;
88        let signed = if code & 1 == 1 {
89            ((code + 1) / 2) as i32
90        } else {
91            -((code / 2) as i32)
92        };
93        Some(signed)
94    }
95
96    /// Current bit position within `data`. Used by AV1 parsers to find
97    /// the byte-aligned end of uncompressed_header().
98    fn bit_pos(&self) -> usize {
99        self.pos
100    }
101
102    /// Advance the bit cursor to the next byte boundary. AV1 spec
103    /// byte_alignment() per §5.3.5: skip bits until `pos % 8 == 0`.
104    fn byte_align(&mut self) {
105        let rem = self.pos & 7;
106        if rem != 0 {
107            self.pos += 8 - rem;
108        }
109    }
110
111    /// AV1 signed `su(n)` — n-bit two's-complement signed integer
112    /// (§4.10.5). Read n bits, sign-extend from bit n-1.
113    fn read_su(&mut self, n: usize) -> Option<i32> {
114        let raw = self.read_bits(n)?;
115        let sign_bit = 1u32 << (n - 1);
116        let signed = if raw & sign_bit != 0 {
117            (raw as i32) - (1i32 << n)
118        } else {
119            raw as i32
120        };
121        Some(signed)
122    }
123}
124
125// ─── H.264 SPS parser ─────────────────────────────────────────────
126// See ITU-T H.264 §7.3.2.1.1. Profile-gated fields: only profile_idc
127// values in { 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139,
128// 134, 135 } carry the chroma_format_idc + bit_depth fields we want.
129// Everything else is 4:2:0 8-bit by spec.
130fn detect_h264(sample: &[u8]) -> Option<PixelFormat> {
131    let sps = find_h264_sps(sample)?;
132    let rbsp = remove_h264_rbsp_stuffing(sps);
133    let mut br = BitReader::new(&rbsp);
134
135    let profile_idc = br.read_bits(8)? as u8;
136    let _constraint_flags = br.read_bits(8)?;
137    let _level_idc = br.read_bits(8)?;
138    let _seq_parameter_set_id = br.read_ue()?;
139
140    let profile_gates_chroma = matches!(
141        profile_idc,
142        100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
143    );
144
145    let (chroma_format_idc, bit_depth_luma) = if profile_gates_chroma {
146        let chroma_format_idc = br.read_ue()? as u8;
147        if chroma_format_idc == 3 {
148            let _separate_colour_plane_flag = br.read_bits(1)?;
149        }
150        let bit_depth_luma_minus8 = br.read_ue()? as u8;
151        (chroma_format_idc, bit_depth_luma_minus8 + 8)
152    } else {
153        // Baseline / Main / Extended: spec-guaranteed 4:2:0 8-bit.
154        (1, 8)
155    };
156
157    Some(PixelFormat::from_chroma_and_depth(
158        chroma_format_idc,
159        bit_depth_luma,
160    ))
161}
162
163/// Return the SPS RBSP bytes (everything after the nal_unit_type byte,
164/// up to but not including the next start code). Handles both 3-byte
165/// and 4-byte start codes.
166fn find_h264_sps(data: &[u8]) -> Option<&[u8]> {
167    let mut i = 0;
168    while i + 4 < data.len() {
169        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
170            (3, i + 3)
171        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
172            (4, i + 4)
173        } else {
174            i += 1;
175            continue;
176        };
177        if nal_byte >= data.len() {
178            return None;
179        }
180        let nal_unit_type = data[nal_byte] & 0x1F;
181        if nal_unit_type == 7 {
182            // Skip the NAL unit type byte itself; caller parses the RBSP.
183            let start = nal_byte + 1;
184            let end = find_next_start_code(&data[start..])
185                .map(|off| start + off)
186                .unwrap_or(data.len());
187            return Some(&data[start..end]);
188        }
189        i += start_len;
190    }
191    None
192}
193
194fn find_next_start_code(data: &[u8]) -> Option<usize> {
195    (0..data.len().saturating_sub(3)).find(|&i| {
196        data[i] == 0
197            && data[i + 1] == 0
198            && (data[i + 2] == 1 || (data[i + 2] == 0 && data[i + 3] == 1))
199    })
200}
201
202/// Strip H.264 / HEVC emulation-prevention bytes (0x00 0x00 0x03 → 0x00 0x00).
203fn remove_h264_rbsp_stuffing(sps: &[u8]) -> Vec<u8> {
204    let mut out = Vec::with_capacity(sps.len());
205    let mut i = 0;
206    while i < sps.len() {
207        if i + 2 < sps.len() && sps[i] == 0 && sps[i + 1] == 0 && sps[i + 2] == 3 {
208            out.push(0);
209            out.push(0);
210            i += 3;
211        } else {
212            out.push(sps[i]);
213            i += 1;
214        }
215    }
216    out
217}
218
219// ─── HEVC SPS parser ──────────────────────────────────────────────
220// See ITU-T H.265 §7.3.2.2.1. We skip profile_tier_level and jump to
221// chroma_format_idc + bit_depth_luma_minus8 + bit_depth_chroma_minus8.
222fn detect_hevc(sample: &[u8]) -> Option<PixelFormat> {
223    let sps = find_hevc_sps(sample)?;
224    let rbsp = remove_h264_rbsp_stuffing(sps);
225    let mut br = BitReader::new(&rbsp);
226
227    let _sps_video_parameter_set_id = br.read_bits(4)?;
228    let sps_max_sub_layers_minus1 = br.read_bits(3)? as usize;
229    let _sps_temporal_id_nesting_flag = br.read_bits(1)?;
230
231    // profile_tier_level: 88 bits for general, plus sub-layer loops.
232    // The widths are fixed — we skip by the exact bit count instead
233    // of semantically parsing.
234    skip_hevc_profile_tier_level(&mut br, sps_max_sub_layers_minus1)?;
235
236    let _sps_seq_parameter_set_id = br.read_ue()?;
237    let chroma_format_idc = br.read_ue()? as u8;
238    if chroma_format_idc == 3 {
239        let _separate_colour_plane_flag = br.read_bits(1)?;
240    }
241    let _pic_width = br.read_ue()?;
242    let _pic_height = br.read_ue()?;
243    let conformance_window_flag = br.read_bits(1)?;
244    if conformance_window_flag == 1 {
245        let _ = br.read_ue()?;
246        let _ = br.read_ue()?;
247        let _ = br.read_ue()?;
248        let _ = br.read_ue()?;
249    }
250    let bit_depth_luma = br.read_ue()? as u8 + 8;
251    let _bit_depth_chroma_minus8 = br.read_ue()?;
252
253    Some(PixelFormat::from_chroma_and_depth(
254        chroma_format_idc,
255        bit_depth_luma,
256    ))
257}
258
259fn find_hevc_sps(data: &[u8]) -> Option<&[u8]> {
260    let mut i = 0;
261    while i + 4 < data.len() {
262        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
263            (3, i + 3)
264        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
265            (4, i + 4)
266        } else {
267            i += 1;
268            continue;
269        };
270        if nal_byte + 1 >= data.len() {
271            return None;
272        }
273        // HEVC NAL header is 2 bytes; nal_unit_type is bits 1..7 of byte 0.
274        let nal_unit_type = (data[nal_byte] >> 1) & 0x3F;
275        if nal_unit_type == 33 {
276            // Skip the 2-byte NAL header; RBSP starts after.
277            let start = nal_byte + 2;
278            let end = find_next_start_code(&data[start..])
279                .map(|off| start + off)
280                .unwrap_or(data.len());
281            return Some(&data[start..end]);
282        }
283        i += start_len;
284    }
285    None
286}
287
288fn skip_hevc_profile_tier_level(br: &mut BitReader, max_sub_layers_minus1: usize) -> Option<()> {
289    // general_profile_space(2) + general_tier_flag(1) + general_profile_idc(5)
290    let _ = br.read_bits(8)?;
291    // general_profile_compatibility_flag[32]
292    let _ = br.read_bits(32)?;
293    // general_progressive_source_flag + interlaced + non_packed + frame_only +
294    // 43 reserved + general_inbld/one_picture_only + level_idc
295    let _ = br.read_bits(48)?;
296    let _ = br.read_bits(8)?;
297
298    // Sub-layer flags
299    let mut sub_layer_profile_present = Vec::with_capacity(max_sub_layers_minus1);
300    let mut sub_layer_level_present = Vec::with_capacity(max_sub_layers_minus1);
301    for _ in 0..max_sub_layers_minus1 {
302        sub_layer_profile_present.push(br.read_bits(1)?);
303        sub_layer_level_present.push(br.read_bits(1)?);
304    }
305    if max_sub_layers_minus1 > 0 {
306        // 2 bits reserved × (8 - max_sub_layers_minus1) — spec-mandated padding
307        for _ in max_sub_layers_minus1..8 {
308            let _ = br.read_bits(2)?;
309        }
310    }
311    for i in 0..max_sub_layers_minus1 {
312        if sub_layer_profile_present[i] == 1 {
313            let _ = br.read_bits(8)?;
314            let _ = br.read_bits(32)?;
315            let _ = br.read_bits(48)?;
316        }
317        if sub_layer_level_present[i] == 1 {
318            let _ = br.read_bits(8)?;
319        }
320    }
321    Some(())
322}
323
324// ─── VP9 uncompressed header parser ───────────────────────────────
325// See VP9 bitstream specification §6.2. We only need profile + bit_depth
326// + subsampling_x + subsampling_y from the header.
327fn detect_vp9(sample: &[u8]) -> Option<PixelFormat> {
328    if sample.len() < 2 {
329        return None;
330    }
331    let mut br = BitReader::new(sample);
332    let frame_marker = br.read_bits(2)?;
333    if frame_marker != 2 {
334        return None;
335    }
336    let profile_low = br.read_bits(1)?;
337    let profile_high = br.read_bits(1)?;
338    let profile = (profile_high << 1) | profile_low;
339    if profile == 3 {
340        let _reserved_zero = br.read_bits(1)?;
341    }
342    let show_existing_frame = br.read_bits(1)?;
343    if show_existing_frame == 1 {
344        return None;
345    }
346    let frame_type = br.read_bits(1)?;
347    let _show_frame = br.read_bits(1)?;
348    let _error_resilient = br.read_bits(1)?;
349
350    // color_config only appears on keyframes.
351    if frame_type != 0 {
352        return None;
353    }
354
355    // Keyframe sync code: 3 bytes {0x49, 0x83, 0x42}. 24 bits.
356    let sync = br.read_bits(24)?;
357    if sync != 0x498342 {
358        return None;
359    }
360
361    let bit_depth = if profile >= 2 {
362        if br.read_bits(1)? == 0 { 10 } else { 12 }
363    } else {
364        8
365    };
366    let _color_space = br.read_bits(3)?;
367    // color_range + subsampling — layout depends on color_space
368    // For simplicity: for Profile 0/2 the subsampling is 4:2:0. Profile
369    // 1/3 read subsampling_x/y fields to distinguish 4:2:2 vs 4:4:4.
370    let (sx, sy) = if profile == 1 || profile == 3 {
371        let _color_range = br.read_bits(1)?;
372        let sx = br.read_bits(1)?;
373        let sy = br.read_bits(1)?;
374        (sx, sy)
375    } else {
376        (1, 1) // 4:2:0
377    };
378
379    let chroma_idc = match (sx, sy) {
380        (1, 1) => 1, // 4:2:0
381        (1, 0) => 2, // 4:2:2
382        (0, 0) => 3, // 4:4:4
383        _ => 1,
384    };
385
386    Some(PixelFormat::from_chroma_and_depth(chroma_idc, bit_depth))
387}
388
389// ─── AV1 sequence header parser ───────────────────────────────────
390// See AV1 spec §5.5. Full parse is long; we hop through enough fields
391// to reach color_config. Most AV1 content in the wild is 4:2:0 8-bit
392// (Main profile), and 4:2:0 10-bit for HDR (Main-10).
393fn detect_av1(sample: &[u8]) -> Option<PixelFormat> {
394    // AV1 wraps sequence headers in an OBU with obu_type == 1.
395    let obu = find_av1_obu(sample, 1)?;
396    let mut br = BitReader::new(obu);
397
398    let _seq_profile = br.read_bits(3)?;
399    let _still_picture = br.read_bits(1)?;
400    let reduced_still_picture_header = br.read_bits(1)?;
401
402    if reduced_still_picture_header == 0 {
403        // timing_info_present, decoder_model_info, initial_display_delay,
404        // operating_points — a lot to skip. Abort safely if any read
405        // fails; fallback to Yuv420p.
406        let timing_info_present = br.read_bits(1)?;
407        if timing_info_present == 1 {
408            let _num_units_in_display_tick = br.read_bits(32)?;
409            let _time_scale = br.read_bits(32)?;
410            let equal_picture_interval = br.read_bits(1)?;
411            if equal_picture_interval == 1 {
412                let _num_ticks_per_picture = br.read_ue()?; // uvlc, not ue(v), but reuse
413            }
414            let decoder_model_info_present = br.read_bits(1)?;
415            if decoder_model_info_present == 1 {
416                let _buffer_delay_length_minus_1 = br.read_bits(5)?;
417                let _num_units_in_decoding_tick = br.read_bits(32)?;
418                let _buffer_removal_time_length_minus_1 = br.read_bits(5)?;
419                let _frame_presentation_time_length_minus_1 = br.read_bits(5)?;
420            }
421        }
422        // Bail out to default — the full operating-points loop is long
423        // and rarely worth the maintenance cost vs accepting that
424        // non-trivial AV1 probes return Yuv420p for now. If the MP4
425        // container advertises codec profile in its track box, we can
426        // use that instead (future follow-up if the data shows 10-bit
427        // AV1 slipping through).
428        return Some(PixelFormat::Yuv420p);
429    }
430
431    // Reduced still-picture path is simpler: go straight to
432    // seq_level_idx + bit depth fields.
433    let _seq_level_idx_0 = br.read_bits(5)?;
434
435    // For full correctness we'd continue into color_config. Since the
436    // reduced path is rare for VOD content we take the safe default
437    // and let downstream validation surface anything unexpected.
438    Some(PixelFormat::Yuv420p)
439}
440
441/// Find the first AV1 OBU of the given obu_type. AV1 OBU header:
442///   obu_forbidden_bit(1) | obu_type(4) | obu_extension_flag(1)
443///   | obu_has_size_field(1) | obu_reserved_1bit(1)
444/// followed by an optional 1-byte extension, optional LEB128 size,
445/// then payload. For simplicity we require obu_has_size_field=1 which
446/// all muxed AV1 satisfies.
447fn find_av1_obu(data: &[u8], target_type: u8) -> Option<&[u8]> {
448    find_av1_obu_with_offset(data, target_type).map(|(bytes, _)| bytes)
449}
450
451/// Public re-export so the Vulkan Video decoder can extract the byte
452/// range of an OBU from a demuxed sample.
453pub fn find_av1_obu_with_offset_pub(data: &[u8], target_type: u8) -> Option<(&[u8], usize)> {
454    find_av1_obu_with_offset(data, target_type)
455}
456
457/// Returns the OBU payload slice AND the byte offset at which it
458/// starts inside `data`. The offset is what callers need to translate
459/// an in-OBU bit/byte position (e.g. tile_group start after
460/// byte_alignment()) to an absolute position in the sample buffer.
461fn find_av1_obu_with_offset(data: &[u8], target_type: u8) -> Option<(&[u8], usize)> {
462    let mut i = 0;
463    while i < data.len() {
464        let header = data[i];
465        let obu_type = (header >> 3) & 0x0F;
466        let extension_flag = (header >> 2) & 0x01;
467        let has_size_field = (header >> 1) & 0x01;
468        i += 1;
469        if extension_flag == 1 {
470            i += 1;
471        }
472        if has_size_field == 0 {
473            return None;
474        }
475        let (size, leb_bytes) = read_leb128(&data[i..])?;
476        i += leb_bytes;
477        if obu_type == target_type {
478            let end = (i + size as usize).min(data.len());
479            return Some((&data[i..end], i));
480        }
481        i += size as usize;
482    }
483    None
484}
485
486fn read_leb128(data: &[u8]) -> Option<(u64, usize)> {
487    let mut value = 0u64;
488    for i in 0..8 {
489        if i >= data.len() {
490            return None;
491        }
492        let byte = data[i];
493        value |= ((byte & 0x7F) as u64) << (i * 7);
494        if byte & 0x80 == 0 {
495            return Some((value, i + 1));
496        }
497    }
498    None
499}
500
501// ─── Deep sequence-header parse: width / height extraction ────────
502//
503// The `detect` entry points above stop at chroma_format_idc + bit_depth,
504// which is all they need for pixel-format mapping. MPEG-TS can't carry
505// width/height at the container layer (no sample-entry atom; SPS is the
506// only source), so we need parsers that go deeper — through scaling
507// lists, pic_order_cnt_type branches, and frame cropping — to extract
508// the displayable width/height.
509//
510// Consumers: `container::ts` calls `detect_dims` during demux to populate
511// `StreamInfo.width` / `.height`; the H.264 decoder's chroma-reject sniff
512// (`codec::decode::h264`) uses `parse_h264_sps` to read profile +
513// chroma_format_idc in a single pass instead of a second scan.
514
515/// Parsed H.264 SPS fields relevant to the pipeline.
516///
517/// Populated by `parse_h264_sps` which walks the SPS through the frame
518/// cropping offsets per ITU-T H.264 §7.3.2.1.1 and applies the display
519/// rectangle derivation of §7.4.2.1.1 + Table 6-1 (SubWidthC /
520/// SubHeightC) to produce the post-crop displayable width/height.
521///
522/// Width/height are `Option<u32>` because the full SPS walk can bail on
523/// a malformed scaling list or an exotic `pic_order_cnt_type` branch;
524/// `profile_idc` / `chroma_format_idc` are always populated on a
525/// successful `Some(_)` return since they live in the SPS prefix before
526/// any of the variable-length sections.
527#[derive(Debug, Clone, PartialEq, Eq)]
528pub struct H264SpsInfo {
529    pub profile_idc: u8,
530    /// Packed 8-bit constraint_set_flags (Ch) — constraint_set0..5_flag
531    /// in the high 6 bits, 2 reserved_zero bits. Preserved verbatim for
532    /// Std struct output.
533    pub constraint_set_flags: u8,
534    pub level_idc: u8,
535    pub chroma_format_idc: u8,
536    pub separate_colour_plane_flag: bool,
537    pub bit_depth_luma: u8,
538    pub bit_depth_chroma: u8,
539    pub frame_mbs_only: bool,
540    /// Post-crop width in luma samples, or None if the parse stopped
541    /// before reaching the cropping fields.
542    pub width: Option<u32>,
543    pub height: Option<u32>,
544    // ─── Slice-header branching predicates (filled by full parse) ─
545    /// `log2_max_frame_num_minus4` — slice headers carry
546    /// `frame_num` as `u(log2_max_frame_num_minus4 + 4)` bits.
547    /// `None` if the dims parse bailed before reaching this field.
548    pub log2_max_frame_num_minus4: Option<u8>,
549    /// 0 / 1 / 2 per §7.4.2.1. Controls which POC fields the slice
550    /// header carries.
551    pub pic_order_cnt_type: Option<u8>,
552    /// Valid when `pic_order_cnt_type == 0`. Bit width of
553    /// `pic_order_cnt_lsb` in the slice header: `log2_max_pic_order_cnt_lsb_minus4 + 4`.
554    pub log2_max_pic_order_cnt_lsb_minus4: Option<u8>,
555    /// Valid when `pic_order_cnt_type == 1`. Gates the slice header's
556    /// `delta_pic_order_cnt[0..1]` branch.
557    pub delta_pic_order_always_zero_flag: Option<bool>,
558    // ─── Fields needed to build StdVideoH264SequenceParameterSet ──
559    pub qpprime_y_zero_transform_bypass_flag: Option<bool>,
560    pub seq_scaling_matrix_present_flag: Option<bool>,
561    pub max_num_ref_frames: Option<u8>,
562    pub gaps_in_frame_num_value_allowed_flag: Option<bool>,
563    /// Only meaningful when `!frame_mbs_only`.
564    pub mb_adaptive_frame_field_flag: Option<bool>,
565    pub direct_8x8_inference_flag: Option<bool>,
566    pub frame_cropping_flag: Option<bool>,
567    pub frame_crop_left_offset: Option<u32>,
568    pub frame_crop_right_offset: Option<u32>,
569    pub frame_crop_top_offset: Option<u32>,
570    pub frame_crop_bottom_offset: Option<u32>,
571    /// Valid when `pic_order_cnt_type == 1`.
572    pub offset_for_non_ref_pic: Option<i32>,
573    pub offset_for_top_to_bottom_field: Option<i32>,
574    pub num_ref_frames_in_pic_order_cnt_cycle: Option<u8>,
575    /// Populated only when `pic_order_cnt_type == 1`. Length equals
576    /// `num_ref_frames_in_pic_order_cnt_cycle` (0..=255). Spec allows
577    /// up to 256 entries but no real-world stream exercises the full
578    /// range.
579    pub offset_for_ref_frame: Vec<i32>,
580}
581
582/// Parsed HEVC SPS fields relevant to the pipeline.
583///
584/// Width/height are post-conformance-window (§7.4.3.2.1): per the spec,
585/// output luma dimensions = `pic_width_in_luma_samples - SubWidthC *
586/// (conf_win_left + conf_win_right)` (and analogously for height).
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub struct HevcSpsInfo {
589    pub sps_video_parameter_set_id: u8,
590    pub sps_seq_parameter_set_id: u8,
591    pub sps_max_sub_layers_minus1: u8,
592    pub sps_temporal_id_nesting_flag: bool,
593    pub chroma_format_idc: u8,
594    pub separate_colour_plane_flag: bool,
595    pub bit_depth_luma: u8,
596    pub bit_depth_chroma: u8,
597    pub width: Option<u32>,
598    pub height: Option<u32>,
599    /// Post-conformance-window crop offsets in chroma samples.
600    pub conf_win_left_offset: u32,
601    pub conf_win_right_offset: u32,
602    pub conf_win_top_offset: u32,
603    pub conf_win_bottom_offset: u32,
604    pub log2_max_pic_order_cnt_lsb_minus4: u8,
605    pub log2_min_luma_coding_block_size_minus3: u8,
606    pub log2_diff_max_min_luma_coding_block_size: u8,
607    pub log2_min_luma_transform_block_size_minus2: u8,
608    pub log2_diff_max_min_luma_transform_block_size: u8,
609    pub max_transform_hierarchy_depth_inter: u8,
610    pub max_transform_hierarchy_depth_intra: u8,
611    pub scaling_list_enabled_flag: bool,
612    pub sps_sub_layer_ordering_info_present_flag: bool,
613    pub amp_enabled_flag: bool,
614    pub sample_adaptive_offset_enabled_flag: bool,
615    pub pcm_enabled_flag: bool,
616    /// Only meaningful when pcm_enabled_flag is set; defaults to false.
617    pub pcm_loop_filter_disabled_flag: bool,
618    pub num_short_term_ref_pic_sets: u8,
619    pub long_term_ref_pics_present_flag: bool,
620    pub sps_temporal_mvp_enabled_flag: bool,
621    pub strong_intra_smoothing_enabled_flag: bool,
622    pub profile_idc: u8,
623    pub level_idc: u8,
624    pub tier_flag: bool,
625    /// Sub-layer DPB management triple, one per sub-layer. Index 0..=sps_max_sub_layers_minus1
626    /// are populated; indices above are left at defaults. Vulkan Video
627    /// requires these to be conveyed via `StdVideoH265DecPicBufMgr`.
628    pub max_dec_pic_buffering_minus1: [u8; 7],
629    pub max_num_reorder_pics: [u8; 7],
630    pub max_latency_increase_plus1: [u32; 7],
631    /// `profile_compatibility_flag[32]` — high bit at index 0. Needed
632    /// for the Std PTL struct.
633    pub profile_compatibility_flags: u32,
634}
635
636/// Parsed HEVC VPS — minimum fields needed for StdVideoH265VideoParameterSet.
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
638pub struct H265VpsInfo {
639    pub vps_video_parameter_set_id: u8,
640    pub vps_max_sub_layers_minus1: u8,
641    pub vps_temporal_id_nesting_flag: bool,
642    pub profile_idc: u8,
643    pub level_idc: u8,
644    pub tier_flag: bool,
645}
646
647/// Parsed HEVC PPS.
648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub struct H265PpsInfo {
650    pub pps_pic_parameter_set_id: u8,
651    pub pps_seq_parameter_set_id: u8,
652    pub dependent_slice_segments_enabled_flag: bool,
653    pub output_flag_present_flag: bool,
654    pub num_extra_slice_header_bits: u8,
655    pub sign_data_hiding_enabled_flag: bool,
656    pub cabac_init_present_flag: bool,
657    pub num_ref_idx_l0_default_active_minus1: u8,
658    pub num_ref_idx_l1_default_active_minus1: u8,
659    pub init_qp_minus26: i8,
660    pub constrained_intra_pred_flag: bool,
661    pub transform_skip_enabled_flag: bool,
662    pub cu_qp_delta_enabled_flag: bool,
663    pub diff_cu_qp_delta_depth: u8,
664    pub pps_cb_qp_offset: i8,
665    pub pps_cr_qp_offset: i8,
666    pub pps_slice_chroma_qp_offsets_present_flag: bool,
667    pub weighted_pred_flag: bool,
668    pub weighted_bipred_flag: bool,
669    pub transquant_bypass_enabled_flag: bool,
670    pub tiles_enabled_flag: bool,
671    pub entropy_coding_sync_enabled_flag: bool,
672    // Tile layout (§7.3.2.3) — only meaningful when tiles_enabled_flag.
673    // Defaults below model a 1×1 uniform tile spanning the frame.
674    pub num_tile_columns_minus1: u8,
675    pub num_tile_rows_minus1: u8,
676    pub uniform_spacing_flag: bool,
677    pub loop_filter_across_tiles_enabled_flag: bool,
678    // Slice / deblocking / merge controls
679    pub pps_loop_filter_across_slices_enabled_flag: bool,
680    pub deblocking_filter_control_present_flag: bool,
681    pub deblocking_filter_override_enabled_flag: bool,
682    pub pps_deblocking_filter_disabled_flag: bool,
683    pub pps_beta_offset_div2: i8,
684    pub pps_tc_offset_div2: i8,
685    pub pps_scaling_list_data_present_flag: bool,
686    pub lists_modification_present_flag: bool,
687    pub log2_parallel_merge_level_minus2: u8,
688    pub slice_segment_header_extension_present_flag: bool,
689    pub pps_extension_present_flag: bool,
690}
691
692/// HEVC slice header — subset needed for StdVideoDecodeH265PictureInfo.
693#[derive(Debug, Clone, Copy, PartialEq, Eq)]
694pub struct H265SliceHeader {
695    pub first_slice_segment_in_pic_flag: bool,
696    pub nal_unit_type: u8,
697    pub slice_pic_parameter_set_id: u8,
698    pub slice_type: H265SliceType,
699    pub pic_order_cnt_lsb: u32,
700    pub short_term_ref_pic_set_sps_flag: bool,
701    pub short_term_ref_pic_set_idx: Option<u8>,
702    /// True for IRAP pictures (IDR / CRA / BLA): nal_unit_type ∈ 16..=23.
703    pub is_irap: bool,
704    /// True for IDR specifically: nal_unit_type ∈ 19..=20.
705    pub is_idr: bool,
706}
707
708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub enum H265SliceType {
710    B,
711    P,
712    I,
713}
714
715impl H265SliceType {
716    fn from_ue(v: u32) -> Option<Self> {
717        match v {
718            0 => Some(Self::B),
719            1 => Some(Self::P),
720            2 => Some(Self::I),
721            _ => None,
722        }
723    }
724}
725
726/// Parsed AV1 sequence header fields (from OBU type 1, §5.5.2).
727/// Minimum subset needed to build `StdVideoAV1SequenceHeader` for
728/// Vulkan AV1 decode session parameters.
729#[derive(Debug, Clone, Copy, PartialEq, Eq)]
730pub struct Av1SequenceHeader {
731    pub seq_profile: u8,
732    pub still_picture: bool,
733    pub reduced_still_picture_header: bool,
734    pub max_frame_width_minus1: u32,
735    pub max_frame_height_minus1: u32,
736    pub seq_level_idx_0: u8,
737    /// `seq_tier[0]` from AV1 §5.5.1. Only carried in the bitstream
738    /// when `seq_level_idx_0 > 7` (i.e. level >= 4.0); below that the
739    /// spec says tier is implicitly 0 (Main). 0 = Main, 1 = High.
740    /// Required for the AV1 ISOBMFF codec string `av01.P.LLT.DD...`
741    /// (the `T` character).
742    pub seq_tier_0: u8,
743    pub bit_depth: u8,
744    pub monochrome: bool,
745    pub color_primaries: u8,
746    pub transfer_characteristics: u8,
747    pub matrix_coefficients: u8,
748    pub color_range: bool,
749    pub chroma_subsampling_x: bool,
750    pub chroma_subsampling_y: bool,
751    pub film_grain_params_present: bool,
752    pub enable_filter_intra: bool,
753    pub enable_intra_edge_filter: bool,
754    pub enable_interintra_compound: bool,
755    pub enable_masked_compound: bool,
756    pub enable_warped_motion: bool,
757    pub enable_dual_filter: bool,
758    pub enable_order_hint: bool,
759    pub enable_jnt_comp: bool,
760    pub enable_ref_frame_mvs: bool,
761    pub enable_superres: bool,
762    pub enable_cdef: bool,
763    pub enable_restoration: bool,
764    pub order_hint_bits: u8,
765    /// Per AV1 §5.5.1: 0 = all frames block screen-content tools,
766    /// 1 = all frames enable them, 2 = SELECT (each frame signals
767    /// its own bit in the uncompressed_header). Our frame-header
768    /// parser reads a per-frame bit only when this field == 2.
769    pub seq_force_screen_content_tools: u8,
770    /// 0 = all frames force non-integer MV, 1 = all force integer,
771    /// 2 = SELECT. Only relevant when screen-content tools allow.
772    pub seq_force_integer_mv: u8,
773    /// Bit-width of max_frame_width_minus_1 / max_frame_height_minus_1
774    /// fields in the sequence header. Vulkan's Std SPS requires these
775    /// to match so the session parameters object is byte-compatible
776    /// with what the driver re-parses from the bitstream.
777    pub frame_width_bits_minus_1: u8,
778    pub frame_height_bits_minus_1: u8,
779    pub use_128x128_superblock: bool,
780    /// AV1 §5.5.2 color_config bit — signals that U and V planes
781    /// carry separate q-delta values. Feeds
782    /// `StdVideoAV1ColorConfigFlags.separate_uv_delta_q` which the
783    /// Vulkan AV1 decoder reads at session-parameters creation.
784    pub separate_uv_delta_q: bool,
785}
786
787/// Parsed AV1 frame header — full §5.9.1 uncompressed_header parse.
788/// Provides everything needed to populate `StdVideoDecodeAV1PictureInfo`
789/// + its 7 sub-struct pointers for a Vulkan Video AV1 decode submit.
790/// Vec fields (tile MI-unit arrays) forced the drop of `Copy`.
791#[derive(Debug, Clone)]
792pub struct Av1FrameHeader {
793    pub show_frame: bool,
794    pub showable_frame: bool,
795    pub frame_type: Av1FrameType,
796    pub error_resilient_mode: bool,
797    pub disable_cdf_update: bool,
798    pub allow_screen_content_tools: bool,
799    pub force_integer_mv: bool,
800    pub order_hint: u32,
801    pub primary_ref_frame: u8,
802    pub refresh_frame_flags: u8,
803    pub frame_width: u32,
804    pub frame_height: u32,
805    pub render_width: u32,
806    pub render_height: u32,
807    pub use_ref_frame_mvs: bool,
808    pub allow_high_precision_mv: bool,
809    pub is_filter_switchable: bool,
810    pub disable_frame_end_update_cdf: bool,
811    pub allow_warped_motion: bool,
812    pub reduced_tx_set: bool,
813    // ─── Extended fields (full §5.9.1 parse) ────────────────────
814    pub allow_intrabc: bool,
815    pub frame_size_override_flag: bool,
816    pub use_superres: bool,
817    pub is_motion_mode_switchable: bool,
818    pub reference_select: bool,
819    pub skip_mode_present: bool,
820    // Tile info (§5.9.15) — derived MI-unit arrays feed
821    // `StdVideoAV1TileInfo.pMi{Col,Row}Starts` etc.
822    pub tile_cols: u8,
823    pub tile_rows: u8,
824    pub uniform_tile_spacing_flag: bool,
825    pub tile_cols_log2: u8,
826    pub tile_rows_log2: u8,
827    pub mi_col_starts: Vec<u16>,         // len = tile_cols + 1
828    pub mi_row_starts: Vec<u16>,         // len = tile_rows + 1
829    pub width_in_sbs_minus_1: Vec<u16>,  // len = tile_cols
830    pub height_in_sbs_minus_1: Vec<u16>, // len = tile_rows
831    pub context_update_tile_id: u16,
832    pub tile_size_bytes_minus_1: u8,
833    // Quantization (§5.9.12)
834    pub base_q_idx: u8,
835    pub delta_q_y_dc: i8,
836    pub delta_q_u_dc: i8,
837    pub delta_q_u_ac: i8,
838    pub delta_q_v_dc: i8,
839    pub delta_q_v_ac: i8,
840    pub using_qmatrix: bool,
841    pub qm_y: u8,
842    pub qm_u: u8,
843    pub qm_v: u8,
844    // Delta-Q / delta-LF (§5.9.17 / §5.9.18)
845    pub delta_q_present: bool,
846    pub delta_q_res: u8,
847    pub delta_lf_present: bool,
848    pub delta_lf_res: u8,
849    pub delta_lf_multi: bool,
850    // Segmentation (§5.9.14) — scaffolded as "disabled" for the
851    // Vulkan scope; real feature arrays populated when
852    // segmentation_enabled is 1.
853    pub segmentation_enabled: bool,
854    pub segmentation_update_map: bool,
855    pub segmentation_temporal_update: bool,
856    pub segmentation_update_data: bool,
857    pub feature_enabled: [[bool; 8]; 8],
858    pub feature_data: [[i16; 8]; 8],
859    // Loop filter (§5.9.11)
860    pub loop_filter_level: [u8; 4],
861    pub loop_filter_sharpness: u8,
862    pub loop_filter_delta_enabled: bool,
863    pub loop_filter_delta_update: bool,
864    pub update_ref_delta_mask: u8, // 8 bits
865    pub loop_filter_ref_deltas: [i8; 8],
866    pub update_mode_delta_mask: u8, // 2 bits (modes 0..=1)
867    pub loop_filter_mode_deltas: [i8; 2],
868    // CDEF (§5.9.19)
869    pub cdef_damping_minus_3: u8,
870    pub cdef_bits: u8,
871    pub cdef_y_pri_strength: [u8; 8],
872    pub cdef_y_sec_strength: [u8; 8],
873    pub cdef_uv_pri_strength: [u8; 8],
874    pub cdef_uv_sec_strength: [u8; 8],
875    // Loop restoration (§5.9.20)
876    pub lr_type: [u8; 3], // per-plane: 0=None, 1=Wiener, 2=SGrproj, 3=Switchable
877    pub lr_unit_shift: u8,
878    pub lr_uv_shift: u8,
879    // TX mode (§5.9.22) — 0=ONLY_4X4, 1=LARGEST, 2=SELECT
880    pub tx_mode: u8,
881    pub interpolation_filter: u8,
882    // Byte offset from the start of the OBU payload (NOT from the
883    // start of the sample buffer) at which tile_group data begins.
884    // For a Frame OBU (type 6) this is after uncompressed_header +
885    // byte_alignment. For a pair of separate frame_header + tile_group
886    // OBUs (types 3 and 4), the caller looks up the type 4 OBU's
887    // payload start directly and ignores this value.
888    pub tile_group_offset_in_obu: u32,
889    // Coded lossless flag (derived from q-idx 0 + deltas all zero)
890    pub coded_lossless: bool,
891}
892
893impl Default for Av1FrameHeader {
894    fn default() -> Self {
895        Self {
896            show_frame: false,
897            showable_frame: false,
898            frame_type: Av1FrameType::Key,
899            error_resilient_mode: false,
900            disable_cdf_update: false,
901            allow_screen_content_tools: false,
902            force_integer_mv: false,
903            order_hint: 0,
904            primary_ref_frame: 7,
905            refresh_frame_flags: 0,
906            frame_width: 0,
907            frame_height: 0,
908            render_width: 0,
909            render_height: 0,
910            use_ref_frame_mvs: false,
911            allow_high_precision_mv: false,
912            is_filter_switchable: false,
913            disable_frame_end_update_cdf: false,
914            allow_warped_motion: false,
915            reduced_tx_set: false,
916            allow_intrabc: false,
917            frame_size_override_flag: false,
918            use_superres: false,
919            is_motion_mode_switchable: false,
920            reference_select: false,
921            skip_mode_present: false,
922            tile_cols: 1,
923            tile_rows: 1,
924            uniform_tile_spacing_flag: true,
925            tile_cols_log2: 0,
926            tile_rows_log2: 0,
927            mi_col_starts: Vec::new(),
928            mi_row_starts: Vec::new(),
929            width_in_sbs_minus_1: Vec::new(),
930            height_in_sbs_minus_1: Vec::new(),
931            context_update_tile_id: 0,
932            tile_size_bytes_minus_1: 3,
933            base_q_idx: 0,
934            delta_q_y_dc: 0,
935            delta_q_u_dc: 0,
936            delta_q_u_ac: 0,
937            delta_q_v_dc: 0,
938            delta_q_v_ac: 0,
939            using_qmatrix: false,
940            qm_y: 0,
941            qm_u: 0,
942            qm_v: 0,
943            delta_q_present: false,
944            delta_q_res: 0,
945            delta_lf_present: false,
946            delta_lf_res: 0,
947            delta_lf_multi: false,
948            segmentation_enabled: false,
949            segmentation_update_map: false,
950            segmentation_temporal_update: false,
951            segmentation_update_data: false,
952            feature_enabled: [[false; 8]; 8],
953            feature_data: [[0; 8]; 8],
954            loop_filter_level: [0; 4],
955            loop_filter_sharpness: 0,
956            loop_filter_delta_enabled: false,
957            loop_filter_delta_update: false,
958            update_ref_delta_mask: 0,
959            loop_filter_ref_deltas: [0; 8],
960            update_mode_delta_mask: 0,
961            loop_filter_mode_deltas: [0; 2],
962            cdef_damping_minus_3: 0,
963            cdef_bits: 0,
964            cdef_y_pri_strength: [0; 8],
965            cdef_y_sec_strength: [0; 8],
966            cdef_uv_pri_strength: [0; 8],
967            cdef_uv_sec_strength: [0; 8],
968            lr_type: [0; 3],
969            lr_unit_shift: 0,
970            lr_uv_shift: 0,
971            tx_mode: 0,
972            interpolation_filter: 0,
973            tile_group_offset_in_obu: 0,
974            coded_lossless: false,
975        }
976    }
977}
978
979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
980pub enum Av1FrameType {
981    Key,
982    Inter,
983    IntraOnly,
984    Switch,
985}
986
987/// Parse the AV1 sequence header OBU (obu_type=1). Returns the
988/// subset of §5.5.2 fields needed for Vulkan decode-session-params.
989/// Partial parse: we stop after color_config + film_grain_params_present
990/// (everything Vulkan's StdVideoAV1SequenceHeader cares about).
991pub fn parse_av1_sequence_header(sample: &[u8]) -> Option<Av1SequenceHeader> {
992    let obu = find_av1_obu(sample, 1)?;
993    let mut br = BitReader::new(obu);
994    let seq_profile = br.read_bits(3)? as u8;
995    let still_picture = br.read_bits(1)? == 1;
996    let reduced_still_picture_header = br.read_bits(1)? == 1;
997
998    let mut seq_level_idx_0 = 0u8;
999    let mut seq_tier_0 = 0u8;
1000    let (_operating_points_cnt_minus_1, _timing_info_present_flag);
1001    let mut order_hint_bits = 0u8;
1002    let mut enable_order_hint = false;
1003
1004    if reduced_still_picture_header {
1005        seq_level_idx_0 = br.read_bits(5)? as u8;
1006        _operating_points_cnt_minus_1 = 0;
1007        _timing_info_present_flag = false;
1008    } else {
1009        let timing_info_present_flag = br.read_bits(1)? == 1;
1010        _timing_info_present_flag = timing_info_present_flag;
1011        let mut decoder_model_info_present_flag = false;
1012        let mut buffer_delay_length_minus_1 = 0u32;
1013        if timing_info_present_flag {
1014            let _num_units_in_display_tick = br.read_bits(32)?;
1015            let _time_scale = br.read_bits(32)?;
1016            let equal_picture_interval = br.read_bits(1)? == 1;
1017            if equal_picture_interval {
1018                let _num_ticks_per_picture_minus_1 = read_av1_uvlc(&mut br)?;
1019            }
1020            decoder_model_info_present_flag = br.read_bits(1)? == 1;
1021            if decoder_model_info_present_flag {
1022                buffer_delay_length_minus_1 = br.read_bits(5)?;
1023                let _num_units_in_decoding_tick = br.read_bits(32)?;
1024                let _buffer_removal_time_length_minus_1 = br.read_bits(5)?;
1025                let _frame_presentation_time_length_minus_1 = br.read_bits(5)?;
1026            }
1027        }
1028        // initial_display_delay_present_flag lives OUTSIDE the
1029        // timing-info-present branch per AV1 §5.5.1 — my earlier
1030        // parse had it nested, which desynced every field that
1031        // followed on streams with timing_info absent.
1032        let initial_display_delay_present_flag = br.read_bits(1)? == 1;
1033        let operating_points_cnt_minus_1 = br.read_bits(5)? as u8;
1034        _operating_points_cnt_minus_1 = operating_points_cnt_minus_1;
1035        for i in 0..=operating_points_cnt_minus_1 {
1036            let _operating_point_idc = br.read_bits(12)?;
1037            let seq_level_idx_i = br.read_bits(5)? as u8;
1038            // Per AV1 §5.5.1, seq_tier is present only for levels
1039            // >= 4.0 (level_idx > 7); below that it's implicitly 0.
1040            let seq_tier_i = if seq_level_idx_i > 7 {
1041                br.read_bits(1)? as u8
1042            } else {
1043                0
1044            };
1045            if i == 0 {
1046                seq_level_idx_0 = seq_level_idx_i;
1047                seq_tier_0 = seq_tier_i;
1048            }
1049            // operating_parameters_info(i) — one per-op-point
1050            // decoder_model_present_for_this_op gate.
1051            if decoder_model_info_present_flag {
1052                let decoder_model_present_for_this_op = br.read_bits(1)? == 1;
1053                if decoder_model_present_for_this_op {
1054                    let n = (buffer_delay_length_minus_1 + 1) as usize;
1055                    let _buffer_delay = br.read_bits(n)?;
1056                    let _encoder_buffer_delay = br.read_bits(n)?;
1057                    let _low_delay_mode_flag = br.read_bits(1)?;
1058                }
1059            }
1060            if initial_display_delay_present_flag {
1061                let idd_present_for_this_op = br.read_bits(1)? == 1;
1062                if idd_present_for_this_op {
1063                    let _initial_display_delay_minus_1 = br.read_bits(4)?;
1064                }
1065            }
1066        }
1067    }
1068    let frame_width_bits_minus_1 = br.read_bits(4)? as usize;
1069    let frame_height_bits_minus_1 = br.read_bits(4)? as usize;
1070    let max_frame_width_minus1 = br.read_bits(frame_width_bits_minus_1 + 1)?;
1071    let max_frame_height_minus1 = br.read_bits(frame_height_bits_minus_1 + 1)?;
1072
1073    let frame_id_numbers_present_flag = if reduced_still_picture_header {
1074        false
1075    } else {
1076        br.read_bits(1)? == 1
1077    };
1078    if frame_id_numbers_present_flag {
1079        let _delta_frame_id_length_minus_2 = br.read_bits(4)?;
1080        let _additional_frame_id_length_minus_1 = br.read_bits(3)?;
1081    }
1082    let use_128x128_superblock = br.read_bits(1)? == 1;
1083    let enable_filter_intra = br.read_bits(1)? == 1;
1084    let enable_intra_edge_filter = br.read_bits(1)? == 1;
1085    let mut enable_interintra_compound = false;
1086    let mut enable_masked_compound = false;
1087    let mut enable_warped_motion = false;
1088    let mut enable_dual_filter = false;
1089    let mut enable_jnt_comp = false;
1090    let mut enable_ref_frame_mvs = false;
1091    let mut seq_force_screen_content_tools: u8 = 2; // SELECT when reduced_still_picture_header
1092    let mut seq_force_integer_mv: u8 = 2;
1093    if !reduced_still_picture_header {
1094        enable_interintra_compound = br.read_bits(1)? == 1;
1095        enable_masked_compound = br.read_bits(1)? == 1;
1096        enable_warped_motion = br.read_bits(1)? == 1;
1097        enable_dual_filter = br.read_bits(1)? == 1;
1098        enable_order_hint = br.read_bits(1)? == 1;
1099        if enable_order_hint {
1100            enable_jnt_comp = br.read_bits(1)? == 1;
1101            enable_ref_frame_mvs = br.read_bits(1)? == 1;
1102        }
1103        let seq_choose_screen_content_tools = br.read_bits(1)? == 1;
1104        seq_force_screen_content_tools = if seq_choose_screen_content_tools {
1105            2u8
1106        } else {
1107            br.read_bits(1)? as u8
1108        };
1109        if seq_force_screen_content_tools > 0 {
1110            let seq_choose_integer_mv = br.read_bits(1)? == 1;
1111            seq_force_integer_mv = if seq_choose_integer_mv {
1112                2u8
1113            } else {
1114                br.read_bits(1)? as u8
1115            };
1116        }
1117        if enable_order_hint {
1118            order_hint_bits = br.read_bits(3)? as u8 + 1;
1119        }
1120    }
1121    let enable_superres = br.read_bits(1)? == 1;
1122    let enable_cdef = br.read_bits(1)? == 1;
1123    let enable_restoration = br.read_bits(1)? == 1;
1124
1125    // color_config(seq_profile)
1126    let high_bitdepth = br.read_bits(1)? == 1;
1127    let bit_depth = if seq_profile == 2 && high_bitdepth {
1128        if br.read_bits(1)? == 1 { 12 } else { 10 }
1129    } else if high_bitdepth {
1130        10
1131    } else {
1132        8
1133    };
1134    let monochrome = if seq_profile == 1 {
1135        false
1136    } else {
1137        br.read_bits(1)? == 1
1138    };
1139    let color_description_present_flag = br.read_bits(1)? == 1;
1140    let (color_primaries, transfer_characteristics, matrix_coefficients) =
1141        if color_description_present_flag {
1142            (
1143                br.read_bits(8)? as u8,
1144                br.read_bits(8)? as u8,
1145                br.read_bits(8)? as u8,
1146            )
1147        } else {
1148            (2u8, 2u8, 2u8) // unspecified
1149        };
1150    let color_range;
1151    let (subx, suby);
1152    let mut separate_uv_delta_q = false;
1153    if monochrome {
1154        color_range = br.read_bits(1)? == 1;
1155        subx = true;
1156        suby = true;
1157    } else if color_primaries == 1 && transfer_characteristics == 13 && matrix_coefficients == 0 {
1158        color_range = true;
1159        subx = false;
1160        suby = false;
1161    } else {
1162        color_range = br.read_bits(1)? == 1;
1163        match seq_profile {
1164            0 => {
1165                subx = true;
1166                suby = true;
1167            }
1168            1 => {
1169                subx = false;
1170                suby = false;
1171            }
1172            2 => {
1173                if bit_depth == 12 {
1174                    subx = br.read_bits(1)? == 1;
1175                    suby = if subx { br.read_bits(1)? == 1 } else { false };
1176                } else {
1177                    subx = true;
1178                    suby = false;
1179                }
1180            }
1181            _ => {
1182                subx = true;
1183                suby = true;
1184            }
1185        }
1186        if subx && suby {
1187            let _chroma_sample_position = br.read_bits(2)?;
1188        }
1189        separate_uv_delta_q = br.read_bits(1)? == 1;
1190    }
1191    let film_grain_params_present = br.read_bits(1)? == 1;
1192
1193    Some(Av1SequenceHeader {
1194        seq_profile,
1195        still_picture,
1196        reduced_still_picture_header,
1197        max_frame_width_minus1,
1198        max_frame_height_minus1,
1199        seq_level_idx_0,
1200        seq_tier_0,
1201        bit_depth,
1202        monochrome,
1203        color_primaries,
1204        transfer_characteristics,
1205        matrix_coefficients,
1206        color_range,
1207        chroma_subsampling_x: subx,
1208        chroma_subsampling_y: suby,
1209        film_grain_params_present,
1210        enable_filter_intra,
1211        enable_intra_edge_filter,
1212        enable_interintra_compound,
1213        enable_masked_compound,
1214        enable_warped_motion,
1215        enable_dual_filter,
1216        enable_order_hint,
1217        enable_jnt_comp,
1218        enable_ref_frame_mvs,
1219        enable_superres,
1220        enable_cdef,
1221        enable_restoration,
1222        order_hint_bits,
1223        seq_force_screen_content_tools,
1224        seq_force_integer_mv,
1225        frame_width_bits_minus_1: frame_width_bits_minus_1 as u8,
1226        frame_height_bits_minus_1: frame_height_bits_minus_1 as u8,
1227        use_128x128_superblock,
1228        separate_uv_delta_q,
1229    })
1230}
1231
1232/// Parse an AV1 frame_header_obu (or the frame_header part of a
1233/// frame_obu) from the given sample. Requires the sequence header
1234/// for branch predicates (order_hint_bits, enable flags).
1235///
1236/// Returns an `Av1FrameHeader` with just enough fields populated for
1237/// Vulkan Video decode to build `StdVideoDecodeAV1PictureInfo` +
1238/// sub-structs. Does NOT fully parse the bitstream (skips large
1239/// parts of the uncompressed_header — tile_info, segmentation,
1240/// global motion, etc. — that can be defaulted for key frames).
1241///
1242/// Per AV1 spec §5.9.1 — complex, branching parse. Only handles
1243/// single-tile key frames at first; inter frames need more work
1244/// on ref_frame_idx + delta_frame_id resolution.
1245pub fn parse_av1_frame_header(sample: &[u8], seq: &Av1SequenceHeader) -> Option<Av1FrameHeader> {
1246    let obu_bytes = find_av1_obu(sample, 3).or_else(|| find_av1_obu(sample, 6))?;
1247    let mut br = BitReader::new(obu_bytes);
1248    let mut h = Av1FrameHeader::default();
1249
1250    // ─── Phase 1: frame-level flags ────────────────────────────
1251    if seq.reduced_still_picture_header {
1252        h.frame_type = Av1FrameType::Key;
1253        h.show_frame = true;
1254        h.showable_frame = false;
1255        h.error_resilient_mode = true;
1256    } else {
1257        let show_existing_frame = br.read_bits(1)? == 1;
1258        if show_existing_frame {
1259            // Early-out: a show-existing-frame OBU is a thin pointer
1260            // to a previously-decoded DPB slot. No new bitstream to
1261            // decode, no uncompressed_header payload past this point.
1262            // Return a minimal header marked with show_frame=true so
1263            // callers know to skip bitstream decode.
1264            let _frame_to_show_map_idx = br.read_bits(3)?;
1265            h.show_frame = true;
1266            h.showable_frame = true;
1267            h.frame_type = Av1FrameType::Key;
1268            h.frame_width = seq.max_frame_width_minus1 + 1;
1269            h.frame_height = seq.max_frame_height_minus1 + 1;
1270            h.render_width = h.frame_width;
1271            h.render_height = h.frame_height;
1272            return Some(h);
1273        }
1274        let ft_code = br.read_bits(2)?;
1275        h.frame_type = match ft_code {
1276            0 => Av1FrameType::Key,
1277            1 => Av1FrameType::Inter,
1278            2 => Av1FrameType::IntraOnly,
1279            3 => Av1FrameType::Switch,
1280            _ => return None,
1281        };
1282        h.show_frame = br.read_bits(1)? == 1;
1283        h.showable_frame = if h.show_frame {
1284            !matches!(h.frame_type, Av1FrameType::Key)
1285        } else {
1286            br.read_bits(1)? == 1
1287        };
1288        let is_key = matches!(h.frame_type, Av1FrameType::Key);
1289        let is_switch = matches!(h.frame_type, Av1FrameType::Switch);
1290        h.error_resilient_mode = if is_switch || (is_key && h.show_frame) {
1291            true
1292        } else {
1293            br.read_bits(1)? == 1
1294        };
1295    }
1296
1297    let frame_is_intra = matches!(h.frame_type, Av1FrameType::Key | Av1FrameType::IntraOnly);
1298
1299    h.disable_cdf_update = br.read_bits(1)? == 1;
1300    // Per AV1 §5.9.1 — when seq_force_screen_content_tools == SELECT (2),
1301    // each frame signals its own bit; otherwise the seq-level force
1302    // fully determines the frame-level value.
1303    h.allow_screen_content_tools = if seq.seq_force_screen_content_tools == 2 {
1304        br.read_bits(1)? == 1
1305    } else {
1306        seq.seq_force_screen_content_tools == 1
1307    };
1308    if h.allow_screen_content_tools {
1309        h.force_integer_mv = if seq.seq_force_integer_mv == 2 {
1310            br.read_bits(1)? == 1
1311        } else {
1312            seq.seq_force_integer_mv == 1
1313        };
1314    } else {
1315        h.force_integer_mv = false;
1316    }
1317    if frame_is_intra {
1318        h.force_integer_mv = true;
1319    }
1320
1321    // frame_size_override_flag
1322    let is_switch = matches!(h.frame_type, Av1FrameType::Switch);
1323    h.frame_size_override_flag = if is_switch {
1324        true
1325    } else if seq.reduced_still_picture_header {
1326        false
1327    } else {
1328        br.read_bits(1)? == 1
1329    };
1330
1331    // order_hint
1332    if seq.enable_order_hint && seq.order_hint_bits > 0 {
1333        h.order_hint = br.read_bits(seq.order_hint_bits as usize)?;
1334    }
1335
1336    // primary_ref_frame (only for non-intra, non-error-resilient)
1337    h.primary_ref_frame = if frame_is_intra || h.error_resilient_mode {
1338        7 // PRIMARY_REF_NONE
1339    } else {
1340        br.read_bits(3)? as u8
1341    };
1342
1343    // refresh_frame_flags
1344    let all_frames = 0xFFu8;
1345    h.refresh_frame_flags = if matches!(h.frame_type, Av1FrameType::Key) && h.show_frame {
1346        all_frames
1347    } else if is_switch {
1348        all_frames
1349    } else {
1350        br.read_bits(8)? as u8
1351    };
1352
1353    // ─── Phase 2: size / render size / ref frames ──────────────
1354    let (frame_width, frame_height) = if frame_is_intra {
1355        let (w, h2) = parse_av1_frame_size(&mut br, seq, h.frame_size_override_flag)?;
1356        // superres_params() is INSIDE frame_size() per §5.9.5 /
1357        // §5.9.6 — before render_size().
1358        h.use_superres = if seq.enable_superres {
1359            br.read_bits(1)? == 1
1360        } else {
1361            false
1362        };
1363        if h.use_superres {
1364            let _superres_denom_minus9 = br.read_bits(3)?;
1365        }
1366        parse_av1_render_size(&mut br, w, h2, &mut h.render_width, &mut h.render_height)?;
1367        if h.allow_screen_content_tools
1368        /* && UpscaledWidth == FrameWidth */
1369        {
1370            h.allow_intrabc = br.read_bits(1)? == 1;
1371        }
1372        (w, h2)
1373    } else {
1374        // Inter-frame path: ref_frame_idx[], frame_size_with_refs,
1375        // interpolation_filter, is_motion_mode_switchable,
1376        // use_ref_frame_mvs. For our key-frame-focused scope, this
1377        // branch ISN'T the critical path — but we still read bits
1378        // to keep the parser position in sync.
1379        let frame_refs_short_signaling = if seq.enable_order_hint {
1380            br.read_bits(1)? == 1
1381        } else {
1382            false
1383        };
1384        if frame_refs_short_signaling {
1385            let _last_frame_idx = br.read_bits(3)?;
1386            let _gold_frame_idx = br.read_bits(3)?;
1387        }
1388        for _ in 0..7u8
1389        /* REFS_PER_FRAME */
1390        {
1391            if !frame_refs_short_signaling {
1392                let _ref_frame_idx = br.read_bits(3)?;
1393            }
1394            // frame_id_numbers_present_flag is false in our minimal
1395            // seq, so no delta_frame_id read.
1396        }
1397        let (w, h2) = if h.frame_size_override_flag && !h.error_resilient_mode {
1398            parse_av1_frame_size_with_refs(&mut br, seq)?
1399        } else {
1400            let (w, h2) = parse_av1_frame_size(&mut br, seq, h.frame_size_override_flag)?;
1401            // superres_params() inside frame_size() per spec.
1402            h.use_superres = if seq.enable_superres {
1403                br.read_bits(1)? == 1
1404            } else {
1405                false
1406            };
1407            if h.use_superres {
1408                let _superres_denom_minus9 = br.read_bits(3)?;
1409            }
1410            parse_av1_render_size(&mut br, w, h2, &mut h.render_width, &mut h.render_height)?;
1411            (w, h2)
1412        };
1413        h.allow_high_precision_mv = if h.force_integer_mv {
1414            false
1415        } else {
1416            br.read_bits(1)? == 1
1417        };
1418        // read_interpolation_filter (§5.9.10)
1419        h.is_filter_switchable = br.read_bits(1)? == 1;
1420        h.interpolation_filter = if h.is_filter_switchable {
1421            4 // SWITCHABLE
1422        } else {
1423            br.read_bits(2)? as u8
1424        };
1425        h.is_motion_mode_switchable = br.read_bits(1)? == 1;
1426        h.use_ref_frame_mvs = if h.error_resilient_mode || !seq.enable_ref_frame_mvs {
1427            false
1428        } else {
1429            br.read_bits(1)? == 1
1430        };
1431        (w, h2)
1432    };
1433    h.frame_width = frame_width;
1434    h.frame_height = frame_height;
1435    if h.render_width == 0 {
1436        h.render_width = frame_width;
1437    }
1438    if h.render_height == 0 {
1439        h.render_height = frame_height;
1440    }
1441
1442    h.disable_frame_end_update_cdf = if seq.reduced_still_picture_header {
1443        true
1444    } else {
1445        br.read_bits(1)? == 1
1446    };
1447
1448    // ─── Phase 5: tile_info() (§5.9.15) ────────────────────────
1449    // MI (mode-info) units = 4 luma samples. SB (superblock) size in
1450    // MI units = 16 (64x64 SB) or 32 (128x128 SB). Our seq parser
1451    // doesn't capture `use_128x128_superblock` yet; default to 16 MI
1452    // per SB — the common case for current AV1 streams.
1453    let sb_size_log2: u32 = 4; // log2(16)
1454    let mi_cols_raw = 2 * ((frame_width.saturating_sub(1) + 8) >> 3);
1455    let mi_rows_raw = 2 * ((frame_height.saturating_sub(1) + 8) >> 3);
1456    // Align MI dims to SB boundaries for tile-spacing math.
1457    let sb_cols = (mi_cols_raw + (1 << sb_size_log2) - 1) >> sb_size_log2;
1458    let sb_rows = (mi_rows_raw + (1 << sb_size_log2) - 1) >> sb_size_log2;
1459    parse_av1_tile_info(
1460        &mut br,
1461        &mut h,
1462        sb_cols,
1463        sb_rows,
1464        sb_size_log2,
1465        mi_cols_raw,
1466        mi_rows_raw,
1467    )?;
1468
1469    // ─── Phase 6: quantization_params() (§5.9.12) ──────────────
1470    parse_av1_quantization_params(&mut br, &mut h, seq)?;
1471
1472    // ─── Phase 7: segmentation_params() (§5.9.14) ──────────────
1473    parse_av1_segmentation_params(&mut br, &mut h)?;
1474
1475    // ─── Phase 8: delta_q_params / delta_lf_params ─────────────
1476    h.delta_q_present = if h.base_q_idx > 0 {
1477        br.read_bits(1)? == 1
1478    } else {
1479        false
1480    };
1481    h.delta_q_res = if h.delta_q_present {
1482        br.read_bits(2)? as u8
1483    } else {
1484        0
1485    };
1486    h.delta_lf_present = if h.delta_q_present && !h.allow_intrabc {
1487        br.read_bits(1)? == 1
1488    } else {
1489        false
1490    };
1491    if h.delta_lf_present {
1492        h.delta_lf_res = br.read_bits(2)? as u8;
1493        h.delta_lf_multi = br.read_bits(1)? == 1;
1494    }
1495
1496    // ─── Compute CodedLossless (§5.9.1) ─────────────────────────
1497    // lossless requires base_q_idx=0 and ALL delta-q values == 0.
1498    // We don't iterate segment features for per-seg q deltas here;
1499    // coded_lossless only affects the later cdef_params gate and
1500    // tx_mode coding (both set to 0 when lossless).
1501    h.coded_lossless = h.base_q_idx == 0
1502        && h.delta_q_y_dc == 0
1503        && h.delta_q_u_dc == 0
1504        && h.delta_q_u_ac == 0
1505        && h.delta_q_v_dc == 0
1506        && h.delta_q_v_ac == 0;
1507
1508    // ─── Phase 9: loop_filter_params() (§5.9.11) ───────────────
1509    parse_av1_loop_filter_params(&mut br, &mut h, frame_is_intra)?;
1510
1511    // ─── Phase 10: cdef_params() (§5.9.19) ─────────────────────
1512    let num_planes_u32: u32 = if seq.monochrome { 1 } else { 3 };
1513    if !h.coded_lossless && !h.allow_intrabc && seq.enable_cdef {
1514        parse_av1_cdef_params(&mut br, &mut h, num_planes_u32)?;
1515    } else {
1516        // Spec defaults when cdef is skipped (§5.9.19 conformance).
1517        h.cdef_bits = 0;
1518        h.cdef_damping_minus_3 = 0;
1519        h.cdef_y_pri_strength = [0; 8];
1520        h.cdef_y_sec_strength = [0; 8];
1521        h.cdef_uv_pri_strength = [0; 8];
1522        h.cdef_uv_sec_strength = [0; 8];
1523    }
1524
1525    // ─── Phase 11: lr_params() (§5.9.20) ───────────────────────
1526    if !h.coded_lossless && !h.allow_intrabc && seq.enable_restoration {
1527        parse_av1_lr_params(&mut br, &mut h, num_planes_u32, seq)?;
1528    }
1529
1530    // ─── Phase 12: read_tx_mode (§5.9.22) ──────────────────────
1531    h.tx_mode = if h.coded_lossless {
1532        0 // ONLY_4X4
1533    } else if br.read_bits(1)? == 1 {
1534        2 // TX_MODE_SELECT
1535    } else {
1536        1 // TX_MODE_LARGEST
1537    };
1538
1539    // ─── Phase 13: frame_reference_mode (§5.9.23) ──────────────
1540    h.reference_select = if !frame_is_intra {
1541        br.read_bits(1)? == 1
1542    } else {
1543        false
1544    };
1545
1546    // ─── Phase 14: skip_mode_params (§5.9.24) ──────────────────
1547    let skip_mode_allowed = false; // For KEY/INTRA_ONLY, skip_mode is
1548    // implicitly disabled (requires 2
1549    // forward/backward refs). Inter
1550    // would derive from ref_frame_idx
1551    // + order hints — scaffolded.
1552    h.skip_mode_present = if skip_mode_allowed {
1553        br.read_bits(1)? == 1
1554    } else {
1555        false
1556    };
1557
1558    // allow_warped_motion (§5.9.1) — 1 bit gated by seq.enable_warped_motion
1559    // AND !error_resilient_mode AND !FrameIsIntra.
1560    h.allow_warped_motion =
1561        if !frame_is_intra && !h.error_resilient_mode && seq.enable_warped_motion {
1562            br.read_bits(1)? == 1
1563        } else {
1564            false
1565        };
1566
1567    // reduced_tx_set (1 bit) — the last bitstream bit we care about
1568    // for Vulkan Std picture info. global_motion_params,
1569    // film_grain_params, and the tile_group_obu() that follows the
1570    // byte_alignment() at the end are all parsed by the driver from
1571    // the bitstream, not from our Std struct.
1572    h.reduced_tx_set = br.read_bits(1)? == 1;
1573
1574    // ─── Phase 15: global_motion_params (§5.9.21) ──────────────
1575    // Read-only — we don't carry gm params across into Vulkan's
1576    // StdVideoAV1GlobalMotion at this time (zero-init GmType[]
1577    // → IDENTITY for every ref, matching the implicit default).
1578    if !frame_is_intra {
1579        skip_av1_global_motion_params(&mut br)?;
1580    }
1581
1582    // ─── Phase 16: film_grain_params (§5.9.25) ─────────────────
1583    if seq.film_grain_params_present && (h.show_frame || h.showable_frame) {
1584        skip_av1_film_grain_params(&mut br, seq)?;
1585    }
1586
1587    // ─── Phase 17: byte_align() + record tile_group_offset ────
1588    // Per §5.3.5, uncompressed_header ends with byte_alignment. The
1589    // tile_group_obu starts at the next byte boundary in the same
1590    // Frame OBU (type 6) or in a separate Tile Group OBU (type 4).
1591    br.byte_align();
1592    h.tile_group_offset_in_obu = (br.bit_pos() / 8) as u32;
1593
1594    Some(h)
1595}
1596
1597/// §5.9.5 frame_size()
1598fn parse_av1_frame_size(
1599    br: &mut BitReader,
1600    seq: &Av1SequenceHeader,
1601    frame_size_override_flag: bool,
1602) -> Option<(u32, u32)> {
1603    if frame_size_override_flag {
1604        let w_bits = av1_bits_for_max(seq.max_frame_width_minus1 + 1);
1605        let h_bits = av1_bits_for_max(seq.max_frame_height_minus1 + 1);
1606        let w = br.read_bits(w_bits)? + 1;
1607        let hgt = br.read_bits(h_bits)? + 1;
1608        Some((w, hgt))
1609    } else {
1610        Some((
1611            seq.max_frame_width_minus1 + 1,
1612            seq.max_frame_height_minus1 + 1,
1613        ))
1614    }
1615}
1616
1617/// §5.9.6 render_size()
1618fn parse_av1_render_size(
1619    br: &mut BitReader,
1620    frame_w: u32,
1621    frame_h: u32,
1622    out_w: &mut u32,
1623    out_h: &mut u32,
1624) -> Option<()> {
1625    let render_and_frame_size_different = br.read_bits(1)? == 1;
1626    if render_and_frame_size_different {
1627        *out_w = br.read_bits(16)? + 1;
1628        *out_h = br.read_bits(16)? + 1;
1629    } else {
1630        *out_w = frame_w;
1631        *out_h = frame_h;
1632    }
1633    Some(())
1634}
1635
1636/// §5.9.7 frame_size_with_refs() — for inter frames with size override.
1637/// Returns (frame_width, frame_height). The per-ref "found_ref" loop
1638/// here requires access to the ref frames' dims, which our scaffold
1639/// doesn't track. We treat `found_ref=0` uniformly (falls back to
1640/// frame_size()).
1641fn parse_av1_frame_size_with_refs(
1642    br: &mut BitReader,
1643    seq: &Av1SequenceHeader,
1644) -> Option<(u32, u32)> {
1645    let mut found_ref = false;
1646    for _ in 0..7u8 {
1647        if br.read_bits(1)? == 1 {
1648            found_ref = true;
1649        }
1650    }
1651    if !found_ref {
1652        let (w, hgt) = parse_av1_frame_size(br, seq, true)?;
1653        let mut rw = 0;
1654        let mut rh = 0;
1655        parse_av1_render_size(br, w, hgt, &mut rw, &mut rh)?;
1656        // superres_params inlined
1657        if seq.enable_superres && br.read_bits(1)? == 1 {
1658            let _denom = br.read_bits(3)?;
1659        }
1660        Some((w, hgt))
1661    } else {
1662        // found_ref branch: dims come from one of the refs. No ref
1663        // tracking → fall back to the sequence header max.
1664        Some((
1665            seq.max_frame_width_minus1 + 1,
1666            seq.max_frame_height_minus1 + 1,
1667        ))
1668    }
1669}
1670
1671fn av1_bits_for_max(v: u32) -> usize {
1672    // Inclusive ceil-log2 for a max-value field (AV1 uses
1673    // `n_bits = ceil(log2(max + 1))`).
1674    let mut bits = 0usize;
1675    let mut x = v.saturating_sub(1);
1676    while x > 0 {
1677        bits += 1;
1678        x >>= 1;
1679    }
1680    bits.max(1)
1681}
1682
1683/// §5.9.15 tile_info()
1684fn parse_av1_tile_info(
1685    br: &mut BitReader,
1686    h: &mut Av1FrameHeader,
1687    sb_cols: u32,
1688    sb_rows: u32,
1689    sb_size_log2: u32,
1690    mi_cols: u32,
1691    mi_rows: u32,
1692) -> Option<()> {
1693    // Derive MAX_TILE_AREA_SB, MAX_TILE_WIDTH_SB etc. (§5.9.15)
1694    // Constants from AV1 spec for 64x64 SB (log2=4).
1695    let max_tile_width_sb = 4096 >> (sb_size_log2 + 2); // typically 64
1696    let max_tile_area_sb = (4096 * 2304) >> (2 * sb_size_log2 + 4); // 4608
1697    let min_log2_tile_cols = av1_tile_log2(max_tile_width_sb, sb_cols);
1698    let max_log2_tile_cols = av1_tile_log2(1, sb_cols.min(64));
1699    let max_log2_tile_rows = av1_tile_log2(1, sb_rows.min(64));
1700    let min_log2_tiles = min_log2_tile_cols.max(av1_tile_log2(max_tile_area_sb, sb_rows * sb_cols));
1701
1702    h.uniform_tile_spacing_flag = br.read_bits(1)? == 1;
1703    let tile_cols_log2: u32;
1704    let tile_rows_log2: u32;
1705    h.mi_col_starts.clear();
1706    h.mi_row_starts.clear();
1707    h.width_in_sbs_minus_1.clear();
1708    h.height_in_sbs_minus_1.clear();
1709
1710    if h.uniform_tile_spacing_flag {
1711        let mut tcl = min_log2_tile_cols;
1712        while tcl < max_log2_tile_cols {
1713            if br.read_bits(1)? == 0 {
1714                break;
1715            }
1716            tcl += 1;
1717        }
1718        tile_cols_log2 = tcl;
1719        let tile_width_sb = (sb_cols + (1 << tile_cols_log2) - 1) >> tile_cols_log2;
1720        let mut start_sb = 0u32;
1721        let mut mi_starts: Vec<u16> = vec![0];
1722        let mut widths: Vec<u16> = Vec::new();
1723        while start_sb < sb_cols {
1724            let size_sb = tile_width_sb.min(sb_cols - start_sb);
1725            widths.push((size_sb - 1) as u16);
1726            start_sb += size_sb;
1727            mi_starts.push(((start_sb << sb_size_log2).min(mi_cols)) as u16);
1728        }
1729        h.mi_col_starts = mi_starts;
1730        h.width_in_sbs_minus_1 = widths;
1731        h.tile_cols = h.width_in_sbs_minus_1.len() as u8;
1732
1733        let min_log2_tile_rows = min_log2_tiles.saturating_sub(tile_cols_log2);
1734        let mut trl = min_log2_tile_rows;
1735        while trl < max_log2_tile_rows {
1736            if br.read_bits(1)? == 0 {
1737                break;
1738            }
1739            trl += 1;
1740        }
1741        tile_rows_log2 = trl;
1742        let tile_height_sb = (sb_rows + (1 << tile_rows_log2) - 1) >> tile_rows_log2;
1743        let mut start_sb_r = 0u32;
1744        let mut mi_starts_r: Vec<u16> = vec![0];
1745        let mut heights: Vec<u16> = Vec::new();
1746        while start_sb_r < sb_rows {
1747            let size_sb = tile_height_sb.min(sb_rows - start_sb_r);
1748            heights.push((size_sb - 1) as u16);
1749            start_sb_r += size_sb;
1750            mi_starts_r.push(((start_sb_r << sb_size_log2).min(mi_rows)) as u16);
1751        }
1752        h.mi_row_starts = mi_starts_r;
1753        h.height_in_sbs_minus_1 = heights;
1754        h.tile_rows = h.height_in_sbs_minus_1.len() as u8;
1755    } else {
1756        // Non-uniform tile spacing
1757        let mut start_sb = 0u32;
1758        let mut mi_starts: Vec<u16> = vec![0];
1759        let mut widths: Vec<u16> = Vec::new();
1760        while start_sb < sb_cols {
1761            let max_width = (sb_cols - start_sb).min(max_tile_width_sb);
1762            let size_minus_1 = av1_read_ns(br, max_width)?;
1763            let size = size_minus_1 + 1;
1764            widths.push(size_minus_1 as u16);
1765            start_sb += size;
1766            mi_starts.push(((start_sb << sb_size_log2).min(mi_cols)) as u16);
1767        }
1768        h.mi_col_starts = mi_starts;
1769        h.width_in_sbs_minus_1 = widths;
1770        h.tile_cols = h.width_in_sbs_minus_1.len() as u8;
1771        tile_cols_log2 = av1_tile_log2(1, h.tile_cols as u32);
1772
1773        let tile_cols = h.tile_cols as u32;
1774        let max_tile_area_sb_r = if min_log2_tiles > 0 {
1775            (sb_rows * sb_cols) >> (min_log2_tiles + 1)
1776        } else {
1777            sb_rows * sb_cols
1778        };
1779        let max_tile_height_sb = (max_tile_area_sb_r / tile_cols).max(1);
1780
1781        let mut start_sb_r = 0u32;
1782        let mut mi_starts_r: Vec<u16> = vec![0];
1783        let mut heights: Vec<u16> = Vec::new();
1784        while start_sb_r < sb_rows {
1785            let max_height = (sb_rows - start_sb_r).min(max_tile_height_sb);
1786            let size_minus_1 = av1_read_ns(br, max_height)?;
1787            let size = size_minus_1 + 1;
1788            heights.push(size_minus_1 as u16);
1789            start_sb_r += size;
1790            mi_starts_r.push(((start_sb_r << sb_size_log2).min(mi_rows)) as u16);
1791        }
1792        h.mi_row_starts = mi_starts_r;
1793        h.height_in_sbs_minus_1 = heights;
1794        h.tile_rows = h.height_in_sbs_minus_1.len() as u8;
1795        tile_rows_log2 = av1_tile_log2(1, h.tile_rows as u32);
1796    }
1797    h.tile_cols_log2 = tile_cols_log2 as u8;
1798    h.tile_rows_log2 = tile_rows_log2 as u8;
1799
1800    if (tile_cols_log2 + tile_rows_log2) > 0 {
1801        let n = (tile_cols_log2 + tile_rows_log2) as usize;
1802        h.context_update_tile_id = br.read_bits(n)? as u16;
1803        h.tile_size_bytes_minus_1 = br.read_bits(2)? as u8;
1804    } else {
1805        h.context_update_tile_id = 0;
1806        h.tile_size_bytes_minus_1 = 0;
1807    }
1808    Some(())
1809}
1810
1811/// AV1 tile_log2 helper (§5.9.15) — smallest k s.t. (blksize << k) >= target.
1812fn av1_tile_log2(blksize: u32, target: u32) -> u32 {
1813    let mut k = 0u32;
1814    while (blksize << k) < target {
1815        k += 1;
1816    }
1817    k
1818}
1819
1820/// AV1 ns(n) — non-symmetric fixed-length code (§4.10.6)
1821fn av1_read_ns(br: &mut BitReader, n: u32) -> Option<u32> {
1822    if n == 0 {
1823        return Some(0);
1824    }
1825    let w = av1_ceil_log2(n);
1826    if w == 0 {
1827        return Some(0);
1828    }
1829    let m = (1u32 << w) - n;
1830    let v = br.read_bits((w - 1) as usize)?;
1831    if v < m {
1832        Some(v)
1833    } else {
1834        let extra = br.read_bits(1)?;
1835        Some((v << 1) - m + extra)
1836    }
1837}
1838
1839fn av1_ceil_log2(n: u32) -> u32 {
1840    if n <= 1 {
1841        return 1;
1842    }
1843    let mut k = 0;
1844    let mut x = n - 1;
1845    while x > 0 {
1846        k += 1;
1847        x >>= 1;
1848    }
1849    k
1850}
1851
1852/// §5.9.12 quantization_params()
1853fn parse_av1_quantization_params(
1854    br: &mut BitReader,
1855    h: &mut Av1FrameHeader,
1856    seq: &Av1SequenceHeader,
1857) -> Option<()> {
1858    h.base_q_idx = br.read_bits(8)? as u8;
1859    h.delta_q_y_dc = read_delta_q(br)?;
1860    let (diff_uv_delta, num_planes) = if seq.monochrome {
1861        (false, 1u32)
1862    } else {
1863        let diff = if seq.seq_profile == 2 {
1864            br.read_bits(1)? == 1
1865        } else {
1866            false
1867        };
1868        (diff, 3u32)
1869    };
1870    if num_planes > 1 {
1871        h.delta_q_u_dc = read_delta_q(br)?;
1872        h.delta_q_u_ac = read_delta_q(br)?;
1873        if diff_uv_delta {
1874            h.delta_q_v_dc = read_delta_q(br)?;
1875            h.delta_q_v_ac = read_delta_q(br)?;
1876        } else {
1877            h.delta_q_v_dc = h.delta_q_u_dc;
1878            h.delta_q_v_ac = h.delta_q_u_ac;
1879        }
1880    }
1881    h.using_qmatrix = br.read_bits(1)? == 1;
1882    if h.using_qmatrix {
1883        h.qm_y = br.read_bits(4)? as u8;
1884        h.qm_u = br.read_bits(4)? as u8;
1885        h.qm_v = if seq.monochrome {
1886            h.qm_u
1887        } else if br.read_bits(1)? == 0 {
1888            h.qm_u
1889        } else {
1890            br.read_bits(4)? as u8
1891        };
1892    }
1893    Some(())
1894}
1895
1896fn read_delta_q(br: &mut BitReader) -> Option<i8> {
1897    let present = br.read_bits(1)? == 1;
1898    if present {
1899        Some(br.read_su(7)? as i8)
1900    } else {
1901        Some(0)
1902    }
1903}
1904
1905/// §5.9.14 segmentation_params()
1906fn parse_av1_segmentation_params(br: &mut BitReader, h: &mut Av1FrameHeader) -> Option<()> {
1907    h.segmentation_enabled = br.read_bits(1)? == 1;
1908    if h.segmentation_enabled {
1909        if h.primary_ref_frame == 7 {
1910            // PRIMARY_REF_NONE → forced-fresh segment tree
1911            h.segmentation_update_map = true;
1912            h.segmentation_temporal_update = false;
1913            h.segmentation_update_data = true;
1914        } else {
1915            h.segmentation_update_map = br.read_bits(1)? == 1;
1916            if h.segmentation_update_map {
1917                h.segmentation_temporal_update = br.read_bits(1)? == 1;
1918            }
1919            h.segmentation_update_data = br.read_bits(1)? == 1;
1920        }
1921        if h.segmentation_update_data {
1922            // SEG_FEATURE_DATA table (§5.9.14) — per-feature bit counts
1923            // and sign flags.
1924            // (bits, signed)
1925            const FEAT_INFO: [(u32, bool); 8] = [
1926                (8, true),  // SEG_LVL_ALT_Q
1927                (6, true),  // SEG_LVL_ALT_LF_Y_V
1928                (6, true),  // SEG_LVL_ALT_LF_Y_H
1929                (6, true),  // SEG_LVL_ALT_LF_U
1930                (6, true),  // SEG_LVL_ALT_LF_V
1931                (3, false), // SEG_LVL_REF_FRAME
1932                (0, false), // SEG_LVL_SKIP
1933                (0, false), // SEG_LVL_GLOBALMV
1934            ];
1935            for seg in 0..8 {
1936                for (feat, &(bits, signed)) in FEAT_INFO.iter().enumerate() {
1937                    let enabled = br.read_bits(1)? == 1;
1938                    h.feature_enabled[seg][feat] = enabled;
1939                    if enabled {
1940                        if bits == 0 {
1941                            h.feature_data[seg][feat] = 1;
1942                        } else if signed {
1943                            h.feature_data[seg][feat] = br.read_su(bits as usize + 1)? as i16;
1944                        } else {
1945                            h.feature_data[seg][feat] = br.read_bits(bits as usize)? as i16;
1946                        }
1947                    }
1948                }
1949            }
1950        }
1951    }
1952    Some(())
1953}
1954
1955/// §5.9.11 loop_filter_params()
1956fn parse_av1_loop_filter_params(
1957    br: &mut BitReader,
1958    h: &mut Av1FrameHeader,
1959    frame_is_intra: bool,
1960) -> Option<()> {
1961    if h.coded_lossless || h.allow_intrabc {
1962        h.loop_filter_level = [0; 4];
1963        h.loop_filter_sharpness = 0;
1964        h.loop_filter_delta_enabled = false;
1965        h.loop_filter_ref_deltas = [1, 0, 0, 0, -1, 0, -1, -1];
1966        h.loop_filter_mode_deltas = [0, 0];
1967        return Some(());
1968    }
1969    h.loop_filter_level[0] = br.read_bits(6)? as u8;
1970    h.loop_filter_level[1] = br.read_bits(6)? as u8;
1971    if h.loop_filter_level[0] > 0 || h.loop_filter_level[1] > 0 {
1972        h.loop_filter_level[2] = br.read_bits(6)? as u8;
1973        h.loop_filter_level[3] = br.read_bits(6)? as u8;
1974    }
1975    h.loop_filter_sharpness = br.read_bits(3)? as u8;
1976    h.loop_filter_delta_enabled = br.read_bits(1)? == 1;
1977    // Defaults for ref/mode deltas (§5.9.11)
1978    h.loop_filter_ref_deltas = [1, 0, 0, 0, -1, 0, -1, -1];
1979    h.loop_filter_mode_deltas = [0, 0];
1980    if h.loop_filter_delta_enabled {
1981        h.loop_filter_delta_update = br.read_bits(1)? == 1;
1982        if h.loop_filter_delta_update {
1983            let mut update_mask = 0u8;
1984            for i in 0..8 {
1985                let update = br.read_bits(1)? == 1;
1986                if update {
1987                    update_mask |= 1 << i;
1988                    h.loop_filter_ref_deltas[i] = br.read_su(7)? as i8;
1989                }
1990            }
1991            h.update_ref_delta_mask = update_mask;
1992            let mut mode_mask = 0u8;
1993            for i in 0..2 {
1994                let update = br.read_bits(1)? == 1;
1995                if update {
1996                    mode_mask |= 1 << i;
1997                    h.loop_filter_mode_deltas[i] = br.read_su(7)? as i8;
1998                }
1999            }
2000            h.update_mode_delta_mask = mode_mask;
2001        }
2002    }
2003    let _ = frame_is_intra; // reserved for future spec tweaks
2004    Some(())
2005}
2006
2007/// §5.9.19 cdef_params()
2008fn parse_av1_cdef_params(
2009    br: &mut BitReader,
2010    h: &mut Av1FrameHeader,
2011    num_planes: u32,
2012) -> Option<()> {
2013    h.cdef_damping_minus_3 = br.read_bits(2)? as u8;
2014    h.cdef_bits = br.read_bits(2)? as u8;
2015    let count = 1usize << h.cdef_bits;
2016    for i in 0..count {
2017        h.cdef_y_pri_strength[i] = br.read_bits(4)? as u8;
2018        let y_sec = br.read_bits(2)? as u8;
2019        // Spec §5.9.19: after reading cdef_y_sec_strength, if the
2020        // decoded value == 3 it's remapped to 4 (the "== 3 → 4" gap
2021        // in the 2-bit encoding). Same for chroma below.
2022        h.cdef_y_sec_strength[i] = if y_sec == 3 { 4 } else { y_sec };
2023        if num_planes > 1 {
2024            h.cdef_uv_pri_strength[i] = br.read_bits(4)? as u8;
2025            let uv_sec = br.read_bits(2)? as u8;
2026            h.cdef_uv_sec_strength[i] = if uv_sec == 3 { 4 } else { uv_sec };
2027        }
2028    }
2029    Some(())
2030}
2031
2032/// §5.9.20 lr_params()
2033fn parse_av1_lr_params(
2034    br: &mut BitReader,
2035    h: &mut Av1FrameHeader,
2036    num_planes: u32,
2037    seq: &Av1SequenceHeader,
2038) -> Option<()> {
2039    let mut uses_lr = false;
2040    let mut uses_chroma_lr = false;
2041    for i in 0..(num_planes as usize) {
2042        let lr_type = br.read_bits(2)? as u8;
2043        h.lr_type[i] = lr_type;
2044        if lr_type != 0 {
2045            uses_lr = true;
2046            if i > 0 {
2047                uses_chroma_lr = true;
2048            }
2049        }
2050    }
2051    if uses_lr {
2052        // 64x64 SB path (use_128x128_superblock=0 — we assume this):
2053        // read 1 bit; if set, read another for lr_unit_extra_shift.
2054        // 128x128 SB path: read 1 bit and add 1 (to get 128/256).
2055        // We don't track use_128x128_superblock — stick to 64x64.
2056        let base = br.read_bits(1)? as u8;
2057        h.lr_unit_shift = if base != 0 {
2058            let extra = br.read_bits(1)? as u8;
2059            base + extra
2060        } else {
2061            0
2062        };
2063        // lr_uv_shift only present when chroma is 4:2:0 (subx && suby)
2064        // AND chroma plane has LR enabled.
2065        if num_planes > 1 && uses_chroma_lr && seq.chroma_subsampling_x && seq.chroma_subsampling_y
2066        {
2067            h.lr_uv_shift = br.read_bits(1)? as u8;
2068        }
2069    }
2070    Some(())
2071}
2072
2073/// §5.9.21 global_motion_params() — read-only; we don't populate
2074/// StdVideoAV1GlobalMotion so just consume the bits to keep the
2075/// parser position in sync.
2076fn skip_av1_global_motion_params(br: &mut BitReader) -> Option<()> {
2077    for _ in 0..7 {
2078        let is_global = br.read_bits(1)? == 1;
2079        let is_rot_zoom = if is_global {
2080            br.read_bits(1)? == 1
2081        } else {
2082            false
2083        };
2084        let _is_translation = if is_global && !is_rot_zoom {
2085            br.read_bits(1)? == 1
2086        } else {
2087            false
2088        };
2089        let gm_type = if is_global && !is_rot_zoom {
2090            2u8 /*TRANSLATION*/
2091        } else if is_rot_zoom {
2092            3u8 /*ROTZOOM*/
2093        } else if is_global {
2094            4u8 /*AFFINE*/
2095        } else {
2096            0u8 /*IDENTITY*/
2097        };
2098        if gm_type >= 3 {
2099            // 2 × 6 subexp params
2100            for _ in 0..2 {
2101                let _a = av1_read_subexp(br, 12, 0)?;
2102                let _b = av1_read_subexp(br, 12, 0)?;
2103            }
2104        }
2105        if gm_type >= 2 {
2106            // 2 × 6 subexp params for translation
2107            for _ in 0..2 {
2108                let _a = av1_read_subexp(br, 12, 0)?;
2109            }
2110        }
2111    }
2112    Some(())
2113}
2114
2115fn av1_read_subexp(br: &mut BitReader, num_syms: u32, _ref: i32) -> Option<i32> {
2116    // Simplified: read the inv_remap_and_deltaAV1 signed field. We
2117    // only need to advance the bit cursor — value is discarded.
2118    // §5.11.21: inv_remap_and_delta recurrence. The simplified "skip
2119    // enough bits" form reads ceil(log2(num_syms)) + sign bits.
2120    let bits = av1_ceil_log2(num_syms) as usize + 1; // value + sign
2121    let _ = br.read_bits(bits.min(16))?;
2122    Some(0)
2123}
2124
2125/// §5.9.25 film_grain_params() — we don't ship film-grain support in
2126/// the Vulkan scope; skip past the bits to keep parser position in
2127/// sync for byte_align().
2128fn skip_av1_film_grain_params(br: &mut BitReader, seq: &Av1SequenceHeader) -> Option<()> {
2129    let apply_grain = br.read_bits(1)? == 1;
2130    if !apply_grain {
2131        return Some(());
2132    }
2133    let _grain_seed = br.read_bits(16)?;
2134    let update_grain = br.read_bits(1)? == 1;
2135    if !update_grain {
2136        let _film_grain_params_ref_idx = br.read_bits(3)?;
2137        return Some(());
2138    }
2139    let num_y_points = br.read_bits(4)?;
2140    for _ in 0..num_y_points {
2141        let _point_y_value = br.read_bits(8)?;
2142        let _point_y_scaling = br.read_bits(8)?;
2143    }
2144    let chroma_scaling_from_luma = if seq.monochrome {
2145        false
2146    } else {
2147        br.read_bits(1)? == 1
2148    };
2149    let num_cb_points: u32;
2150    let num_cr_points: u32;
2151    if seq.monochrome
2152        || chroma_scaling_from_luma
2153        || (seq.chroma_subsampling_x && seq.chroma_subsampling_y && num_y_points == 0)
2154    {
2155        num_cb_points = 0;
2156        num_cr_points = 0;
2157    } else {
2158        num_cb_points = br.read_bits(4)?;
2159        for _ in 0..num_cb_points {
2160            let _point_cb_value = br.read_bits(8)?;
2161            let _point_cb_scaling = br.read_bits(8)?;
2162        }
2163        num_cr_points = br.read_bits(4)?;
2164        for _ in 0..num_cr_points {
2165            let _point_cr_value = br.read_bits(8)?;
2166            let _point_cr_scaling = br.read_bits(8)?;
2167        }
2168    }
2169    let _grain_scaling_minus_8 = br.read_bits(2)?;
2170    let ar_coeff_lag = br.read_bits(2)?;
2171    let num_pos_y = 2 * ar_coeff_lag * (ar_coeff_lag + 1);
2172    let num_pos_chroma = if num_y_points > 0 {
2173        num_pos_y + 1
2174    } else {
2175        num_pos_y
2176    };
2177    for _ in 0..num_pos_y {
2178        let _ar_coeff_y_plus_128 = br.read_bits(8)?;
2179    }
2180    if chroma_scaling_from_luma || num_cb_points > 0 {
2181        for _ in 0..num_pos_chroma {
2182            let _ar_coeff_cb_plus_128 = br.read_bits(8)?;
2183        }
2184    }
2185    if chroma_scaling_from_luma || num_cr_points > 0 {
2186        for _ in 0..num_pos_chroma {
2187            let _ar_coeff_cr_plus_128 = br.read_bits(8)?;
2188        }
2189    }
2190    let _ar_coeff_shift_minus_6 = br.read_bits(2)?;
2191    let _grain_scale_shift = br.read_bits(2)?;
2192    if num_cb_points > 0 {
2193        let _cb_mult = br.read_bits(8)?;
2194        let _cb_luma_mult = br.read_bits(8)?;
2195        let _cb_offset = br.read_bits(9)?;
2196    }
2197    if num_cr_points > 0 {
2198        let _cr_mult = br.read_bits(8)?;
2199        let _cr_luma_mult = br.read_bits(8)?;
2200        let _cr_offset = br.read_bits(9)?;
2201    }
2202    let _overlap_flag = br.read_bits(1)?;
2203    let _clip_to_restricted_range = br.read_bits(1)?;
2204    Some(())
2205}
2206
2207/// Locate the byte offset, within `sample`, of the uncompressed_header
2208/// payload of the first Frame OBU (obu_type 3 or 6). Returns None if
2209/// no such OBU is found.
2210///
2211/// AV1 OBU layout: 1-byte header + optional 1-byte extension + LEB128
2212/// size + payload. For a Frame OBU (type 6), the payload begins with
2213/// uncompressed_header_obu() — so the byte offset we return is the
2214/// first byte of uncompressed_header() in the original sample buffer.
2215/// Vulkan `VkVideoDecodeAV1PictureInfoKHR::frameHeaderOffset` wants
2216/// exactly this value.
2217pub fn av1_frame_header_offset(sample: &[u8]) -> Option<u32> {
2218    let mut i = 0usize;
2219    while i < sample.len() {
2220        let header = sample[i];
2221        let obu_type = (header >> 3) & 0x0F;
2222        let extension_flag = (header >> 2) & 0x01;
2223        let has_size_field = (header >> 1) & 0x01;
2224        let mut p = i + 1;
2225        if extension_flag == 1 {
2226            p += 1;
2227        }
2228        let (size, leb) = if has_size_field == 1 {
2229            let (s, n) = read_leb128(&sample[p..])?;
2230            p += n;
2231            (s as usize, n)
2232        } else {
2233            // OBU has_size_field=0 is legal but we don't handle it
2234            // (AV1 in MP4 always sets it).
2235            return None;
2236        };
2237        let _ = leb;
2238        if obu_type == 3 || obu_type == 6 {
2239            return Some(p as u32);
2240        }
2241        p += size;
2242        i = p;
2243    }
2244    None
2245}
2246
2247/// Locate the byte offset of the first tile_group_obu payload within
2248/// the sample buffer, used for
2249/// `VkVideoDecodeAV1PictureInfoKHR::pTileOffsets`. Two shapes:
2250/// - Separate Frame Header OBU (type 3) + Tile Group OBU (type 4):
2251///   return the type-4 OBU payload start.
2252/// - Frame OBU (type 6) (frame header + tile group in one OBU):
2253///   return `frame_OBU_payload_start + tile_group_offset_in_obu`
2254///   where the in-OBU offset comes from `parse_av1_frame_header`
2255///   (the byte-aligned position after uncompressed_header).
2256///
2257/// Returns None when neither shape is found or the parser bails.
2258pub fn av1_tile_group_offset(sample: &[u8], seq: &Av1SequenceHeader) -> Option<u32> {
2259    // If a standalone Tile Group OBU (type 4) exists, use its payload
2260    // start directly — no uncompressed_header to skip past.
2261    let mut i = 0usize;
2262    while i < sample.len() {
2263        let header = sample[i];
2264        let obu_type = (header >> 3) & 0x0F;
2265        let extension_flag = (header >> 2) & 0x01;
2266        let has_size_field = (header >> 1) & 0x01;
2267        let mut p = i + 1;
2268        if extension_flag == 1 {
2269            p += 1;
2270        }
2271        let size = if has_size_field == 1 {
2272            let (s, n) = read_leb128(&sample[p..])?;
2273            p += n;
2274            s as usize
2275        } else {
2276            return None;
2277        };
2278        if obu_type == 4 {
2279            return Some(p as u32);
2280        }
2281        p += size;
2282        i = p;
2283    }
2284    // Frame OBU (type 6): combine the OBU payload start with the
2285    // in-OBU offset from the parsed frame header.
2286    let (_obu_bytes, payload_offset) = find_av1_obu_with_offset(sample, 6)?;
2287    let hdr = parse_av1_frame_header(sample, seq)?;
2288    Some(payload_offset as u32 + hdr.tile_group_offset_in_obu)
2289}
2290
2291/// Backwards-compatible shim — uses an empty-ish sequence header
2292/// default that only works for the fallback path (standalone type-4
2293/// OBU). Callers with access to the parsed sequence header should
2294/// use `av1_tile_group_offset` (the seq-aware form) instead.
2295pub fn av1_tile_group_offset_fallback(sample: &[u8]) -> Option<u32> {
2296    let mut i = 0usize;
2297    while i < sample.len() {
2298        let header = sample[i];
2299        let obu_type = (header >> 3) & 0x0F;
2300        let extension_flag = (header >> 2) & 0x01;
2301        let has_size_field = (header >> 1) & 0x01;
2302        let mut p = i + 1;
2303        if extension_flag == 1 {
2304            p += 1;
2305        }
2306        let size = if has_size_field == 1 {
2307            let (s, n) = read_leb128(&sample[p..])?;
2308            p += n;
2309            s as usize
2310        } else {
2311            return None;
2312        };
2313        if obu_type == 4 {
2314            return Some(p as u32);
2315        }
2316        p += size;
2317        i = p;
2318    }
2319    av1_frame_header_offset(sample)
2320}
2321
2322/// AV1 uvlc (unsigned variable-length code) — count leading zero bits
2323/// up to 31; then read that many bits as the suffix; value = (1<<N)-1+suffix.
2324fn read_av1_uvlc(br: &mut BitReader) -> Option<u32> {
2325    let mut leading_zeros = 0;
2326    while leading_zeros < 32 {
2327        if br.read_bits(1)? == 1 {
2328            break;
2329        }
2330        leading_zeros += 1;
2331    }
2332    if leading_zeros >= 32 {
2333        return None;
2334    }
2335    if leading_zeros == 0 {
2336        return Some(0);
2337    }
2338    let suffix = br.read_bits(leading_zeros)?;
2339    Some((1u32 << leading_zeros) - 1 + suffix)
2340}
2341
2342/// Parsed MPEG-2 sequence header + (optional) sequence extension.
2343///
2344/// MPEG-2 video §6.2.2.1/§6.2.2.3 (ISO/IEC 13818-2): the 12-bit
2345/// `horizontal_size_value` / `vertical_size_value` from the sequence
2346/// header, optionally extended to 14 bits by the 2-bit
2347/// `horizontal_size_extension` / `vertical_size_extension` fields in a
2348/// `sequence_extension()` start-code-prefixed NAL. Pure MPEG-1
2349/// (start code 0xB3 but no 0xB5 extension) stays 12-bit — produces
2350/// the same 12-bit result via the extension-less path.
2351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2352pub struct Mpeg2SeqInfo {
2353    pub width: u32,
2354    pub height: u32,
2355}
2356
2357/// Public entry point — dispatch by codec and return `Some((width,
2358/// height))` if the sequence header in `samples[0]` is parseable,
2359/// `None` otherwise.
2360///
2361/// Callers should treat `None` as "keep the existing width/height" —
2362/// it's load-bearing for MPEG-TS where `StreamInfo` would otherwise
2363/// carry `0×0`, but a parse failure on MP4/MKV (which already have
2364/// width/height in the sample-entry / track-header) is a no-op.
2365pub fn detect_dims(codec: &str, samples: &[Vec<u8>]) -> Option<(u32, u32)> {
2366    if samples.is_empty() {
2367        return None;
2368    }
2369    let sample = &samples[0];
2370    match codec.to_lowercase().as_str() {
2371        "h264" | "avc1" | "avc" | "avc3" => {
2372            let info = parse_h264_sps(sample)?;
2373            Some((info.width?, info.height?))
2374        }
2375        "h265" | "hevc" | "hvc1" | "hev1" | "hvc2" | "hev2" => {
2376            let info = parse_hevc_sps(sample)?;
2377            Some((info.width?, info.height?))
2378        }
2379        "mpeg2" | "mpeg2video" | "mp2v" => {
2380            let info = parse_mpeg2_sequence_header(sample)?;
2381            Some((info.width, info.height))
2382        }
2383        _ => None,
2384    }
2385}
2386
2387/// Full H.264 SPS walker — see §7.3.2.1.1 + §7.4.2.1.1. The parse is
2388/// greedy: profile_idc + chroma fields are populated first, then we
2389/// walk the variable-length sections (scaling lists,
2390/// pic_order_cnt_type branch) to reach pic_width_in_mbs_minus1 etc.
2391/// If any of those sections hit end-of-buffer the dims come back as
2392/// None but the early fields are returned.
2393pub fn parse_h264_sps(sample: &[u8]) -> Option<H264SpsInfo> {
2394    let sps = find_h264_sps(sample)?;
2395    let rbsp = remove_h264_rbsp_stuffing(sps);
2396    let mut br = BitReader::new(&rbsp);
2397
2398    let profile_idc = br.read_bits(8)? as u8;
2399    let constraint_set_flags = br.read_bits(8)? as u8;
2400    let level_idc = br.read_bits(8)? as u8;
2401    let _seq_parameter_set_id = br.read_ue()?;
2402
2403    let profile_gates_chroma = matches!(
2404        profile_idc,
2405        100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
2406    );
2407
2408    let (
2409        chroma_format_idc,
2410        separate_colour_plane_flag,
2411        bit_depth_luma,
2412        bit_depth_chroma,
2413        qpprime_y_zero,
2414        scaling_matrix,
2415    ) = if profile_gates_chroma {
2416        let chroma = br.read_ue()? as u8;
2417        let separate = if chroma == 3 {
2418            br.read_bits(1)? == 1
2419        } else {
2420            false
2421        };
2422        let bit_depth_luma_m8 = br.read_ue()?;
2423        let bit_depth_chroma_m8 = br.read_ue()?;
2424        let qpprime = br.read_bits(1)? == 1;
2425        let scaling_matrix_present = br.read_bits(1)? == 1;
2426        if scaling_matrix_present {
2427            // 8 scaling lists for chroma_format_idc != 3, 12 otherwise
2428            // (§7.3.2.1.1.1). Each list is size 16 for i<6, 64 otherwise.
2429            // Deltas are se(v); missing-list flag is u(1).
2430            let num_lists = if chroma == 3 { 12 } else { 8 };
2431            for i in 0..num_lists {
2432                if br.read_bits(1)? == 1 {
2433                    let size = if i < 6 { 16 } else { 64 };
2434                    let mut last_scale: i32 = 8;
2435                    let mut next_scale: i32 = 8;
2436                    for _j in 0..size {
2437                        if next_scale != 0 {
2438                            let delta = br.read_se()?;
2439                            next_scale = (last_scale + delta + 256).rem_euclid(256);
2440                        }
2441                        if next_scale != 0 {
2442                            last_scale = next_scale;
2443                        }
2444                    }
2445                }
2446            }
2447        }
2448        (
2449            chroma,
2450            separate,
2451            bit_depth_luma_m8 as u8 + 8,
2452            bit_depth_chroma_m8 as u8 + 8,
2453            qpprime,
2454            scaling_matrix_present,
2455        )
2456    } else {
2457        (1u8, false, 8u8, 8u8, false, false)
2458    };
2459
2460    // At this point we've cleared the chroma/depth prefix. Everything
2461    // from here on is what we need for width/height and the slice-
2462    // header branching predicates. Any read failure below returns the
2463    // partial info with width/height = None.
2464    let info_prefix = H264SpsInfo {
2465        profile_idc,
2466        constraint_set_flags,
2467        level_idc,
2468        chroma_format_idc,
2469        separate_colour_plane_flag,
2470        bit_depth_luma,
2471        bit_depth_chroma,
2472        frame_mbs_only: true,
2473        width: None,
2474        height: None,
2475        log2_max_frame_num_minus4: None,
2476        pic_order_cnt_type: None,
2477        log2_max_pic_order_cnt_lsb_minus4: None,
2478        delta_pic_order_always_zero_flag: None,
2479        qpprime_y_zero_transform_bypass_flag: Some(qpprime_y_zero),
2480        seq_scaling_matrix_present_flag: Some(scaling_matrix),
2481        max_num_ref_frames: None,
2482        gaps_in_frame_num_value_allowed_flag: None,
2483        mb_adaptive_frame_field_flag: None,
2484        direct_8x8_inference_flag: None,
2485        frame_cropping_flag: None,
2486        frame_crop_left_offset: None,
2487        frame_crop_right_offset: None,
2488        frame_crop_top_offset: None,
2489        frame_crop_bottom_offset: None,
2490        offset_for_non_ref_pic: None,
2491        offset_for_top_to_bottom_field: None,
2492        num_ref_frames_in_pic_order_cnt_cycle: None,
2493        offset_for_ref_frame: Vec::new(),
2494    };
2495
2496    let Some(dims) = parse_h264_sps_dims(&mut br, chroma_format_idc, separate_colour_plane_flag)
2497    else {
2498        return Some(info_prefix);
2499    };
2500
2501    Some(H264SpsInfo {
2502        frame_mbs_only: dims.frame_mbs_only,
2503        width: Some(dims.width),
2504        height: Some(dims.height),
2505        log2_max_frame_num_minus4: Some(dims.log2_max_frame_num_minus4),
2506        pic_order_cnt_type: Some(dims.pic_order_cnt_type),
2507        log2_max_pic_order_cnt_lsb_minus4: dims.log2_max_pic_order_cnt_lsb_minus4,
2508        delta_pic_order_always_zero_flag: dims.delta_pic_order_always_zero_flag,
2509        max_num_ref_frames: Some(dims.max_num_ref_frames),
2510        gaps_in_frame_num_value_allowed_flag: Some(dims.gaps_in_frame_num_value_allowed_flag),
2511        mb_adaptive_frame_field_flag: dims.mb_adaptive_frame_field_flag,
2512        direct_8x8_inference_flag: Some(dims.direct_8x8_inference_flag),
2513        frame_cropping_flag: Some(dims.frame_cropping_flag),
2514        frame_crop_left_offset: Some(dims.crop_left),
2515        frame_crop_right_offset: Some(dims.crop_right),
2516        frame_crop_top_offset: Some(dims.crop_top),
2517        frame_crop_bottom_offset: Some(dims.crop_bottom),
2518        offset_for_non_ref_pic: dims.offset_for_non_ref_pic,
2519        offset_for_top_to_bottom_field: dims.offset_for_top_to_bottom_field,
2520        num_ref_frames_in_pic_order_cnt_cycle: dims.num_ref_frames_in_pic_order_cnt_cycle,
2521        offset_for_ref_frame: dims.offset_for_ref_frame,
2522        ..info_prefix
2523    })
2524}
2525
2526struct H264Dims {
2527    width: u32,
2528    height: u32,
2529    frame_mbs_only: bool,
2530    log2_max_frame_num_minus4: u8,
2531    pic_order_cnt_type: u8,
2532    log2_max_pic_order_cnt_lsb_minus4: Option<u8>,
2533    delta_pic_order_always_zero_flag: Option<bool>,
2534    offset_for_non_ref_pic: Option<i32>,
2535    offset_for_top_to_bottom_field: Option<i32>,
2536    num_ref_frames_in_pic_order_cnt_cycle: Option<u8>,
2537    offset_for_ref_frame: Vec<i32>,
2538    max_num_ref_frames: u8,
2539    gaps_in_frame_num_value_allowed_flag: bool,
2540    mb_adaptive_frame_field_flag: Option<bool>,
2541    direct_8x8_inference_flag: bool,
2542    frame_cropping_flag: bool,
2543    crop_left: u32,
2544    crop_right: u32,
2545    crop_top: u32,
2546    crop_bottom: u32,
2547}
2548
2549fn parse_h264_sps_dims(
2550    br: &mut BitReader,
2551    chroma_format_idc: u8,
2552    separate_colour_plane_flag: bool,
2553) -> Option<H264Dims> {
2554    let log2_max_frame_num_minus4 = br.read_ue()? as u8;
2555    let pic_order_cnt_type = br.read_ue()? as u8;
2556    let mut log2_max_pic_order_cnt_lsb_minus4 = None;
2557    let mut delta_pic_order_always_zero_flag = None;
2558    let mut offset_for_non_ref_pic = None;
2559    let mut offset_for_top_to_bottom_field = None;
2560    let mut num_ref_frames_in_pic_order_cnt_cycle: Option<u8> = None;
2561    let mut offset_for_ref_frame: Vec<i32> = Vec::new();
2562    match pic_order_cnt_type {
2563        0 => {
2564            log2_max_pic_order_cnt_lsb_minus4 = Some(br.read_ue()? as u8);
2565        }
2566        1 => {
2567            delta_pic_order_always_zero_flag = Some(br.read_bits(1)? == 1);
2568            offset_for_non_ref_pic = Some(br.read_se()?);
2569            offset_for_top_to_bottom_field = Some(br.read_se()?);
2570            let cycle_len = br.read_ue()?;
2571            // Cap at 255 to fit u8 + bound the loop — spec allows up
2572            // to 255, so no real loss of precision.
2573            let capped = cycle_len.min(255) as u8;
2574            num_ref_frames_in_pic_order_cnt_cycle = Some(capped);
2575            offset_for_ref_frame.reserve(capped as usize);
2576            for _ in 0..capped {
2577                offset_for_ref_frame.push(br.read_se()?);
2578            }
2579        }
2580        2 => { /* no fields */ }
2581        _ => return None, // reserved; spec says no other values are valid
2582    }
2583    let max_num_ref_frames = br.read_ue()?.min(u8::MAX as u32) as u8;
2584    let gaps_in_frame_num_value_allowed_flag = br.read_bits(1)? == 1;
2585    let pic_width_in_mbs_minus1 = br.read_ue()?;
2586    let pic_height_in_map_units_minus1 = br.read_ue()?;
2587    let frame_mbs_only_flag = br.read_bits(1)?;
2588    let mut mb_adaptive_frame_field_flag = None;
2589    if frame_mbs_only_flag == 0 {
2590        mb_adaptive_frame_field_flag = Some(br.read_bits(1)? == 1);
2591    }
2592    let direct_8x8_inference_flag = br.read_bits(1)? == 1;
2593    let frame_cropping_flag = br.read_bits(1)? == 1;
2594    let (cl, cr, ct, cb) = if frame_cropping_flag {
2595        (br.read_ue()?, br.read_ue()?, br.read_ue()?, br.read_ue()?)
2596    } else {
2597        (0, 0, 0, 0)
2598    };
2599
2600    let pic_width_in_mbs = pic_width_in_mbs_minus1.saturating_add(1);
2601    let pic_height_in_map_units = pic_height_in_map_units_minus1.saturating_add(1);
2602    let frame_mbs_only = frame_mbs_only_flag == 1;
2603    let frame_height_in_mbs = if frame_mbs_only {
2604        pic_height_in_map_units
2605    } else {
2606        pic_height_in_map_units.saturating_mul(2)
2607    };
2608
2609    // §6.2 Table 6-1 + §7.4.2.1.1
2610    let chroma_array_type = if separate_colour_plane_flag {
2611        0
2612    } else {
2613        chroma_format_idc
2614    };
2615    let (sub_w, sub_h) = match chroma_array_type {
2616        0 => (1u32, 1u32), // monochrome (cropping units below use 1,2-flag)
2617        1 => (2, 2),       // 4:2:0
2618        2 => (2, 1),       // 4:2:2
2619        3 => (1, 1),       // 4:4:4
2620        _ => (1, 1),
2621    };
2622    let (crop_x, crop_y) = if chroma_array_type == 0 {
2623        (1u32, 2u32 - frame_mbs_only_flag)
2624    } else {
2625        (sub_w, sub_h * (2 - frame_mbs_only_flag))
2626    };
2627
2628    let width = pic_width_in_mbs
2629        .saturating_mul(16)
2630        .saturating_sub(crop_x.saturating_mul(cl.saturating_add(cr)));
2631    let height = frame_height_in_mbs
2632        .saturating_mul(16)
2633        .saturating_sub(crop_y.saturating_mul(ct.saturating_add(cb)));
2634
2635    Some(H264Dims {
2636        width,
2637        height,
2638        frame_mbs_only,
2639        log2_max_frame_num_minus4,
2640        pic_order_cnt_type,
2641        log2_max_pic_order_cnt_lsb_minus4,
2642        delta_pic_order_always_zero_flag,
2643        offset_for_non_ref_pic,
2644        offset_for_top_to_bottom_field,
2645        num_ref_frames_in_pic_order_cnt_cycle,
2646        offset_for_ref_frame,
2647        max_num_ref_frames,
2648        gaps_in_frame_num_value_allowed_flag,
2649        mb_adaptive_frame_field_flag,
2650        direct_8x8_inference_flag,
2651        frame_cropping_flag,
2652        crop_left: cl,
2653        crop_right: cr,
2654        crop_top: ct,
2655        crop_bottom: cb,
2656    })
2657}
2658
2659/// Full HEVC SPS walker — see H.265 §7.3.2.2.1 + §7.4.3.2.1. Consumes
2660/// `profile_tier_level` via the existing `skip_hevc_profile_tier_level`
2661/// helper, then reads pic_width_in_luma_samples + pic_height_in_luma_samples
2662/// and applies the conformance window crop if present.
2663pub fn parse_hevc_sps(sample: &[u8]) -> Option<HevcSpsInfo> {
2664    let sps = find_hevc_sps(sample)?;
2665    let rbsp = remove_h264_rbsp_stuffing(sps);
2666    let mut br = BitReader::new(&rbsp);
2667
2668    let sps_video_parameter_set_id = br.read_bits(4)? as u8;
2669    let sps_max_sub_layers_minus1 = br.read_bits(3)? as u8;
2670    let sps_temporal_id_nesting_flag = br.read_bits(1)? == 1;
2671    // profile_tier_level: capture general_profile_idc + tier + level
2672    // for the VPS mirror + Std struct. The rest is skipped via the
2673    // same helper we already had.
2674    let general_profile_space = br.read_bits(2)?;
2675    let tier_flag = br.read_bits(1)? == 1;
2676    let profile_idc = br.read_bits(5)? as u8;
2677    let _ = general_profile_space;
2678    // general_profile_compatibility_flag[32] — captured for Std PTL.
2679    let profile_compatibility_flags = br.read_bits(32)?;
2680    // constraint flags (48 bits) — ignored here; Std PTL has them as
2681    // individual flag bits we're not reporting (conservative default).
2682    let _ = br.read_bits(48)?;
2683    let level_idc = br.read_bits(8)? as u8;
2684    // Skip sub-layer profile/level blocks — matches
2685    // skip_hevc_profile_tier_level's tail logic.
2686    let mut spl = Vec::with_capacity(sps_max_sub_layers_minus1 as usize);
2687    let mut sll = Vec::with_capacity(sps_max_sub_layers_minus1 as usize);
2688    for _ in 0..sps_max_sub_layers_minus1 {
2689        spl.push(br.read_bits(1)?);
2690        sll.push(br.read_bits(1)?);
2691    }
2692    if sps_max_sub_layers_minus1 > 0 {
2693        for _ in sps_max_sub_layers_minus1 as usize..8 {
2694            let _ = br.read_bits(2)?;
2695        }
2696    }
2697    for i in 0..sps_max_sub_layers_minus1 as usize {
2698        if spl[i] == 1 {
2699            let _ = br.read_bits(8)?;
2700            let _ = br.read_bits(32)?;
2701            let _ = br.read_bits(48)?;
2702        }
2703        if sll[i] == 1 {
2704            let _ = br.read_bits(8)?;
2705        }
2706    }
2707
2708    let sps_seq_parameter_set_id = br.read_ue()? as u8;
2709    let chroma_format_idc = br.read_ue()? as u8;
2710    let separate_colour_plane_flag = if chroma_format_idc == 3 {
2711        br.read_bits(1)? == 1
2712    } else {
2713        false
2714    };
2715    let pic_width = br.read_ue()?;
2716    let pic_height = br.read_ue()?;
2717    let conformance_window_flag = br.read_bits(1)?;
2718    let (cl, cr, ct, cb) = if conformance_window_flag == 1 {
2719        (br.read_ue()?, br.read_ue()?, br.read_ue()?, br.read_ue()?)
2720    } else {
2721        (0u32, 0u32, 0u32, 0u32)
2722    };
2723    let bit_depth_luma_m8 = br.read_ue()?;
2724    let bit_depth_chroma_m8 = br.read_ue()?;
2725    let log2_max_pic_order_cnt_lsb_minus4 = br.read_ue()? as u8;
2726
2727    // sps_sub_layer_ordering_info_present_flag branch.
2728    // Spec §7.3.2.2.1: when the flag is 0 only the top sub-layer's
2729    // triple is signalled, but the DPB buf-mgr should mirror that
2730    // value across all sub-layers i < max_sub_layers_minus1. We do
2731    // that unification here so Std DecPicBufMgr has all entries
2732    // populated regardless of how the bitstream flagged them.
2733    let sps_sub_layer_ordering_info_present_flag = br.read_bits(1)? == 1;
2734    let mut max_dec_pic_buffering_minus1 = [0u8; 7];
2735    let mut max_num_reorder_pics = [0u8; 7];
2736    let mut max_latency_increase_plus1 = [0u32; 7];
2737    let start = if sps_sub_layer_ordering_info_present_flag {
2738        0
2739    } else {
2740        sps_max_sub_layers_minus1
2741    };
2742    for i in start..=sps_max_sub_layers_minus1 {
2743        let dec = br.read_ue()?;
2744        let nro = br.read_ue()?;
2745        let latency = br.read_ue()?;
2746        let idx = (i as usize).min(6);
2747        max_dec_pic_buffering_minus1[idx] = dec.min(u8::MAX as u32) as u8;
2748        max_num_reorder_pics[idx] = nro.min(u8::MAX as u32) as u8;
2749        max_latency_increase_plus1[idx] = latency;
2750    }
2751    // Fill unsignalled lower sub-layers with the top-layer values.
2752    if !sps_sub_layer_ordering_info_present_flag {
2753        let top = sps_max_sub_layers_minus1 as usize;
2754        for i in 0..top {
2755            max_dec_pic_buffering_minus1[i] = max_dec_pic_buffering_minus1[top];
2756            max_num_reorder_pics[i] = max_num_reorder_pics[top];
2757            max_latency_increase_plus1[i] = max_latency_increase_plus1[top];
2758        }
2759    }
2760
2761    let log2_min_luma_coding_block_size_minus3 = br.read_ue()? as u8;
2762    let log2_diff_max_min_luma_coding_block_size = br.read_ue()? as u8;
2763    let log2_min_luma_transform_block_size_minus2 = br.read_ue()? as u8;
2764    let log2_diff_max_min_luma_transform_block_size = br.read_ue()? as u8;
2765    let max_transform_hierarchy_depth_inter = br.read_ue()? as u8;
2766    let max_transform_hierarchy_depth_intra = br.read_ue()? as u8;
2767
2768    let scaling_list_enabled_flag = br.read_bits(1)? == 1;
2769    if scaling_list_enabled_flag {
2770        let sps_scaling_list_data_present_flag = br.read_bits(1)? == 1;
2771        if sps_scaling_list_data_present_flag {
2772            skip_hevc_scaling_list_data(&mut br)?;
2773        }
2774    }
2775    let amp_enabled_flag = br.read_bits(1)? == 1;
2776    let sample_adaptive_offset_enabled_flag = br.read_bits(1)? == 1;
2777    let pcm_enabled_flag = br.read_bits(1)? == 1;
2778    let mut pcm_loop_filter_disabled_flag = false;
2779    if pcm_enabled_flag {
2780        let _pcm_sample_bit_depth_luma_minus1 = br.read_bits(4)?;
2781        let _pcm_sample_bit_depth_chroma_minus1 = br.read_bits(4)?;
2782        let _log2_min_pcm_luma_cb_size_minus3 = br.read_ue()?;
2783        let _log2_diff_max_min_pcm_luma_cb_size = br.read_ue()?;
2784        pcm_loop_filter_disabled_flag = br.read_bits(1)? == 1;
2785    }
2786    let num_short_term_ref_pic_sets = br.read_ue()? as u8;
2787    // Skip the short-term RPS syntax parsing — we don't need the
2788    // values to build Std SPS, but we do need to advance past them.
2789    // The full parse is complex; use a conservative skip that
2790    // tolerates simple streams. For a production decoder, this needs
2791    // a proper RPS parser — this is a scaffold.
2792    let mut st_rps_offsets: Vec<()> = Vec::with_capacity(num_short_term_ref_pic_sets as usize);
2793    for rps_idx in 0..num_short_term_ref_pic_sets {
2794        skip_hevc_short_term_rps(&mut br, rps_idx, num_short_term_ref_pic_sets)?;
2795        st_rps_offsets.push(());
2796    }
2797    let long_term_ref_pics_present_flag = br.read_bits(1)? == 1;
2798    if long_term_ref_pics_present_flag {
2799        let num_long_term_ref_pics_sps = br.read_ue()?;
2800        let lsb_bits = (log2_max_pic_order_cnt_lsb_minus4 as usize) + 4;
2801        for _ in 0..num_long_term_ref_pics_sps {
2802            let _lt_ref_pic_poc_lsb_sps = br.read_bits(lsb_bits)?;
2803            let _used_by_curr_pic_lt_sps_flag = br.read_bits(1)?;
2804        }
2805    }
2806    let sps_temporal_mvp_enabled_flag = br.read_bits(1)? == 1;
2807    let strong_intra_smoothing_enabled_flag = br.read_bits(1)? == 1;
2808    // vui / extension — stop here.
2809
2810    let chroma_array_type = if separate_colour_plane_flag {
2811        0
2812    } else {
2813        chroma_format_idc
2814    };
2815    let (sub_w, sub_h) = match chroma_array_type {
2816        0 => (1u32, 1u32),
2817        1 => (2, 2),
2818        2 => (2, 1),
2819        3 => (1, 1),
2820        _ => (1, 1),
2821    };
2822    let width = pic_width.saturating_sub(sub_w.saturating_mul(cl.saturating_add(cr)));
2823    let height = pic_height.saturating_sub(sub_h.saturating_mul(ct.saturating_add(cb)));
2824
2825    Some(HevcSpsInfo {
2826        sps_video_parameter_set_id,
2827        sps_seq_parameter_set_id,
2828        sps_max_sub_layers_minus1,
2829        sps_temporal_id_nesting_flag,
2830        chroma_format_idc,
2831        separate_colour_plane_flag,
2832        bit_depth_luma: bit_depth_luma_m8 as u8 + 8,
2833        bit_depth_chroma: bit_depth_chroma_m8 as u8 + 8,
2834        width: Some(width),
2835        height: Some(height),
2836        conf_win_left_offset: cl,
2837        conf_win_right_offset: cr,
2838        conf_win_top_offset: ct,
2839        conf_win_bottom_offset: cb,
2840        log2_max_pic_order_cnt_lsb_minus4,
2841        log2_min_luma_coding_block_size_minus3,
2842        log2_diff_max_min_luma_coding_block_size,
2843        log2_min_luma_transform_block_size_minus2,
2844        log2_diff_max_min_luma_transform_block_size,
2845        max_transform_hierarchy_depth_inter,
2846        max_transform_hierarchy_depth_intra,
2847        scaling_list_enabled_flag,
2848        sps_sub_layer_ordering_info_present_flag,
2849        amp_enabled_flag,
2850        sample_adaptive_offset_enabled_flag,
2851        pcm_enabled_flag,
2852        pcm_loop_filter_disabled_flag,
2853        num_short_term_ref_pic_sets,
2854        long_term_ref_pics_present_flag,
2855        sps_temporal_mvp_enabled_flag,
2856        strong_intra_smoothing_enabled_flag,
2857        profile_idc,
2858        level_idc,
2859        tier_flag,
2860        max_dec_pic_buffering_minus1,
2861        max_num_reorder_pics,
2862        max_latency_increase_plus1,
2863        profile_compatibility_flags,
2864    })
2865}
2866
2867/// Skip HEVC scaling_list_data() syntax — §7.3.4. Four size IDs,
2868/// each size 4..=64 depending on sizeId + matrixId. For Std SPS
2869/// construction we skip the values; they're only needed when we
2870/// convey them in StdVideoH265ScalingLists (not currently wired).
2871fn skip_hevc_scaling_list_data(br: &mut BitReader) -> Option<()> {
2872    for size_id in 0..4 {
2873        let matrix_count = if size_id == 3 { 2 } else { 6 };
2874        for _matrix_id in 0..matrix_count {
2875            let scaling_list_pred_mode_flag = br.read_bits(1)? == 1;
2876            if !scaling_list_pred_mode_flag {
2877                let _scaling_list_pred_matrix_id_delta = br.read_ue()?;
2878            } else {
2879                let coef_num: usize = (1 << (4 + (size_id << 1))).min(64);
2880                if size_id > 1 {
2881                    let _scaling_list_dc_coef_minus8 = br.read_se()?;
2882                }
2883                for _ in 0..coef_num {
2884                    let _scaling_list_delta_coef = br.read_se()?;
2885                }
2886            }
2887        }
2888    }
2889    Some(())
2890}
2891
2892/// Skip HEVC short_term_ref_pic_set(stRpsIdx) — §7.3.7. Complex;
2893/// we advance past the bits without populating state (we don't
2894/// need the values to build Std SPS).
2895fn skip_hevc_short_term_rps(br: &mut BitReader, st_rps_idx: u8, num_st_rps: u8) -> Option<()> {
2896    let inter_ref_pic_set_prediction_flag = if st_rps_idx != 0 {
2897        br.read_bits(1)? == 1
2898    } else {
2899        false
2900    };
2901    if inter_ref_pic_set_prediction_flag {
2902        if st_rps_idx == num_st_rps {
2903            let _delta_idx_minus1 = br.read_ue()?;
2904        }
2905        let _delta_rps_sign = br.read_bits(1)?;
2906        let _abs_delta_rps_minus1 = br.read_ue()?;
2907        // Per spec, NumDeltaPocs[RefRpsIdx] — we don't track that.
2908        // Approximation: assume up to 16 entries; each entry is
2909        // 1-2 bits. This works for typical streams but is a
2910        // known gap. A production parser needs real state tracking.
2911        for _ in 0..16 {
2912            let used = br.read_bits(1)?;
2913            if used == 0 {
2914                let _use_delta_flag = br.read_bits(1)?;
2915            }
2916        }
2917    } else {
2918        let num_negative_pics = br.read_ue()?;
2919        let num_positive_pics = br.read_ue()?;
2920        for _ in 0..num_negative_pics {
2921            let _delta_poc_s0_minus1 = br.read_ue()?;
2922            let _used_by_curr_pic_s0_flag = br.read_bits(1)?;
2923        }
2924        for _ in 0..num_positive_pics {
2925            let _delta_poc_s1_minus1 = br.read_ue()?;
2926            let _used_by_curr_pic_s1_flag = br.read_bits(1)?;
2927        }
2928    }
2929    Some(())
2930}
2931
2932/// Parse the HEVC VPS (NAL type 32). Minimum fields for Std VPS.
2933pub fn parse_h265_vps(sample: &[u8]) -> Option<H265VpsInfo> {
2934    let nal = find_hevc_nal_by_type(sample, 32)?;
2935    let rbsp = remove_h264_rbsp_stuffing(nal);
2936    let mut br = BitReader::new(&rbsp);
2937    let vps_video_parameter_set_id = br.read_bits(4)? as u8;
2938    let _vps_base_layer_internal_flag = br.read_bits(1)?;
2939    let _vps_base_layer_available_flag = br.read_bits(1)?;
2940    let _vps_max_layers_minus1 = br.read_bits(6)?;
2941    let vps_max_sub_layers_minus1 = br.read_bits(3)? as u8;
2942    let vps_temporal_id_nesting_flag = br.read_bits(1)? == 1;
2943    let _vps_reserved_0xffff_16bits = br.read_bits(16)?;
2944    // profile_tier_level — reuse the pattern. We only need profile/
2945    // tier/level for the Std VPS + for our own info.
2946    let _gp_space = br.read_bits(2)?;
2947    let tier_flag = br.read_bits(1)? == 1;
2948    let profile_idc = br.read_bits(5)? as u8;
2949    let _ = br.read_bits(32)?; // profile_compatibility_flag
2950    let _ = br.read_bits(48)?; // constraint flags
2951    let level_idc = br.read_bits(8)? as u8;
2952    Some(H265VpsInfo {
2953        vps_video_parameter_set_id,
2954        vps_max_sub_layers_minus1,
2955        vps_temporal_id_nesting_flag,
2956        profile_idc,
2957        level_idc,
2958        tier_flag,
2959    })
2960}
2961
2962/// Parse the HEVC PPS (NAL type 34). Subset needed for Std PPS.
2963pub fn parse_h265_pps(sample: &[u8]) -> Option<H265PpsInfo> {
2964    let nal = find_hevc_nal_by_type(sample, 34)?;
2965    let rbsp = remove_h264_rbsp_stuffing(nal);
2966    let mut br = BitReader::new(&rbsp);
2967    let pps_pic_parameter_set_id = br.read_ue()? as u8;
2968    let pps_seq_parameter_set_id = br.read_ue()? as u8;
2969    let dependent_slice_segments_enabled_flag = br.read_bits(1)? == 1;
2970    let output_flag_present_flag = br.read_bits(1)? == 1;
2971    let num_extra_slice_header_bits = br.read_bits(3)? as u8;
2972    let sign_data_hiding_enabled_flag = br.read_bits(1)? == 1;
2973    let cabac_init_present_flag = br.read_bits(1)? == 1;
2974    let num_ref_idx_l0_default_active_minus1 = br.read_ue()? as u8;
2975    let num_ref_idx_l1_default_active_minus1 = br.read_ue()? as u8;
2976    let init_qp_minus26 = clamp_to_i8(br.read_se()?);
2977    let constrained_intra_pred_flag = br.read_bits(1)? == 1;
2978    let transform_skip_enabled_flag = br.read_bits(1)? == 1;
2979    let cu_qp_delta_enabled_flag = br.read_bits(1)? == 1;
2980    let diff_cu_qp_delta_depth = if cu_qp_delta_enabled_flag {
2981        br.read_ue()? as u8
2982    } else {
2983        0
2984    };
2985    let pps_cb_qp_offset = clamp_to_i8(br.read_se()?);
2986    let pps_cr_qp_offset = clamp_to_i8(br.read_se()?);
2987    let pps_slice_chroma_qp_offsets_present_flag = br.read_bits(1)? == 1;
2988    let weighted_pred_flag = br.read_bits(1)? == 1;
2989    let weighted_bipred_flag = br.read_bits(1)? == 1;
2990    let transquant_bypass_enabled_flag = br.read_bits(1)? == 1;
2991    let tiles_enabled_flag = br.read_bits(1)? == 1;
2992    let entropy_coding_sync_enabled_flag = br.read_bits(1)? == 1;
2993
2994    // ─── Past the original parse boundary (§7.3.2.3 continuation) ───
2995    // Tile layout — only present when tiles_enabled_flag.
2996    // Defaults below model the single-tile-spanning-frame case, which
2997    // is what the Vulkan Std PPS needs when tiles are disabled.
2998    let mut num_tile_columns_minus1 = 0u8;
2999    let mut num_tile_rows_minus1 = 0u8;
3000    let mut uniform_spacing_flag = true;
3001    let mut loop_filter_across_tiles_enabled_flag = true;
3002    if tiles_enabled_flag {
3003        num_tile_columns_minus1 = br.read_ue().unwrap_or(0) as u8;
3004        num_tile_rows_minus1 = br.read_ue().unwrap_or(0) as u8;
3005        uniform_spacing_flag = br.read_bits(1).unwrap_or(1) == 1;
3006        if !uniform_spacing_flag {
3007            // column_width_minus1[0..num_tile_columns_minus1] + row_height_minus1[]
3008            // — we skip but must advance the bit cursor exactly.
3009            for _ in 0..num_tile_columns_minus1 {
3010                let _ = br.read_ue();
3011            }
3012            for _ in 0..num_tile_rows_minus1 {
3013                let _ = br.read_ue();
3014            }
3015        }
3016        loop_filter_across_tiles_enabled_flag = br.read_bits(1).unwrap_or(1) == 1;
3017    }
3018    let pps_loop_filter_across_slices_enabled_flag = br.read_bits(1)? == 1;
3019
3020    // Deblocking control
3021    let deblocking_filter_control_present_flag = br.read_bits(1)? == 1;
3022    let mut deblocking_filter_override_enabled_flag = false;
3023    let mut pps_deblocking_filter_disabled_flag = false;
3024    let mut pps_beta_offset_div2 = 0i8;
3025    let mut pps_tc_offset_div2 = 0i8;
3026    if deblocking_filter_control_present_flag {
3027        deblocking_filter_override_enabled_flag = br.read_bits(1)? == 1;
3028        pps_deblocking_filter_disabled_flag = br.read_bits(1)? == 1;
3029        if !pps_deblocking_filter_disabled_flag {
3030            pps_beta_offset_div2 = clamp_to_i8(br.read_se()?);
3031            pps_tc_offset_div2 = clamp_to_i8(br.read_se()?);
3032        }
3033    }
3034
3035    // Scaling list
3036    let pps_scaling_list_data_present_flag = br.read_bits(1)? == 1;
3037    // If present, scaling_list_data() is a sub-syntax we skip —
3038    // the Vulkan Std PPS exposes scaling lists via pScalingLists
3039    // which we leave null for now (FFmpeg populates; we don't
3040    // and accept the silent driver fallback risk until a scaling-
3041    // list builder is wired).
3042
3043    let lists_modification_present_flag = br.read_bits(1)? == 1;
3044    let log2_parallel_merge_level_minus2 = br.read_ue().unwrap_or(0) as u8;
3045    let slice_segment_header_extension_present_flag = br.read_bits(1)? == 1;
3046    let pps_extension_present_flag = br.read_bits(1).unwrap_or(0) == 1;
3047
3048    Some(H265PpsInfo {
3049        pps_pic_parameter_set_id,
3050        pps_seq_parameter_set_id,
3051        dependent_slice_segments_enabled_flag,
3052        output_flag_present_flag,
3053        num_extra_slice_header_bits,
3054        sign_data_hiding_enabled_flag,
3055        cabac_init_present_flag,
3056        num_ref_idx_l0_default_active_minus1,
3057        num_ref_idx_l1_default_active_minus1,
3058        init_qp_minus26,
3059        constrained_intra_pred_flag,
3060        transform_skip_enabled_flag,
3061        cu_qp_delta_enabled_flag,
3062        diff_cu_qp_delta_depth,
3063        pps_cb_qp_offset,
3064        pps_cr_qp_offset,
3065        pps_slice_chroma_qp_offsets_present_flag,
3066        weighted_pred_flag,
3067        weighted_bipred_flag,
3068        transquant_bypass_enabled_flag,
3069        tiles_enabled_flag,
3070        entropy_coding_sync_enabled_flag,
3071        num_tile_columns_minus1,
3072        num_tile_rows_minus1,
3073        uniform_spacing_flag,
3074        loop_filter_across_tiles_enabled_flag,
3075        pps_loop_filter_across_slices_enabled_flag,
3076        deblocking_filter_control_present_flag,
3077        deblocking_filter_override_enabled_flag,
3078        pps_deblocking_filter_disabled_flag,
3079        pps_beta_offset_div2,
3080        pps_tc_offset_div2,
3081        pps_scaling_list_data_present_flag,
3082        lists_modification_present_flag,
3083        log2_parallel_merge_level_minus2,
3084        slice_segment_header_extension_present_flag,
3085        pps_extension_present_flag,
3086    })
3087}
3088
3089/// Parse the HEVC slice header — subset needed for StdVideoDecodeH265PictureInfo.
3090/// `sps` / `pps` provide context for bit-width of POC lsb and branch
3091/// predicates.
3092pub fn parse_h265_slice_header(
3093    sample: &[u8],
3094    sps: &HevcSpsInfo,
3095    pps: &H265PpsInfo,
3096) -> Option<H265SliceHeader> {
3097    let (nal_unit_type, rbsp) = find_hevc_slice_nal(sample)?;
3098    let mut br = BitReader::new(&rbsp);
3099    let first_slice_segment_in_pic_flag = br.read_bits(1)? == 1;
3100    let is_irap = (16..=23).contains(&nal_unit_type);
3101    let is_idr = matches!(nal_unit_type, 19 | 20);
3102    if is_irap {
3103        let _no_output_of_prior_pics_flag = br.read_bits(1)?;
3104    }
3105    let slice_pic_parameter_set_id = br.read_ue()? as u8;
3106    let dependent_slice_segment_flag =
3107        if !first_slice_segment_in_pic_flag && pps.dependent_slice_segments_enabled_flag {
3108            br.read_bits(1)? == 1
3109        } else {
3110            false
3111        };
3112    if !first_slice_segment_in_pic_flag {
3113        // slice_segment_address — ceil(log2(PicSizeInCtbsY)) bits.
3114        // For our purposes this is a skip; we don't need the value.
3115        // Conservative upper bound: 32 bits. In practice streams don't
3116        // have streams this large. If this bit width is wrong, the
3117        // rest of our parse will be misaligned — which is why we
3118        // bail early on non-first-slice headers for now.
3119        return None;
3120    }
3121    let _ = dependent_slice_segment_flag;
3122    // num_extra_slice_header_bits
3123    for _ in 0..pps.num_extra_slice_header_bits {
3124        let _ = br.read_bits(1)?;
3125    }
3126    let slice_type_code = br.read_ue()?;
3127    let slice_type = H265SliceType::from_ue(slice_type_code)?;
3128    if pps.output_flag_present_flag {
3129        let _pic_output_flag = br.read_bits(1)?;
3130    }
3131    if sps.separate_colour_plane_flag {
3132        let _colour_plane_id = br.read_bits(2)?;
3133    }
3134
3135    let (pic_order_cnt_lsb, short_term_ref_pic_set_sps_flag, short_term_ref_pic_set_idx) =
3136        if !is_idr {
3137            let lsb_bits = (sps.log2_max_pic_order_cnt_lsb_minus4 as usize) + 4;
3138            let lsb = br.read_bits(lsb_bits)?;
3139            let sps_flag = br.read_bits(1)? == 1;
3140            let idx = if sps_flag {
3141                if sps.num_short_term_ref_pic_sets > 1 {
3142                    let bits =
3143                        ((sps.num_short_term_ref_pic_sets as f64).log2().ceil() as usize).max(1);
3144                    Some(br.read_bits(bits)? as u8)
3145                } else {
3146                    Some(0)
3147                }
3148            } else {
3149                None
3150            };
3151            (lsb, sps_flag, idx)
3152        } else {
3153            (0, false, None)
3154        };
3155
3156    Some(H265SliceHeader {
3157        first_slice_segment_in_pic_flag,
3158        nal_unit_type,
3159        slice_pic_parameter_set_id,
3160        slice_type,
3161        pic_order_cnt_lsb,
3162        short_term_ref_pic_set_sps_flag,
3163        short_term_ref_pic_set_idx,
3164        is_irap,
3165        is_idr,
3166    })
3167}
3168
3169/// Find the first HEVC NAL with `nal_unit_type == target` in `data`.
3170fn find_hevc_nal_by_type(data: &[u8], target: u8) -> Option<&[u8]> {
3171    let mut i = 0;
3172    while i + 4 < data.len() {
3173        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3174            (3, i + 3)
3175        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
3176            (4, i + 4)
3177        } else {
3178            i += 1;
3179            continue;
3180        };
3181        if nal_byte + 1 >= data.len() {
3182            return None;
3183        }
3184        let nal_unit_type = (data[nal_byte] >> 1) & 0x3F;
3185        if nal_unit_type == target {
3186            let start = nal_byte + 2; // 2-byte NAL header
3187            let end = find_next_start_code(&data[start..])
3188                .map(|off| start + off)
3189                .unwrap_or(data.len());
3190            return Some(&data[start..end]);
3191        }
3192        i += start_len;
3193    }
3194    None
3195}
3196
3197/// Scan an Annex-B HEVC sample and return the offset, in bytes from
3198/// the start of `data`, where the first coded-slice NAL begins (the
3199/// byte AFTER the start code). Vulkan `slice_segment_offsets` wants
3200/// offsets to NAL-unit first bytes, not to start codes.
3201pub fn hevc_first_slice_nal_offset(data: &[u8]) -> Option<u32> {
3202    let mut i = 0;
3203    while i + 4 < data.len() {
3204        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3205            (3usize, i + 3)
3206        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
3207            (4usize, i + 4)
3208        } else {
3209            i += 1;
3210            continue;
3211        };
3212        if nal_byte + 1 >= data.len() {
3213            return None;
3214        }
3215        let t = (data[nal_byte] >> 1) & 0x3F;
3216        if (0..=9).contains(&t) || (16..=23).contains(&t) {
3217            return Some(nal_byte as u32);
3218        }
3219        i += start_len;
3220    }
3221    None
3222}
3223
3224/// Scan an Annex-B H.264 sample for the first coded-slice NAL
3225/// (types 1 / 5 / 19) and return its byte offset within `data`.
3226/// Parallel to `hevc_first_slice_nal_offset`.
3227pub fn h264_first_slice_nal_offset(data: &[u8]) -> Option<u32> {
3228    let mut i = 0;
3229    while i + 4 < data.len() {
3230        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3231            (3usize, i + 3)
3232        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
3233            (4usize, i + 4)
3234        } else {
3235            i += 1;
3236            continue;
3237        };
3238        if nal_byte >= data.len() {
3239            return None;
3240        }
3241        let t = data[nal_byte] & 0x1F;
3242        if matches!(t, 1 | 5 | 19) {
3243            return Some(nal_byte as u32);
3244        }
3245        i += start_len;
3246    }
3247    None
3248}
3249
3250/// Find the first HEVC coded-slice NAL: types 0..=9 (regular slices)
3251/// or 16..=23 (IRAP slices). Returns (nal_unit_type, RBSP bytes).
3252fn find_hevc_slice_nal(data: &[u8]) -> Option<(u8, Vec<u8>)> {
3253    let mut i = 0;
3254    while i + 4 < data.len() {
3255        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3256            (3, i + 3)
3257        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
3258            (4, i + 4)
3259        } else {
3260            i += 1;
3261            continue;
3262        };
3263        if nal_byte + 1 >= data.len() {
3264            return None;
3265        }
3266        let t = (data[nal_byte] >> 1) & 0x3F;
3267        if (0..=9).contains(&t) || (16..=23).contains(&t) {
3268            let start = nal_byte + 2;
3269            let end = find_next_start_code(&data[start..])
3270                .map(|off| start + off)
3271                .unwrap_or(data.len());
3272            return Some((t, remove_h264_rbsp_stuffing(&data[start..end])));
3273        }
3274        i += start_len;
3275    }
3276    None
3277}
3278
3279/// MPEG-2 sequence header scan — ISO/IEC 13818-2 §6.2.2.1 (sequence
3280/// header, start code `00 00 01 B3`) + §6.2.2.3 (sequence extension,
3281/// start code `00 00 01 B5` with `extension_start_code_identifier==1`).
3282///
3283/// The sequence header carries 12-bit `horizontal_size_value` and
3284/// `vertical_size_value`, tight for sizes ≤ 4095. The optional sequence
3285/// extension prepends 2-bit `_extension` fields that, when combined,
3286/// bring the total to 14 bits (sizes ≤ 16383). Pure MPEG-1 (start code
3287/// 0xB3 only, no 0xB5) never has the extension and stays 12-bit.
3288pub fn parse_mpeg2_sequence_header(sample: &[u8]) -> Option<Mpeg2SeqInfo> {
3289    // Walk bytes looking for 00 00 01 B3 (sequence_header_code). The
3290    // following 3 bytes carry horizontal(12) + vertical(12).
3291    let seq_hdr_start = find_mpeg2_start_code(sample, 0xB3)?;
3292    let hdr_body_off = seq_hdr_start + 4;
3293    if hdr_body_off + 3 > sample.len() {
3294        return None;
3295    }
3296    let b = &sample[hdr_body_off..hdr_body_off + 3];
3297    let mut width = (((b[0] as u32) << 4) | ((b[1] as u32) >> 4)) & 0x0FFF;
3298    let mut height = (((b[1] as u32 & 0x0F) << 8) | (b[2] as u32)) & 0x0FFF;
3299
3300    // Look for a subsequent sequence_extension that upgrades the 12-bit
3301    // values to 14-bit. Only scan forward from seq_hdr_start; a
3302    // sequence_extension before the first sequence_header is
3303    // nonsensical and we shouldn't confuse the parse.
3304    let search_from = hdr_body_off + 3;
3305    if search_from < sample.len()
3306        && let Some(ext_start) = find_mpeg2_start_code(&sample[search_from..], 0xB5)
3307    {
3308        let ext_body_off = search_from + ext_start + 4;
3309        if ext_body_off + 3 <= sample.len() {
3310            let mut br = BitReader::new(&sample[ext_body_off..]);
3311            if let Some(id) = br.read_bits(4)
3312                && id == 1
3313            {
3314                // sequence_extension §6.2.2.3:
3315                //   extension_start_code_identifier  u(4) = 0001   (already read)
3316                //   profile_and_level_indication     u(8)
3317                //   progressive_sequence             u(1)
3318                //   chroma_format                    u(2)
3319                //   horizontal_size_extension        u(2)
3320                //   vertical_size_extension          u(2)
3321                let _profile_level = br.read_bits(8)?;
3322                let _progressive = br.read_bits(1)?;
3323                let _chroma = br.read_bits(2)?;
3324                let h_ext = br.read_bits(2)?;
3325                let v_ext = br.read_bits(2)?;
3326                width |= h_ext << 12;
3327                height |= v_ext << 12;
3328            }
3329        }
3330    }
3331
3332    if width == 0 || height == 0 {
3333        return None;
3334    }
3335    Some(Mpeg2SeqInfo { width, height })
3336}
3337
3338/// Scan for an MPEG-2 start code (0x00 0x00 0x01 <target>) byte-aligned.
3339/// Returns the file offset of the leading 0x00 on success.
3340fn find_mpeg2_start_code(data: &[u8], target: u8) -> Option<usize> {
3341    let mut i = 0;
3342    while i + 4 <= data.len() {
3343        if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 && data[i + 3] == target {
3344            return Some(i);
3345        }
3346        i += 1;
3347    }
3348    None
3349}
3350
3351// ─── H.264 PPS parse (Vulkan Video + slice-header support) ────────
3352//
3353// Vulkan Video H.264 decode requires the app to build a
3354// `StdVideoH264PictureParameterSet` from the PPS NAL (type 8) — the
3355// driver does not parse bitstreams. Every field below lands in the
3356// Std header struct; the flags pack into bitfields per the Std video
3357// spec. See ITU-T H.264 §7.3.2.2 + §7.4.2.2.
3358
3359/// Parsed H.264 PPS fields. Consumers: Vulkan Video decoder (fills
3360/// `StdVideoH264PictureParameterSet`), slice-header parser (needs
3361/// `bottom_field_pic_order_in_frame_present_flag` +
3362/// `redundant_pic_cnt_present_flag` as branching predicates).
3363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3364pub struct H264PpsInfo {
3365    pub pic_parameter_set_id: u8,
3366    pub seq_parameter_set_id: u8,
3367    pub entropy_coding_mode_flag: bool,
3368    /// Aka `pic_order_present_flag` in older spec editions. Controls
3369    /// whether slice headers carry `delta_pic_order_cnt_bottom` and
3370    /// `delta_pic_order_cnt[1]`.
3371    pub bottom_field_pic_order_in_frame_present_flag: bool,
3372    pub num_slice_groups_minus1: u8,
3373    pub num_ref_idx_l0_default_active_minus1: u8,
3374    pub num_ref_idx_l1_default_active_minus1: u8,
3375    pub weighted_pred_flag: bool,
3376    pub weighted_bipred_idc: u8,
3377    pub pic_init_qp_minus26: i8,
3378    pub pic_init_qs_minus26: i8,
3379    pub chroma_qp_index_offset: i8,
3380    pub deblocking_filter_control_present_flag: bool,
3381    pub constrained_intra_pred_flag: bool,
3382    pub redundant_pic_cnt_present_flag: bool,
3383    /// Extended fields — present only when the PPS RBSP has trailing
3384    /// data beyond the baseline syntax. All three were added in the
3385    /// 2005 amendment alongside High profile.
3386    pub transform_8x8_mode_flag: Option<bool>,
3387    pub pic_scaling_matrix_present_flag: Option<bool>,
3388    pub second_chroma_qp_index_offset: Option<i8>,
3389}
3390
3391/// Walk an Annex-B sample looking for the first NAL of type 8 (PPS)
3392/// and decode its syntax elements. Returns `None` when no PPS is in
3393/// the sample or the syntax is truncated before
3394/// `redundant_pic_cnt_present_flag` (the last required field).
3395///
3396/// The FMO (Flexible Macroblock Ordering) sub-branches for
3397/// `num_slice_groups_minus1 > 0` / `slice_group_map_type`=0/2/3..5/6
3398/// are skipped correctly but not reported — no consumer today needs
3399/// the slice-group map (FMO is forbidden in Main and High profiles,
3400/// and every stream our decoder touches is Main/High).
3401pub fn parse_h264_pps(sample: &[u8]) -> Option<H264PpsInfo> {
3402    let pps = find_h264_nal_by_type(sample, 8)?;
3403    let rbsp = remove_h264_rbsp_stuffing(pps);
3404    let mut br = BitReader::new(&rbsp);
3405
3406    let pic_parameter_set_id = br.read_ue()? as u8;
3407    let seq_parameter_set_id = br.read_ue()? as u8;
3408    let entropy_coding_mode_flag = br.read_bits(1)? == 1;
3409    let bottom_field_pic_order_in_frame_present_flag = br.read_bits(1)? == 1;
3410
3411    let num_slice_groups_minus1 = br.read_ue()?;
3412    if num_slice_groups_minus1 > 0 {
3413        // FMO sub-branches — skip.
3414        let slice_group_map_type = br.read_ue()?;
3415        match slice_group_map_type {
3416            0 => {
3417                for _ in 0..=num_slice_groups_minus1 {
3418                    let _run_length_minus1 = br.read_ue()?;
3419                }
3420            }
3421            2 => {
3422                for _ in 0..num_slice_groups_minus1 {
3423                    let _top_left = br.read_ue()?;
3424                    let _bottom_right = br.read_ue()?;
3425                }
3426            }
3427            3..=5 => {
3428                let _slice_group_change_direction_flag = br.read_bits(1)?;
3429                let _slice_group_change_rate_minus1 = br.read_ue()?;
3430            }
3431            6 => {
3432                let pic_size_in_map_units_minus1 = br.read_ue()?;
3433                let bits = ((num_slice_groups_minus1 + 1) as f64).log2().ceil() as usize;
3434                let bits = bits.max(1);
3435                for _ in 0..=pic_size_in_map_units_minus1 {
3436                    let _slice_group_id = br.read_bits(bits)?;
3437                }
3438            }
3439            _ => {}
3440        }
3441    }
3442
3443    let num_ref_idx_l0_default_active_minus1 = br.read_ue()? as u8;
3444    let num_ref_idx_l1_default_active_minus1 = br.read_ue()? as u8;
3445    let weighted_pred_flag = br.read_bits(1)? == 1;
3446    let weighted_bipred_idc = br.read_bits(2)? as u8;
3447    let pic_init_qp_minus26 = clamp_to_i8(br.read_se()?);
3448    let pic_init_qs_minus26 = clamp_to_i8(br.read_se()?);
3449    let chroma_qp_index_offset = clamp_to_i8(br.read_se()?);
3450    let deblocking_filter_control_present_flag = br.read_bits(1)? == 1;
3451    let constrained_intra_pred_flag = br.read_bits(1)? == 1;
3452    let redundant_pic_cnt_present_flag = br.read_bits(1)? == 1;
3453
3454    // Extended fields — present only when more_rbsp_data() indicates
3455    // the PPS carried them. Detect by checking if any bits remain
3456    // beyond the rbsp_trailing_bits stop. We do a best-effort read:
3457    // fill from Some(...) on success, fall back to None if the trailer
3458    // runs out mid-field.
3459    let (transform_8x8_mode_flag, pic_scaling_matrix_present_flag, second_chroma_qp_index_offset) =
3460        if more_rbsp_data(&br, &rbsp) {
3461            let t8 = br.read_bits(1).map(|v| v == 1);
3462            let psm = br.read_bits(1).map(|v| v == 1);
3463            // If pic_scaling_matrix_present_flag is set, scaling_list
3464            // blocks follow before second_chroma_qp_index_offset. Skip
3465            // them (conservative — we don't consume these values).
3466            if let Some(true) = psm {
3467                // Number of scaling lists per §7.3.2.2:
3468                //   6 + ((chroma_format_idc != 3) ? 2 : 6) * transform_8x8_mode_flag
3469                // We don't know chroma_format_idc from the PPS alone;
3470                // assume 4:2:0 (most common) → 8 total lists when t8=1.
3471                let count = 6 + if let Some(true) = t8 { 2 } else { 0 };
3472                for i in 0..count {
3473                    if br.read_bits(1) == Some(1) {
3474                        let size = if i < 6 { 16 } else { 64 };
3475                        let mut last_scale: i32 = 8;
3476                        let mut next_scale: i32 = 8;
3477                        for _ in 0..size {
3478                            if next_scale != 0 {
3479                                let delta = br.read_se().unwrap_or(0);
3480                                next_scale = (last_scale + delta + 256).rem_euclid(256);
3481                            }
3482                            if next_scale != 0 {
3483                                last_scale = next_scale;
3484                            }
3485                        }
3486                    }
3487                }
3488            }
3489            let s2 = br.read_se().map(clamp_to_i8);
3490            (t8, psm, s2)
3491        } else {
3492            (None, None, None)
3493        };
3494
3495    Some(H264PpsInfo {
3496        pic_parameter_set_id,
3497        seq_parameter_set_id,
3498        entropy_coding_mode_flag,
3499        bottom_field_pic_order_in_frame_present_flag,
3500        num_slice_groups_minus1: num_slice_groups_minus1.min(u8::MAX as u32) as u8,
3501        num_ref_idx_l0_default_active_minus1,
3502        num_ref_idx_l1_default_active_minus1,
3503        weighted_pred_flag,
3504        weighted_bipred_idc,
3505        pic_init_qp_minus26,
3506        pic_init_qs_minus26,
3507        chroma_qp_index_offset,
3508        deblocking_filter_control_present_flag,
3509        constrained_intra_pred_flag,
3510        redundant_pic_cnt_present_flag,
3511        transform_8x8_mode_flag,
3512        pic_scaling_matrix_present_flag,
3513        second_chroma_qp_index_offset,
3514    })
3515}
3516
3517/// Heuristic more_rbsp_data() check — the spec defines it precisely
3518/// (position in RBSP trailing bits) but needs byte-alignment awareness
3519/// we don't expose from BitReader. Approximation: at least one more
3520/// full byte of input remains after the current cursor. Good enough
3521/// for the PPS extended-field branch since the trailing byte is a
3522/// stop bit + zero pad — parsing a spurious bit from that gives
3523/// `transform_8x8_mode_flag = true` which the caller tolerates.
3524fn more_rbsp_data(br: &BitReader, rbsp: &[u8]) -> bool {
3525    let pos = br.pos;
3526    let total_bits = rbsp.len() * 8;
3527    // We need at least 1 payload bit + the 1-bit stop + up to 7 zero
3528    // pad bits. "More data" = at least 9 bits remain.
3529    total_bits.saturating_sub(pos) > 8
3530}
3531
3532fn clamp_to_i8(v: i32) -> i8 {
3533    v.clamp(i8::MIN as i32, i8::MAX as i32) as i8
3534}
3535
3536/// Slice type name (decoded from `slice_type` ue(v) value). Per
3537/// H.264 §7.4.3 Table 7-6, values 0..=4 are one iteration of the
3538/// slice types; values 5..=9 are the same types but mark "all
3539/// slices in the current picture have this type" (aka
3540/// `slice_type_all_same`). Both halves collapse to the same enum.
3541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3542pub enum H264SliceType {
3543    P,
3544    B,
3545    I,
3546    SP,
3547    SI,
3548}
3549
3550impl H264SliceType {
3551    fn from_ue(v: u32) -> Option<Self> {
3552        match v % 5 {
3553            0 => Some(Self::P),
3554            1 => Some(Self::B),
3555            2 => Some(Self::I),
3556            3 => Some(Self::SP),
3557            4 => Some(Self::SI),
3558            _ => None,
3559        }
3560    }
3561}
3562
3563/// Parsed H.264 slice header — just the fields the Vulkan Video
3564/// decoder + our DPB manager need. See ITU-T H.264 §7.3.3. Full slice
3565/// header has ref_pic_list_modification, weighted_prediction tables,
3566/// dec_ref_pic_marking, etc., which we don't consume (the driver
3567/// re-derives them from the PPS + `StdVideoDecodeH264PictureInfo`
3568/// flags).
3569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3570pub struct H264SliceHeader {
3571    pub first_mb_in_slice: u32,
3572    pub slice_type: H264SliceType,
3573    pub pic_parameter_set_id: u8,
3574    /// From the NAL header: `nal_unit_type == 5` — set by the caller
3575    /// when it picks the NAL to parse. Affects whether `idr_pic_id` is
3576    /// carried.
3577    pub is_idr: bool,
3578    pub frame_num: u32,
3579    /// True when the slice encodes a single field of an interlaced
3580    /// frame (spec: `!frame_mbs_only_flag && field_pic_flag`). False
3581    /// for progressive frames or MBAFF pairs.
3582    pub field_pic_flag: bool,
3583    pub bottom_field_flag: bool,
3584    pub colour_plane_id: Option<u8>,
3585    /// Set when `is_idr`; otherwise `None`.
3586    pub idr_pic_id: Option<u32>,
3587    /// Set when SPS `pic_order_cnt_type == 0`.
3588    pub pic_order_cnt_lsb: Option<u32>,
3589    pub delta_pic_order_cnt_bottom: Option<i32>,
3590    /// Set when SPS `pic_order_cnt_type == 1` and
3591    /// `!delta_pic_order_always_zero_flag`. `[0]` always present in
3592    /// that branch, `[1]` present only when the PPS carried
3593    /// `bottom_field_pic_order_in_frame_present_flag` and we're in a
3594    /// frame (not field) slice.
3595    pub delta_pic_order_cnt: [Option<i32>; 2],
3596}
3597
3598/// Parse the first slice-NAL in `sample`, using the SPS + PPS for
3599/// branch predicates. The NAL header's `nal_unit_type` gates which
3600/// slice types we accept: 1 (non-IDR), 5 (IDR), 19 (auxiliary coded
3601/// slice) all share the same syntax. Returns `None` when the sample
3602/// contains no slice NAL or the SPS/PPS didn't provide the required
3603/// context (e.g., SPS `pic_order_cnt_type` was `None` so we can't
3604/// branch into the POC reads).
3605pub fn parse_h264_slice_header(
3606    sample: &[u8],
3607    sps: &H264SpsInfo,
3608    pps: &H264PpsInfo,
3609) -> Option<H264SliceHeader> {
3610    // nal_unit_type values for coded slices: 1 (non-IDR), 2/3/4
3611    // (partition A/B/C, deprecated), 5 (IDR), 19 (aux). We accept
3612    // 1, 5, 19 — the common cases.
3613    let (nal_type, rbsp) = find_h264_slice_nal(sample)?;
3614    let is_idr = nal_type == 5;
3615
3616    let mut br = BitReader::new(&rbsp);
3617    let first_mb_in_slice = br.read_ue()?;
3618    let slice_type_code = br.read_ue()?;
3619    let slice_type = H264SliceType::from_ue(slice_type_code)?;
3620    let pic_parameter_set_id = br.read_ue()? as u8;
3621
3622    let colour_plane_id = if sps.separate_colour_plane_flag {
3623        Some(br.read_bits(2)? as u8)
3624    } else {
3625        None
3626    };
3627
3628    let frame_num_bits = (sps.log2_max_frame_num_minus4? as usize) + 4;
3629    let frame_num = br.read_bits(frame_num_bits)?;
3630
3631    let (field_pic_flag, bottom_field_flag) = if !sps.frame_mbs_only {
3632        let f = br.read_bits(1)? == 1;
3633        let b = if f { br.read_bits(1)? == 1 } else { false };
3634        (f, b)
3635    } else {
3636        (false, false)
3637    };
3638
3639    let idr_pic_id = if is_idr { Some(br.read_ue()?) } else { None };
3640
3641    let poc_type = sps.pic_order_cnt_type?;
3642    let mut pic_order_cnt_lsb = None;
3643    let mut delta_pic_order_cnt_bottom = None;
3644    let mut delta_pic_order_cnt: [Option<i32>; 2] = [None, None];
3645    match poc_type {
3646        0 => {
3647            let bits = (sps.log2_max_pic_order_cnt_lsb_minus4? as usize) + 4;
3648            pic_order_cnt_lsb = Some(br.read_bits(bits)?);
3649            if pps.bottom_field_pic_order_in_frame_present_flag && !field_pic_flag {
3650                delta_pic_order_cnt_bottom = Some(br.read_se()?);
3651            }
3652        }
3653        1 => {
3654            let always_zero = sps.delta_pic_order_always_zero_flag.unwrap_or(false);
3655            if !always_zero {
3656                delta_pic_order_cnt[0] = Some(br.read_se()?);
3657                if pps.bottom_field_pic_order_in_frame_present_flag && !field_pic_flag {
3658                    delta_pic_order_cnt[1] = Some(br.read_se()?);
3659                }
3660            }
3661        }
3662        2 => { /* implicit POC derivation; no fields */ }
3663        _ => return None,
3664    }
3665
3666    Some(H264SliceHeader {
3667        first_mb_in_slice,
3668        slice_type,
3669        pic_parameter_set_id,
3670        is_idr,
3671        frame_num,
3672        field_pic_flag,
3673        bottom_field_flag,
3674        colour_plane_id,
3675        idr_pic_id,
3676        pic_order_cnt_lsb,
3677        delta_pic_order_cnt_bottom,
3678        delta_pic_order_cnt,
3679    })
3680}
3681
3682/// Find the first coded-slice NAL (nal_unit_type ∈ {1, 5, 19}) in
3683/// `data` and return `(nal_unit_type, rbsp_bytes_with_stuffing_removed)`.
3684fn find_h264_slice_nal(data: &[u8]) -> Option<(u8, Vec<u8>)> {
3685    let mut i = 0;
3686    while i + 4 < data.len() {
3687        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3688            (3, i + 3)
3689        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
3690            (4, i + 4)
3691        } else {
3692            i += 1;
3693            continue;
3694        };
3695        if nal_byte >= data.len() {
3696            return None;
3697        }
3698        let nal_unit_type = data[nal_byte] & 0x1F;
3699        if matches!(nal_unit_type, 1 | 5 | 19) {
3700            let start = nal_byte + 1;
3701            let end = find_next_start_code(&data[start..])
3702                .map(|off| start + off)
3703                .unwrap_or(data.len());
3704            let rbsp = remove_h264_rbsp_stuffing(&data[start..end]);
3705            return Some((nal_unit_type, rbsp));
3706        }
3707        i += start_len;
3708    }
3709    None
3710}
3711
3712/// Generic "find the first Annex-B NAL whose `nal_unit_type` matches
3713/// `target_type`" helper. Factored out of `find_h264_sps` so the PPS
3714/// parser and future consumers (slice header, SEI) share one scanner.
3715fn find_h264_nal_by_type(data: &[u8], target_type: u8) -> Option<&[u8]> {
3716    let mut i = 0;
3717    while i + 4 < data.len() {
3718        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
3719            (3, i + 3)
3720        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
3721            (4, i + 4)
3722        } else {
3723            i += 1;
3724            continue;
3725        };
3726        if nal_byte >= data.len() {
3727            return None;
3728        }
3729        let nal_unit_type = data[nal_byte] & 0x1F;
3730        if nal_unit_type == target_type {
3731            let start = nal_byte + 1;
3732            let end = find_next_start_code(&data[start..])
3733                .map(|off| start + off)
3734                .unwrap_or(data.len());
3735            return Some(&data[start..end]);
3736        }
3737        i += start_len;
3738    }
3739    None
3740}
3741
3742#[cfg(test)]
3743mod tests {
3744    use super::*;
3745
3746    #[test]
3747    fn detects_h264_baseline_yuv420p() {
3748        // Minimal H.264 baseline SPS: profile=66 → spec-forced 4:2:0 8-bit.
3749        let sps_rbsp = vec![
3750            66, // profile_idc = 66 (baseline)
3751            0,  // constraints + reserved
3752            30, // level_idc
3753            // seq_parameter_set_id = ue(0) = 1 bit, value 0 → bit "1"
3754            0b1000_0000,
3755        ];
3756        let mut sample = vec![0, 0, 0, 1, 0x27]; // start code + NAL header (type=7)
3757        sample.extend_from_slice(&sps_rbsp);
3758        let pf = detect_h264(&sample).unwrap();
3759        assert_eq!(pf, PixelFormat::Yuv420p);
3760    }
3761
3762    #[test]
3763    fn empty_samples_returns_default() {
3764        let pf = detect("h264", &[]);
3765        assert_eq!(pf, PixelFormat::Yuv420p);
3766    }
3767
3768    #[test]
3769    fn unknown_codec_returns_default() {
3770        let pf = detect("prores", &[vec![1, 2, 3]]);
3771        assert_eq!(pf, PixelFormat::Yuv420p);
3772    }
3773
3774    #[test]
3775    fn from_chroma_and_depth_420_8bit() {
3776        assert_eq!(
3777            PixelFormat::from_chroma_and_depth(1, 8),
3778            PixelFormat::Yuv420p
3779        );
3780        assert_eq!(
3781            PixelFormat::from_chroma_and_depth(1, 10),
3782            PixelFormat::Yuv420p10le
3783        );
3784        assert_eq!(
3785            PixelFormat::from_chroma_and_depth(2, 8),
3786            PixelFormat::Yuv422p
3787        );
3788        assert_eq!(
3789            PixelFormat::from_chroma_and_depth(3, 8),
3790            PixelFormat::Yuv444p
3791        );
3792    }
3793
3794    #[test]
3795    fn as_ffmpeg_str_matches_python_names() {
3796        assert_eq!(PixelFormat::Yuv420p.as_ffmpeg_str(), "yuv420p");
3797        assert_eq!(PixelFormat::Yuv420p10le.as_ffmpeg_str(), "yuv420p10le");
3798        assert_eq!(PixelFormat::Yuv444p.as_ffmpeg_str(), "yuv444p");
3799    }
3800
3801    // ─── Deep-parse: BitWriter + SPS synthesis helpers ────────────
3802    //
3803    // `BitWriter` mirrors `BitReader` MSB-first layout so synthesised
3804    // samples round-trip through `parse_h264_sps` / `parse_hevc_sps`
3805    // with byte-for-byte fidelity. `write_ue` inverts `read_ue` by
3806    // encoding codeNum `v` as `z` leading zeros (where `z =
3807    // floor(log2(v+1))`) + a `1` marker + `z` suffix bits equal to
3808    // `v + 1 - (1 << z)`.
3809
3810    struct BitWriter {
3811        bytes: Vec<u8>,
3812        bit_pos: usize, // 0..=8 (when ==8 we allocate a fresh byte on next write)
3813    }
3814
3815    impl BitWriter {
3816        fn new() -> Self {
3817            Self {
3818                bytes: Vec::new(),
3819                bit_pos: 8,
3820            }
3821        }
3822        fn write_bit(&mut self, b: u8) {
3823            if self.bit_pos == 8 {
3824                self.bytes.push(0);
3825                self.bit_pos = 0;
3826            }
3827            if b != 0 {
3828                let idx = self.bytes.len() - 1;
3829                self.bytes[idx] |= 1 << (7 - self.bit_pos);
3830            }
3831            self.bit_pos += 1;
3832        }
3833        fn write_bits(&mut self, val: u64, n: usize) {
3834            // u64 is wide enough for every H.264 / HEVC SPS field we
3835            // synthesise (longest contiguous run is the 48-bit HEVC
3836            // profile_tier_level constraint-flags block).
3837            for i in 0..n {
3838                let bit = ((val >> (n - 1 - i)) & 1) as u8;
3839                self.write_bit(bit);
3840            }
3841        }
3842        fn write_ue(&mut self, v: u32) {
3843            let z = if v == 0 { 0 } else { (v + 1).ilog2() as usize };
3844            for _ in 0..z {
3845                self.write_bit(0);
3846            }
3847            self.write_bit(1);
3848            if z > 0 {
3849                let suffix = (v + 1) - (1u32 << z);
3850                self.write_bits(suffix as u64, z);
3851            }
3852        }
3853        fn bytes(self) -> Vec<u8> {
3854            self.bytes
3855        }
3856    }
3857
3858    /// Build a minimal H.264 baseline SPS RBSP for the given dims with
3859    /// no scaling lists, no pic_order_cnt_type==1 branch, no cropping.
3860    /// Profile=66 skips the chroma_format / bit_depth / scaling_matrix
3861    /// block entirely per §7.3.2.1.1. `width_in_mbs` / `height_in_mbs`
3862    /// are the `pic_width_in_mbs_minus1 + 1` and
3863    /// `pic_height_in_map_units_minus1 + 1` values — the helper
3864    /// encodes the minus1 forms on the wire.
3865    fn build_h264_baseline_sps(width_in_mbs: u32, height_in_mbs: u32) -> Vec<u8> {
3866        let mut w = BitWriter::new();
3867        w.write_bits(66, 8); // profile_idc = Baseline
3868        w.write_bits(0, 8); // constraint_set_flags + reserved
3869        w.write_bits(30, 8); // level_idc
3870        w.write_ue(0); // seq_parameter_set_id
3871        w.write_ue(0); // log2_max_frame_num_minus4
3872        w.write_ue(0); // pic_order_cnt_type
3873        w.write_ue(0); // log2_max_pic_order_cnt_lsb_minus4
3874        w.write_ue(1); // max_num_ref_frames
3875        w.write_bit(0); // gaps_in_frame_num_value_allowed_flag
3876        w.write_ue(width_in_mbs - 1); // pic_width_in_mbs_minus1
3877        w.write_ue(height_in_mbs - 1); // pic_height_in_map_units_minus1
3878        w.write_bit(1); // frame_mbs_only_flag
3879        w.write_bit(1); // direct_8x8_inference_flag
3880        w.write_bit(0); // frame_cropping_flag
3881        w.write_bit(0); // vui_parameters_present_flag
3882        w.write_bit(1); // rbsp_trailing_bits stop bit
3883        // zero-align is implicit — trailing bits in partial last byte are 0
3884        let mut sample = vec![0x00, 0x00, 0x00, 0x01, 0x67]; // Annex-B + NAL header type=7
3885        sample.extend_from_slice(&w.bytes());
3886        sample
3887    }
3888
3889    #[test]
3890    fn parse_h264_sps_baseline_1280x720() {
3891        let sample = build_h264_baseline_sps(1280 / 16, 720 / 16);
3892        let info = parse_h264_sps(&sample).expect("parse");
3893        assert_eq!(info.profile_idc, 66);
3894        assert_eq!(info.chroma_format_idc, 1); // spec-forced for Baseline
3895        assert_eq!(info.width, Some(1280));
3896        assert_eq!(info.height, Some(720));
3897        assert!(info.frame_mbs_only);
3898    }
3899
3900    #[test]
3901    fn parse_h264_sps_baseline_640x480() {
3902        let sample = build_h264_baseline_sps(640 / 16, 480 / 16);
3903        let info = parse_h264_sps(&sample).expect("parse");
3904        assert_eq!(info.width, Some(640));
3905        assert_eq!(info.height, Some(480));
3906    }
3907
3908    #[test]
3909    fn parse_h264_sps_with_cropping_1920x1080() {
3910        // 1920×1088 coded → cropped to 1920×1080 via crop_bottom=4 chroma
3911        // samples (SubHeightC=2, CropUnitY=2 → 8 luma samples cropped).
3912        let mut w = BitWriter::new();
3913        w.write_bits(66, 8);
3914        w.write_bits(0, 8);
3915        w.write_bits(40, 8);
3916        w.write_ue(0);
3917        w.write_ue(0);
3918        w.write_ue(0);
3919        w.write_ue(0);
3920        w.write_ue(1);
3921        w.write_bit(0);
3922        w.write_ue(1920 / 16 - 1); // pic_width_in_mbs_minus1
3923        w.write_ue(1088 / 16 - 1); // pic_height_in_map_units_minus1
3924        w.write_bit(1); // frame_mbs_only_flag
3925        w.write_bit(1); // direct_8x8_inference_flag
3926        w.write_bit(1); // frame_cropping_flag
3927        w.write_ue(0); // frame_crop_left_offset
3928        w.write_ue(0); // frame_crop_right_offset
3929        w.write_ue(0); // frame_crop_top_offset
3930        w.write_ue(4); // frame_crop_bottom_offset (chroma samples)
3931        w.write_bit(0); // vui_parameters_present_flag
3932        w.write_bit(1); // rbsp trailing stop bit
3933        let mut sample = vec![0, 0, 0, 1, 0x67];
3934        sample.extend_from_slice(&w.bytes());
3935        let info = parse_h264_sps(&sample).expect("parse");
3936        assert_eq!(info.width, Some(1920));
3937        assert_eq!(info.height, Some(1080));
3938    }
3939
3940    #[test]
3941    fn parse_h264_sps_high_profile_422_returns_chroma_even_without_dims() {
3942        // Profile=122 (High 4:2:2) gates chroma_format_idc=2. We don't
3943        // synthesise scaling lists or the rest of the SPS (would be
3944        // significantly larger), so width/height come back as None but
3945        // chroma_format_idc must still be populated — this is the
3946        // contract that `decode::h264`'s reject path depends on.
3947        let mut w = BitWriter::new();
3948        w.write_bits(122, 8); // profile_idc = High 4:2:2
3949        w.write_bits(0, 8);
3950        w.write_bits(40, 8);
3951        w.write_ue(0); // sps_id
3952        w.write_ue(2); // chroma_format_idc = 4:2:2
3953        w.write_ue(0); // bit_depth_luma_minus8
3954        w.write_ue(0); // bit_depth_chroma_minus8
3955        w.write_bit(0); // qpprime_y_zero_transform_bypass_flag
3956        w.write_bit(0); // seq_scaling_matrix_present_flag
3957        // Truncate here — the remainder would be log2_max_frame_num etc
3958        // but this is enough for the chroma-reject contract to hold.
3959        let mut sample = vec![0, 0, 0, 1, 0x67];
3960        sample.extend_from_slice(&w.bytes());
3961        let info = parse_h264_sps(&sample).expect("parse");
3962        assert_eq!(info.profile_idc, 122);
3963        assert_eq!(info.chroma_format_idc, 2);
3964        // width/height may be None here — we truncated the SPS; that's OK.
3965    }
3966
3967    /// Build a minimal HEVC SPS at given pic_width / pic_height with
3968    /// chroma_format_idc=1 (4:2:0), bit_depth=8, no conformance window.
3969    /// profile_tier_level is a default Main 8-bit profile with
3970    /// max_sub_layers_minus1=0 (no sub-layer loop).
3971    fn build_hevc_sps(pic_width: u32, pic_height: u32) -> Vec<u8> {
3972        build_hevc_sps_full(pic_width, pic_height, false, 0, 0, 0, 0)
3973    }
3974
3975    fn build_hevc_sps_full(
3976        pic_width: u32,
3977        pic_height: u32,
3978        conformance_window: bool,
3979        cwl: u32,
3980        cwr: u32,
3981        cwt: u32,
3982        cwb: u32,
3983    ) -> Vec<u8> {
3984        let mut w = BitWriter::new();
3985        w.write_bits(0, 4); // sps_video_parameter_set_id
3986        w.write_bits(0, 3); // sps_max_sub_layers_minus1 = 0
3987        w.write_bits(1, 1); // sps_temporal_id_nesting_flag
3988        w.write_bits(0b0_0_00001, 8); // profile_space=0, tier=0, profile_idc=1 (Main)
3989        w.write_bits(0x40000000, 32); // profile_compatibility_flag[32]
3990        w.write_bits(0, 48); // constraint flags
3991        w.write_bits(93, 8); // general_level_idc
3992
3993        w.write_ue(0); // sps_seq_parameter_set_id
3994        w.write_ue(1); // chroma_format_idc = 4:2:0
3995        w.write_ue(pic_width);
3996        w.write_ue(pic_height);
3997        if conformance_window {
3998            w.write_bit(1);
3999            w.write_ue(cwl);
4000            w.write_ue(cwr);
4001            w.write_ue(cwt);
4002            w.write_ue(cwb);
4003        } else {
4004            w.write_bit(0); // conformance_window_flag
4005        }
4006        w.write_ue(0); // bit_depth_luma_minus8
4007        w.write_ue(0); // bit_depth_chroma_minus8
4008        w.write_ue(4); // log2_max_pic_order_cnt_lsb_minus4 (8-bit POC)
4009        w.write_bit(1); // sps_sub_layer_ordering_info_present_flag
4010        // Single entry for max_sub_layers_minus1 == 0:
4011        w.write_ue(4); // sps_max_dec_pic_buffering_minus1
4012        w.write_ue(0); // sps_max_num_reorder_pics
4013        w.write_ue(0); // sps_max_latency_increase_plus1
4014        w.write_ue(0); // log2_min_luma_coding_block_size_minus3
4015        w.write_ue(3); // log2_diff_max_min_luma_coding_block_size
4016        w.write_ue(0); // log2_min_luma_transform_block_size_minus2
4017        w.write_ue(3); // log2_diff_max_min_luma_transform_block_size
4018        w.write_ue(2); // max_transform_hierarchy_depth_inter
4019        w.write_ue(2); // max_transform_hierarchy_depth_intra
4020        w.write_bit(0); // scaling_list_enabled_flag
4021        w.write_bit(1); // amp_enabled_flag
4022        w.write_bit(1); // sample_adaptive_offset_enabled_flag
4023        w.write_bit(0); // pcm_enabled_flag
4024        w.write_ue(0); // num_short_term_ref_pic_sets (none)
4025        w.write_bit(0); // long_term_ref_pics_present_flag
4026        w.write_bit(1); // sps_temporal_mvp_enabled_flag
4027        w.write_bit(0); // strong_intra_smoothing_enabled_flag
4028        w.write_bit(0); // vui_parameters_present_flag
4029        w.write_bit(0); // sps_extension_present_flag
4030        w.write_bit(1); // rbsp trailing stop bit
4031        let mut sample = vec![0, 0, 0, 1, 0x42, 0x01];
4032        sample.extend_from_slice(&w.bytes());
4033        sample
4034    }
4035
4036    #[test]
4037    fn parse_hevc_sps_1920x1080_no_crop() {
4038        let sample = build_hevc_sps(1920, 1080);
4039        let info = parse_hevc_sps(&sample).expect("parse");
4040        assert_eq!(info.chroma_format_idc, 1);
4041        assert_eq!(info.bit_depth_luma, 8);
4042        assert_eq!(info.width, Some(1920));
4043        assert_eq!(info.height, Some(1080));
4044    }
4045
4046    #[test]
4047    fn parse_hevc_sps_with_conformance_window() {
4048        // Coded 1920×1088, conformance window crops 8 luma samples
4049        // off the bottom → 1920×1080 output.
4050        let sample = build_hevc_sps_full(1920, 1088, true, 0, 0, 0, 4);
4051        let info = parse_hevc_sps(&sample).expect("parse");
4052        assert_eq!(info.width, Some(1920));
4053        assert_eq!(info.height, Some(1080));
4054    }
4055
4056    #[test]
4057    fn parse_mpeg2_sequence_header_no_extension_640x480() {
4058        // start code 00 00 01 B3 + 3-byte body: 12 bits h + 12 bits v.
4059        // 640 = 0x280 → high 8 bits = 0x28, low 4 = 0. 480 = 0x1E0 →
4060        // high 4 = 1, low 8 = 0xE0. So bytes: 0x28 0x01 0xE0.
4061        let sample = vec![0x00, 0x00, 0x01, 0xB3, 0x28, 0x01, 0xE0, 0x13, 0xFF, 0xFF];
4062        let info = parse_mpeg2_sequence_header(&sample).expect("parse");
4063        assert_eq!(info.width, 640);
4064        assert_eq!(info.height, 480);
4065    }
4066
4067    #[test]
4068    fn parse_mpeg2_sequence_header_with_extension_upgrades_to_14bit() {
4069        // 1920 = 0x780 (fits in 12 bits: high 8=0x78 low 4=0). 1080 =
4070        // 0x438 (fits 12 bits: high 4=4 low 8=0x38). So sequence header
4071        // alone would return 1920×1080 — same as the extended form with
4072        // h_ext=0 v_ext=0. Set h_ext=1, v_ext=0 so the extension MUST
4073        // flip the value (otherwise the test would pass even if the
4074        // extension parse was broken).
4075        let mut bytes = vec![0x00, 0x00, 0x01, 0xB3, 0x78, 0x04, 0x38, 0x13, 0xFF, 0xFF];
4076        // Now tack on 00 00 01 B5 + extension body:
4077        // Extension body (bit layout, MSB first within each byte):
4078        //   ext_id(4)=0001 | profile_level(8)=0 | progressive(1)=1 |
4079        //   chroma(2)=01 (4:2:0) | h_ext(2)=01 | v_ext(2)=10
4080        // = 19 bits. Use BitWriter to avoid manual packing errors.
4081        let mut w = BitWriter::new();
4082        w.write_bits(1, 4); // extension_start_code_identifier = 0001 (seq ext)
4083        w.write_bits(0, 8); // profile_and_level_indication
4084        w.write_bit(1); // progressive_sequence
4085        w.write_bits(1, 2); // chroma_format = 01 (4:2:0)
4086        w.write_bits(1, 2); // horizontal_size_extension = 01 (h |= 1<<12 = 4096)
4087        w.write_bits(2, 2); // vertical_size_extension = 10 (v |= 2<<12 = 8192)
4088        w.write_bits(0, 1); // pad to byte
4089        bytes.extend_from_slice(&[0x00, 0x00, 0x01, 0xB5]);
4090        bytes.extend_from_slice(&w.bytes());
4091        let info = parse_mpeg2_sequence_header(&bytes).expect("parse");
4092        assert_eq!(info.width, 1920 | (1 << 12)); // 6016
4093        assert_eq!(info.height, 1080 | (2 << 12)); // 9272
4094    }
4095
4096    #[test]
4097    fn parse_mpeg2_sequence_header_none_when_no_start_code() {
4098        let sample = vec![0xFFu8; 128];
4099        assert!(parse_mpeg2_sequence_header(&sample).is_none());
4100    }
4101
4102    #[test]
4103    fn detect_dims_dispatches_by_codec() {
4104        let h264 = build_h264_baseline_sps(1280 / 16, 720 / 16);
4105        let hevc = build_hevc_sps(1920, 1080);
4106        let mpeg2 = vec![0x00, 0x00, 0x01, 0xB3, 0x28, 0x01, 0xE0, 0x13, 0xFF, 0xFF];
4107        assert_eq!(detect_dims("h264", &[h264.clone()]), Some((1280, 720)));
4108        assert_eq!(detect_dims("avc1", &[h264]), Some((1280, 720)));
4109        assert_eq!(detect_dims("h265", &[hevc.clone()]), Some((1920, 1080)));
4110        assert_eq!(detect_dims("hevc", &[hevc]), Some((1920, 1080)));
4111        assert_eq!(detect_dims("mpeg2", &[mpeg2]), Some((640, 480)));
4112        assert_eq!(detect_dims("unknown", &[vec![0u8; 8]]), None);
4113        assert_eq!(detect_dims("h264", &[]), None);
4114    }
4115
4116    /// Build a minimal H.264 PPS NAL (type 8) with the baseline set of
4117    /// fields — no FMO, no extended (High-profile) trailer. Returns
4118    /// the Annex-B sample (start code + NAL header byte + RBSP).
4119    fn build_h264_baseline_pps(pps_id: u32, sps_id: u32) -> Vec<u8> {
4120        let mut w = BitWriter::new();
4121        w.write_ue(pps_id); // pic_parameter_set_id
4122        w.write_ue(sps_id); // seq_parameter_set_id
4123        w.write_bit(0); // entropy_coding_mode_flag = CAVLC
4124        w.write_bit(0); // bottom_field_pic_order_in_frame_present_flag
4125        w.write_ue(0); // num_slice_groups_minus1 = 0 (no FMO)
4126        w.write_ue(0); // num_ref_idx_l0_default_active_minus1 = 0
4127        w.write_ue(0); // num_ref_idx_l1_default_active_minus1 = 0
4128        w.write_bit(0); // weighted_pred_flag
4129        w.write_bits(0, 2); // weighted_bipred_idc = 0
4130        w.write_ue(0); // pic_init_qp_minus26 = 0 (encoded as se(v)=0 → ue 0)
4131        w.write_ue(0); // pic_init_qs_minus26 = 0
4132        w.write_ue(0); // chroma_qp_index_offset = 0
4133        w.write_bit(1); // deblocking_filter_control_present_flag
4134        w.write_bit(0); // constrained_intra_pred_flag
4135        w.write_bit(0); // redundant_pic_cnt_present_flag
4136        w.write_bit(1); // rbsp trailing stop bit
4137        let mut sample = vec![0x00, 0x00, 0x00, 0x01, 0x68]; // NAL header: type=8 (PPS), nal_ref_idc=3
4138        sample.extend_from_slice(&w.bytes());
4139        sample
4140    }
4141
4142    #[test]
4143    fn parse_h264_pps_baseline_roundtrip() {
4144        let sample = build_h264_baseline_pps(0, 0);
4145        let info = parse_h264_pps(&sample).expect("PPS parses");
4146        assert_eq!(info.pic_parameter_set_id, 0);
4147        assert_eq!(info.seq_parameter_set_id, 0);
4148        assert!(!info.entropy_coding_mode_flag);
4149        assert!(!info.bottom_field_pic_order_in_frame_present_flag);
4150        assert_eq!(info.num_slice_groups_minus1, 0);
4151        assert_eq!(info.num_ref_idx_l0_default_active_minus1, 0);
4152        assert_eq!(info.num_ref_idx_l1_default_active_minus1, 0);
4153        assert!(!info.weighted_pred_flag);
4154        assert_eq!(info.weighted_bipred_idc, 0);
4155        assert_eq!(info.pic_init_qp_minus26, 0);
4156        assert_eq!(info.pic_init_qs_minus26, 0);
4157        assert_eq!(info.chroma_qp_index_offset, 0);
4158        assert!(info.deblocking_filter_control_present_flag);
4159        assert!(!info.constrained_intra_pred_flag);
4160        assert!(!info.redundant_pic_cnt_present_flag);
4161    }
4162
4163    #[test]
4164    fn parse_h264_pps_nonzero_ids_and_flags() {
4165        let mut w = BitWriter::new();
4166        w.write_ue(3); // pps_id
4167        w.write_ue(7); // sps_id
4168        w.write_bit(1); // entropy_coding_mode_flag = CABAC
4169        w.write_bit(1); // bottom_field_pic_order_in_frame_present_flag
4170        w.write_ue(0); // num_slice_groups_minus1
4171        w.write_ue(2); // num_ref_idx_l0_default_active_minus1 = 2
4172        w.write_ue(1); // num_ref_idx_l1_default_active_minus1 = 1
4173        w.write_bit(1); // weighted_pred_flag
4174        w.write_bits(2, 2); // weighted_bipred_idc = 2
4175        // pic_init_qp_minus26 = -5 (valid se range). codeNum for -5 = 2*5 = 10.
4176        w.write_ue(10);
4177        // pic_init_qs_minus26 = 3. codeNum for +3 = 2*3 - 1 = 5.
4178        w.write_ue(5);
4179        // chroma_qp_index_offset = 0
4180        w.write_ue(0);
4181        w.write_bit(0); // deblocking_filter_control_present_flag
4182        w.write_bit(1); // constrained_intra_pred_flag
4183        w.write_bit(1); // redundant_pic_cnt_present_flag
4184        w.write_bit(1); // rbsp stop
4185        let mut sample = vec![0x00, 0x00, 0x00, 0x01, 0x68];
4186        sample.extend_from_slice(&w.bytes());
4187        let info = parse_h264_pps(&sample).expect("parse");
4188        assert_eq!(info.pic_parameter_set_id, 3);
4189        assert_eq!(info.seq_parameter_set_id, 7);
4190        assert!(info.entropy_coding_mode_flag);
4191        assert!(info.bottom_field_pic_order_in_frame_present_flag);
4192        assert_eq!(info.num_ref_idx_l0_default_active_minus1, 2);
4193        assert_eq!(info.num_ref_idx_l1_default_active_minus1, 1);
4194        assert!(info.weighted_pred_flag);
4195        assert_eq!(info.weighted_bipred_idc, 2);
4196        assert_eq!(info.pic_init_qp_minus26, -5);
4197        assert_eq!(info.pic_init_qs_minus26, 3);
4198        assert!(!info.deblocking_filter_control_present_flag);
4199        assert!(info.constrained_intra_pred_flag);
4200        assert!(info.redundant_pic_cnt_present_flag);
4201    }
4202
4203    #[test]
4204    fn parse_h264_pps_returns_none_when_no_pps_in_sample() {
4205        // Sample contains only an SPS NAL — PPS parser should bail.
4206        let sample = build_h264_baseline_sps(80, 45); // just a SPS
4207        assert!(parse_h264_pps(&sample).is_none());
4208    }
4209
4210    /// Build a minimal H.264 slice NAL (type 5 for IDR) with:
4211    /// - first_mb_in_slice = 0
4212    /// - slice_type = I (codeNum 2)
4213    /// - pic_parameter_set_id = 0
4214    /// - frame_num = 0 (4 bits, since log2_max_frame_num_minus4 = 0)
4215    /// - idr_pic_id = 0 (only when is_idr)
4216    /// - pic_order_cnt_lsb = 0 (4 bits, since log2_max_pic_order_cnt_lsb_minus4 = 0)
4217    fn build_h264_idr_slice_header_rbsp() -> Vec<u8> {
4218        let mut w = BitWriter::new();
4219        w.write_ue(0); // first_mb_in_slice
4220        w.write_ue(7); // slice_type = 7 → 7 % 5 = 2 → I, "all I" variant
4221        w.write_ue(0); // pic_parameter_set_id
4222        w.write_bits(0, 4); // frame_num (4 bits)
4223        w.write_ue(0); // idr_pic_id
4224        w.write_bits(0, 4); // pic_order_cnt_lsb (4 bits)
4225        // Don't need rbsp trailing bits — caller doesn't look past the
4226        // fields we care about and the BitReader tolerates short data.
4227        w.bytes()
4228    }
4229
4230    #[test]
4231    fn parse_h264_slice_header_idr_i_slice() {
4232        let sps = parse_h264_sps(&build_h264_baseline_sps(1280 / 16, 720 / 16)).expect("sps");
4233        let pps = parse_h264_pps(&build_h264_baseline_pps(0, 0)).expect("pps");
4234        let rbsp = build_h264_idr_slice_header_rbsp();
4235        // NAL header byte for IDR slice: forbidden_zero=0, nal_ref_idc=3, type=5 → 0x65
4236        let mut sample = vec![0x00, 0x00, 0x00, 0x01, 0x65];
4237        sample.extend_from_slice(&rbsp);
4238
4239        let sh = parse_h264_slice_header(&sample, &sps, &pps).expect("slice");
4240        assert_eq!(sh.first_mb_in_slice, 0);
4241        assert_eq!(sh.slice_type, H264SliceType::I);
4242        assert_eq!(sh.pic_parameter_set_id, 0);
4243        assert!(sh.is_idr);
4244        assert_eq!(sh.frame_num, 0);
4245        assert!(!sh.field_pic_flag);
4246        assert_eq!(sh.idr_pic_id, Some(0));
4247        assert_eq!(sh.pic_order_cnt_lsb, Some(0));
4248    }
4249
4250    #[test]
4251    fn parse_h264_slice_header_returns_none_without_sps_context() {
4252        // Build an SPS with profile 122 (High 4:2:2) — chroma-reject
4253        // path stops parsing before pic_order_cnt_type is reached, so
4254        // sps.pic_order_cnt_type = None. Slice header parser should
4255        // gracefully bail.
4256        let mut w = BitWriter::new();
4257        w.write_bits(122, 8);
4258        w.write_bits(0, 8);
4259        w.write_bits(40, 8);
4260        w.write_ue(0); // sps_id
4261        w.write_ue(2); // chroma_format_idc
4262        w.write_ue(0);
4263        w.write_ue(0);
4264        w.write_bit(0); // qpprime
4265        w.write_bit(0); // scaling_matrix_present = 0
4266        let mut sample = vec![0, 0, 0, 1, 0x67];
4267        sample.extend_from_slice(&w.bytes());
4268        let sps = parse_h264_sps(&sample).expect("sps parses");
4269        assert!(sps.pic_order_cnt_type.is_none());
4270
4271        let pps = parse_h264_pps(&build_h264_baseline_pps(0, 0)).expect("pps");
4272        let rbsp = build_h264_idr_slice_header_rbsp();
4273        let mut slice_sample = vec![0x00, 0x00, 0x00, 0x01, 0x65];
4274        slice_sample.extend_from_slice(&rbsp);
4275        // sps.pic_order_cnt_type is None → parser bails via `?`.
4276        // Technically this tests the _early-exit_ because log2_max_frame_num_minus4
4277        // is None too. Either way, slice header parsing requires a full SPS.
4278        assert!(parse_h264_slice_header(&slice_sample, &sps, &pps).is_none());
4279    }
4280
4281    #[test]
4282    fn parse_h264_slice_type_ue_mapping_covers_both_halves() {
4283        // codeNum 0..=4 → {P, B, I, SP, SI}, codeNum 5..=9 → same
4284        // five types ("all same" annotation). Both map identically.
4285        for (code, expected) in [
4286            (0, H264SliceType::P),
4287            (5, H264SliceType::P),
4288            (1, H264SliceType::B),
4289            (6, H264SliceType::B),
4290            (2, H264SliceType::I),
4291            (7, H264SliceType::I),
4292            (3, H264SliceType::SP),
4293            (8, H264SliceType::SP),
4294            (4, H264SliceType::SI),
4295            (9, H264SliceType::SI),
4296        ] {
4297            assert_eq!(
4298                H264SliceType::from_ue(code),
4299                Some(expected),
4300                "code {}",
4301                code
4302            );
4303        }
4304    }
4305
4306    #[test]
4307    fn bit_reader_read_se_exp_golomb_mapping() {
4308        // codeNum → signed: 0→0, 1→+1, 2→-1, 3→+2, 4→-2.
4309        // Encode each via BitWriter::write_ue and verify read_se.
4310        for (code, expected) in [(0u32, 0i32), (1, 1), (2, -1), (3, 2), (4, -2), (5, 3)] {
4311            let mut w = BitWriter::new();
4312            w.write_ue(code);
4313            let bytes = w.bytes();
4314            let mut br = BitReader::new(&bytes);
4315            assert_eq!(
4316                br.read_se(),
4317                Some(expected),
4318                "codeNum={} expected={}",
4319                code,
4320                expected
4321            );
4322        }
4323    }
4324}