Skip to main content

codec/pixel_format/
hevc.rs

1//! HEVC / H.265 pixel-format detection and SPS/VPS/PPS/slice-header parsers.
2//! See ITU-T H.265 §7.3.2.x.
3
4use super::bitreader::{BitReader, find_next_start_code, remove_h264_rbsp_stuffing, clamp_to_i8};
5
6// ─── HEVC SPS parser ──────────────────────────────────────────────
7// See ITU-T H.265 §7.3.2.2.1. We skip profile_tier_level and jump to
8// chroma_format_idc + bit_depth_luma_minus8 + bit_depth_chroma_minus8.
9pub(super) fn detect_hevc(sample: &[u8]) -> Option<crate::frame::PixelFormat> {
10    let sps = find_hevc_sps(sample)?;
11    let rbsp = remove_h264_rbsp_stuffing(sps);
12    let mut br = BitReader::new(&rbsp);
13
14    let _sps_video_parameter_set_id = br.read_bits(4)?;
15    let sps_max_sub_layers_minus1 = br.read_bits(3)? as usize;
16    let _sps_temporal_id_nesting_flag = br.read_bits(1)?;
17
18    // profile_tier_level: 88 bits for general, plus sub-layer loops.
19    // The widths are fixed — we skip by the exact bit count instead
20    // of semantically parsing.
21    skip_hevc_profile_tier_level(&mut br, sps_max_sub_layers_minus1)?;
22
23    let _sps_seq_parameter_set_id = br.read_ue()?;
24    let chroma_format_idc = br.read_ue()? as u8;
25    if chroma_format_idc == 3 {
26        let _separate_colour_plane_flag = br.read_bits(1)?;
27    }
28    let _pic_width = br.read_ue()?;
29    let _pic_height = br.read_ue()?;
30    let conformance_window_flag = br.read_bits(1)?;
31    if conformance_window_flag == 1 {
32        let _ = br.read_ue()?;
33        let _ = br.read_ue()?;
34        let _ = br.read_ue()?;
35        let _ = br.read_ue()?;
36    }
37    let bit_depth_luma = br.read_ue()? as u8 + 8;
38    let _bit_depth_chroma_minus8 = br.read_ue()?;
39
40    Some(crate::frame::PixelFormat::from_chroma_and_depth(
41        chroma_format_idc,
42        bit_depth_luma,
43    ))
44}
45
46fn find_hevc_sps(data: &[u8]) -> Option<&[u8]> {
47    let mut i = 0;
48    while i + 4 < data.len() {
49        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
50            (3, i + 3)
51        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
52            (4, i + 4)
53        } else {
54            i += 1;
55            continue;
56        };
57        if nal_byte + 1 >= data.len() {
58            return None;
59        }
60        // HEVC NAL header is 2 bytes; nal_unit_type is bits 1..7 of byte 0.
61        let nal_unit_type = (data[nal_byte] >> 1) & 0x3F;
62        if nal_unit_type == 33 {
63            // Skip the 2-byte NAL header; RBSP starts after.
64            let start = nal_byte + 2;
65            let end = find_next_start_code(&data[start..])
66                .map(|off| start + off)
67                .unwrap_or(data.len());
68            return Some(&data[start..end]);
69        }
70        i += start_len;
71    }
72    None
73}
74
75pub(super) fn skip_hevc_profile_tier_level(
76    br: &mut BitReader,
77    max_sub_layers_minus1: usize,
78) -> Option<()> {
79    // general_profile_space(2) + general_tier_flag(1) + general_profile_idc(5)
80    let _ = br.read_bits(8)?;
81    // general_profile_compatibility_flag[32]
82    let _ = br.read_bits(32)?;
83    // general_progressive_source_flag + interlaced + non_packed + frame_only +
84    // 43 reserved + general_inbld/one_picture_only + level_idc
85    let _ = br.read_bits(48)?;
86    let _ = br.read_bits(8)?;
87
88    // Sub-layer flags
89    let mut sub_layer_profile_present = Vec::with_capacity(max_sub_layers_minus1);
90    let mut sub_layer_level_present = Vec::with_capacity(max_sub_layers_minus1);
91    for _ in 0..max_sub_layers_minus1 {
92        sub_layer_profile_present.push(br.read_bits(1)?);
93        sub_layer_level_present.push(br.read_bits(1)?);
94    }
95    if max_sub_layers_minus1 > 0 {
96        // 2 bits reserved × (8 - max_sub_layers_minus1) — spec-mandated padding
97        for _ in max_sub_layers_minus1..8 {
98            let _ = br.read_bits(2)?;
99        }
100    }
101    for i in 0..max_sub_layers_minus1 {
102        if sub_layer_profile_present[i] == 1 {
103            let _ = br.read_bits(8)?;
104            let _ = br.read_bits(32)?;
105            let _ = br.read_bits(48)?;
106        }
107        if sub_layer_level_present[i] == 1 {
108            let _ = br.read_bits(8)?;
109        }
110    }
111    Some(())
112}
113
114// ─── HevcSpsInfo struct ────────────────────────────────────────────
115
116/// Parsed HEVC SPS fields relevant to the pipeline.
117///
118/// Width/height are post-conformance-window (§7.4.3.2.1): per the spec,
119/// output luma dimensions = `pic_width_in_luma_samples - SubWidthC *
120/// (conf_win_left + conf_win_right)` (and analogously for height).
121#[derive(Debug, Clone, PartialEq, Eq, Default)]
122pub struct HevcSpsInfo {
123    pub sps_video_parameter_set_id: u8,
124    pub sps_seq_parameter_set_id: u8,
125    pub sps_max_sub_layers_minus1: u8,
126    pub sps_temporal_id_nesting_flag: bool,
127    pub chroma_format_idc: u8,
128    pub separate_colour_plane_flag: bool,
129    pub bit_depth_luma: u8,
130    pub bit_depth_chroma: u8,
131    pub width: Option<u32>,
132    pub height: Option<u32>,
133    /// Post-conformance-window crop offsets in chroma samples.
134    pub conf_win_left_offset: u32,
135    pub conf_win_right_offset: u32,
136    pub conf_win_top_offset: u32,
137    pub conf_win_bottom_offset: u32,
138    pub log2_max_pic_order_cnt_lsb_minus4: u8,
139    pub log2_min_luma_coding_block_size_minus3: u8,
140    pub log2_diff_max_min_luma_coding_block_size: u8,
141    pub log2_min_luma_transform_block_size_minus2: u8,
142    pub log2_diff_max_min_luma_transform_block_size: u8,
143    pub max_transform_hierarchy_depth_inter: u8,
144    pub max_transform_hierarchy_depth_intra: u8,
145    pub scaling_list_enabled_flag: bool,
146    pub sps_sub_layer_ordering_info_present_flag: bool,
147    pub amp_enabled_flag: bool,
148    pub sample_adaptive_offset_enabled_flag: bool,
149    pub pcm_enabled_flag: bool,
150    /// Only meaningful when pcm_enabled_flag is set; defaults to false.
151    pub pcm_loop_filter_disabled_flag: bool,
152    pub num_short_term_ref_pic_sets: u8,
153    pub long_term_ref_pics_present_flag: bool,
154    pub sps_temporal_mvp_enabled_flag: bool,
155    pub strong_intra_smoothing_enabled_flag: bool,
156    pub profile_idc: u8,
157    pub level_idc: u8,
158    pub tier_flag: bool,
159    /// Sub-layer DPB management triple, one per sub-layer. Index 0..=sps_max_sub_layers_minus1
160    /// are populated; indices above are left at defaults. Vulkan Video
161    /// requires these to be conveyed via `StdVideoH265DecPicBufMgr`.
162    pub max_dec_pic_buffering_minus1: [u8; 7],
163    pub max_num_reorder_pics: [u8; 7],
164    pub max_latency_increase_plus1: [u32; 7],
165    /// `profile_compatibility_flag[32]` — high bit at index 0. Needed
166    /// for the Std PTL struct.
167    pub profile_compatibility_flags: u32,
168    /// `general_profile_space` (2 bits) — almost always 0. Part of the
169    /// `hvc1.*` codec string prefix.
170    pub general_profile_space: u8,
171    /// `general_constraint_indicator_flags` (48 bits), right-aligned in a u64
172    /// (byte 0 = bits 47..40). Emitted as the trailing `.XX` segments of the
173    /// `hvc1.*` codec string.
174    pub general_constraint_flags: u64,
175}
176
177/// Parsed HEVC VPS — minimum fields needed for StdVideoH265VideoParameterSet.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct H265VpsInfo {
180    pub vps_video_parameter_set_id: u8,
181    pub vps_max_sub_layers_minus1: u8,
182    pub vps_temporal_id_nesting_flag: bool,
183    pub profile_idc: u8,
184    pub level_idc: u8,
185    pub tier_flag: bool,
186}
187
188/// Parsed HEVC PPS.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct H265PpsInfo {
191    pub pps_pic_parameter_set_id: u8,
192    pub pps_seq_parameter_set_id: u8,
193    pub dependent_slice_segments_enabled_flag: bool,
194    pub output_flag_present_flag: bool,
195    pub num_extra_slice_header_bits: u8,
196    pub sign_data_hiding_enabled_flag: bool,
197    pub cabac_init_present_flag: bool,
198    pub num_ref_idx_l0_default_active_minus1: u8,
199    pub num_ref_idx_l1_default_active_minus1: u8,
200    pub init_qp_minus26: i8,
201    pub constrained_intra_pred_flag: bool,
202    pub transform_skip_enabled_flag: bool,
203    pub cu_qp_delta_enabled_flag: bool,
204    pub diff_cu_qp_delta_depth: u8,
205    pub pps_cb_qp_offset: i8,
206    pub pps_cr_qp_offset: i8,
207    pub pps_slice_chroma_qp_offsets_present_flag: bool,
208    pub weighted_pred_flag: bool,
209    pub weighted_bipred_flag: bool,
210    pub transquant_bypass_enabled_flag: bool,
211    pub tiles_enabled_flag: bool,
212    pub entropy_coding_sync_enabled_flag: bool,
213    // Tile layout (§7.3.2.3) — only meaningful when tiles_enabled_flag.
214    // Defaults below model a 1×1 uniform tile spanning the frame.
215    pub num_tile_columns_minus1: u8,
216    pub num_tile_rows_minus1: u8,
217    pub uniform_spacing_flag: bool,
218    pub loop_filter_across_tiles_enabled_flag: bool,
219    // Slice / deblocking / merge controls
220    pub pps_loop_filter_across_slices_enabled_flag: bool,
221    pub deblocking_filter_control_present_flag: bool,
222    pub deblocking_filter_override_enabled_flag: bool,
223    pub pps_deblocking_filter_disabled_flag: bool,
224    pub pps_beta_offset_div2: i8,
225    pub pps_tc_offset_div2: i8,
226    pub pps_scaling_list_data_present_flag: bool,
227    pub lists_modification_present_flag: bool,
228    pub log2_parallel_merge_level_minus2: u8,
229    pub slice_segment_header_extension_present_flag: bool,
230    pub pps_extension_present_flag: bool,
231}
232
233/// HEVC slice header — subset needed for StdVideoDecodeH265PictureInfo.
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub struct H265SliceHeader {
236    pub first_slice_segment_in_pic_flag: bool,
237    pub nal_unit_type: u8,
238    pub slice_pic_parameter_set_id: u8,
239    pub slice_type: H265SliceType,
240    pub pic_order_cnt_lsb: u32,
241    pub short_term_ref_pic_set_sps_flag: bool,
242    pub short_term_ref_pic_set_idx: Option<u8>,
243    /// True for IRAP pictures (IDR / CRA / BLA): nal_unit_type ∈ 16..=23.
244    pub is_irap: bool,
245    /// True for IDR specifically: nal_unit_type ∈ 19..=20.
246    pub is_idr: bool,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum H265SliceType {
251    B,
252    P,
253    I,
254}
255
256impl H265SliceType {
257    fn from_ue(v: u32) -> Option<Self> {
258        match v {
259            0 => Some(Self::B),
260            1 => Some(Self::P),
261            2 => Some(Self::I),
262            _ => None,
263        }
264    }
265}
266
267// ─── Full SPS walker ───────────────────────────────────────────────
268
269/// Full HEVC SPS walker — see H.265 §7.3.2.2.1 + §7.4.3.2.1. Consumes
270/// `profile_tier_level` via the existing `skip_hevc_profile_tier_level`
271/// helper, then reads pic_width_in_luma_samples + pic_height_in_luma_samples
272/// and applies the conformance window crop if present.
273pub fn parse_hevc_sps(sample: &[u8]) -> Option<HevcSpsInfo> {
274    let sps = find_hevc_sps(sample)?;
275    let rbsp = remove_h264_rbsp_stuffing(sps);
276    let mut br = BitReader::new(&rbsp);
277
278    let sps_video_parameter_set_id = br.read_bits(4)? as u8;
279    let sps_max_sub_layers_minus1 = br.read_bits(3)? as u8;
280    let sps_temporal_id_nesting_flag = br.read_bits(1)? == 1;
281    // profile_tier_level: capture general_profile_idc + tier + level
282    // for the VPS mirror + Std struct. The rest is skipped via the
283    // same helper we already had.
284    let general_profile_space = br.read_bits(2)? as u8;
285    let tier_flag = br.read_bits(1)? == 1;
286    let profile_idc = br.read_bits(5)? as u8;
287    // general_profile_compatibility_flag[32] — captured for Std PTL + codec str.
288    let profile_compatibility_flags = br.read_bits(32)?;
289    // general_constraint_indicator_flags (48 bits) — captured for the hvc1.*
290    // codec string's trailing constraint bytes. Read as two halves because
291    // BitReader::read_bits returns a u32.
292    let constraint_hi = br.read_bits(24)? as u64;
293    let constraint_lo = br.read_bits(24)? as u64;
294    let general_constraint_flags = (constraint_hi << 24) | constraint_lo;
295    let level_idc = br.read_bits(8)? as u8;
296    // Skip sub-layer profile/level blocks — matches
297    // skip_hevc_profile_tier_level's tail logic.
298    let mut spl = Vec::with_capacity(sps_max_sub_layers_minus1 as usize);
299    let mut sll = Vec::with_capacity(sps_max_sub_layers_minus1 as usize);
300    for _ in 0..sps_max_sub_layers_minus1 {
301        spl.push(br.read_bits(1)?);
302        sll.push(br.read_bits(1)?);
303    }
304    if sps_max_sub_layers_minus1 > 0 {
305        for _ in sps_max_sub_layers_minus1 as usize..8 {
306            let _ = br.read_bits(2)?;
307        }
308    }
309    for i in 0..sps_max_sub_layers_minus1 as usize {
310        if spl[i] == 1 {
311            let _ = br.read_bits(8)?;
312            let _ = br.read_bits(32)?;
313            let _ = br.read_bits(48)?;
314        }
315        if sll[i] == 1 {
316            let _ = br.read_bits(8)?;
317        }
318    }
319
320    let sps_seq_parameter_set_id = br.read_ue()? as u8;
321    let chroma_format_idc = br.read_ue()? as u8;
322    let separate_colour_plane_flag = if chroma_format_idc == 3 {
323        br.read_bits(1)? == 1
324    } else {
325        false
326    };
327    let pic_width = br.read_ue()?;
328    let pic_height = br.read_ue()?;
329    let conformance_window_flag = br.read_bits(1)?;
330    let (cl, cr, ct, cb) = if conformance_window_flag == 1 {
331        (br.read_ue()?, br.read_ue()?, br.read_ue()?, br.read_ue()?)
332    } else {
333        (0u32, 0u32, 0u32, 0u32)
334    };
335    let bit_depth_luma_m8 = br.read_ue()?;
336    let bit_depth_chroma_m8 = br.read_ue()?;
337    let log2_max_pic_order_cnt_lsb_minus4 = br.read_ue()? as u8;
338
339    // sps_sub_layer_ordering_info_present_flag branch.
340    // Spec §7.3.2.2.1: when the flag is 0 only the top sub-layer's
341    // triple is signalled, but the DPB buf-mgr should mirror that
342    // value across all sub-layers i < max_sub_layers_minus1. We do
343    // that unification here so Std DecPicBufMgr has all entries
344    // populated regardless of how the bitstream flagged them.
345    let sps_sub_layer_ordering_info_present_flag = br.read_bits(1)? == 1;
346    let mut max_dec_pic_buffering_minus1 = [0u8; 7];
347    let mut max_num_reorder_pics = [0u8; 7];
348    let mut max_latency_increase_plus1 = [0u32; 7];
349    let start = if sps_sub_layer_ordering_info_present_flag {
350        0
351    } else {
352        sps_max_sub_layers_minus1
353    };
354    for i in start..=sps_max_sub_layers_minus1 {
355        let dec = br.read_ue()?;
356        let nro = br.read_ue()?;
357        let latency = br.read_ue()?;
358        let idx = (i as usize).min(6);
359        max_dec_pic_buffering_minus1[idx] = dec.min(u8::MAX as u32) as u8;
360        max_num_reorder_pics[idx] = nro.min(u8::MAX as u32) as u8;
361        max_latency_increase_plus1[idx] = latency;
362    }
363    // Fill unsignalled lower sub-layers with the top-layer values.
364    if !sps_sub_layer_ordering_info_present_flag {
365        let top = sps_max_sub_layers_minus1 as usize;
366        for i in 0..top {
367            max_dec_pic_buffering_minus1[i] = max_dec_pic_buffering_minus1[top];
368            max_num_reorder_pics[i] = max_num_reorder_pics[top];
369            max_latency_increase_plus1[i] = max_latency_increase_plus1[top];
370        }
371    }
372
373    let log2_min_luma_coding_block_size_minus3 = br.read_ue()? as u8;
374    let log2_diff_max_min_luma_coding_block_size = br.read_ue()? as u8;
375    let log2_min_luma_transform_block_size_minus2 = br.read_ue()? as u8;
376    let log2_diff_max_min_luma_transform_block_size = br.read_ue()? as u8;
377    let max_transform_hierarchy_depth_inter = br.read_ue()? as u8;
378    let max_transform_hierarchy_depth_intra = br.read_ue()? as u8;
379
380    let scaling_list_enabled_flag = br.read_bits(1)? == 1;
381    if scaling_list_enabled_flag {
382        let sps_scaling_list_data_present_flag = br.read_bits(1)? == 1;
383        if sps_scaling_list_data_present_flag {
384            skip_hevc_scaling_list_data(&mut br)?;
385        }
386    }
387    let amp_enabled_flag = br.read_bits(1)? == 1;
388    let sample_adaptive_offset_enabled_flag = br.read_bits(1)? == 1;
389    let pcm_enabled_flag = br.read_bits(1)? == 1;
390    let mut pcm_loop_filter_disabled_flag = false;
391    if pcm_enabled_flag {
392        let _pcm_sample_bit_depth_luma_minus1 = br.read_bits(4)?;
393        let _pcm_sample_bit_depth_chroma_minus1 = br.read_bits(4)?;
394        let _log2_min_pcm_luma_cb_size_minus3 = br.read_ue()?;
395        let _log2_diff_max_min_pcm_luma_cb_size = br.read_ue()?;
396        pcm_loop_filter_disabled_flag = br.read_bits(1)? == 1;
397    }
398    let num_short_term_ref_pic_sets = br.read_ue()? as u8;
399    // Skip the short-term RPS syntax parsing — we don't need the
400    // values to build Std SPS, but we do need to advance past them.
401    // The full parse is complex; use a conservative skip that
402    // tolerates simple streams. For a production decoder, this needs
403    // a proper RPS parser — this is a scaffold.
404    let mut st_rps_offsets: Vec<()> = Vec::with_capacity(num_short_term_ref_pic_sets as usize);
405    for rps_idx in 0..num_short_term_ref_pic_sets {
406        skip_hevc_short_term_rps(&mut br, rps_idx, num_short_term_ref_pic_sets)?;
407        st_rps_offsets.push(());
408    }
409    let long_term_ref_pics_present_flag = br.read_bits(1)? == 1;
410    if long_term_ref_pics_present_flag {
411        let num_long_term_ref_pics_sps = br.read_ue()?;
412        let lsb_bits = (log2_max_pic_order_cnt_lsb_minus4 as usize) + 4;
413        for _ in 0..num_long_term_ref_pics_sps {
414            let _lt_ref_pic_poc_lsb_sps = br.read_bits(lsb_bits)?;
415            let _used_by_curr_pic_lt_sps_flag = br.read_bits(1)?;
416        }
417    }
418    let sps_temporal_mvp_enabled_flag = br.read_bits(1)? == 1;
419    let strong_intra_smoothing_enabled_flag = br.read_bits(1)? == 1;
420    // vui / extension — stop here.
421
422    let chroma_array_type = if separate_colour_plane_flag {
423        0
424    } else {
425        chroma_format_idc
426    };
427    let (sub_w, sub_h) = match chroma_array_type {
428        0 => (1u32, 1u32),
429        1 => (2, 2),
430        2 => (2, 1),
431        3 => (1, 1),
432        _ => (1, 1),
433    };
434    let width = pic_width.saturating_sub(sub_w.saturating_mul(cl.saturating_add(cr)));
435    let height = pic_height.saturating_sub(sub_h.saturating_mul(ct.saturating_add(cb)));
436
437    Some(HevcSpsInfo {
438        sps_video_parameter_set_id,
439        sps_seq_parameter_set_id,
440        sps_max_sub_layers_minus1,
441        sps_temporal_id_nesting_flag,
442        chroma_format_idc,
443        separate_colour_plane_flag,
444        bit_depth_luma: bit_depth_luma_m8 as u8 + 8,
445        bit_depth_chroma: bit_depth_chroma_m8 as u8 + 8,
446        width: Some(width),
447        height: Some(height),
448        conf_win_left_offset: cl,
449        conf_win_right_offset: cr,
450        conf_win_top_offset: ct,
451        conf_win_bottom_offset: cb,
452        log2_max_pic_order_cnt_lsb_minus4,
453        log2_min_luma_coding_block_size_minus3,
454        log2_diff_max_min_luma_coding_block_size,
455        log2_min_luma_transform_block_size_minus2,
456        log2_diff_max_min_luma_transform_block_size,
457        max_transform_hierarchy_depth_inter,
458        max_transform_hierarchy_depth_intra,
459        scaling_list_enabled_flag,
460        sps_sub_layer_ordering_info_present_flag,
461        amp_enabled_flag,
462        sample_adaptive_offset_enabled_flag,
463        pcm_enabled_flag,
464        pcm_loop_filter_disabled_flag,
465        num_short_term_ref_pic_sets,
466        long_term_ref_pics_present_flag,
467        sps_temporal_mvp_enabled_flag,
468        strong_intra_smoothing_enabled_flag,
469        profile_idc,
470        level_idc,
471        tier_flag,
472        max_dec_pic_buffering_minus1,
473        max_num_reorder_pics,
474        max_latency_increase_plus1,
475        profile_compatibility_flags,
476        general_profile_space,
477        general_constraint_flags,
478    })
479}
480
481/// Skip HEVC scaling_list_data() syntax — §7.3.4. Four size IDs,
482/// each size 4..=64 depending on sizeId + matrixId. For Std SPS
483/// construction we skip the values; they're only needed when we
484/// convey them in StdVideoH265ScalingLists (not currently wired).
485fn skip_hevc_scaling_list_data(br: &mut BitReader) -> Option<()> {
486    for size_id in 0..4 {
487        let matrix_count = if size_id == 3 { 2 } else { 6 };
488        for _matrix_id in 0..matrix_count {
489            let scaling_list_pred_mode_flag = br.read_bits(1)? == 1;
490            if !scaling_list_pred_mode_flag {
491                let _scaling_list_pred_matrix_id_delta = br.read_ue()?;
492            } else {
493                let coef_num: usize = (1 << (4 + (size_id << 1))).min(64);
494                if size_id > 1 {
495                    let _scaling_list_dc_coef_minus8 = br.read_se()?;
496                }
497                for _ in 0..coef_num {
498                    let _scaling_list_delta_coef = br.read_se()?;
499                }
500            }
501        }
502    }
503    Some(())
504}
505
506/// Skip HEVC short_term_ref_pic_set(stRpsIdx) — §7.3.7. Complex;
507/// we advance past the bits without populating state (we don't
508/// need the values to build Std SPS).
509fn skip_hevc_short_term_rps(br: &mut BitReader, st_rps_idx: u8, num_st_rps: u8) -> Option<()> {
510    let inter_ref_pic_set_prediction_flag = if st_rps_idx != 0 {
511        br.read_bits(1)? == 1
512    } else {
513        false
514    };
515    if inter_ref_pic_set_prediction_flag {
516        if st_rps_idx == num_st_rps {
517            let _delta_idx_minus1 = br.read_ue()?;
518        }
519        let _delta_rps_sign = br.read_bits(1)?;
520        let _abs_delta_rps_minus1 = br.read_ue()?;
521        // Per spec, NumDeltaPocs[RefRpsIdx] — we don't track that.
522        // Approximation: assume up to 16 entries; each entry is
523        // 1-2 bits. This works for typical streams but is a
524        // known gap. A production parser needs real state tracking.
525        for _ in 0..16 {
526            let used = br.read_bits(1)?;
527            if used == 0 {
528                let _use_delta_flag = br.read_bits(1)?;
529            }
530        }
531    } else {
532        let num_negative_pics = br.read_ue()?;
533        let num_positive_pics = br.read_ue()?;
534        for _ in 0..num_negative_pics {
535            let _delta_poc_s0_minus1 = br.read_ue()?;
536            let _used_by_curr_pic_s0_flag = br.read_bits(1)?;
537        }
538        for _ in 0..num_positive_pics {
539            let _delta_poc_s1_minus1 = br.read_ue()?;
540            let _used_by_curr_pic_s1_flag = br.read_bits(1)?;
541        }
542    }
543    Some(())
544}
545
546// ─── VPS parser ────────────────────────────────────────────────────
547
548/// Parse the HEVC VPS (NAL type 32). Minimum fields for Std VPS.
549pub fn parse_h265_vps(sample: &[u8]) -> Option<H265VpsInfo> {
550    let nal = find_hevc_nal_by_type(sample, 32)?;
551    let rbsp = remove_h264_rbsp_stuffing(nal);
552    let mut br = BitReader::new(&rbsp);
553    let vps_video_parameter_set_id = br.read_bits(4)? as u8;
554    let _vps_base_layer_internal_flag = br.read_bits(1)?;
555    let _vps_base_layer_available_flag = br.read_bits(1)?;
556    let _vps_max_layers_minus1 = br.read_bits(6)?;
557    let vps_max_sub_layers_minus1 = br.read_bits(3)? as u8;
558    let vps_temporal_id_nesting_flag = br.read_bits(1)? == 1;
559    let _vps_reserved_0xffff_16bits = br.read_bits(16)?;
560    // profile_tier_level — reuse the pattern. We only need profile/
561    // tier/level for the Std VPS + for our own info.
562    let _gp_space = br.read_bits(2)?;
563    let tier_flag = br.read_bits(1)? == 1;
564    let profile_idc = br.read_bits(5)? as u8;
565    let _ = br.read_bits(32)?; // profile_compatibility_flag
566    let _ = br.read_bits(48)?; // constraint flags
567    let level_idc = br.read_bits(8)? as u8;
568    Some(H265VpsInfo {
569        vps_video_parameter_set_id,
570        vps_max_sub_layers_minus1,
571        vps_temporal_id_nesting_flag,
572        profile_idc,
573        level_idc,
574        tier_flag,
575    })
576}
577
578// ─── PPS parser ────────────────────────────────────────────────────
579
580/// Parse the HEVC PPS (NAL type 34). Subset needed for Std PPS.
581pub fn parse_h265_pps(sample: &[u8]) -> Option<H265PpsInfo> {
582    let nal = find_hevc_nal_by_type(sample, 34)?;
583    let rbsp = remove_h264_rbsp_stuffing(nal);
584    let mut br = BitReader::new(&rbsp);
585    let pps_pic_parameter_set_id = br.read_ue()? as u8;
586    let pps_seq_parameter_set_id = br.read_ue()? as u8;
587    let dependent_slice_segments_enabled_flag = br.read_bits(1)? == 1;
588    let output_flag_present_flag = br.read_bits(1)? == 1;
589    let num_extra_slice_header_bits = br.read_bits(3)? as u8;
590    let sign_data_hiding_enabled_flag = br.read_bits(1)? == 1;
591    let cabac_init_present_flag = br.read_bits(1)? == 1;
592    let num_ref_idx_l0_default_active_minus1 = br.read_ue()? as u8;
593    let num_ref_idx_l1_default_active_minus1 = br.read_ue()? as u8;
594    let init_qp_minus26 = clamp_to_i8(br.read_se()?);
595    let constrained_intra_pred_flag = br.read_bits(1)? == 1;
596    let transform_skip_enabled_flag = br.read_bits(1)? == 1;
597    let cu_qp_delta_enabled_flag = br.read_bits(1)? == 1;
598    let diff_cu_qp_delta_depth = if cu_qp_delta_enabled_flag {
599        br.read_ue()? as u8
600    } else {
601        0
602    };
603    let pps_cb_qp_offset = clamp_to_i8(br.read_se()?);
604    let pps_cr_qp_offset = clamp_to_i8(br.read_se()?);
605    let pps_slice_chroma_qp_offsets_present_flag = br.read_bits(1)? == 1;
606    let weighted_pred_flag = br.read_bits(1)? == 1;
607    let weighted_bipred_flag = br.read_bits(1)? == 1;
608    let transquant_bypass_enabled_flag = br.read_bits(1)? == 1;
609    let tiles_enabled_flag = br.read_bits(1)? == 1;
610    let entropy_coding_sync_enabled_flag = br.read_bits(1)? == 1;
611
612    // ─── Past the original parse boundary (§7.3.2.3 continuation) ───
613    // Tile layout — only present when tiles_enabled_flag.
614    // Defaults below model the single-tile-spanning-frame case, which
615    // is what the Vulkan Std PPS needs when tiles are disabled.
616    let mut num_tile_columns_minus1 = 0u8;
617    let mut num_tile_rows_minus1 = 0u8;
618    let mut uniform_spacing_flag = true;
619    let mut loop_filter_across_tiles_enabled_flag = true;
620    if tiles_enabled_flag {
621        num_tile_columns_minus1 = br.read_ue().unwrap_or(0) as u8;
622        num_tile_rows_minus1 = br.read_ue().unwrap_or(0) as u8;
623        uniform_spacing_flag = br.read_bits(1).unwrap_or(1) == 1;
624        if !uniform_spacing_flag {
625            // column_width_minus1[0..num_tile_columns_minus1] + row_height_minus1[]
626            // — we skip but must advance the bit cursor exactly.
627            for _ in 0..num_tile_columns_minus1 {
628                let _ = br.read_ue();
629            }
630            for _ in 0..num_tile_rows_minus1 {
631                let _ = br.read_ue();
632            }
633        }
634        loop_filter_across_tiles_enabled_flag = br.read_bits(1).unwrap_or(1) == 1;
635    }
636    let pps_loop_filter_across_slices_enabled_flag = br.read_bits(1)? == 1;
637
638    // Deblocking control
639    let deblocking_filter_control_present_flag = br.read_bits(1)? == 1;
640    let mut deblocking_filter_override_enabled_flag = false;
641    let mut pps_deblocking_filter_disabled_flag = false;
642    let mut pps_beta_offset_div2 = 0i8;
643    let mut pps_tc_offset_div2 = 0i8;
644    if deblocking_filter_control_present_flag {
645        deblocking_filter_override_enabled_flag = br.read_bits(1)? == 1;
646        pps_deblocking_filter_disabled_flag = br.read_bits(1)? == 1;
647        if !pps_deblocking_filter_disabled_flag {
648            pps_beta_offset_div2 = clamp_to_i8(br.read_se()?);
649            pps_tc_offset_div2 = clamp_to_i8(br.read_se()?);
650        }
651    }
652
653    // Scaling list
654    let pps_scaling_list_data_present_flag = br.read_bits(1)? == 1;
655    // If present, scaling_list_data() is a sub-syntax we skip —
656    // the Vulkan Std PPS exposes scaling lists via pScalingLists
657    // which we leave null for now (FFmpeg populates; we don't
658    // and accept the silent driver fallback risk until a scaling-
659    // list builder is wired).
660
661    let lists_modification_present_flag = br.read_bits(1)? == 1;
662    let log2_parallel_merge_level_minus2 = br.read_ue().unwrap_or(0) as u8;
663    let slice_segment_header_extension_present_flag = br.read_bits(1)? == 1;
664    let pps_extension_present_flag = br.read_bits(1).unwrap_or(0) == 1;
665
666    Some(H265PpsInfo {
667        pps_pic_parameter_set_id,
668        pps_seq_parameter_set_id,
669        dependent_slice_segments_enabled_flag,
670        output_flag_present_flag,
671        num_extra_slice_header_bits,
672        sign_data_hiding_enabled_flag,
673        cabac_init_present_flag,
674        num_ref_idx_l0_default_active_minus1,
675        num_ref_idx_l1_default_active_minus1,
676        init_qp_minus26,
677        constrained_intra_pred_flag,
678        transform_skip_enabled_flag,
679        cu_qp_delta_enabled_flag,
680        diff_cu_qp_delta_depth,
681        pps_cb_qp_offset,
682        pps_cr_qp_offset,
683        pps_slice_chroma_qp_offsets_present_flag,
684        weighted_pred_flag,
685        weighted_bipred_flag,
686        transquant_bypass_enabled_flag,
687        tiles_enabled_flag,
688        entropy_coding_sync_enabled_flag,
689        num_tile_columns_minus1,
690        num_tile_rows_minus1,
691        uniform_spacing_flag,
692        loop_filter_across_tiles_enabled_flag,
693        pps_loop_filter_across_slices_enabled_flag,
694        deblocking_filter_control_present_flag,
695        deblocking_filter_override_enabled_flag,
696        pps_deblocking_filter_disabled_flag,
697        pps_beta_offset_div2,
698        pps_tc_offset_div2,
699        pps_scaling_list_data_present_flag,
700        lists_modification_present_flag,
701        log2_parallel_merge_level_minus2,
702        slice_segment_header_extension_present_flag,
703        pps_extension_present_flag,
704    })
705}
706
707// ─── Slice header parser ───────────────────────────────────────────
708
709/// Parse the HEVC slice header — subset needed for StdVideoDecodeH265PictureInfo.
710/// `sps` / `pps` provide context for bit-width of POC lsb and branch
711/// predicates.
712pub fn parse_h265_slice_header(
713    sample: &[u8],
714    sps: &HevcSpsInfo,
715    pps: &H265PpsInfo,
716) -> Option<H265SliceHeader> {
717    let (nal_unit_type, rbsp) = find_hevc_slice_nal(sample)?;
718    let mut br = BitReader::new(&rbsp);
719    let first_slice_segment_in_pic_flag = br.read_bits(1)? == 1;
720    let is_irap = (16..=23).contains(&nal_unit_type);
721    let is_idr = matches!(nal_unit_type, 19 | 20);
722    if is_irap {
723        let _no_output_of_prior_pics_flag = br.read_bits(1)?;
724    }
725    let slice_pic_parameter_set_id = br.read_ue()? as u8;
726    let dependent_slice_segment_flag =
727        if !first_slice_segment_in_pic_flag && pps.dependent_slice_segments_enabled_flag {
728            br.read_bits(1)? == 1
729        } else {
730            false
731        };
732    if !first_slice_segment_in_pic_flag {
733        // slice_segment_address — ceil(log2(PicSizeInCtbsY)) bits.
734        // For our purposes this is a skip; we don't need the value.
735        // Conservative upper bound: 32 bits. In practice streams don't
736        // have streams this large. If this bit width is wrong, the
737        // rest of our parse will be misaligned — which is why we
738        // bail early on non-first-slice headers for now.
739        return None;
740    }
741    let _ = dependent_slice_segment_flag;
742    // num_extra_slice_header_bits
743    for _ in 0..pps.num_extra_slice_header_bits {
744        let _ = br.read_bits(1)?;
745    }
746    let slice_type_code = br.read_ue()?;
747    let slice_type = H265SliceType::from_ue(slice_type_code)?;
748    if pps.output_flag_present_flag {
749        let _pic_output_flag = br.read_bits(1)?;
750    }
751    if sps.separate_colour_plane_flag {
752        let _colour_plane_id = br.read_bits(2)?;
753    }
754
755    let (pic_order_cnt_lsb, short_term_ref_pic_set_sps_flag, short_term_ref_pic_set_idx) =
756        if !is_idr {
757            let lsb_bits = (sps.log2_max_pic_order_cnt_lsb_minus4 as usize) + 4;
758            let lsb = br.read_bits(lsb_bits)?;
759            let sps_flag = br.read_bits(1)? == 1;
760            let idx = if sps_flag {
761                if sps.num_short_term_ref_pic_sets > 1 {
762                    let bits =
763                        ((sps.num_short_term_ref_pic_sets as f64).log2().ceil() as usize).max(1);
764                    Some(br.read_bits(bits)? as u8)
765                } else {
766                    Some(0)
767                }
768            } else {
769                None
770            };
771            (lsb, sps_flag, idx)
772        } else {
773            (0, false, None)
774        };
775
776    Some(H265SliceHeader {
777        first_slice_segment_in_pic_flag,
778        nal_unit_type,
779        slice_pic_parameter_set_id,
780        slice_type,
781        pic_order_cnt_lsb,
782        short_term_ref_pic_set_sps_flag,
783        short_term_ref_pic_set_idx,
784        is_irap,
785        is_idr,
786    })
787}
788
789// ─── NAL scanners ─────────────────────────────────────────────────
790
791/// Find the first HEVC NAL with `nal_unit_type == target` in `data`.
792fn find_hevc_nal_by_type(data: &[u8], target: u8) -> Option<&[u8]> {
793    let mut i = 0;
794    while i + 4 < data.len() {
795        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
796            (3, i + 3)
797        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
798            (4, i + 4)
799        } else {
800            i += 1;
801            continue;
802        };
803        if nal_byte + 1 >= data.len() {
804            return None;
805        }
806        let nal_unit_type = (data[nal_byte] >> 1) & 0x3F;
807        if nal_unit_type == target {
808            let start = nal_byte + 2; // 2-byte NAL header
809            let end = find_next_start_code(&data[start..])
810                .map(|off| start + off)
811                .unwrap_or(data.len());
812            return Some(&data[start..end]);
813        }
814        i += start_len;
815    }
816    None
817}
818
819/// Scan an Annex-B HEVC sample and return the offset, in bytes from
820/// the start of `data`, where the first coded-slice NAL begins (the
821/// byte AFTER the start code). Vulkan `slice_segment_offsets` wants
822/// offsets to NAL-unit first bytes, not to start codes.
823pub fn hevc_first_slice_nal_offset(data: &[u8]) -> Option<u32> {
824    let mut i = 0;
825    while i + 4 < data.len() {
826        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
827            (3usize, i + 3)
828        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
829            (4usize, i + 4)
830        } else {
831            i += 1;
832            continue;
833        };
834        if nal_byte + 1 >= data.len() {
835            return None;
836        }
837        let t = (data[nal_byte] >> 1) & 0x3F;
838        if (0..=9).contains(&t) || (16..=23).contains(&t) {
839            return Some(nal_byte as u32);
840        }
841        i += start_len;
842    }
843    None
844}
845
846/// Find the first HEVC coded-slice NAL: types 0..=9 (regular slices)
847/// or 16..=23 (IRAP slices). Returns (nal_unit_type, RBSP bytes).
848fn find_hevc_slice_nal(data: &[u8]) -> Option<(u8, Vec<u8>)> {
849    let mut i = 0;
850    while i + 4 < data.len() {
851        let (start_len, nal_byte) = if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 {
852            (3, i + 3)
853        } else if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1 {
854            (4, i + 4)
855        } else {
856            i += 1;
857            continue;
858        };
859        if nal_byte + 1 >= data.len() {
860            return None;
861        }
862        let t = (data[nal_byte] >> 1) & 0x3F;
863        if (0..=9).contains(&t) || (16..=23).contains(&t) {
864            let start = nal_byte + 2;
865            let end = find_next_start_code(&data[start..])
866                .map(|off| start + off)
867                .unwrap_or(data.len());
868            return Some((t, remove_h264_rbsp_stuffing(&data[start..end])));
869        }
870        i += start_len;
871    }
872    None
873}