Skip to main content

codec/pixel_format/
h264.rs

1//! H.264 / AVC pixel-format detection and SPS/PPS/slice-header parsers.
2//! See ITU-T H.264 §7.3.2.x.
3
4use crate::frame::PixelFormat;
5use super::bitreader::{BitReader, find_next_start_code, remove_h264_rbsp_stuffing,
6                       clamp_to_i8, more_rbsp_data};
7
8// ─── H.264 SPS parser ─────────────────────────────────────────────
9// See ITU-T H.264 §7.3.2.1.1. Profile-gated fields: only profile_idc
10// values in { 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139,
11// 134, 135 } carry the chroma_format_idc + bit_depth fields we want.
12// Everything else is 4:2:0 8-bit by spec.
13pub(super) fn detect_h264(sample: &[u8]) -> Option<PixelFormat> {
14    let sps = find_h264_sps(sample)?;
15    let rbsp = remove_h264_rbsp_stuffing(sps);
16    let mut br = BitReader::new(&rbsp);
17
18    let profile_idc = br.read_bits(8)? as u8;
19    let _constraint_flags = br.read_bits(8)?;
20    let _level_idc = br.read_bits(8)?;
21    let _seq_parameter_set_id = br.read_ue()?;
22
23    let profile_gates_chroma = matches!(
24        profile_idc,
25        100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
26    );
27
28    let (chroma_format_idc, bit_depth_luma) = if profile_gates_chroma {
29        let chroma_format_idc = br.read_ue()? as u8;
30        if chroma_format_idc == 3 {
31            let _separate_colour_plane_flag = br.read_bits(1)?;
32        }
33        let bit_depth_luma_minus8 = br.read_ue()? as u8;
34        (chroma_format_idc, bit_depth_luma_minus8 + 8)
35    } else {
36        // Baseline / Main / Extended: spec-guaranteed 4:2:0 8-bit.
37        (1, 8)
38    };
39
40    Some(PixelFormat::from_chroma_and_depth(
41        chroma_format_idc,
42        bit_depth_luma,
43    ))
44}
45
46/// Return the SPS RBSP bytes (everything after the nal_unit_type byte,
47/// up to but not including the next start code). Handles both 3-byte
48/// and 4-byte start codes.
49fn find_h264_sps(data: &[u8]) -> Option<&[u8]> {
50    let mut i = 0;
51    while i + 4 < data.len() {
52        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
53            (3, i + 3)
54        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
55            (4, i + 4)
56        } else {
57            i += 1;
58            continue;
59        };
60        if nal_byte >= data.len() {
61            return None;
62        }
63        let nal_unit_type = data[nal_byte] & 0x1F;
64        if nal_unit_type == 7 {
65            // Skip the NAL unit type byte itself; caller parses the RBSP.
66            let start = nal_byte + 1;
67            let end = find_next_start_code(&data[start..])
68                .map(|off| start + off)
69                .unwrap_or(data.len());
70            return Some(&data[start..end]);
71        }
72        i += start_len;
73    }
74    None
75}
76
77// ─── H264SpsInfo struct ────────────────────────────────────────────
78
79#[derive(Debug, Clone, PartialEq, Eq, Default)]
80pub struct H264SpsInfo {
81    pub profile_idc: u8,
82    /// Packed 8-bit constraint_set_flags (Ch) — constraint_set0..5_flag
83    /// in the high 6 bits, 2 reserved_zero bits. Preserved verbatim for
84    /// Std struct output.
85    pub constraint_set_flags: u8,
86    pub level_idc: u8,
87    pub chroma_format_idc: u8,
88    pub separate_colour_plane_flag: bool,
89    pub bit_depth_luma: u8,
90    pub bit_depth_chroma: u8,
91    pub frame_mbs_only: bool,
92    /// Post-crop width in luma samples, or None if the parse stopped
93    /// before reaching the cropping fields.
94    pub width: Option<u32>,
95    pub height: Option<u32>,
96    // ─── Slice-header branching predicates (filled by full parse) ─
97    /// `log2_max_frame_num_minus4` — slice headers carry
98    /// `frame_num` as `u(log2_max_frame_num_minus4 + 4)` bits.
99    /// `None` if the dims parse bailed before reaching this field.
100    pub log2_max_frame_num_minus4: Option<u8>,
101    /// 0 / 1 / 2 per §7.4.2.1. Controls which POC fields the slice
102    /// header carries.
103    pub pic_order_cnt_type: Option<u8>,
104    /// Valid when `pic_order_cnt_type == 0`. Bit width of
105    /// `pic_order_cnt_lsb` in the slice header: `log2_max_pic_order_cnt_lsb_minus4 + 4`.
106    pub log2_max_pic_order_cnt_lsb_minus4: Option<u8>,
107    /// Valid when `pic_order_cnt_type == 1`. Gates the slice header's
108    /// `delta_pic_order_cnt[0..1]` branch.
109    pub delta_pic_order_always_zero_flag: Option<bool>,
110    // ─── Fields needed to build StdVideoH264SequenceParameterSet ──
111    pub qpprime_y_zero_transform_bypass_flag: Option<bool>,
112    pub seq_scaling_matrix_present_flag: Option<bool>,
113    pub max_num_ref_frames: Option<u8>,
114    pub gaps_in_frame_num_value_allowed_flag: Option<bool>,
115    /// Only meaningful when `!frame_mbs_only`.
116    pub mb_adaptive_frame_field_flag: Option<bool>,
117    pub direct_8x8_inference_flag: Option<bool>,
118    pub frame_cropping_flag: Option<bool>,
119    pub frame_crop_left_offset: Option<u32>,
120    pub frame_crop_right_offset: Option<u32>,
121    pub frame_crop_top_offset: Option<u32>,
122    pub frame_crop_bottom_offset: Option<u32>,
123    /// Valid when `pic_order_cnt_type == 1`.
124    pub offset_for_non_ref_pic: Option<i32>,
125    pub offset_for_top_to_bottom_field: Option<i32>,
126    pub num_ref_frames_in_pic_order_cnt_cycle: Option<u8>,
127    /// Populated only when `pic_order_cnt_type == 1`. Length equals
128    /// `num_ref_frames_in_pic_order_cnt_cycle` (0..=255). Spec allows
129    /// up to 256 entries but no real-world stream exercises the full
130    /// range.
131    pub offset_for_ref_frame: Vec<i32>,
132}
133
134// ─── Full SPS walker ───────────────────────────────────────────────
135
136/// Full H.264 SPS walker — see §7.3.2.1.1 + §7.4.2.1.1. The parse is
137/// greedy: profile_idc + chroma fields are populated first, then we
138/// walk the variable-length sections (scaling lists,
139/// pic_order_cnt_type branch) to reach pic_width_in_mbs_minus1 etc.
140/// If any of those sections hit end-of-buffer the dims come back as
141/// None but the early fields are returned.
142pub fn parse_h264_sps(sample: &[u8]) -> Option<H264SpsInfo> {
143    let sps = find_h264_sps(sample)?;
144    let rbsp = remove_h264_rbsp_stuffing(sps);
145    let mut br = BitReader::new(&rbsp);
146
147    let profile_idc = br.read_bits(8)? as u8;
148    let constraint_set_flags = br.read_bits(8)? as u8;
149    let level_idc = br.read_bits(8)? as u8;
150    let _seq_parameter_set_id = br.read_ue()?;
151
152    let profile_gates_chroma = matches!(
153        profile_idc,
154        100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
155    );
156
157    let (
158        chroma_format_idc,
159        separate_colour_plane_flag,
160        bit_depth_luma,
161        bit_depth_chroma,
162        qpprime_y_zero,
163        scaling_matrix,
164    ) = if profile_gates_chroma {
165        let chroma = br.read_ue()? as u8;
166        let separate = if chroma == 3 {
167            br.read_bits(1)? == 1
168        } else {
169            false
170        };
171        let bit_depth_luma_m8 = br.read_ue()?;
172        let bit_depth_chroma_m8 = br.read_ue()?;
173        let qpprime = br.read_bits(1)? == 1;
174        let scaling_matrix_present = br.read_bits(1)? == 1;
175        if scaling_matrix_present {
176            // 8 scaling lists for chroma_format_idc != 3, 12 otherwise
177            // (§7.3.2.1.1.1). Each list is size 16 for i<6, 64 otherwise.
178            // Deltas are se(v); missing-list flag is u(1).
179            let num_lists = if chroma == 3 { 12 } else { 8 };
180            for i in 0..num_lists {
181                if br.read_bits(1)? == 1 {
182                    let size = if i < 6 { 16 } else { 64 };
183                    let mut last_scale: i32 = 8;
184                    let mut next_scale: i32 = 8;
185                    for _j in 0..size {
186                        if next_scale != 0 {
187                            let delta = br.read_se()?;
188                            next_scale = (last_scale + delta + 256).rem_euclid(256);
189                        }
190                        if next_scale != 0 {
191                            last_scale = next_scale;
192                        }
193                    }
194                }
195            }
196        }
197        (
198            chroma,
199            separate,
200            bit_depth_luma_m8 as u8 + 8,
201            bit_depth_chroma_m8 as u8 + 8,
202            qpprime,
203            scaling_matrix_present,
204        )
205    } else {
206        (1u8, false, 8u8, 8u8, false, false)
207    };
208
209    // At this point we've cleared the chroma/depth prefix. Everything
210    // from here on is what we need for width/height and the slice-
211    // header branching predicates. Any read failure below returns the
212    // partial info with width/height = None.
213    let info_prefix = H264SpsInfo {
214        profile_idc,
215        constraint_set_flags,
216        level_idc,
217        chroma_format_idc,
218        separate_colour_plane_flag,
219        bit_depth_luma,
220        bit_depth_chroma,
221        frame_mbs_only: true,
222        width: None,
223        height: None,
224        log2_max_frame_num_minus4: None,
225        pic_order_cnt_type: None,
226        log2_max_pic_order_cnt_lsb_minus4: None,
227        delta_pic_order_always_zero_flag: None,
228        qpprime_y_zero_transform_bypass_flag: Some(qpprime_y_zero),
229        seq_scaling_matrix_present_flag: Some(scaling_matrix),
230        max_num_ref_frames: None,
231        gaps_in_frame_num_value_allowed_flag: None,
232        mb_adaptive_frame_field_flag: None,
233        direct_8x8_inference_flag: None,
234        frame_cropping_flag: None,
235        frame_crop_left_offset: None,
236        frame_crop_right_offset: None,
237        frame_crop_top_offset: None,
238        frame_crop_bottom_offset: None,
239        offset_for_non_ref_pic: None,
240        offset_for_top_to_bottom_field: None,
241        num_ref_frames_in_pic_order_cnt_cycle: None,
242        offset_for_ref_frame: Vec::new(),
243    };
244
245    let Some(dims) = parse_h264_sps_dims(&mut br, chroma_format_idc, separate_colour_plane_flag)
246    else {
247        return Some(info_prefix);
248    };
249
250    Some(H264SpsInfo {
251        frame_mbs_only: dims.frame_mbs_only,
252        width: Some(dims.width),
253        height: Some(dims.height),
254        log2_max_frame_num_minus4: Some(dims.log2_max_frame_num_minus4),
255        pic_order_cnt_type: Some(dims.pic_order_cnt_type),
256        log2_max_pic_order_cnt_lsb_minus4: dims.log2_max_pic_order_cnt_lsb_minus4,
257        delta_pic_order_always_zero_flag: dims.delta_pic_order_always_zero_flag,
258        max_num_ref_frames: Some(dims.max_num_ref_frames),
259        gaps_in_frame_num_value_allowed_flag: Some(dims.gaps_in_frame_num_value_allowed_flag),
260        mb_adaptive_frame_field_flag: dims.mb_adaptive_frame_field_flag,
261        direct_8x8_inference_flag: Some(dims.direct_8x8_inference_flag),
262        frame_cropping_flag: Some(dims.frame_cropping_flag),
263        frame_crop_left_offset: Some(dims.crop_left),
264        frame_crop_right_offset: Some(dims.crop_right),
265        frame_crop_top_offset: Some(dims.crop_top),
266        frame_crop_bottom_offset: Some(dims.crop_bottom),
267        offset_for_non_ref_pic: dims.offset_for_non_ref_pic,
268        offset_for_top_to_bottom_field: dims.offset_for_top_to_bottom_field,
269        num_ref_frames_in_pic_order_cnt_cycle: dims.num_ref_frames_in_pic_order_cnt_cycle,
270        offset_for_ref_frame: dims.offset_for_ref_frame,
271        ..info_prefix
272    })
273}
274
275struct H264Dims {
276    width: u32,
277    height: u32,
278    frame_mbs_only: bool,
279    log2_max_frame_num_minus4: u8,
280    pic_order_cnt_type: u8,
281    log2_max_pic_order_cnt_lsb_minus4: Option<u8>,
282    delta_pic_order_always_zero_flag: Option<bool>,
283    offset_for_non_ref_pic: Option<i32>,
284    offset_for_top_to_bottom_field: Option<i32>,
285    num_ref_frames_in_pic_order_cnt_cycle: Option<u8>,
286    offset_for_ref_frame: Vec<i32>,
287    max_num_ref_frames: u8,
288    gaps_in_frame_num_value_allowed_flag: bool,
289    mb_adaptive_frame_field_flag: Option<bool>,
290    direct_8x8_inference_flag: bool,
291    frame_cropping_flag: bool,
292    crop_left: u32,
293    crop_right: u32,
294    crop_top: u32,
295    crop_bottom: u32,
296}
297
298fn parse_h264_sps_dims(
299    br: &mut BitReader,
300    chroma_format_idc: u8,
301    separate_colour_plane_flag: bool,
302) -> Option<H264Dims> {
303    let log2_max_frame_num_minus4 = br.read_ue()? as u8;
304    let pic_order_cnt_type = br.read_ue()? as u8;
305    let mut log2_max_pic_order_cnt_lsb_minus4 = None;
306    let mut delta_pic_order_always_zero_flag = None;
307    let mut offset_for_non_ref_pic = None;
308    let mut offset_for_top_to_bottom_field = None;
309    let mut num_ref_frames_in_pic_order_cnt_cycle: Option<u8> = None;
310    let mut offset_for_ref_frame: Vec<i32> = Vec::new();
311    match pic_order_cnt_type {
312        0 => {
313            log2_max_pic_order_cnt_lsb_minus4 = Some(br.read_ue()? as u8);
314        }
315        1 => {
316            delta_pic_order_always_zero_flag = Some(br.read_bits(1)? == 1);
317            offset_for_non_ref_pic = Some(br.read_se()?);
318            offset_for_top_to_bottom_field = Some(br.read_se()?);
319            let cycle_len = br.read_ue()?;
320            // Cap at 255 to fit u8 + bound the loop — spec allows up
321            // to 255, so no real loss of precision.
322            let capped = cycle_len.min(255) as u8;
323            num_ref_frames_in_pic_order_cnt_cycle = Some(capped);
324            offset_for_ref_frame.reserve(capped as usize);
325            for _ in 0..capped {
326                offset_for_ref_frame.push(br.read_se()?);
327            }
328        }
329        2 => { /* no fields */ }
330        _ => return None, // reserved; spec says no other values are valid
331    }
332    let max_num_ref_frames = br.read_ue()?.min(u8::MAX as u32) as u8;
333    let gaps_in_frame_num_value_allowed_flag = br.read_bits(1)? == 1;
334    let pic_width_in_mbs_minus1 = br.read_ue()?;
335    let pic_height_in_map_units_minus1 = br.read_ue()?;
336    let frame_mbs_only_flag = br.read_bits(1)?;
337    let mut mb_adaptive_frame_field_flag = None;
338    if frame_mbs_only_flag == 0 {
339        mb_adaptive_frame_field_flag = Some(br.read_bits(1)? == 1);
340    }
341    let direct_8x8_inference_flag = br.read_bits(1)? == 1;
342    let frame_cropping_flag = br.read_bits(1)? == 1;
343    let (cl, cr, ct, cb) = if frame_cropping_flag {
344        (br.read_ue()?, br.read_ue()?, br.read_ue()?, br.read_ue()?)
345    } else {
346        (0, 0, 0, 0)
347    };
348
349    let pic_width_in_mbs = pic_width_in_mbs_minus1.saturating_add(1);
350    let pic_height_in_map_units = pic_height_in_map_units_minus1.saturating_add(1);
351    let frame_mbs_only = frame_mbs_only_flag == 1;
352    let frame_height_in_mbs = if frame_mbs_only {
353        pic_height_in_map_units
354    } else {
355        pic_height_in_map_units.saturating_mul(2)
356    };
357
358    // §6.2 Table 6-1 + §7.4.2.1.1
359    let chroma_array_type = if separate_colour_plane_flag {
360        0
361    } else {
362        chroma_format_idc
363    };
364    let (sub_w, sub_h) = match chroma_array_type {
365        0 => (1u32, 1u32), // monochrome (cropping units below use 1,2-flag)
366        1 => (2, 2),       // 4:2:0
367        2 => (2, 1),       // 4:2:2
368        3 => (1, 1),       // 4:4:4
369        _ => (1, 1),
370    };
371    let (crop_x, crop_y) = if chroma_array_type == 0 {
372        (1u32, 2u32 - frame_mbs_only_flag)
373    } else {
374        (sub_w, sub_h * (2 - frame_mbs_only_flag))
375    };
376
377    let width = pic_width_in_mbs
378        .saturating_mul(16)
379        .saturating_sub(crop_x.saturating_mul(cl.saturating_add(cr)));
380    let height = frame_height_in_mbs
381        .saturating_mul(16)
382        .saturating_sub(crop_y.saturating_mul(ct.saturating_add(cb)));
383
384    Some(H264Dims {
385        width,
386        height,
387        frame_mbs_only,
388        log2_max_frame_num_minus4,
389        pic_order_cnt_type,
390        log2_max_pic_order_cnt_lsb_minus4,
391        delta_pic_order_always_zero_flag,
392        offset_for_non_ref_pic,
393        offset_for_top_to_bottom_field,
394        num_ref_frames_in_pic_order_cnt_cycle,
395        offset_for_ref_frame,
396        max_num_ref_frames,
397        gaps_in_frame_num_value_allowed_flag,
398        mb_adaptive_frame_field_flag,
399        direct_8x8_inference_flag,
400        frame_cropping_flag,
401        crop_left: cl,
402        crop_right: cr,
403        crop_top: ct,
404        crop_bottom: cb,
405    })
406}
407
408// ─── H.264 first-slice offset ─────────────────────────────────────
409
410/// Scan an Annex-B H.264 sample for the first coded-slice NAL
411/// (types 1 / 5 / 19) and return its byte offset within `data`.
412/// Parallel to `hevc_first_slice_nal_offset`.
413pub fn h264_first_slice_nal_offset(data: &[u8]) -> Option<u32> {
414    let mut i = 0;
415    while i + 4 < data.len() {
416        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
417            (3usize, i + 3)
418        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
419            (4usize, i + 4)
420        } else {
421            i += 1;
422            continue;
423        };
424        if nal_byte >= data.len() {
425            return None;
426        }
427        let t = data[nal_byte] & 0x1F;
428        if matches!(t, 1 | 5 | 19) {
429            return Some(nal_byte as u32);
430        }
431        i += start_len;
432    }
433    None
434}
435
436// ─── H.264 PPS parse (Vulkan Video + slice-header support) ────────
437//
438// Vulkan Video H.264 decode requires the app to build a
439// `StdVideoH264PictureParameterSet` from the PPS NAL (type 8) — the
440// driver does not parse bitstreams. Every field below lands in the
441// Std header struct; the flags pack into bitfields per the Std video
442// spec. See ITU-T H.264 §7.3.2.2 + §7.4.2.2.
443
444/// Parsed H.264 PPS fields. Consumers: Vulkan Video decoder (fills
445/// `StdVideoH264PictureParameterSet`), slice-header parser (needs
446/// `bottom_field_pic_order_in_frame_present_flag` +
447/// `redundant_pic_cnt_present_flag` as branching predicates).
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct H264PpsInfo {
450    pub pic_parameter_set_id: u8,
451    pub seq_parameter_set_id: u8,
452    pub entropy_coding_mode_flag: bool,
453    /// Aka `pic_order_present_flag` in older spec editions. Controls
454    /// whether slice headers carry `delta_pic_order_cnt_bottom` and
455    /// `delta_pic_order_cnt[1]`.
456    pub bottom_field_pic_order_in_frame_present_flag: bool,
457    pub num_slice_groups_minus1: u8,
458    pub num_ref_idx_l0_default_active_minus1: u8,
459    pub num_ref_idx_l1_default_active_minus1: u8,
460    pub weighted_pred_flag: bool,
461    pub weighted_bipred_idc: u8,
462    pub pic_init_qp_minus26: i8,
463    pub pic_init_qs_minus26: i8,
464    pub chroma_qp_index_offset: i8,
465    pub deblocking_filter_control_present_flag: bool,
466    pub constrained_intra_pred_flag: bool,
467    pub redundant_pic_cnt_present_flag: bool,
468    /// Extended fields — present only when the PPS RBSP has trailing
469    /// data beyond the baseline syntax. All three were added in the
470    /// 2005 amendment alongside High profile.
471    pub transform_8x8_mode_flag: Option<bool>,
472    pub pic_scaling_matrix_present_flag: Option<bool>,
473    pub second_chroma_qp_index_offset: Option<i8>,
474}
475
476/// Walk an Annex-B sample looking for the first NAL of type 8 (PPS)
477/// and decode its syntax elements. Returns `None` when no PPS is in
478/// the sample or the syntax is truncated before
479/// `redundant_pic_cnt_present_flag` (the last required field).
480///
481/// The FMO (Flexible Macroblock Ordering) sub-branches for
482/// `num_slice_groups_minus1 > 0` / `slice_group_map_type`=0/2/3..5/6
483/// are skipped correctly but not reported — no consumer today needs
484/// the slice-group map (FMO is forbidden in Main and High profiles,
485/// and every stream our decoder touches is Main/High).
486pub fn parse_h264_pps(sample: &[u8]) -> Option<H264PpsInfo> {
487    let pps = find_h264_nal_by_type(sample, 8)?;
488    let rbsp = remove_h264_rbsp_stuffing(pps);
489    let mut br = BitReader::new(&rbsp);
490
491    let pic_parameter_set_id = br.read_ue()? as u8;
492    let seq_parameter_set_id = br.read_ue()? as u8;
493    let entropy_coding_mode_flag = br.read_bits(1)? == 1;
494    let bottom_field_pic_order_in_frame_present_flag = br.read_bits(1)? == 1;
495
496    let num_slice_groups_minus1 = br.read_ue()?;
497    if num_slice_groups_minus1 > 0 {
498        // FMO sub-branches — skip.
499        let slice_group_map_type = br.read_ue()?;
500        match slice_group_map_type {
501            0 => {
502                for _ in 0..=num_slice_groups_minus1 {
503                    let _run_length_minus1 = br.read_ue()?;
504                }
505            }
506            2 => {
507                for _ in 0..num_slice_groups_minus1 {
508                    let _top_left = br.read_ue()?;
509                    let _bottom_right = br.read_ue()?;
510                }
511            }
512            3..=5 => {
513                let _slice_group_change_direction_flag = br.read_bits(1)?;
514                let _slice_group_change_rate_minus1 = br.read_ue()?;
515            }
516            6 => {
517                let pic_size_in_map_units_minus1 = br.read_ue()?;
518                let bits = ((num_slice_groups_minus1 + 1) as f64).log2().ceil() as usize;
519                let bits = bits.max(1);
520                for _ in 0..=pic_size_in_map_units_minus1 {
521                    let _slice_group_id = br.read_bits(bits)?;
522                }
523            }
524            _ => {}
525        }
526    }
527
528    let num_ref_idx_l0_default_active_minus1 = br.read_ue()? as u8;
529    let num_ref_idx_l1_default_active_minus1 = br.read_ue()? as u8;
530    let weighted_pred_flag = br.read_bits(1)? == 1;
531    let weighted_bipred_idc = br.read_bits(2)? as u8;
532    let pic_init_qp_minus26 = clamp_to_i8(br.read_se()?);
533    let pic_init_qs_minus26 = clamp_to_i8(br.read_se()?);
534    let chroma_qp_index_offset = clamp_to_i8(br.read_se()?);
535    let deblocking_filter_control_present_flag = br.read_bits(1)? == 1;
536    let constrained_intra_pred_flag = br.read_bits(1)? == 1;
537    let redundant_pic_cnt_present_flag = br.read_bits(1)? == 1;
538
539    // Extended fields — present only when more_rbsp_data() indicates
540    // the PPS carried them. Detect by checking if any bits remain
541    // beyond the rbsp_trailing_bits stop. We do a best-effort read:
542    // fill from Some(...) on success, fall back to None if the trailer
543    // runs out mid-field.
544    let (transform_8x8_mode_flag, pic_scaling_matrix_present_flag, second_chroma_qp_index_offset) =
545        if more_rbsp_data(&br, &rbsp) {
546            let t8 = br.read_bits(1).map(|v| v == 1);
547            let psm = br.read_bits(1).map(|v| v == 1);
548            // If pic_scaling_matrix_present_flag is set, scaling_list
549            // blocks follow before second_chroma_qp_index_offset. Skip
550            // them (conservative — we don't consume these values).
551            if let Some(true) = psm {
552                // Number of scaling lists per §7.3.2.2:
553                //   6 + ((chroma_format_idc != 3) ? 2 : 6) * transform_8x8_mode_flag
554                // We don't know chroma_format_idc from the PPS alone;
555                // assume 4:2:0 (most common) → 8 total lists when t8=1.
556                let count = 6 + if let Some(true) = t8 { 2 } else { 0 };
557                for i in 0..count {
558                    if br.read_bits(1) == Some(1) {
559                        let size = if i < 6 { 16 } else { 64 };
560                        let mut last_scale: i32 = 8;
561                        let mut next_scale: i32 = 8;
562                        for _ in 0..size {
563                            if next_scale != 0 {
564                                let delta = br.read_se().unwrap_or(0);
565                                next_scale = (last_scale + delta + 256).rem_euclid(256);
566                            }
567                            if next_scale != 0 {
568                                last_scale = next_scale;
569                            }
570                        }
571                    }
572                }
573            }
574            let s2 = br.read_se().map(clamp_to_i8);
575            (t8, psm, s2)
576        } else {
577            (None, None, None)
578        };
579
580    Some(H264PpsInfo {
581        pic_parameter_set_id,
582        seq_parameter_set_id,
583        entropy_coding_mode_flag,
584        bottom_field_pic_order_in_frame_present_flag,
585        num_slice_groups_minus1: num_slice_groups_minus1.min(u8::MAX as u32) as u8,
586        num_ref_idx_l0_default_active_minus1,
587        num_ref_idx_l1_default_active_minus1,
588        weighted_pred_flag,
589        weighted_bipred_idc,
590        pic_init_qp_minus26,
591        pic_init_qs_minus26,
592        chroma_qp_index_offset,
593        deblocking_filter_control_present_flag,
594        constrained_intra_pred_flag,
595        redundant_pic_cnt_present_flag,
596        transform_8x8_mode_flag,
597        pic_scaling_matrix_present_flag,
598        second_chroma_qp_index_offset,
599    })
600}
601
602// ─── H.264 slice header ───────────────────────────────────────────
603
604/// Slice type name (decoded from `slice_type` ue(v) value). Per
605/// H.264 §7.4.3 Table 7-6, values 0..=4 are one iteration of the
606/// slice types; values 5..=9 are the same types but mark "all
607/// slices in the current picture have this type" (aka
608/// `slice_type_all_same`). Both halves collapse to the same enum.
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum H264SliceType {
611    P,
612    B,
613    I,
614    SP,
615    SI,
616}
617
618impl H264SliceType {
619    pub(super) fn from_ue(v: u32) -> Option<Self> {
620        match v % 5 {
621            0 => Some(Self::P),
622            1 => Some(Self::B),
623            2 => Some(Self::I),
624            3 => Some(Self::SP),
625            4 => Some(Self::SI),
626            _ => None,
627        }
628    }
629}
630
631/// Parsed H.264 slice header — just the fields the Vulkan Video
632/// decoder + our DPB manager need. See ITU-T H.264 §7.3.3. Full slice
633/// header has ref_pic_list_modification, weighted_prediction tables,
634/// dec_ref_pic_marking, etc., which we don't consume (the driver
635/// re-derives them from the PPS + `StdVideoDecodeH264PictureInfo`
636/// flags).
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
638pub struct H264SliceHeader {
639    pub first_mb_in_slice: u32,
640    pub slice_type: H264SliceType,
641    pub pic_parameter_set_id: u8,
642    /// From the NAL header: `nal_unit_type == 5` — set by the caller
643    /// when it picks the NAL to parse. Affects whether `idr_pic_id` is
644    /// carried.
645    pub is_idr: bool,
646    pub frame_num: u32,
647    /// True when the slice encodes a single field of an interlaced
648    /// frame (spec: `!frame_mbs_only_flag && field_pic_flag`). False
649    /// for progressive frames or MBAFF pairs.
650    pub field_pic_flag: bool,
651    pub bottom_field_flag: bool,
652    pub colour_plane_id: Option<u8>,
653    /// Set when `is_idr`; otherwise `None`.
654    pub idr_pic_id: Option<u32>,
655    /// Set when SPS `pic_order_cnt_type == 0`.
656    pub pic_order_cnt_lsb: Option<u32>,
657    pub delta_pic_order_cnt_bottom: Option<i32>,
658    /// Set when SPS `pic_order_cnt_type == 1` and
659    /// `!delta_pic_order_always_zero_flag`. `[0]` always present in
660    /// that branch, `[1]` present only when the PPS carried
661    /// `bottom_field_pic_order_in_frame_present_flag` and we're in a
662    /// frame (not field) slice.
663    pub delta_pic_order_cnt: [Option<i32>; 2],
664}
665
666/// Parse the first slice-NAL in `sample`, using the SPS + PPS for
667/// branch predicates. The NAL header's `nal_unit_type` gates which
668/// slice types we accept: 1 (non-IDR), 5 (IDR), 19 (auxiliary coded
669/// slice) all share the same syntax. Returns `None` when the sample
670/// contains no slice NAL or the SPS/PPS didn't provide the required
671/// context (e.g., SPS `pic_order_cnt_type` was `None` so we can't
672/// branch into the POC reads).
673pub fn parse_h264_slice_header(
674    sample: &[u8],
675    sps: &H264SpsInfo,
676    pps: &H264PpsInfo,
677) -> Option<H264SliceHeader> {
678    // nal_unit_type values for coded slices: 1 (non-IDR), 2/3/4
679    // (partition A/B/C, deprecated), 5 (IDR), 19 (aux). We accept
680    // 1, 5, 19 — the common cases.
681    let (nal_type, rbsp) = find_h264_slice_nal(sample)?;
682    let is_idr = nal_type == 5;
683
684    let mut br = BitReader::new(&rbsp);
685    let first_mb_in_slice = br.read_ue()?;
686    let slice_type_code = br.read_ue()?;
687    let slice_type = H264SliceType::from_ue(slice_type_code)?;
688    let pic_parameter_set_id = br.read_ue()? as u8;
689
690    let colour_plane_id = if sps.separate_colour_plane_flag {
691        Some(br.read_bits(2)? as u8)
692    } else {
693        None
694    };
695
696    let frame_num_bits = (sps.log2_max_frame_num_minus4? as usize) + 4;
697    let frame_num = br.read_bits(frame_num_bits)?;
698
699    let (field_pic_flag, bottom_field_flag) = if !sps.frame_mbs_only {
700        let f = br.read_bits(1)? == 1;
701        let b = if f { br.read_bits(1)? == 1 } else { false };
702        (f, b)
703    } else {
704        (false, false)
705    };
706
707    let idr_pic_id = if is_idr { Some(br.read_ue()?) } else { None };
708
709    let poc_type = sps.pic_order_cnt_type?;
710    let mut pic_order_cnt_lsb = None;
711    let mut delta_pic_order_cnt_bottom = None;
712    let mut delta_pic_order_cnt: [Option<i32>; 2] = [None, None];
713    match poc_type {
714        0 => {
715            let bits = (sps.log2_max_pic_order_cnt_lsb_minus4? as usize) + 4;
716            pic_order_cnt_lsb = Some(br.read_bits(bits)?);
717            if pps.bottom_field_pic_order_in_frame_present_flag && !field_pic_flag {
718                delta_pic_order_cnt_bottom = Some(br.read_se()?);
719            }
720        }
721        1 => {
722            let always_zero = sps.delta_pic_order_always_zero_flag.unwrap_or(false);
723            if !always_zero {
724                delta_pic_order_cnt[0] = Some(br.read_se()?);
725                if pps.bottom_field_pic_order_in_frame_present_flag && !field_pic_flag {
726                    delta_pic_order_cnt[1] = Some(br.read_se()?);
727                }
728            }
729        }
730        2 => { /* implicit POC derivation; no fields */ }
731        _ => return None,
732    }
733
734    Some(H264SliceHeader {
735        first_mb_in_slice,
736        slice_type,
737        pic_parameter_set_id,
738        is_idr,
739        frame_num,
740        field_pic_flag,
741        bottom_field_flag,
742        colour_plane_id,
743        idr_pic_id,
744        pic_order_cnt_lsb,
745        delta_pic_order_cnt_bottom,
746        delta_pic_order_cnt,
747    })
748}
749
750/// Find the first coded-slice NAL (nal_unit_type ∈ {1, 5, 19}) in
751/// `data` and return `(nal_unit_type, rbsp_bytes_with_stuffing_removed)`.
752fn find_h264_slice_nal(data: &[u8]) -> Option<(u8, Vec<u8>)> {
753    let mut i = 0;
754    while i + 4 < data.len() {
755        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
756            (3, i + 3)
757        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
758            (4, i + 4)
759        } else {
760            i += 1;
761            continue;
762        };
763        if nal_byte >= data.len() {
764            return None;
765        }
766        let nal_unit_type = data[nal_byte] & 0x1F;
767        if matches!(nal_unit_type, 1 | 5 | 19) {
768            let start = nal_byte + 1;
769            let end = find_next_start_code(&data[start..])
770                .map(|off| start + off)
771                .unwrap_or(data.len());
772            let rbsp = remove_h264_rbsp_stuffing(&data[start..end]);
773            return Some((nal_unit_type, rbsp));
774        }
775        i += start_len;
776    }
777    None
778}
779
780/// Generic "find the first Annex-B NAL whose `nal_unit_type` matches
781/// `target_type`" helper. Factored out of `find_h264_sps` so the PPS
782/// parser and future consumers (slice header, SEI) share one scanner.
783fn find_h264_nal_by_type(data: &[u8], target_type: u8) -> Option<&[u8]> {
784    let mut i = 0;
785    while i + 4 < data.len() {
786        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
787            (3, i + 3)
788        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
789            (4, i + 4)
790        } else {
791            i += 1;
792            continue;
793        };
794        if nal_byte >= data.len() {
795            return None;
796        }
797        let nal_unit_type = data[nal_byte] & 0x1F;
798        if nal_unit_type == target_type {
799            let start = nal_byte + 1;
800            let end = find_next_start_code(&data[start..])
801                .map(|off| start + off)
802                .unwrap_or(data.len());
803            return Some(&data[start..end]);
804        }
805        i += start_len;
806    }
807    None
808}