Skip to main content

oxideav_bitstream/
hevc.rs

1//! HEVC / H.265 minimal IDR header parsing.
2//!
3//! This module parses just enough VPS / SPS / PPS / slice-segment
4//! header to populate the slice-data HW backends' parameter buffers
5//! (`VAPictureParameterBufferHEVC`, `VdpPictureInfoHEVC`,
6//! `VkVideoDecodeH265PictureInfoKHR`). Same scope philosophy as the
7//! H.264 module: enough fields to drive the GPU decode of an IDR
8//! access unit, nothing more.
9//!
10//! Refused (returned as [`BitstreamError::Unsupported`]):
11//!
12//! - SPS scaling list (`scaling_list_enabled_flag = 1`).
13//! - PCM coding (`pcm_enabled_flag = 1`).
14//! - Multiple short-term RPS (`num_short_term_ref_pic_sets > 0`).
15//! - Long-term RPS (`long_term_ref_pics_present_flag = 1`).
16//! - PPS scaling list, dependent slice segments, tiles, WPP.
17//! - Range / SCC / Scalable / Multiview extensions.
18//!
19//! # Spec references
20//!
21//! ITU-T H.265 v9 (a.k.a. ISO/IEC 23008-2). Sections of interest:
22//! 7.3.2.1 (VPS), 7.3.2.2 (SPS), 7.3.2.3 (PPS), 7.3.6 (slice
23//! segment header), 7.4.3.2 (SPS semantics), 9.2 (Exp-Golomb).
24
25use crate::bit_reader::BitReader;
26use crate::BitstreamError;
27
28// ─────────────────────────── NAL unit types ──────────────────────────────────
29
30/// 7.4.2.2 — IDR_W_RADL slice.
31pub const NAL_TYPE_IDR_W_RADL: u8 = 19;
32/// 7.4.2.2 — IDR_N_LP slice.
33pub const NAL_TYPE_IDR_N_LP: u8 = 20;
34/// 7.4.2.2 — CRA slice.
35pub const NAL_TYPE_CRA: u8 = 21;
36/// 7.4.2.2 — VPS_NUT.
37pub const NAL_TYPE_VPS: u8 = 32;
38/// 7.4.2.2 — SPS_NUT.
39pub const NAL_TYPE_SPS: u8 = 33;
40/// 7.4.2.2 — PPS_NUT.
41pub const NAL_TYPE_PPS: u8 = 34;
42/// 7.4.2.2 — AUD_NUT.
43pub const NAL_TYPE_AUD: u8 = 35;
44
45// ─────────────────────────── Annex-B framing ─────────────────────────────────
46
47/// Locate every NAL unit in an Annex-B HEVC bitstream and return
48/// slices pointing at the NAL body (start code stripped, two-byte
49/// NAL header at index 0..1, emulation bytes still in place — strip
50/// those with [`ebsp_to_rbsp`] before bit-level parsing).
51pub fn split_annex_b(buf: &[u8]) -> Vec<&[u8]> {
52    let mut out = Vec::new();
53    let mut i = 0;
54    let n = buf.len();
55    let mut last_payload_start: Option<usize> = None;
56    while i < n {
57        let four =
58            i + 3 < n && buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 0 && buf[i + 3] == 1;
59        let three = !four && i + 2 < n && buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1;
60        if four || three {
61            if let Some(start) = last_payload_start.take() {
62                out.push(&buf[start..i]);
63            }
64            i += if four { 4 } else { 3 };
65            last_payload_start = Some(i);
66            continue;
67        }
68        i += 1;
69    }
70    if let Some(start) = last_payload_start.take() {
71        out.push(&buf[start..n]);
72    }
73    out
74}
75
76/// Strip H.265 emulation-prevention `0x03` bytes (7.4.1.1). Same
77/// shape as in H.264.
78pub fn ebsp_to_rbsp(ebsp: &[u8]) -> Vec<u8> {
79    let mut out = Vec::with_capacity(ebsp.len());
80    let mut i = 0;
81    let n = ebsp.len();
82    while i < n {
83        if i + 2 < n && ebsp[i] == 0 && ebsp[i + 1] == 0 && ebsp[i + 2] == 3 {
84            out.push(0);
85            out.push(0);
86            i += 3;
87        } else {
88            out.push(ebsp[i]);
89            i += 1;
90        }
91    }
92    out
93}
94
95/// Inspect the two-byte HEVC NAL header. Returns
96/// `(forbidden_zero_bit, nal_unit_type, layer_id, temporal_id_plus1)`.
97pub fn nal_header(b0: u8, b1: u8) -> (u8, u8, u8, u8) {
98    let forbidden = (b0 >> 7) & 1;
99    let nal_unit_type = (b0 >> 1) & 0x3f;
100    let layer_id = ((b0 & 1) << 5) | ((b1 >> 3) & 0x1f);
101    let temporal_id_plus1 = b1 & 0x7;
102    (forbidden, nal_unit_type, layer_id, temporal_id_plus1)
103}
104
105/// True if `nal_unit_type` is one of the IRAP slice types that this
106/// crate treats as a decode entry point (IDR_W_RADL / IDR_N_LP / CRA).
107pub fn is_irap(nal_unit_type: u8) -> bool {
108    matches!(
109        nal_unit_type,
110        NAL_TYPE_IDR_W_RADL | NAL_TYPE_IDR_N_LP | NAL_TYPE_CRA
111    )
112}
113
114// ─────────────────────────── Output structs ──────────────────────────────────
115
116/// Profile / tier / level info from `profile_tier_level()`. Just the
117/// fields the HW backends look at; the 32 sub-layer profiles and
118/// flags are intentionally skipped.
119#[derive(Debug, Clone, Default, PartialEq, Eq)]
120pub struct HevcProfileTierLevel {
121    pub general_profile_space: u8,
122    pub general_tier_flag: bool,
123    pub general_profile_idc: u8,
124    /// 32-bit packed `general_profile_compatibility_flag[0..32]`.
125    pub general_profile_compatibility_flags: u32,
126    pub general_level_idc: u8,
127}
128
129/// Conformance cropping rectangle, in luma samples.
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub struct HevcConformanceWindow {
132    pub left: u32,
133    pub right: u32,
134    pub top: u32,
135    pub bottom: u32,
136}
137
138/// Video parameter set (7.3.2.1). Reduced — most VPS contents are
139/// not consumed by the slice-data HW APIs (they look at the SPS).
140#[derive(Debug, Clone, Default, PartialEq, Eq)]
141pub struct HevcVps {
142    pub vps_video_parameter_set_id: u8,
143    pub vps_max_layers_minus1: u8,
144    pub vps_max_sub_layers_minus1: u8,
145    pub vps_temporal_id_nesting_flag: bool,
146    pub profile_tier_level: HevcProfileTierLevel,
147}
148
149/// Sequence parameter set (7.3.2.2).
150#[derive(Debug, Clone, Default, PartialEq, Eq)]
151pub struct HevcSps {
152    pub sps_video_parameter_set_id: u8,
153    pub sps_max_sub_layers_minus1: u8,
154    pub sps_temporal_id_nesting_flag: bool,
155    pub profile_tier_level: HevcProfileTierLevel,
156
157    pub sps_seq_parameter_set_id: u8,
158    pub chroma_format_idc: u8,
159    pub separate_colour_plane_flag: bool,
160
161    pub pic_width_in_luma_samples: u32,
162    pub pic_height_in_luma_samples: u32,
163    pub conformance_window: Option<HevcConformanceWindow>,
164
165    pub bit_depth_luma_minus8: u8,
166    pub bit_depth_chroma_minus8: u8,
167
168    pub log2_max_pic_order_cnt_lsb_minus4: u8,
169    /// `sps_max_dec_pic_buffering_minus1[sps_max_sub_layers_minus1]`.
170    pub sps_max_dec_pic_buffering_minus1: u32,
171    pub sps_max_num_reorder_pics: u32,
172    pub sps_max_latency_increase_plus1: u32,
173
174    pub log2_min_luma_coding_block_size_minus3: u8,
175    pub log2_diff_max_min_luma_coding_block_size: u8,
176    pub log2_min_luma_transform_block_size_minus2: u8,
177    pub log2_diff_max_min_luma_transform_block_size: u8,
178    pub max_transform_hierarchy_depth_inter: u8,
179    pub max_transform_hierarchy_depth_intra: u8,
180
181    pub scaling_list_enabled_flag: bool,
182    pub amp_enabled_flag: bool,
183    pub sample_adaptive_offset_enabled_flag: bool,
184    pub pcm_enabled_flag: bool,
185    pub num_short_term_ref_pic_sets: u32,
186    pub long_term_ref_pics_present_flag: bool,
187    /// `num_long_term_ref_pics_sps`. The minimal parser refuses
188    /// `long_term_ref_pics_present_flag = 1`, so this is always 0
189    /// in practice — but the field is plumbed through to
190    /// `VdpPictureInfoHEVC` regardless and is therefore preserved.
191    pub num_long_term_ref_pics_sps: u32,
192    pub sps_temporal_mvp_enabled_flag: bool,
193    pub strong_intra_smoothing_enabled_flag: bool,
194}
195
196impl HevcSps {
197    /// Effective coded width in luma samples (= `pic_width_in_luma_samples`).
198    pub fn coded_width(&self) -> u32 {
199        self.pic_width_in_luma_samples
200    }
201
202    /// Effective coded height in luma samples.
203    pub fn coded_height(&self) -> u32 {
204        self.pic_height_in_luma_samples
205    }
206
207    /// SubWidthC table — H.265 6.2.
208    fn sub_width_c(&self) -> u32 {
209        match self.chroma_format_idc {
210            1 | 2 => 2,
211            _ => 1,
212        }
213    }
214
215    fn sub_height_c(&self) -> u32 {
216        match self.chroma_format_idc {
217            1 => 2,
218            _ => 1,
219        }
220    }
221
222    /// Display width after applying the conformance window
223    /// (7.4.3.2.1).
224    pub fn display_width(&self) -> u32 {
225        let Some(c) = self.conformance_window else {
226            return self.coded_width();
227        };
228        let sw = self.sub_width_c();
229        self.coded_width().saturating_sub((c.left + c.right) * sw)
230    }
231
232    /// Display height after applying the conformance window.
233    pub fn display_height(&self) -> u32 {
234        let Some(c) = self.conformance_window else {
235            return self.coded_height();
236        };
237        let sh = self.sub_height_c();
238        self.coded_height().saturating_sub((c.top + c.bottom) * sh)
239    }
240}
241
242/// Picture parameter set (7.3.2.3).
243#[derive(Debug, Clone, Default, PartialEq, Eq)]
244pub struct HevcPps {
245    pub pps_pic_parameter_set_id: u8,
246    pub pps_seq_parameter_set_id: u8,
247    pub dependent_slice_segments_enabled_flag: bool,
248    pub output_flag_present_flag: bool,
249    pub num_extra_slice_header_bits: u8,
250    pub sign_data_hiding_enabled_flag: bool,
251    pub cabac_init_present_flag: bool,
252    pub num_ref_idx_l0_default_active_minus1: u8,
253    pub num_ref_idx_l1_default_active_minus1: u8,
254    pub init_qp_minus26: i32,
255    pub constrained_intra_pred_flag: bool,
256    pub transform_skip_enabled_flag: bool,
257    pub cu_qp_delta_enabled_flag: bool,
258    pub diff_cu_qp_delta_depth: u32,
259    pub pps_cb_qp_offset: i32,
260    pub pps_cr_qp_offset: i32,
261    pub pps_slice_chroma_qp_offsets_present_flag: bool,
262    pub weighted_pred_flag: bool,
263    pub weighted_bipred_flag: bool,
264    pub transquant_bypass_enabled_flag: bool,
265    pub tiles_enabled_flag: bool,
266    pub entropy_coding_sync_enabled_flag: bool,
267    /// 7.4.3.3.1 — controls whether the in-loop deblocking filter
268    /// crosses slice boundaries.
269    pub pps_loop_filter_across_slices_enabled_flag: bool,
270    /// 7.4.3.3.1 — gates the deblocking-filter syntax block below.
271    pub deblocking_filter_control_present_flag: bool,
272    /// 7.4.3.3.1 — when set, allows the slice header to override
273    /// the PPS deblocking-filter parameters.
274    pub deblocking_filter_override_enabled_flag: bool,
275    /// 7.4.3.3.1 — disables the deblocking filter when set.
276    pub pps_deblocking_filter_disabled_flag: bool,
277    /// 7.4.3.3.1 — beta offset / 2 (signed Exp-Golomb), -6..6.
278    pub pps_beta_offset_div2: i32,
279    /// 7.4.3.3.1 — tc offset / 2 (signed Exp-Golomb), -6..6.
280    pub pps_tc_offset_div2: i32,
281    /// 7.4.3.3.1 — controls whether ref-pic-list modification is
282    /// signalled in the slice header.
283    pub lists_modification_present_flag: bool,
284    /// 7.4.3.3.1 — log2 of the minimum CU size that allows
285    /// parallel merge candidate derivation.
286    pub log2_parallel_merge_level_minus2: u32,
287    /// 7.4.3.3.1 — gates the slice-segment header extension block.
288    pub slice_segment_header_extension_present_flag: bool,
289}
290
291/// Minimal slice segment header (7.3.6).
292#[derive(Debug, Clone, Default, PartialEq, Eq)]
293pub struct HevcSliceHeader {
294    pub first_slice_segment_in_pic_flag: bool,
295    pub no_output_of_prior_pics_flag: bool,
296    pub slice_pic_parameter_set_id: u8,
297    /// 0 = B, 1 = P, 2 = I.
298    pub slice_type: u8,
299    pub slice_pic_order_cnt_lsb: u32,
300    pub slice_temporal_mvp_enabled_flag: bool,
301    /// True iff this header was parsed from an IRAP NAL
302    /// (IDR_*, CRA).
303    pub is_irap: bool,
304}
305
306/// Convenience structure for [`parse_idr_only`].
307#[derive(Debug)]
308pub struct HevcIdrParse<'a> {
309    pub vps: HevcVps,
310    pub sps: HevcSps,
311    pub pps: HevcPps,
312    pub slice_header: HevcSliceHeader,
313    pub nal_unit_type: u8,
314    pub idr_access_unit: &'a [u8],
315}
316
317// ─────────────────────────── profile_tier_level() ────────────────────────────
318
319fn parse_profile_tier_level(
320    r: &mut BitReader<'_>,
321    profile_present: bool,
322    max_num_sub_layers_minus1: u8,
323) -> Result<HevcProfileTierLevel, BitstreamError> {
324    let mut ptl = HevcProfileTierLevel::default();
325    if profile_present {
326        ptl.general_profile_space = r.u(2) as u8;
327        ptl.general_tier_flag = r.u(1) != 0;
328        ptl.general_profile_idc = r.u(5) as u8;
329        ptl.general_profile_compatibility_flags = r.u(32);
330        // general_progressive_source_flag, general_interlaced_source_flag,
331        // general_non_packed_constraint_flag, general_frame_only_constraint_flag,
332        // 43 + 1 = 44 reserved bits.
333        let _flags_top = r.u(4);
334        // The 43 reserved-zero bits + 1 inbld follow; their semantics
335        // depend on profile, but for parsing we just consume them.
336        let _flags_mid = r.u(32);
337        let _flags_bot = r.u(11);
338        let _general_inbld_or_reserved = r.u(1);
339    }
340    ptl.general_level_idc = r.u(8) as u8;
341
342    // Per-sub-layer flags. We don't expose them, but we have to read
343    // them to keep our bit position correct.
344    if max_num_sub_layers_minus1 > 0 {
345        let mut sub_layer_profile_present = [false; 7];
346        let mut sub_layer_level_present = [false; 7];
347        for i in 0..max_num_sub_layers_minus1 as usize {
348            sub_layer_profile_present[i] = r.u(1) != 0;
349            sub_layer_level_present[i] = r.u(1) != 0;
350        }
351        // 2*(8 - max_num_sub_layers_minus1) reserved-zero bits.
352        for _ in max_num_sub_layers_minus1..8 {
353            let _ = r.u(2);
354        }
355        for i in 0..max_num_sub_layers_minus1 as usize {
356            if sub_layer_profile_present[i] {
357                let _profile_space = r.u(2);
358                let _tier_flag = r.u(1);
359                let _profile_idc = r.u(5);
360                let _compat = r.u(32);
361                // 4 source/constraint flags + 43 reserved + 1 inbld
362                let _ = r.u(4);
363                let _ = r.u(32);
364                let _ = r.u(11);
365                let _ = r.u(1);
366            }
367            if sub_layer_level_present[i] {
368                let _sub_level_idc = r.u(8);
369            }
370        }
371    }
372    Ok(ptl)
373}
374
375// ─────────────────────────── ST-RPS skipper ──────────────────────────────────
376
377/// Skip past a single `st_ref_pic_set( stRpsIdx )` entry (7.3.7).
378/// We can't keep state across multiple sets, so this is only callable
379/// when the current set is the first one (or, in the version used
380/// inside the SPS, when there is only ever going to be one). For the
381/// minimal-parser scope we *forbid* `num_short_term_ref_pic_sets > 0`
382/// in the SPS, so this routine is here mainly for the slice-header
383/// path's potential future use; today it short-circuits.
384#[allow(dead_code)]
385fn skip_st_ref_pic_set(_r: &mut BitReader<'_>) -> Result<(), BitstreamError> {
386    Err(BitstreamError::unsupported(
387        "HEVC st_ref_pic_set parsing not implemented in minimal parser",
388    ))
389}
390
391// ─────────────────────────── Parsers ─────────────────────────────────────────
392
393/// Parse a VPS NAL (with two-byte NAL header at index 0..1).
394pub fn parse_vps_nal(nal: &[u8]) -> Result<HevcVps, BitstreamError> {
395    if nal.len() < 2 {
396        return Err(BitstreamError::unexpected_end(
397            "VPS NAL shorter than 2 bytes",
398        ));
399    }
400    let (_, nal_type, _, _) = nal_header(nal[0], nal[1]);
401    if nal_type != NAL_TYPE_VPS {
402        return Err(BitstreamError::invalid(format!(
403            "expected VPS NAL (type=32), got type={nal_type}"
404        )));
405    }
406    let rbsp = ebsp_to_rbsp(&nal[2..]);
407    let mut r = BitReader::new(&rbsp);
408    let mut vps = HevcVps {
409        vps_video_parameter_set_id: r.u(4) as u8,
410        ..HevcVps::default()
411    };
412    let _vps_base_layer_internal_flag = r.u(1);
413    let _vps_base_layer_available_flag = r.u(1);
414    vps.vps_max_layers_minus1 = r.u(6) as u8;
415    vps.vps_max_sub_layers_minus1 = r.u(3) as u8;
416    vps.vps_temporal_id_nesting_flag = r.u(1) != 0;
417    let _vps_reserved_0xffff_16bits = r.u(16);
418    vps.profile_tier_level = parse_profile_tier_level(&mut r, true, vps.vps_max_sub_layers_minus1)?;
419    // Remaining VPS fields (vps_sub_layer_ordering_info, layer_id_included…,
420    // vps_num_hrd_parameters, VUI, extensions) are not consumed.
421    Ok(vps)
422}
423
424/// Parse an SPS NAL (with two-byte NAL header at index 0..1).
425pub fn parse_sps_nal(nal: &[u8]) -> Result<HevcSps, BitstreamError> {
426    if nal.len() < 2 {
427        return Err(BitstreamError::unexpected_end(
428            "SPS NAL shorter than 2 bytes",
429        ));
430    }
431    let (_, nal_type, _, _) = nal_header(nal[0], nal[1]);
432    if nal_type != NAL_TYPE_SPS {
433        return Err(BitstreamError::invalid(format!(
434            "expected SPS NAL (type=33), got type={nal_type}"
435        )));
436    }
437    let rbsp = ebsp_to_rbsp(&nal[2..]);
438    let mut r = BitReader::new(&rbsp);
439    let mut sps = HevcSps {
440        sps_video_parameter_set_id: r.u(4) as u8,
441        sps_max_sub_layers_minus1: r.u(3) as u8,
442        sps_temporal_id_nesting_flag: r.u(1) != 0,
443        ..HevcSps::default()
444    };
445    sps.profile_tier_level = parse_profile_tier_level(&mut r, true, sps.sps_max_sub_layers_minus1)?;
446    sps.sps_seq_parameter_set_id = r.ue()? as u8;
447    sps.chroma_format_idc = r.ue()? as u8;
448    if sps.chroma_format_idc == 3 {
449        sps.separate_colour_plane_flag = r.u(1) != 0;
450    }
451    sps.pic_width_in_luma_samples = r.ue()?;
452    sps.pic_height_in_luma_samples = r.ue()?;
453    let conformance_window_flag = r.u(1);
454    if conformance_window_flag != 0 {
455        sps.conformance_window = Some(HevcConformanceWindow {
456            left: r.ue()?,
457            right: r.ue()?,
458            top: r.ue()?,
459            bottom: r.ue()?,
460        });
461    }
462    sps.bit_depth_luma_minus8 = r.ue()? as u8;
463    sps.bit_depth_chroma_minus8 = r.ue()? as u8;
464    sps.log2_max_pic_order_cnt_lsb_minus4 = r.ue()? as u8;
465
466    let sps_sub_layer_ordering_info_present_flag = r.u(1);
467    let start = if sps_sub_layer_ordering_info_present_flag != 0 {
468        0
469    } else {
470        sps.sps_max_sub_layers_minus1 as usize
471    };
472    // For each sub-layer in [start, sps_max_sub_layers_minus1], read
473    // three ue(v) values. Keep only the [sps_max_sub_layers_minus1]
474    // entry (the highest layer — the one DPB sizing uses).
475    let mut last_max_dec = 0u32;
476    let mut last_max_reorder = 0u32;
477    let mut last_max_latency = 0u32;
478    for _ in start..=sps.sps_max_sub_layers_minus1 as usize {
479        last_max_dec = r.ue()?;
480        last_max_reorder = r.ue()?;
481        last_max_latency = r.ue()?;
482    }
483    sps.sps_max_dec_pic_buffering_minus1 = last_max_dec;
484    sps.sps_max_num_reorder_pics = last_max_reorder;
485    sps.sps_max_latency_increase_plus1 = last_max_latency;
486
487    sps.log2_min_luma_coding_block_size_minus3 = r.ue()? as u8;
488    sps.log2_diff_max_min_luma_coding_block_size = r.ue()? as u8;
489    sps.log2_min_luma_transform_block_size_minus2 = r.ue()? as u8;
490    sps.log2_diff_max_min_luma_transform_block_size = r.ue()? as u8;
491    sps.max_transform_hierarchy_depth_inter = r.ue()? as u8;
492    sps.max_transform_hierarchy_depth_intra = r.ue()? as u8;
493
494    sps.scaling_list_enabled_flag = r.u(1) != 0;
495    if sps.scaling_list_enabled_flag {
496        return Err(BitstreamError::unsupported(
497            "HEVC SPS scaling_list_enabled_flag=1 not supported by minimal parser",
498        ));
499    }
500    sps.amp_enabled_flag = r.u(1) != 0;
501    sps.sample_adaptive_offset_enabled_flag = r.u(1) != 0;
502    sps.pcm_enabled_flag = r.u(1) != 0;
503    if sps.pcm_enabled_flag {
504        return Err(BitstreamError::unsupported(
505            "HEVC SPS pcm_enabled_flag=1 not supported by minimal parser",
506        ));
507    }
508    sps.num_short_term_ref_pic_sets = r.ue()?;
509    if sps.num_short_term_ref_pic_sets > 0 {
510        return Err(BitstreamError::unsupported(
511            "HEVC SPS num_short_term_ref_pic_sets>0 not supported by minimal parser",
512        ));
513    }
514    sps.long_term_ref_pics_present_flag = r.u(1) != 0;
515    if sps.long_term_ref_pics_present_flag {
516        return Err(BitstreamError::unsupported(
517            "HEVC SPS long_term_ref_pics_present_flag=1 not supported by minimal parser",
518        ));
519    }
520    sps.sps_temporal_mvp_enabled_flag = r.u(1) != 0;
521    sps.strong_intra_smoothing_enabled_flag = r.u(1) != 0;
522    // VUI / sps_extension flags follow but are not consumed here.
523    Ok(sps)
524}
525
526/// Parse a PPS NAL (with two-byte NAL header at index 0..1).
527pub fn parse_pps_nal(nal: &[u8]) -> Result<HevcPps, BitstreamError> {
528    if nal.len() < 2 {
529        return Err(BitstreamError::unexpected_end(
530            "PPS NAL shorter than 2 bytes",
531        ));
532    }
533    let (_, nal_type, _, _) = nal_header(nal[0], nal[1]);
534    if nal_type != NAL_TYPE_PPS {
535        return Err(BitstreamError::invalid(format!(
536            "expected PPS NAL (type=34), got type={nal_type}"
537        )));
538    }
539    let rbsp = ebsp_to_rbsp(&nal[2..]);
540    let mut r = BitReader::new(&rbsp);
541    let mut pps = HevcPps {
542        pps_pic_parameter_set_id: r.ue()? as u8,
543        pps_seq_parameter_set_id: r.ue()? as u8,
544        dependent_slice_segments_enabled_flag: r.u(1) != 0,
545        output_flag_present_flag: r.u(1) != 0,
546        num_extra_slice_header_bits: r.u(3) as u8,
547        sign_data_hiding_enabled_flag: r.u(1) != 0,
548        cabac_init_present_flag: r.u(1) != 0,
549        num_ref_idx_l0_default_active_minus1: r.ue()? as u8,
550        num_ref_idx_l1_default_active_minus1: r.ue()? as u8,
551        init_qp_minus26: r.se()?,
552        constrained_intra_pred_flag: r.u(1) != 0,
553        transform_skip_enabled_flag: r.u(1) != 0,
554        cu_qp_delta_enabled_flag: r.u(1) != 0,
555        ..HevcPps::default()
556    };
557    if pps.cu_qp_delta_enabled_flag {
558        pps.diff_cu_qp_delta_depth = r.ue()?;
559    }
560    pps.pps_cb_qp_offset = r.se()?;
561    pps.pps_cr_qp_offset = r.se()?;
562    pps.pps_slice_chroma_qp_offsets_present_flag = r.u(1) != 0;
563    pps.weighted_pred_flag = r.u(1) != 0;
564    pps.weighted_bipred_flag = r.u(1) != 0;
565    pps.transquant_bypass_enabled_flag = r.u(1) != 0;
566    pps.tiles_enabled_flag = r.u(1) != 0;
567    pps.entropy_coding_sync_enabled_flag = r.u(1) != 0;
568    if pps.tiles_enabled_flag {
569        return Err(BitstreamError::unsupported(
570            "HEVC PPS tiles_enabled_flag=1 not supported by minimal parser",
571        ));
572    }
573    if pps.entropy_coding_sync_enabled_flag {
574        return Err(BitstreamError::unsupported(
575            "HEVC PPS entropy_coding_sync_enabled_flag=1 (WPP) not supported by minimal parser",
576        ));
577    }
578    // 7.3.2.3.1 continued. The tile-related block is skipped because
579    // we already refused `tiles_enabled_flag=1` above.
580    pps.pps_loop_filter_across_slices_enabled_flag = r.u(1) != 0;
581    pps.deblocking_filter_control_present_flag = r.u(1) != 0;
582    if pps.deblocking_filter_control_present_flag {
583        pps.deblocking_filter_override_enabled_flag = r.u(1) != 0;
584        pps.pps_deblocking_filter_disabled_flag = r.u(1) != 0;
585        if !pps.pps_deblocking_filter_disabled_flag {
586            pps.pps_beta_offset_div2 = r.se()?;
587            pps.pps_tc_offset_div2 = r.se()?;
588        }
589    }
590    let pps_scaling_list_data_present_flag = r.u(1);
591    if pps_scaling_list_data_present_flag != 0 {
592        return Err(BitstreamError::unsupported(
593            "HEVC PPS pps_scaling_list_data_present_flag=1 not supported by minimal parser",
594        ));
595    }
596    pps.lists_modification_present_flag = r.u(1) != 0;
597    pps.log2_parallel_merge_level_minus2 = r.ue()?;
598    pps.slice_segment_header_extension_present_flag = r.u(1) != 0;
599    // pps_extension_present_flag and any extension blocks are not
600    // consulted — they belong to the range / SCC / etc. extensions
601    // which the minimal parser does not surface.
602    Ok(pps)
603}
604
605/// Parse a minimal IRAP slice-segment header (7.3.6.1). Only the
606/// fields needed to confirm the slice is intra-coded and to populate
607/// the HW backends' picture-info struct are read.
608pub fn parse_slice_header_minimal(
609    nal: &[u8],
610    sps: &HevcSps,
611    pps: &HevcPps,
612) -> Result<HevcSliceHeader, BitstreamError> {
613    if nal.len() < 2 {
614        return Err(BitstreamError::unexpected_end(
615            "slice NAL shorter than 2 bytes",
616        ));
617    }
618    let (_, nal_unit_type, _, _) = nal_header(nal[0], nal[1]);
619    let is_irap_slice = is_irap(nal_unit_type);
620
621    let rbsp = ebsp_to_rbsp(&nal[2..]);
622    let mut r = BitReader::new(&rbsp);
623    let mut sh = HevcSliceHeader {
624        first_slice_segment_in_pic_flag: r.u(1) != 0,
625        ..HevcSliceHeader::default()
626    };
627
628    if is_irap_slice {
629        sh.no_output_of_prior_pics_flag = r.u(1) != 0;
630    }
631    sh.slice_pic_parameter_set_id = r.ue()? as u8;
632
633    let dependent_slice_segment_flag =
634        if !sh.first_slice_segment_in_pic_flag && pps.dependent_slice_segments_enabled_flag {
635            r.u(1) != 0
636        } else {
637            false
638        };
639    if dependent_slice_segment_flag {
640        return Err(BitstreamError::unsupported(
641            "HEVC dependent slice segments not supported by minimal parser",
642        ));
643    }
644    if !sh.first_slice_segment_in_pic_flag {
645        // slice_segment_address — ceil(log2(num CTBs)) bits. We don't
646        // need it for the HW IDR submit; just consume.
647        let ctb_log2 = sps.log2_min_luma_coding_block_size_minus3 as u32
648            + 3
649            + sps.log2_diff_max_min_luma_coding_block_size as u32;
650        let ctb_size = 1u32 << ctb_log2;
651        let pic_w_in_ctbs = sps.pic_width_in_luma_samples.div_ceil(ctb_size);
652        let pic_h_in_ctbs = sps.pic_height_in_luma_samples.div_ceil(ctb_size);
653        let total_ctbs = pic_w_in_ctbs as u64 * pic_h_in_ctbs as u64;
654        let bits = 64 - total_ctbs.leading_zeros();
655        let _slice_segment_address = r.u(bits);
656    }
657
658    // Skip the encoder-defined extra slice header bits.
659    for _ in 0..pps.num_extra_slice_header_bits {
660        let _ = r.u(1);
661    }
662    sh.slice_type = r.ue()? as u8;
663
664    if pps.output_flag_present_flag {
665        let _pic_output_flag = r.u(1);
666    }
667    if sps.separate_colour_plane_flag {
668        let _colour_plane_id = r.u(2);
669    }
670
671    // For non-IDR slices, slice_pic_order_cnt_lsb is present.
672    if !matches!(nal_unit_type, NAL_TYPE_IDR_W_RADL | NAL_TYPE_IDR_N_LP) {
673        sh.slice_pic_order_cnt_lsb = r.u(sps.log2_max_pic_order_cnt_lsb_minus4 as u32 + 4);
674        // … plus more ref-pic-set logic. We don't follow it in v0;
675        // the rest of the slice header is irrelevant to the IRAP HW
676        // submit.
677    } else {
678        sh.slice_pic_order_cnt_lsb = 0;
679    }
680
681    sh.is_irap = is_irap_slice;
682    // We deliberately do NOT keep parsing past this point. The HW
683    // backends only need the fields above to populate the picture
684    // info struct; the remainder (slice_temporal_mvp_enabled_flag,
685    // sao flags, num_ref_idx_active overrides, …) varies wildly
686    // between profiles and is not needed for an IRAP-only submit.
687    Ok(sh)
688}
689
690/// Walk an Annex-B HEVC stream, locate the first IRAP slice (IDR or
691/// CRA), parse VPS / SPS / PPS / slice header, and return everything
692/// plus a slice of the original input covering the IRAP access unit.
693pub fn parse_idr_only(stream: &[u8]) -> Result<HevcIdrParse<'_>, BitstreamError> {
694    let nals = locate_annex_b(stream);
695    if nals.is_empty() {
696        return Err(BitstreamError::invalid("no NAL units found in stream"));
697    }
698
699    let mut vps: Option<HevcVps> = None;
700    let mut sps: Option<HevcSps> = None;
701    let mut pps: Option<HevcPps> = None;
702    let mut irap_idx: Option<usize> = None;
703    let mut irap_nal_type: u8 = 0;
704    for (idx, n) in nals.iter().enumerate() {
705        let body = &stream[n.body_start..n.body_end];
706        if body.len() < 2 {
707            continue;
708        }
709        let (_, nal_type, _, _) = nal_header(body[0], body[1]);
710        match nal_type {
711            NAL_TYPE_VPS if vps.is_none() => vps = Some(parse_vps_nal(body)?),
712            NAL_TYPE_SPS if sps.is_none() => sps = Some(parse_sps_nal(body)?),
713            NAL_TYPE_PPS if pps.is_none() => pps = Some(parse_pps_nal(body)?),
714            t if is_irap(t) && irap_idx.is_none() => {
715                irap_idx = Some(idx);
716                irap_nal_type = t;
717            }
718            _ => {}
719        }
720    }
721    let vps = vps.ok_or_else(|| BitstreamError::invalid("stream has no VPS"))?;
722    let sps = sps.ok_or_else(|| BitstreamError::invalid("stream has no SPS"))?;
723    let pps = pps.ok_or_else(|| BitstreamError::invalid("stream has no PPS"))?;
724    let irap_idx = irap_idx.ok_or_else(|| BitstreamError::invalid("stream has no IRAP slice"))?;
725    let irap = &nals[irap_idx];
726    let body = &stream[irap.body_start..irap.body_end];
727    let slice_header = parse_slice_header_minimal(body, &sps, &pps)?;
728    let access_unit = &stream[irap.start_code_start..];
729    Ok(HevcIdrParse {
730        vps,
731        sps,
732        pps,
733        slice_header,
734        nal_unit_type: irap_nal_type,
735        idr_access_unit: access_unit,
736    })
737}
738
739// ─────────────────────────── Annex-B index helper ────────────────────────────
740
741#[derive(Debug, Clone, Copy)]
742struct NalLoc {
743    start_code_start: usize,
744    body_start: usize,
745    body_end: usize,
746}
747
748fn locate_annex_b(buf: &[u8]) -> Vec<NalLoc> {
749    let mut out = Vec::new();
750    let mut i = 0;
751    let n = buf.len();
752    let mut current: Option<(usize, usize)> = None;
753    while i < n {
754        let four =
755            i + 3 < n && buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 0 && buf[i + 3] == 1;
756        let three = !four && i + 2 < n && buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 1;
757        if four || three {
758            if let Some((sc, body_start)) = current.take() {
759                out.push(NalLoc {
760                    start_code_start: sc,
761                    body_start,
762                    body_end: i,
763                });
764            }
765            let sc = i;
766            i += if four { 4 } else { 3 };
767            current = Some((sc, i));
768            continue;
769        }
770        i += 1;
771    }
772    if let Some((sc, body_start)) = current.take() {
773        out.push(NalLoc {
774            start_code_start: sc,
775            body_start,
776            body_end: n,
777        });
778    }
779    out
780}
781
782// ─────────────────────────── Tests ───────────────────────────────────────────
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787
788    #[test]
789    fn split_finds_three_nals() {
790        let buf = [
791            0, 0, 0, 1, 0x40, 0x01, 0xaa, // VPS
792            0, 0, 1, 0x42, 0x01, 0xbb, // SPS
793            0, 0, 0, 1, 0x28, 0x01, 0xcc, // IDR
794        ];
795        let nals = split_annex_b(&buf);
796        assert_eq!(nals.len(), 3);
797    }
798
799    #[test]
800    fn nal_header_decodes_vps() {
801        // 0x40 = 0100_0000, 0x01 = 0000_0001.
802        // forbidden=0, nal_unit_type = (0x40>>1)&0x3f = 0x20 = 32 (VPS).
803        let (f, t, l, tid) = nal_header(0x40, 0x01);
804        assert_eq!(f, 0);
805        assert_eq!(t, NAL_TYPE_VPS);
806        assert_eq!(l, 0);
807        assert_eq!(tid, 1);
808    }
809
810    #[test]
811    fn ebsp_to_rbsp_strips_03_in_zero_zero_run() {
812        let ebsp = [0x00, 0x00, 0x03, 0x01];
813        let rbsp = ebsp_to_rbsp(&ebsp);
814        assert_eq!(rbsp, &[0x00, 0x00, 0x01]);
815    }
816}