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