Skip to main content

oxideav_h265/
sps.rs

1//! Sequence Parameter Set (SPS) parser per ITU-T Rec. H.265 §7.3.2.2.
2//!
3//! Scope: parse the full SPS RBSP body through the
4//! `strong_intra_smoothing_enabled_flag` field, the
5//! `vui_parameters_present_flag` gate (whose §E.2.1
6//! `vui_parameters()` body is decoded into [`crate::vui::VuiParameters`]),
7//! and the `sps_extension_present_flag` gate. When the extension gate
8//! is set, the trailing bytes (extension payload + RBSP trailing
9//! bits) are surfaced as an **opaque tail**: a copy of the
10//! still-unparsed RBSP bytes plus the bit offset within the first
11//! byte at which the opaque tail begins. The per-extension
12//! `sps_*_extension( )` syntax structures are not materialised yet.
13//!
14//! When `scaling_list_enabled_flag == 1` and
15//! `sps_scaling_list_data_present_flag == 1`, the §7.3.4
16//! `scaling_list_data()` block is parsed via the shared
17//! [`crate::scaling_list`] module; otherwise the §7.4.5 default lists
18//! apply.
19//!
20//! ## Layout summary
21//!
22//! ```text
23//! sps_video_parameter_set_id                       u(4)
24//! sps_max_sub_layers_minus1                        u(3)
25//! sps_temporal_id_nesting_flag                     u(1)
26//! profile_tier_level( 1, sps_max_sub_layers_minus1 )
27//! sps_seq_parameter_set_id                        ue(v)
28//! chroma_format_idc                               ue(v)
29//! if( chroma_format_idc == 3 )
30//!   separate_colour_plane_flag                     u(1)
31//! pic_width_in_luma_samples                       ue(v)
32//! pic_height_in_luma_samples                      ue(v)
33//! conformance_window_flag                          u(1)
34//! if( conformance_window_flag ) {
35//!   conf_win_left_offset                          ue(v)
36//!   conf_win_right_offset                         ue(v)
37//!   conf_win_top_offset                           ue(v)
38//!   conf_win_bottom_offset                        ue(v)
39//! }
40//! bit_depth_luma_minus8                           ue(v)
41//! bit_depth_chroma_minus8                         ue(v)
42//! log2_max_pic_order_cnt_lsb_minus4               ue(v)
43//! sps_sub_layer_ordering_info_present_flag         u(1)
44//! for( i = (...) ; i <= sps_max_sub_layers_minus1; i++ ) {
45//!   sps_max_dec_pic_buffering_minus1[i]           ue(v)
46//!   sps_max_num_reorder_pics[i]                   ue(v)
47//!   sps_max_latency_increase_plus1[i]             ue(v)
48//! }
49//! log2_min_luma_coding_block_size_minus3          ue(v)
50//! log2_diff_max_min_luma_coding_block_size        ue(v)
51//! log2_min_luma_transform_block_size_minus2       ue(v)
52//! log2_diff_max_min_luma_transform_block_size     ue(v)
53//! max_transform_hierarchy_depth_inter             ue(v)
54//! max_transform_hierarchy_depth_intra             ue(v)
55//! scaling_list_enabled_flag                        u(1)
56//!   if( scaling_list_enabled_flag ) {
57//!     sps_scaling_list_data_present_flag            u(1)
58//!     if( sps_scaling_list_data_present_flag )
59//!       scaling_list_data( )                        /* §7.3.4 */
60//!   }
61//! amp_enabled_flag                                 u(1)
62//! sample_adaptive_offset_enabled_flag              u(1)
63//! pcm_enabled_flag                                 u(1)
64//! if( pcm_enabled_flag ) {
65//!   pcm_sample_bit_depth_luma_minus1               u(4)
66//!   pcm_sample_bit_depth_chroma_minus1             u(4)
67//!   log2_min_pcm_luma_coding_block_size_minus3    ue(v)
68//!   log2_diff_max_min_pcm_luma_coding_block_size  ue(v)
69//!   pcm_loop_filter_disabled_flag                  u(1)
70//! }
71//! num_short_term_ref_pic_sets                     ue(v)
72//! for( i = 0; i < num_short_term_ref_pic_sets; i++ )
73//!   st_ref_pic_set( i )
74//! long_term_ref_pics_present_flag                  u(1)
75//! if( long_term_ref_pics_present_flag ) {
76//!   num_long_term_ref_pics_sps                    ue(v)
77//!   for( i = 0; i < num_long_term_ref_pics_sps; i++ ) {
78//!     lt_ref_pic_poc_lsb_sps[i]                    u(v)  /* log2_max_poc_lsb+4 */
79//!     used_by_curr_pic_lt_sps_flag[i]              u(1)
80//!   }
81//! }
82//! sps_temporal_mvp_enabled_flag                    u(1)
83//! strong_intra_smoothing_enabled_flag              u(1)
84//! vui_parameters_present_flag                      u(1)
85//! if( vui_parameters_present_flag )
86//!   vui_parameters( )                              /* §E.2.1 */
87//! sps_extension_present_flag                       u(1)
88//! if( sps_extension_present_flag ) {
89//!   sps_range_extension_flag                        u(1)
90//!   sps_multilayer_extension_flag                   u(1)
91//!   sps_3d_extension_flag                           u(1)
92//!   sps_scc_extension_flag                          u(1)
93//!   sps_extension_4bits                             u(4)
94//!   /* opaque tail begins at the first set body, or stays
95//!      empty when every flag is 0 */
96//! }
97//! ```
98//!
99//! When `sps_extension_present_flag == 1` the eight bits of typed
100//! extension flags (`sps_range_extension_flag`,
101//! `sps_multilayer_extension_flag`, `sps_3d_extension_flag`,
102//! `sps_scc_extension_flag`, `sps_extension_4bits`) are decoded into
103//! [`SpsExtensionFlags`]. When all five sub-fields are zero only the
104//! RBSP trailing byte remains and no opaque tail is captured; otherwise
105//! the extension bodies (`sps_range_extension()`,
106//! `sps_multilayer_extension()`, `sps_3d_extension()`,
107//! `sps_scc_extension()`, and the `sps_extension_data_flag` while-loop
108//! gated by `sps_extension_4bits`) are surfaced as a single
109//! [`OpaqueTail`] starting at the first body's bit position.
110//!
111//! Validity checks performed here, sourced from §7.4.3.2:
112//!
113//! * `sps_max_sub_layers_minus1` range 0..=6.
114//! * `sps_video_parameter_set_id` and `sps_seq_parameter_set_id`
115//!   range 0..=15.
116//! * `chroma_format_idc` range 0..=3.
117//! * `pic_width_in_luma_samples` and `pic_height_in_luma_samples` not
118//!   equal to zero (the modulo-`MinCbSizeY` check is deferred — it
119//!   depends on the not-yet-validated `log2_min_cb_size`).
120//! * `bit_depth_luma_minus8` and `bit_depth_chroma_minus8` range
121//!   0..=8 (§7.4.3.2 caps the encoded bit depth at 16).
122//! * `log2_max_pic_order_cnt_lsb_minus4` range 0..=12.
123//! * `pcm_sample_bit_depth_luma_minus1` / `pcm_sample_bit_depth_chroma_minus1`
124//!   must satisfy `PcmBitDepthY <= BitDepthY` /
125//!   `PcmBitDepthC <= BitDepthC` per §7.4.3.2 / equation (7-25 / 7-26).
126//! * `num_short_term_ref_pic_sets` range 0..=64.
127//! * `num_long_term_ref_pics_sps` range 0..=32.
128//! * `num_negative_pics` / `num_positive_pics` capped at 16 (the
129//!   §7.4.8 bound is
130//!   `sps_max_dec_pic_buffering_minus1[sps_max_sub_layers_minus1]`,
131//!   itself bounded at 16 because `MaxDpbSize` per §A.4.2 is 16).
132//! * `delta_poc_s0_minus1` / `delta_poc_s1_minus1` /
133//!   `abs_delta_rps_minus1` range 0..=2^15-1.
134//! * `sps_scaling_list_data_present_flag == 1` triggers a §7.3.4
135//!   `scaling_list_data()` parse into [`ScalingListData`] via the
136//!   shared [`crate::scaling_list`] module.
137
138use crate::bitreader::{BitReader, BitReaderError};
139use crate::scaling_list::{ScalingListData, ScalingListError};
140use crate::vps::{ProfileTierLevel, SubLayerOrderingInfo, VpsError, HEVC_MAX_SUB_LAYERS};
141use crate::vui::{VuiError, VuiParameters};
142
143/// Maximum number of short-term reference picture sets an SPS may
144/// carry. Per §7.4.3.2 the field is bounded at 64 inclusive.
145pub const HEVC_MAX_NUM_SHORT_TERM_RPS: usize = 64;
146
147/// Maximum number of long-term reference pictures an SPS may carry.
148/// Per §7.4.3.2 the field is bounded at 32 inclusive.
149pub const HEVC_MAX_NUM_LONG_TERM_RPS: usize = 32;
150
151/// Maximum number of negative / positive entries permitted in one
152/// short-term RPS. Per §7.4.8 the bound is
153/// `sps_max_dec_pic_buffering_minus1[sps_max_sub_layers_minus1]`
154/// which §A.4.2 caps at `MaxDpbSize - 1 = 15` (for typical levels);
155/// 16 is a defensive upper bound for the parser.
156pub const HEVC_MAX_RPS_PICS: usize = 16;
157
158/// Errors that can arise while parsing an SPS RBSP.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum SpsError {
161    /// The RBSP ran out of bits before the SPS was fully parsed.
162    Truncated,
163    /// A syntax element's parsed value was outside the legal range
164    /// specified for it in §7.4.3.2.
165    ValueOutOfRange {
166        /// Name of the offending syntax element.
167        field: &'static str,
168        /// The (illegal) value as a `u32` (signed elements are
169        /// re-cast at the call site).
170        got: u32,
171    },
172    /// A `scaling_list_data()` parse (§7.3.4) from the SPS failed.
173    ScalingList(ScalingListError),
174    /// An unexpected bitstream-level error surfaced from the reader.
175    Bitstream(BitReaderError),
176    /// A `profile_tier_level()` parse from the §7.3.3 walk failed.
177    Ptl(VpsError),
178    /// A `vui_parameters()` parse (§E.2.1) from the SPS failed.
179    Vui(VuiError),
180}
181
182impl core::fmt::Display for SpsError {
183    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
184        match self {
185            Self::Truncated => f.write_str("SPS RBSP truncated"),
186            Self::ValueOutOfRange { field, got } => {
187                write!(f, "SPS syntax element {field} out of range: {got}")
188            }
189            Self::ScalingList(e) => write!(f, "scaling-list error during SPS parse: {e}"),
190            Self::Bitstream(e) => write!(f, "bitstream error during SPS parse: {e}"),
191            Self::Ptl(e) => write!(f, "profile_tier_level error during SPS parse: {e}"),
192            Self::Vui(e) => write!(f, "vui_parameters error during SPS parse: {e}"),
193        }
194    }
195}
196
197impl std::error::Error for SpsError {}
198
199impl From<BitReaderError> for SpsError {
200    fn from(e: BitReaderError) -> Self {
201        match e {
202            BitReaderError::EndOfBuffer => Self::Truncated,
203            other => Self::Bitstream(other),
204        }
205    }
206}
207
208impl From<VpsError> for SpsError {
209    fn from(e: VpsError) -> Self {
210        // Surface the inner reader-truncation directly so callers can
211        // distinguish "ran off the end mid-PTL" from other PTL faults.
212        if matches!(e, VpsError::Truncated) {
213            Self::Truncated
214        } else {
215            Self::Ptl(e)
216        }
217    }
218}
219
220impl From<ScalingListError> for SpsError {
221    fn from(e: ScalingListError) -> Self {
222        // Flatten truncation / raw-reader faults to the SPS-level
223        // equivalents so the public surface stays predictable; carry
224        // the structured scaling-list faults through as-is.
225        match e {
226            ScalingListError::Truncated => Self::Truncated,
227            ScalingListError::Bitstream(b) => Self::Bitstream(b),
228            other => Self::ScalingList(other),
229        }
230    }
231}
232
233impl From<VuiError> for SpsError {
234    fn from(e: VuiError) -> Self {
235        // Flatten truncation / raw-reader faults to the SPS-level
236        // equivalents so the public surface stays predictable; carry
237        // the structured VUI faults through as-is.
238        match e {
239            VuiError::Truncated => Self::Truncated,
240            VuiError::Bitstream(b) => Self::Bitstream(b),
241            other => Self::Vui(other),
242        }
243    }
244}
245
246/// Conformance-window offsets, in §6.5.5 SubWidthC/SubHeightC units
247/// (not pixel units — the multiplier depends on
248/// [`SeqParameterSet::chroma_format_idc`]).
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
250pub struct ConformanceWindow {
251    /// `conf_win_left_offset` (`ue(v)`).
252    pub left_offset: u32,
253    /// `conf_win_right_offset` (`ue(v)`).
254    pub right_offset: u32,
255    /// `conf_win_top_offset` (`ue(v)`).
256    pub top_offset: u32,
257    /// `conf_win_bottom_offset` (`ue(v)`).
258    pub bottom_offset: u32,
259}
260
261/// PCM-related SPS block per §7.3.2.2 (`pcm_enabled_flag == 1` only).
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub struct PcmInfo {
264    /// `pcm_sample_bit_depth_luma_minus1` (`u(4)`). The §7.4.3.2
265    /// `PcmBitDepthY` derivation is `value + 1`.
266    pub bit_depth_luma_minus1: u8,
267    /// `pcm_sample_bit_depth_chroma_minus1` (`u(4)`). `PcmBitDepthC = value + 1`.
268    pub bit_depth_chroma_minus1: u8,
269    /// `log2_min_pcm_luma_coding_block_size_minus3` (`ue(v)`).
270    /// `Log2MinIpcmCbSizeY = value + 3`.
271    pub log2_min_pcm_luma_coding_block_size_minus3: u8,
272    /// `log2_diff_max_min_pcm_luma_coding_block_size` (`ue(v)`).
273    /// `Log2MaxIpcmCbSizeY = Log2MinIpcmCbSizeY + value`.
274    pub log2_diff_max_min_pcm_luma_coding_block_size: u8,
275    /// `pcm_loop_filter_disabled_flag` (`u(1)`).
276    pub loop_filter_disabled_flag: bool,
277}
278
279/// Short-term reference picture set per §7.3.7. Only the explicit
280/// (non-inter-predicted) form materialises the per-entry POC and
281/// `used_by_curr_pic` arrays; the inter-RPS-prediction form materialises
282/// only the inputs to the §7.4.8 derivation (`delta_idx_minus1`,
283/// `delta_rps_sign`, `abs_delta_rps_minus1`, and the
284/// `used_by_curr_pic_flag` / `use_delta_flag` arrays of length
285/// `NumDeltaPocs[RefRpsIdx] + 1`).
286#[derive(Debug, Clone, Default, PartialEq, Eq)]
287pub struct ShortTermRefPicSet {
288    /// `inter_ref_pic_set_prediction_flag` — inferred to 0 when not
289    /// signalled (i.e. always 0 for `stRpsIdx == 0`).
290    pub inter_ref_pic_set_prediction_flag: bool,
291    /// `delta_idx_minus1` (only meaningful when
292    /// `inter_ref_pic_set_prediction_flag == 1` and the index this RPS
293    /// is being constructed at equals `num_short_term_ref_pic_sets`;
294    /// otherwise inferred to 0). The §7.4.8 derivation is
295    /// `RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1)`.
296    pub delta_idx_minus1: u32,
297    /// `delta_rps_sign` (1-bit).
298    pub delta_rps_sign: bool,
299    /// `abs_delta_rps_minus1` (`ue(v)`, range 0..=2^15-1).
300    pub abs_delta_rps_minus1: u32,
301    /// Per-entry `used_by_curr_pic_flag[j]` for the inter-RPS-prediction
302    /// case. Length is `NumDeltaPocs[RefRpsIdx] + 1` when the form
303    /// applies, zero otherwise.
304    pub used_by_curr_pic_flag: Vec<bool>,
305    /// Per-entry `use_delta_flag[j]` for the inter-RPS-prediction case.
306    /// Same length / population rule as
307    /// [`used_by_curr_pic_flag`](Self::used_by_curr_pic_flag); per
308    /// §7.4.8 the value is inferred to 1 when the
309    /// `used_by_curr_pic_flag[j]` was 1.
310    pub use_delta_flag: Vec<bool>,
311    /// `num_negative_pics` (`ue(v)`), only meaningful in the explicit
312    /// form.
313    pub num_negative_pics: u32,
314    /// `num_positive_pics` (`ue(v)`), only meaningful in the explicit
315    /// form.
316    pub num_positive_pics: u32,
317    /// `delta_poc_s0_minus1[i]` (`ue(v)`), explicit form. Length
318    /// `num_negative_pics`.
319    pub delta_poc_s0_minus1: Vec<u32>,
320    /// `used_by_curr_pic_s0_flag[i]` (`u(1)`), explicit form. Length
321    /// `num_negative_pics`.
322    pub used_by_curr_pic_s0_flag: Vec<bool>,
323    /// `delta_poc_s1_minus1[i]` (`ue(v)`), explicit form. Length
324    /// `num_positive_pics`.
325    pub delta_poc_s1_minus1: Vec<u32>,
326    /// `used_by_curr_pic_s1_flag[i]` (`u(1)`), explicit form. Length
327    /// `num_positive_pics`.
328    pub used_by_curr_pic_s1_flag: Vec<bool>,
329}
330
331impl ShortTermRefPicSet {
332    /// `NumDeltaPocs[stRpsIdx]` per §7.4.8: in the explicit form this
333    /// is `num_negative_pics + num_positive_pics`; in the inter-RPS
334    /// form the exact count requires the §7.4.8 derivation
335    /// (equations 7-61 / 7-62 / 7-71) against a materialised source
336    /// RPS. See [`Self::materialize`] / [`MaterializedShortTermRefPicSet`]
337    /// for the full derivation.
338    ///
339    /// For the inter-RPS form this helper returns the count of
340    /// `use_delta_flag[j] == 1` entries, which is an upper bound that
341    /// happens to be exact when none of the source POCs flip sign
342    /// across `deltaRps`. Callers that need the exact wire-conformant
343    /// count must materialise the RPS chain.
344    pub fn num_delta_pocs(&self) -> u32 {
345        if self.inter_ref_pic_set_prediction_flag {
346            self.use_delta_flag.iter().filter(|&&v| v).count() as u32
347        } else {
348            self.num_negative_pics + self.num_positive_pics
349        }
350    }
351
352    /// Materialise this `st_ref_pic_set(stRpsIdx)` into the post-§7.4.8
353    /// per-position arrays `(NumNegativePics, DeltaPocS0[],
354    /// UsedByCurrPicS0[], NumPositivePics, DeltaPocS1[],
355    /// UsedByCurrPicS1[])` consumed by §7.4.7.2 and downstream paths.
356    ///
357    /// * For the explicit form (`inter_ref_pic_set_prediction_flag ==
358    ///   0`) this is equations 7-63..7-70: `NumNegativePics =
359    ///   num_negative_pics`, `NumPositivePics = num_positive_pics`,
360    ///   `UsedByCurrPicSk[i] = used_by_curr_pic_sk_flag[i]`, and the
361    ///   cumulative `DeltaPocS0[i] = DeltaPocS0[i-1] -
362    ///   (delta_poc_s0_minus1[i] + 1)` recurrence with the
363    ///   first-element seeds (`DeltaPocS0` at i=0 is
364    ///   `-(delta_poc_s0_minus1 at 0 + 1)` and `DeltaPocS1` at i=0 is
365    ///   `delta_poc_s1_minus1 at 0 + 1`). `source` is ignored in this
366    ///   branch.
367    /// * For the inter-RPS-prediction form
368    ///   (`inter_ref_pic_set_prediction_flag == 1`) this runs
369    ///   equations 7-61 (negative side, source-S1-reverse +
370    ///   `deltaRps`-self + source-S0-forward) and 7-62 (positive side,
371    ///   source-S0-reverse + `deltaRps`-self + source-S1-forward) over
372    ///   the already-materialised source RPS supplied via `source`,
373    ///   with `deltaRps = (1 - 2*delta_rps_sign) *
374    ///   (abs_delta_rps_minus1 + 1)`. The output's
375    ///   `NumNegativePics` / `NumPositivePics` reflect the surviving
376    ///   `dPoc < 0` / `dPoc > 0` entries with their `use_delta_flag[j]
377    ///   == 1` gates, per equation 7-71's `NumDeltaPocs`
378    ///   summation. The `source` argument must be `Some(_)` for the
379    ///   inter form; the function returns
380    ///   [`ShortTermRefPicSetMaterializeError::MissingSource`] if
381    ///   absent. Bounds on `used_by_curr_pic_flag` /
382    ///   `use_delta_flag` length are checked against the source's
383    ///   `NumDeltaPocs + 1`; a mismatch returns
384    ///   [`ShortTermRefPicSetMaterializeError::SourceLengthMismatch`].
385    pub fn materialize(
386        &self,
387        source: Option<&MaterializedShortTermRefPicSet>,
388    ) -> Result<MaterializedShortTermRefPicSet, ShortTermRefPicSetMaterializeError> {
389        if self.inter_ref_pic_set_prediction_flag {
390            let source = source.ok_or(ShortTermRefPicSetMaterializeError::MissingSource)?;
391            // `deltaRps` per equation 7-60. The bit-width of
392            // `abs_delta_rps_minus1` is bounded by `ue(v)` plus the
393            // §7.4.8 range check (<= 2^15-1 -> deltaRps fits in i32).
394            let abs = self.abs_delta_rps_minus1 as i64 + 1;
395            let delta_rps = if self.delta_rps_sign { -abs } else { abs } as i32;
396            let num_neg_src = source.num_negative_pics();
397            let num_pos_src = source.num_positive_pics();
398            let num_delta_src = source.num_delta_pocs(); // == num_neg_src + num_pos_src
399                                                         // Per §7.4.8 the `used_by_curr_pic_flag` / `use_delta_flag`
400                                                         // arrays have length `NumDeltaPocs[RefRpsIdx] + 1`.
401            let expected = (num_delta_src + 1) as usize;
402            if self.used_by_curr_pic_flag.len() != expected || self.use_delta_flag.len() != expected
403            {
404                return Err(ShortTermRefPicSetMaterializeError::SourceLengthMismatch {
405                    expected: expected as u32,
406                    got_used: self.used_by_curr_pic_flag.len(),
407                    got_delta: self.use_delta_flag.len(),
408                });
409            }
410            // Negative side (equation 7-61): walk source's positive
411            // POCs in reverse (their `dPoc = DeltaPocS1[RefRpsIdx][j]
412            // + deltaRps` may have crossed zero), then optionally
413            // `deltaRps` itself (only if negative), then source's
414            // negative POCs in forward order.
415            let mut delta_poc_s0 = Vec::new();
416            let mut used_by_curr_pic_s0 = Vec::new();
417            for j in (0..num_pos_src).rev() {
418                let d_poc = source.delta_poc_s1[j as usize] + delta_rps;
419                if d_poc < 0 && self.use_delta_flag[(num_neg_src + j) as usize] {
420                    delta_poc_s0.push(d_poc);
421                    used_by_curr_pic_s0
422                        .push(self.used_by_curr_pic_flag[(num_neg_src + j) as usize]);
423                }
424            }
425            if delta_rps < 0 && self.use_delta_flag[num_delta_src as usize] {
426                delta_poc_s0.push(delta_rps);
427                used_by_curr_pic_s0.push(self.used_by_curr_pic_flag[num_delta_src as usize]);
428            }
429            for j in 0..num_neg_src {
430                let d_poc = source.delta_poc_s0[j as usize] + delta_rps;
431                if d_poc < 0 && self.use_delta_flag[j as usize] {
432                    delta_poc_s0.push(d_poc);
433                    used_by_curr_pic_s0.push(self.used_by_curr_pic_flag[j as usize]);
434                }
435            }
436            // Positive side (equation 7-62): walk source's negative
437            // POCs in reverse (their `dPoc = DeltaPocS0[RefRpsIdx][j]
438            // + deltaRps` may have crossed zero), then optionally
439            // `deltaRps` itself (only if positive), then source's
440            // positive POCs in forward order.
441            let mut delta_poc_s1 = Vec::new();
442            let mut used_by_curr_pic_s1 = Vec::new();
443            for j in (0..num_neg_src).rev() {
444                let d_poc = source.delta_poc_s0[j as usize] + delta_rps;
445                if d_poc > 0 && self.use_delta_flag[j as usize] {
446                    delta_poc_s1.push(d_poc);
447                    used_by_curr_pic_s1.push(self.used_by_curr_pic_flag[j as usize]);
448                }
449            }
450            if delta_rps > 0 && self.use_delta_flag[num_delta_src as usize] {
451                delta_poc_s1.push(delta_rps);
452                used_by_curr_pic_s1.push(self.used_by_curr_pic_flag[num_delta_src as usize]);
453            }
454            for j in 0..num_pos_src {
455                let d_poc = source.delta_poc_s1[j as usize] + delta_rps;
456                if d_poc > 0 && self.use_delta_flag[(num_neg_src + j) as usize] {
457                    delta_poc_s1.push(d_poc);
458                    used_by_curr_pic_s1
459                        .push(self.used_by_curr_pic_flag[(num_neg_src + j) as usize]);
460                }
461            }
462            Ok(MaterializedShortTermRefPicSet {
463                delta_poc_s0,
464                used_by_curr_pic_s0,
465                delta_poc_s1,
466                used_by_curr_pic_s1,
467            })
468        } else {
469            // Explicit form, equations 7-63..7-70.
470            let mut delta_poc_s0 = Vec::with_capacity(self.num_negative_pics as usize);
471            let mut prev: i32 = 0;
472            for (i, &delta_minus1) in self.delta_poc_s0_minus1.iter().enumerate() {
473                // delta_minus1 has been range-checked to 0..=2^15-1 on
474                // parse; the cumulative sum cannot exceed i32 capacity
475                // given num_negative_pics <= 16.
476                let step = delta_minus1 as i32 + 1;
477                let d = if i == 0 { -step } else { prev - step };
478                delta_poc_s0.push(d);
479                prev = d;
480            }
481            let mut delta_poc_s1 = Vec::with_capacity(self.num_positive_pics as usize);
482            let mut prev: i32 = 0;
483            for (i, &delta_minus1) in self.delta_poc_s1_minus1.iter().enumerate() {
484                let step = delta_minus1 as i32 + 1;
485                let d = if i == 0 { step } else { prev + step };
486                delta_poc_s1.push(d);
487                prev = d;
488            }
489            Ok(MaterializedShortTermRefPicSet {
490                delta_poc_s0,
491                used_by_curr_pic_s0: self.used_by_curr_pic_s0_flag.clone(),
492                delta_poc_s1,
493                used_by_curr_pic_s1: self.used_by_curr_pic_s1_flag.clone(),
494            })
495        }
496    }
497}
498
499/// Errors that can arise while running the §7.4.8 inter-RPS-prediction
500/// derivation via [`ShortTermRefPicSet::materialize`].
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum ShortTermRefPicSetMaterializeError {
503    /// The RPS uses inter-RPS prediction
504    /// (`inter_ref_pic_set_prediction_flag == 1`) but the caller did
505    /// not supply a materialised source RPS.
506    MissingSource,
507    /// The on-wire `used_by_curr_pic_flag` / `use_delta_flag` arrays
508    /// did not match the source RPS's `NumDeltaPocs[RefRpsIdx] + 1`
509    /// length. This indicates the parser and the materialiser saw
510    /// different source RPSes (most likely a `RefRpsIdx` mismatch).
511    SourceLengthMismatch {
512        /// Expected length: `NumDeltaPocs[RefRpsIdx] + 1`.
513        expected: u32,
514        /// Actual length of `used_by_curr_pic_flag`.
515        got_used: usize,
516        /// Actual length of `use_delta_flag`.
517        got_delta: usize,
518    },
519}
520
521impl core::fmt::Display for ShortTermRefPicSetMaterializeError {
522    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
523        match self {
524            Self::MissingSource => f.write_str(
525                "short-term RPS materialise: inter_ref_pic_set_prediction_flag is set but no source RPS supplied",
526            ),
527            Self::SourceLengthMismatch {
528                expected,
529                got_used,
530                got_delta,
531            } => write!(
532                f,
533                "short-term RPS materialise: per-position array length mismatch: expected {expected}, got used={got_used} delta={got_delta}"
534            ),
535        }
536    }
537}
538
539impl std::error::Error for ShortTermRefPicSetMaterializeError {}
540
541/// Materialised short-term reference-picture-set per §7.4.8 — the
542/// post-derivation form that exposes the per-position `DeltaPocS0[]`,
543/// `UsedByCurrPicS0[]`, `DeltaPocS1[]`, `UsedByCurrPicS1[]` arrays as
544/// signed POC deltas. `NumNegativePics[stRpsIdx]` and
545/// `NumPositivePics[stRpsIdx]` are exposed as the array lengths.
546///
547/// The DeltaPocS0 array is in source order — i.e. the negative POCs
548/// in the order §7.4.8 produces them — which for the explicit form
549/// (equations 7-67 / 7-69) is descending (each element is the next
550/// further-in-the-past POC) and for the inter-RPS-prediction form
551/// (equation 7-61) walks source positives in reverse, then optionally
552/// `deltaRps`, then source negatives in forward order.
553#[derive(Debug, Clone, Default, PartialEq, Eq)]
554pub struct MaterializedShortTermRefPicSet {
555    /// `DeltaPocS0[stRpsIdx][i]` for `i` in
556    /// `0..NumNegativePics[stRpsIdx]`. All entries are strictly
557    /// negative.
558    pub delta_poc_s0: Vec<i32>,
559    /// `UsedByCurrPicS0[stRpsIdx][i]` for `i` in
560    /// `0..NumNegativePics[stRpsIdx]`.
561    pub used_by_curr_pic_s0: Vec<bool>,
562    /// `DeltaPocS1[stRpsIdx][i]` for `i` in
563    /// `0..NumPositivePics[stRpsIdx]`. All entries are strictly
564    /// positive.
565    pub delta_poc_s1: Vec<i32>,
566    /// `UsedByCurrPicS1[stRpsIdx][i]` for `i` in
567    /// `0..NumPositivePics[stRpsIdx]`.
568    pub used_by_curr_pic_s1: Vec<bool>,
569}
570
571impl MaterializedShortTermRefPicSet {
572    /// `NumNegativePics[stRpsIdx]` per §7.4.8.
573    pub fn num_negative_pics(&self) -> u32 {
574        self.delta_poc_s0.len() as u32
575    }
576    /// `NumPositivePics[stRpsIdx]` per §7.4.8.
577    pub fn num_positive_pics(&self) -> u32 {
578        self.delta_poc_s1.len() as u32
579    }
580    /// `NumDeltaPocs[stRpsIdx]` per equation 7-71.
581    pub fn num_delta_pocs(&self) -> u32 {
582        self.num_negative_pics() + self.num_positive_pics()
583    }
584}
585
586/// Long-term reference picture entry per the `long_term_ref_pics_present_flag`
587/// block of §7.3.2.2.
588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
589pub struct LongTermRefPicEntry {
590    /// `lt_ref_pic_poc_lsb_sps[i]` — `u(v)` whose width is
591    /// `log2_max_pic_order_cnt_lsb_minus4 + 4` bits.
592    pub poc_lsb: u32,
593    /// `used_by_curr_pic_lt_sps_flag[i]`.
594    pub used_by_curr_pic: bool,
595}
596
597/// Opaque suffix surfaced when the parser hits an extension body it
598/// does not yet decode (any of `sps_range_extension()`,
599/// `sps_multilayer_extension()`, `sps_3d_extension()`,
600/// `sps_scc_extension()`, or the `sps_extension_data_flag` while-loop
601/// gated by `sps_extension_4bits != 0`; also reused by the
602/// [`crate::pps::PicParameterSet`] extension tail). The bytes captured
603/// are the still-unparsed RBSP body, starting at the byte that
604/// contains the next un-read bit. `start_bit_in_first_byte` is the
605/// bit offset of that next bit within `bytes[0]` (0 = MSB).
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub struct OpaqueTail {
608    /// Raw RBSP bytes from the first byte containing the next
609    /// un-read bit onwards (including the `rbsp_trailing_bits()` byte
610    /// at the end).
611    pub bytes: Vec<u8>,
612    /// Bit offset within `bytes[0]` where the opaque tail begins,
613    /// in MSB-first order (0..=7).
614    pub start_bit_in_first_byte: u8,
615}
616
617/// SPS extension-flag block per §7.3.2.2.1
618/// (`sps_extension_present_flag == 1`), holding the four typed
619/// extension-present flags and the reserved-for-future-use
620/// `sps_extension_4bits` group.
621///
622/// Per §7.4.3.2.1, when `sps_extension_present_flag == 0` every flag
623/// in this block is inferred to 0 and `sps_extension_4bits` is
624/// inferred to 0; the parser surfaces that case as
625/// [`SeqParameterSet::extension_flags`] = `None`.
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
627pub struct SpsExtensionFlags {
628    /// `sps_range_extension_flag` (§7.3.2.2.1). When true, a
629    /// `sps_range_extension()` body (§7.3.2.2.2) follows in the bit
630    /// stream and is currently surfaced inside the SPS
631    /// [`SeqParameterSet::opaque_tail`]. This flag selects the §A.3.5
632    /// Format Range Extensions (RExt) profiles family.
633    pub sps_range_extension_flag: bool,
634    /// `sps_multilayer_extension_flag` (§7.3.2.2.1, Annex F /
635    /// scalable & multi-view extensions). When true, a
636    /// `sps_multilayer_extension()` body follows and is surfaced
637    /// inside the opaque tail.
638    pub sps_multilayer_extension_flag: bool,
639    /// `sps_3d_extension_flag` (§7.3.2.2.1, Annex I). When true, a
640    /// `sps_3d_extension()` body follows and is surfaced inside the
641    /// opaque tail.
642    pub sps_3d_extension_flag: bool,
643    /// `sps_scc_extension_flag` (§7.3.2.2.1). When true, a
644    /// `sps_scc_extension()` body follows and is surfaced inside the
645    /// opaque tail. This flag selects the §A.3.7 Screen Content
646    /// Coding (SCC) profiles family.
647    pub sps_scc_extension_flag: bool,
648    /// `sps_extension_4bits` (`u(4)`). For bitstreams conforming to
649    /// the current version of the specification this value shall be
650    /// 0; non-zero values are reserved for future use. The §7.4.3.2.1
651    /// decoder-side rule is to allow any value and (if it is non-zero)
652    /// consume but ignore the `sps_extension_data_flag` while-loop it
653    /// gates, so the parser surfaces the value verbatim. The trailing
654    /// `while( more_rbsp_data() ) sps_extension_data_flag` block (only
655    /// signalled when this field is non-zero) is surfaced inside the
656    /// opaque tail.
657    pub sps_extension_4bits: u8,
658}
659
660impl SpsExtensionFlags {
661    /// True when at least one of the four extension flags is set or
662    /// `sps_extension_4bits` is non-zero — i.e. when at least one
663    /// downstream extension body follows in the bit stream and the
664    /// SPS therefore carries an opaque tail starting at the first
665    /// body's bit position.
666    pub fn has_body(&self) -> bool {
667        self.sps_range_extension_flag
668            || self.sps_multilayer_extension_flag
669            || self.sps_3d_extension_flag
670            || self.sps_scc_extension_flag
671            || self.sps_extension_4bits != 0
672    }
673
674    /// True when the `sps_scc_extension()` body can be decoded in
675    /// place — it is signalled and no still-opaque body
676    /// (`sps_multilayer_extension()` / `sps_3d_extension()`) precedes
677    /// it in the bit stream. When a multilayer / 3D body precedes it,
678    /// the SCC body stays inside the opaque tail.
679    fn scc_decodable_in_place(&self) -> bool {
680        self.sps_scc_extension_flag
681            && !self.sps_multilayer_extension_flag
682            && !self.sps_3d_extension_flag
683    }
684
685    /// True when an extension body still follows the (range +
686    /// optionally SCC) bodies decoded in place — i.e. a multilayer /
687    /// 3D body, the `sps_extension_data_flag` while-loop, or an
688    /// SCC body whose multilayer/3D predecessor kept it opaque.
689    fn has_opaque_body_after_decoded(&self) -> bool {
690        if self.sps_multilayer_extension_flag || self.sps_3d_extension_flag {
691            // The first un-decoded body is the multilayer / 3D one;
692            // everything from there (incl. any SCC body) is opaque.
693            return true;
694        }
695        // No multilayer / 3D body: SCC (if present) was decoded in
696        // place, so only the sps_extension_data_flag while-loop may
697        // remain.
698        self.sps_extension_4bits != 0
699    }
700}
701
702/// Decoded `sps_scc_extension()` body per §7.3.2.2.3, present when
703/// [`SpsExtensionFlags::sps_scc_extension_flag`] is set and no opaque
704/// multilayer / 3D body precedes it. Per §7.4.3.2.3 the absent fields
705/// are inferred to 0 / empty when this struct is `None`.
706#[derive(Debug, Clone, PartialEq, Eq, Default)]
707pub struct SpsSccExtension {
708    /// `sps_curr_pic_ref_enabled_flag` — when 1, a picture referring
709    /// to the SPS may be in a reference picture list of one of its own
710    /// slices (intra block copy).
711    pub sps_curr_pic_ref_enabled_flag: bool,
712    /// `palette_mode_enabled_flag` — when 1, the palette-mode decoding
713    /// process may be used for intra blocks.
714    pub palette_mode_enabled_flag: bool,
715    /// `palette_max_size` (`ue(v)`), present only when
716    /// `palette_mode_enabled_flag`; the maximum allowed palette size
717    /// (inferred 0 otherwise).
718    pub palette_max_size: u32,
719    /// `delta_palette_max_predictor_size` (`ue(v)`), present only when
720    /// `palette_mode_enabled_flag`. `PaletteMaxPredictorSize =
721    /// palette_max_size + value` (eq. 7-35).
722    pub delta_palette_max_predictor_size: u32,
723    /// `sps_palette_predictor_initializers_present_flag` — when 1, the
724    /// sequence palette predictor is initialised from
725    /// [`Self::sps_palette_predictor_initializer`].
726    pub sps_palette_predictor_initializers_present_flag: bool,
727    /// `sps_num_palette_predictor_initializers_minus1` (`ue(v)`),
728    /// present only when the initializers-present flag is set; the
729    /// initializer table then holds `value + 1` entries per component.
730    pub sps_num_palette_predictor_initializers_minus1: u32,
731    /// `sps_palette_predictor_initializer[comp][i]` (§7.3.2.2.3),
732    /// indexed `[comp][i]`. `comp` runs over `numComps` (1 when
733    /// `chroma_format_idc == 0`, else 3). Each value is `u(v)` —
734    /// `BitDepthY` bits for `comp == 0`, `BitDepthC` bits otherwise.
735    /// Empty when no initializers are signalled.
736    pub sps_palette_predictor_initializer: Vec<Vec<u32>>,
737    /// `motion_vector_resolution_control_idc` (`u(2)`) — controls the
738    /// presence / inference of `use_integer_mv_flag`.
739    pub motion_vector_resolution_control_idc: u8,
740    /// `intra_boundary_filtering_disabled_flag` — when 1, the intra
741    /// boundary filtering process is unconditionally disabled.
742    pub intra_boundary_filtering_disabled_flag: bool,
743}
744
745impl SpsSccExtension {
746    /// Decode `sps_scc_extension()` (§7.3.2.2.3). `chroma_format_idc`
747    /// selects `numComps` (1 if 0, else 3) and `bit_depth_luma` /
748    /// `bit_depth_chroma` give the `u(v)` width of each palette
749    /// predictor initializer component.
750    fn parse(
751        br: &mut BitReader,
752        chroma_format_idc: u8,
753        bit_depth_luma: u8,
754        bit_depth_chroma: u8,
755    ) -> Result<Self, SpsError> {
756        let sps_curr_pic_ref_enabled_flag = br.u1()? != 0;
757        let palette_mode_enabled_flag = br.u1()? != 0;
758        let mut palette_max_size = 0u32;
759        let mut delta_palette_max_predictor_size = 0u32;
760        let mut sps_palette_predictor_initializers_present_flag = false;
761        let mut sps_num_palette_predictor_initializers_minus1 = 0u32;
762        let mut sps_palette_predictor_initializer = Vec::new();
763        if palette_mode_enabled_flag {
764            palette_max_size = br.ue()?;
765            delta_palette_max_predictor_size = br.ue()?;
766            // §7.4.3.2.3: when palette_max_size == 0 the
767            // delta_palette_max_predictor_size must be 0 (bitstream
768            // conformance — a zero-size palette cannot grow the
769            // predictor).
770            if palette_max_size == 0 && delta_palette_max_predictor_size != 0 {
771                return Err(SpsError::ValueOutOfRange {
772                    field: "delta_palette_max_predictor_size",
773                    got: delta_palette_max_predictor_size,
774                });
775            }
776            sps_palette_predictor_initializers_present_flag = br.u1()? != 0;
777            // §7.4.3.2.3: likewise the initializers-present flag must be
778            // 0 when palette_max_size == 0.
779            if palette_max_size == 0 && sps_palette_predictor_initializers_present_flag {
780                return Err(SpsError::ValueOutOfRange {
781                    field: "sps_palette_predictor_initializers_present_flag",
782                    got: 1,
783                });
784            }
785            if sps_palette_predictor_initializers_present_flag {
786                sps_num_palette_predictor_initializers_minus1 = br.ue()?;
787                // §7.4.3.2.3: bounded by PaletteMaxPredictorSize − 1,
788                // itself capped by the profile limits (§A.3.7:
789                // PaletteMaxPredictorSize <= 128); reject anything past
790                // the largest representable predictor so a malformed
791                // count cannot drive the initializer allocation.
792                if sps_num_palette_predictor_initializers_minus1 >= 128 {
793                    return Err(SpsError::ValueOutOfRange {
794                        field: "sps_num_palette_predictor_initializers_minus1",
795                        got: sps_num_palette_predictor_initializers_minus1,
796                    });
797                }
798                let num_comps = if chroma_format_idc == 0 { 1 } else { 3 };
799                let num_entries = sps_num_palette_predictor_initializers_minus1 as usize + 1;
800                sps_palette_predictor_initializer.reserve(num_comps);
801                for comp in 0..num_comps {
802                    let width = if comp == 0 {
803                        bit_depth_luma
804                    } else {
805                        bit_depth_chroma
806                    };
807                    let mut row = Vec::with_capacity(num_entries);
808                    for _ in 0..num_entries {
809                        row.push(br.u(width)?);
810                    }
811                    sps_palette_predictor_initializer.push(row);
812                }
813            }
814        }
815        let motion_vector_resolution_control_idc = br.u(2)? as u8;
816        // §7.4.3.2.3: the value 3 is reserved for future use and must
817        // not appear in a conforming bitstream of this version.
818        if motion_vector_resolution_control_idc == 3 {
819            return Err(SpsError::ValueOutOfRange {
820                field: "motion_vector_resolution_control_idc",
821                got: 3,
822            });
823        }
824        let intra_boundary_filtering_disabled_flag = br.u1()? != 0;
825        Ok(Self {
826            sps_curr_pic_ref_enabled_flag,
827            palette_mode_enabled_flag,
828            palette_max_size,
829            delta_palette_max_predictor_size,
830            sps_palette_predictor_initializers_present_flag,
831            sps_num_palette_predictor_initializers_minus1,
832            sps_palette_predictor_initializer,
833            motion_vector_resolution_control_idc,
834            intra_boundary_filtering_disabled_flag,
835        })
836    }
837
838    /// `PaletteMaxPredictorSize` (eq. 7-35) — the maximum allowed
839    /// palette predictor size, `palette_max_size +
840    /// delta_palette_max_predictor_size`.
841    pub fn palette_max_predictor_size(&self) -> u32 {
842        self.palette_max_size + self.delta_palette_max_predictor_size
843    }
844}
845
846/// Decoded `sps_range_extension()` body per §7.3.2.2.2, present when
847/// [`SpsExtensionFlags::sps_range_extension_flag`] is set. Every field
848/// is a single `u(1)` flag; per §7.4.3.2.2 each is inferred to 0 when
849/// the `sps_range_extension()` body is absent (i.e. when
850/// [`SeqParameterSet::sps_range_extension`] is `None`).
851#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
852pub struct SpsRangeExtension {
853    /// `transform_skip_rotation_enabled_flag` — when 1, a rotation is
854    /// applied to the residual of intra 4×4 transform-skip /
855    /// transform-bypass blocks.
856    pub transform_skip_rotation_enabled_flag: bool,
857    /// `transform_skip_context_enabled_flag` — when 1, a particular
858    /// context is used for parsing `sig_coeff_flag` / coefficient
859    /// magnitudes of transform-skip / transform-bypass blocks.
860    pub transform_skip_context_enabled_flag: bool,
861    /// `implicit_rdpcm_enabled_flag` — when 1, residual modification
862    /// for blocks using a transform-bypass may be used for intra
863    /// blocks referring to the SPS.
864    pub implicit_rdpcm_enabled_flag: bool,
865    /// `explicit_rdpcm_enabled_flag` — when 1, residual modification
866    /// for blocks using a transform-bypass may be used for inter
867    /// blocks referring to the SPS.
868    pub explicit_rdpcm_enabled_flag: bool,
869    /// `extended_precision_processing_flag` — when 1, an extended
870    /// dynamic range is used for coefficient parsing and inverse
871    /// transform processing.
872    pub extended_precision_processing_flag: bool,
873    /// `intra_smoothing_disabled_flag` — when 1, the filtering process
874    /// of neighbouring samples is unconditionally disabled for intra
875    /// prediction.
876    pub intra_smoothing_disabled_flag: bool,
877    /// `high_precision_offsets_enabled_flag` — when 1, weighted
878    /// prediction offsets and SAO offsets use a bit depth of
879    /// `BitDepth` rather than the default precision.
880    pub high_precision_offsets_enabled_flag: bool,
881    /// `persistent_rice_adaptation_enabled_flag` — when 1, the Rice
882    /// parameter derivation for the binarization of
883    /// `coeff_abs_level_remaining[]` is initialised at the start of
884    /// each sub-block using mode-dependent statistics accumulated from
885    /// previous sub-blocks.
886    pub persistent_rice_adaptation_enabled_flag: bool,
887    /// `cabac_bypass_alignment_enabled_flag` — when 1, a CABAC
888    /// alignment process is used prior to bypass decoding of the
889    /// syntax elements `coeff_sign_flag[]` and
890    /// `coeff_abs_level_remaining[]`.
891    pub cabac_bypass_alignment_enabled_flag: bool,
892}
893
894impl SpsRangeExtension {
895    /// Decode the nine `u(1)` flags of `sps_range_extension()`
896    /// (§7.3.2.2.2) in bit-stream order.
897    fn parse(br: &mut BitReader) -> Result<Self, SpsError> {
898        Ok(Self {
899            transform_skip_rotation_enabled_flag: br.u1()? != 0,
900            transform_skip_context_enabled_flag: br.u1()? != 0,
901            implicit_rdpcm_enabled_flag: br.u1()? != 0,
902            explicit_rdpcm_enabled_flag: br.u1()? != 0,
903            extended_precision_processing_flag: br.u1()? != 0,
904            intra_smoothing_disabled_flag: br.u1()? != 0,
905            high_precision_offsets_enabled_flag: br.u1()? != 0,
906            persistent_rice_adaptation_enabled_flag: br.u1()? != 0,
907            cabac_bypass_alignment_enabled_flag: br.u1()? != 0,
908        })
909    }
910}
911
912/// Parsed Sequence Parameter Set per §7.3.2.2.
913#[derive(Debug, Clone, PartialEq, Eq)]
914pub struct SeqParameterSet {
915    /// `sps_video_parameter_set_id` (`u(4)`, range 0..=15).
916    pub vps_id: u8,
917    /// `sps_max_sub_layers_minus1` (`u(3)`, range 0..=6).
918    pub max_sub_layers_minus1: u8,
919    /// `sps_temporal_id_nesting_flag`.
920    pub temporal_id_nesting_flag: bool,
921    /// Parsed `profile_tier_level()` subroutine.
922    pub ptl: ProfileTierLevel,
923    /// `sps_seq_parameter_set_id` (`ue(v)`, range 0..=15).
924    pub sps_id: u8,
925    /// `chroma_format_idc` (`ue(v)`, range 0..=3). 0 monochrome, 1
926    /// 4:2:0, 2 4:2:2, 3 4:4:4.
927    pub chroma_format_idc: u8,
928    /// `separate_colour_plane_flag`. Inferred to false when not
929    /// signalled (which is whenever `chroma_format_idc != 3`).
930    pub separate_colour_plane_flag: bool,
931    /// `pic_width_in_luma_samples` (`ue(v)`).
932    pub pic_width_in_luma_samples: u32,
933    /// `pic_height_in_luma_samples` (`ue(v)`).
934    pub pic_height_in_luma_samples: u32,
935    /// `conformance_window_flag`.
936    pub conformance_window_flag: bool,
937    /// `conf_win_*_offset` triple plus bottom — zeroed when
938    /// `conformance_window_flag` is false (§7.4.3.2.1).
939    pub conformance_window: ConformanceWindow,
940    /// `bit_depth_luma_minus8` (`ue(v)`, range 0..=8). The decoded
941    /// `BitDepthY` is `8 + value`.
942    pub bit_depth_luma_minus8: u8,
943    /// `bit_depth_chroma_minus8` (`ue(v)`, range 0..=8).
944    pub bit_depth_chroma_minus8: u8,
945    /// `log2_max_pic_order_cnt_lsb_minus4` (`ue(v)`, range 0..=12).
946    pub log2_max_pic_order_cnt_lsb_minus4: u8,
947    /// `sps_sub_layer_ordering_info_present_flag`.
948    pub sub_layer_ordering_info_present_flag: bool,
949    /// Per-sub-layer DPB / reorder / latency triples. Indices outside
950    /// `0..=max_sub_layers_minus1` are zero-initialised; when the
951    /// present flag was 0 every lower-indexed entry is copied from
952    /// the `[max_sub_layers_minus1]` slot (§7.4.3.2.1).
953    pub sub_layer_ordering_info: [SubLayerOrderingInfo; HEVC_MAX_SUB_LAYERS],
954    /// `log2_min_luma_coding_block_size_minus3` (`ue(v)`).
955    pub log2_min_luma_coding_block_size_minus3: u8,
956    /// `log2_diff_max_min_luma_coding_block_size` (`ue(v)`). Per the
957    /// `log2_ctb_size` derivation in §7.4.3.2.1, the CTU size is
958    /// `1 << (3 + log2_min_cb_minus3 + log2_diff_max_min_luma_cb)`.
959    pub log2_diff_max_min_luma_coding_block_size: u8,
960    /// `log2_min_luma_transform_block_size_minus2` (`ue(v)`).
961    pub log2_min_luma_transform_block_size_minus2: u8,
962    /// `log2_diff_max_min_luma_transform_block_size` (`ue(v)`).
963    pub log2_diff_max_min_luma_transform_block_size: u8,
964    /// `max_transform_hierarchy_depth_inter` (`ue(v)`).
965    pub max_transform_hierarchy_depth_inter: u8,
966    /// `max_transform_hierarchy_depth_intra` (`ue(v)`).
967    pub max_transform_hierarchy_depth_intra: u8,
968    /// `scaling_list_enabled_flag` (§7.3.2.2). When set, the SPS
969    /// either carries an explicit [`Self::scaling_list_data`] (when
970    /// `sps_scaling_list_data_present_flag == 1`) or the §7.4.5 default
971    /// scaling lists apply.
972    pub scaling_list_enabled_flag: bool,
973    /// `sps_scaling_list_data_present_flag` (§7.3.2.2). Inferred to
974    /// `false` (the §7.4.5 default lists apply) when
975    /// `scaling_list_enabled_flag == 0`.
976    pub sps_scaling_list_data_present_flag: bool,
977    /// The parsed §7.3.4 `scaling_list_data()` structure when
978    /// `sps_scaling_list_data_present_flag == 1`; `None` otherwise (the
979    /// §7.4.5 default lists apply).
980    pub scaling_list_data: Option<ScalingListData>,
981    /// `amp_enabled_flag` — asymmetric motion partitions.
982    pub amp_enabled_flag: bool,
983    /// `sample_adaptive_offset_enabled_flag` — SPS-level gate for
984    /// SAO; the per-slice gates are in the slice header.
985    pub sample_adaptive_offset_enabled_flag: bool,
986    /// `pcm_enabled_flag` — when set, [`Self::pcm`] is populated.
987    pub pcm_enabled_flag: bool,
988    /// `pcm_*` block per §7.3.2.2 (only meaningful when
989    /// [`Self::pcm_enabled_flag`] is true).
990    pub pcm: Option<PcmInfo>,
991    /// `num_short_term_ref_pic_sets` (`ue(v)`, range 0..=64).
992    pub num_short_term_ref_pic_sets: u32,
993    /// Parsed `st_ref_pic_set()` entries, length
994    /// [`Self::num_short_term_ref_pic_sets`].
995    pub short_term_ref_pic_sets: Vec<ShortTermRefPicSet>,
996    /// `long_term_ref_pics_present_flag`.
997    pub long_term_ref_pics_present_flag: bool,
998    /// `num_long_term_ref_pics_sps` (`ue(v)`, range 0..=32). Zero
999    /// when [`Self::long_term_ref_pics_present_flag`] is false.
1000    pub num_long_term_ref_pics_sps: u32,
1001    /// Per-entry long-term ref pic POC + used-by-curr-pic flag.
1002    /// Empty when [`Self::long_term_ref_pics_present_flag`] is false.
1003    pub long_term_ref_pics: Vec<LongTermRefPicEntry>,
1004    /// `sps_temporal_mvp_enabled_flag`.
1005    pub sps_temporal_mvp_enabled_flag: bool,
1006    /// `strong_intra_smoothing_enabled_flag`.
1007    pub strong_intra_smoothing_enabled_flag: bool,
1008    /// `vui_parameters_present_flag`. When true, the §E.2.1
1009    /// `vui_parameters( )` body is decoded into
1010    /// [`Self::vui_parameters`] and parsing continues to
1011    /// `sps_extension_present_flag`.
1012    pub vui_parameters_present_flag: bool,
1013    /// Parsed §E.2.1 `vui_parameters()` body when
1014    /// [`Self::vui_parameters_present_flag`] is true; `None`
1015    /// otherwise.
1016    pub vui_parameters: Option<VuiParameters>,
1017    /// `sps_extension_present_flag`. Read in both the VUI-present and
1018    /// VUI-absent paths now that the VUI body is fully decoded. When
1019    /// true, the typed extension flag block is decoded into
1020    /// [`Self::extension_flags`]; any extension body that follows
1021    /// (plus the RBSP trailing bits) is surfaced as
1022    /// [`Self::opaque_tail`].
1023    pub sps_extension_present_flag: bool,
1024    /// Typed extension-flag block, decoded when
1025    /// `sps_extension_present_flag == 1` per §7.3.2.2.1. `None` when
1026    /// the gate is 0; every flag is then inferred to 0 per §7.4.3.2.1.
1027    pub extension_flags: Option<SpsExtensionFlags>,
1028    /// Decoded `sps_range_extension()` body (§7.3.2.2.2), present when
1029    /// `extension_flags.sps_range_extension_flag` is set. `None`
1030    /// otherwise; per §7.4.3.2.2 every field is then inferred to 0.
1031    pub sps_range_extension: Option<SpsRangeExtension>,
1032    /// Decoded `sps_scc_extension()` body (§7.3.2.2.3), present when
1033    /// `extension_flags.sps_scc_extension_flag` is set **and** no
1034    /// opaque multilayer / 3D body precedes it. `None` otherwise; per
1035    /// §7.4.3.2.3 every field is then inferred to 0 / empty.
1036    pub sps_scc_extension: Option<SpsSccExtension>,
1037    /// Opaque suffix of the SPS RBSP. Populated when
1038    /// `sps_extension_present_flag == 1` **and**
1039    /// [`SpsExtensionFlags::has_body`] is true on the decoded flags —
1040    /// the captured bytes start at the first set body
1041    /// (`sps_range_extension()` if `sps_range_extension_flag`,
1042    /// otherwise the next set flag's body) and run through
1043    /// `rbsp_trailing_bits()`. `None` when the SPS ended cleanly
1044    /// after `sps_extension_present_flag == 0` or after a typed flag
1045    /// block in which every flag is 0.
1046    pub opaque_tail: Option<OpaqueTail>,
1047}
1048
1049impl SeqParameterSet {
1050    /// Parse `seq_parameter_set_rbsp()` starting from the first bit
1051    /// of the (already-unescaped) RBSP body — i.e. after the two-byte
1052    /// NAL header has been removed (see [`crate::nal::NalUnit`]).
1053    pub fn parse(rbsp: &[u8]) -> Result<Self, SpsError> {
1054        let mut br = BitReader::new(rbsp);
1055        Self::parse_inner(&mut br, rbsp)
1056    }
1057
1058    /// Materialise the full SPS-level `short_term_ref_pic_sets[]`
1059    /// list into the post-§7.4.8 form, chaining inter-RPS-prediction
1060    /// entries through their `RefRpsIdx = stRpsIdx -
1061    /// (delta_idx_minus1 + 1)` source.
1062    ///
1063    /// The returned vector is the same length as
1064    /// [`Self::short_term_ref_pic_sets`], and the `idx`-th element is
1065    /// the materialisation of
1066    /// `self.short_term_ref_pic_sets[idx]`. Returns an error if any
1067    /// in-chain materialisation fails (e.g. `RefRpsIdx` underflow,
1068    /// `used_by_curr_pic_flag` / `use_delta_flag` length mismatch).
1069    pub fn materialize_short_term_ref_pic_sets(
1070        &self,
1071    ) -> Result<Vec<MaterializedShortTermRefPicSet>, ShortTermRefPicSetMaterializeError> {
1072        let mut out: Vec<MaterializedShortTermRefPicSet> =
1073            Vec::with_capacity(self.short_term_ref_pic_sets.len());
1074        for (st_rps_idx, rps) in self.short_term_ref_pic_sets.iter().enumerate() {
1075            // `RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1)` per
1076            // equation 7-59. For the explicit form `source` is unused
1077            // and `RefRpsIdx` is not derived. For the inter form
1078            // `delta_idx_minus1` was parsed as 0 for any SPS-resident
1079            // entry that did not signal it explicitly (the wire signal
1080            // is only present at the slice-inline call site), which
1081            // maps to the immediately-preceding entry.
1082            let source = if rps.inter_ref_pic_set_prediction_flag {
1083                let ref_rps_idx = (st_rps_idx as i64) - (rps.delta_idx_minus1 as i64 + 1);
1084                if ref_rps_idx < 0 {
1085                    return Err(ShortTermRefPicSetMaterializeError::MissingSource);
1086                }
1087                out.get(ref_rps_idx as usize)
1088            } else {
1089                None
1090            };
1091            out.push(rps.materialize(source)?);
1092        }
1093        Ok(out)
1094    }
1095
1096    fn parse_inner(br: &mut BitReader<'_>, rbsp: &[u8]) -> Result<Self, SpsError> {
1097        let vps_id = br.u(4)? as u8;
1098        let max_sub_layers_minus1 = br.u(3)? as u8;
1099        if max_sub_layers_minus1 > 6 {
1100            return Err(SpsError::ValueOutOfRange {
1101                field: "sps_max_sub_layers_minus1",
1102                got: max_sub_layers_minus1 as u32,
1103            });
1104        }
1105        let temporal_id_nesting_flag = br.u1()? != 0;
1106
1107        // profile_tier_level( 1, sps_max_sub_layers_minus1 )
1108        let ptl = ProfileTierLevel::parse(br, true, max_sub_layers_minus1)?;
1109
1110        let sps_id_raw = br.ue()?;
1111        if sps_id_raw > 15 {
1112            return Err(SpsError::ValueOutOfRange {
1113                field: "sps_seq_parameter_set_id",
1114                got: sps_id_raw,
1115            });
1116        }
1117        let sps_id = sps_id_raw as u8;
1118
1119        let chroma_format_idc_raw = br.ue()?;
1120        if chroma_format_idc_raw > 3 {
1121            return Err(SpsError::ValueOutOfRange {
1122                field: "chroma_format_idc",
1123                got: chroma_format_idc_raw,
1124            });
1125        }
1126        let chroma_format_idc = chroma_format_idc_raw as u8;
1127
1128        let separate_colour_plane_flag = if chroma_format_idc == 3 {
1129            br.u1()? != 0
1130        } else {
1131            false
1132        };
1133
1134        // §A.4.1 items b) / c): each dimension "shall be less than or
1135        // equal to Sqrt( MaxLumaPs * 8 )". The largest Table A.8
1136        // MaxLumaPs is 142 606 336 (levels 7 .. 7.2), giving
1137        // Sqrt( 142 606 336 * 8 ) = 33 776 (integer part). Enforcing
1138        // the ceiling here keeps every downstream PicWidthInCtbsY /
1139        // PicSizeInCtbsY derivation (eqs. 7-15 .. 7-19) inside u32.
1140        const MAX_LUMA_DIMENSION: u32 = 33_776;
1141        let pic_width_in_luma_samples = br.ue()?;
1142        if pic_width_in_luma_samples == 0 || pic_width_in_luma_samples > MAX_LUMA_DIMENSION {
1143            return Err(SpsError::ValueOutOfRange {
1144                field: "pic_width_in_luma_samples",
1145                got: pic_width_in_luma_samples,
1146            });
1147        }
1148        let pic_height_in_luma_samples = br.ue()?;
1149        if pic_height_in_luma_samples == 0 || pic_height_in_luma_samples > MAX_LUMA_DIMENSION {
1150            return Err(SpsError::ValueOutOfRange {
1151                field: "pic_height_in_luma_samples",
1152                got: pic_height_in_luma_samples,
1153            });
1154        }
1155
1156        let conformance_window_flag = br.u1()? != 0;
1157        let conformance_window = if conformance_window_flag {
1158            ConformanceWindow {
1159                left_offset: br.ue()?,
1160                right_offset: br.ue()?,
1161                top_offset: br.ue()?,
1162                bottom_offset: br.ue()?,
1163            }
1164        } else {
1165            ConformanceWindow::default()
1166        };
1167
1168        let bit_depth_luma_minus8_raw = br.ue()?;
1169        if bit_depth_luma_minus8_raw > 8 {
1170            return Err(SpsError::ValueOutOfRange {
1171                field: "bit_depth_luma_minus8",
1172                got: bit_depth_luma_minus8_raw,
1173            });
1174        }
1175        let bit_depth_chroma_minus8_raw = br.ue()?;
1176        if bit_depth_chroma_minus8_raw > 8 {
1177            return Err(SpsError::ValueOutOfRange {
1178                field: "bit_depth_chroma_minus8",
1179                got: bit_depth_chroma_minus8_raw,
1180            });
1181        }
1182
1183        let log2_max_pic_order_cnt_lsb_minus4_raw = br.ue()?;
1184        if log2_max_pic_order_cnt_lsb_minus4_raw > 12 {
1185            return Err(SpsError::ValueOutOfRange {
1186                field: "log2_max_pic_order_cnt_lsb_minus4",
1187                got: log2_max_pic_order_cnt_lsb_minus4_raw,
1188            });
1189        }
1190
1191        let sub_layer_ordering_info_present_flag = br.u1()? != 0;
1192        let last = max_sub_layers_minus1 as usize;
1193        let start = if sub_layer_ordering_info_present_flag {
1194            0usize
1195        } else {
1196            last
1197        };
1198        let mut sub_layer_ordering_info = [SubLayerOrderingInfo::default(); HEVC_MAX_SUB_LAYERS];
1199        for entry in sub_layer_ordering_info
1200            .iter_mut()
1201            .take(last + 1)
1202            .skip(start)
1203        {
1204            let max_dpb = br.ue()?;
1205            let max_reorder = br.ue()?;
1206            let max_lat = br.ue()?;
1207            *entry = SubLayerOrderingInfo {
1208                max_dec_pic_buffering_minus1: max_dpb,
1209                max_num_reorder_pics: max_reorder,
1210                max_latency_increase_plus1: max_lat,
1211            };
1212        }
1213        if !sub_layer_ordering_info_present_flag {
1214            // §7.4.3.2.1: when the present flag is 0, every lower-indexed
1215            // sub-layer inherits the [max_sub_layers_minus1] triple.
1216            let copy = sub_layer_ordering_info[last];
1217            for entry in sub_layer_ordering_info.iter_mut().take(last) {
1218                *entry = copy;
1219            }
1220        }
1221
1222        let log2_min_luma_coding_block_size_minus3_raw = br.ue()?;
1223        let log2_diff_max_min_luma_coding_block_size_raw = br.ue()?;
1224        // §7.4.3.2.1 eqs. 7-10 / 7-11: MinCbLog2SizeY =
1225        // log2_min_luma_coding_block_size_minus3 + 3 and CtbLog2SizeY =
1226        // MinCbLog2SizeY + log2_diff_max_min_luma_coding_block_size.
1227        // Every Annex A profile requires "CtbLog2SizeY derived
1228        // according to active SPSs ... shall be in the range of 4 to 6,
1229        // inclusive" (e.g. the §A.3.2 Main-profile item), and the
1230        // eq.-7-13 `CtbSizeY = 1 << CtbLog2SizeY` shift (re-derived all
1231        // over the slice/CTB layers) is only meaningful under that
1232        // bound — reject out-of-range values here.
1233        let ctb_log2_size_y = log2_min_luma_coding_block_size_minus3_raw
1234            .saturating_add(3)
1235            .saturating_add(log2_diff_max_min_luma_coding_block_size_raw);
1236        if !(4..=6).contains(&ctb_log2_size_y) {
1237            return Err(SpsError::ValueOutOfRange {
1238                field: "CtbLog2SizeY",
1239                got: ctb_log2_size_y,
1240            });
1241        }
1242        let log2_min_luma_coding_block_size_minus3 =
1243            log2_min_luma_coding_block_size_minus3_raw as u8;
1244        let log2_diff_max_min_luma_coding_block_size =
1245            log2_diff_max_min_luma_coding_block_size_raw as u8;
1246        let min_cb_log2_size_y = u32::from(log2_min_luma_coding_block_size_minus3) + 3;
1247
1248        let log2_min_luma_transform_block_size_minus2_raw = br.ue()?;
1249        // §7.4.3.2.1: "The CVS shall not contain data that result in
1250        // MinTbLog2SizeY greater than or equal to MinCbLog2SizeY"
1251        // (MinTbLog2SizeY = log2_min_luma_transform_block_size_minus2
1252        // + 2).
1253        let min_tb_log2_size_y = log2_min_luma_transform_block_size_minus2_raw.saturating_add(2);
1254        if min_tb_log2_size_y >= min_cb_log2_size_y {
1255            return Err(SpsError::ValueOutOfRange {
1256                field: "log2_min_luma_transform_block_size_minus2",
1257                got: log2_min_luma_transform_block_size_minus2_raw,
1258            });
1259        }
1260        let log2_min_luma_transform_block_size_minus2 =
1261            log2_min_luma_transform_block_size_minus2_raw as u8;
1262
1263        let log2_diff_max_min_luma_transform_block_size_raw = br.ue()?;
1264        // §7.4.3.2.1: "The CVS shall not contain data that result in
1265        // MaxTbLog2SizeY greater than Min( CtbLog2SizeY, 5 )".
1266        let max_tb_log2_size_y =
1267            min_tb_log2_size_y.saturating_add(log2_diff_max_min_luma_transform_block_size_raw);
1268        if max_tb_log2_size_y > ctb_log2_size_y.min(5) {
1269            return Err(SpsError::ValueOutOfRange {
1270                field: "log2_diff_max_min_luma_transform_block_size",
1271                got: log2_diff_max_min_luma_transform_block_size_raw,
1272            });
1273        }
1274        let log2_diff_max_min_luma_transform_block_size =
1275            log2_diff_max_min_luma_transform_block_size_raw as u8;
1276
1277        // §7.4.3.2.1: both hierarchy depths "shall be in the range of
1278        // 0 to CtbLog2SizeY − MinTbLog2SizeY, inclusive".
1279        let max_hierarchy_depth = ctb_log2_size_y - min_tb_log2_size_y;
1280        let max_transform_hierarchy_depth_inter_raw = br.ue()?;
1281        if max_transform_hierarchy_depth_inter_raw > max_hierarchy_depth {
1282            return Err(SpsError::ValueOutOfRange {
1283                field: "max_transform_hierarchy_depth_inter",
1284                got: max_transform_hierarchy_depth_inter_raw,
1285            });
1286        }
1287        let max_transform_hierarchy_depth_inter = max_transform_hierarchy_depth_inter_raw as u8;
1288        let max_transform_hierarchy_depth_intra_raw = br.ue()?;
1289        if max_transform_hierarchy_depth_intra_raw > max_hierarchy_depth {
1290            return Err(SpsError::ValueOutOfRange {
1291                field: "max_transform_hierarchy_depth_intra",
1292                got: max_transform_hierarchy_depth_intra_raw,
1293            });
1294        }
1295        let max_transform_hierarchy_depth_intra = max_transform_hierarchy_depth_intra_raw as u8;
1296
1297        let scaling_list_enabled_flag = br.u1()? != 0;
1298        let mut sps_scaling_list_data_present_flag = false;
1299        let mut scaling_list_data = None;
1300        if scaling_list_enabled_flag {
1301            // §7.3.2.2: when scaling_list_enabled_flag == 1, an inner
1302            // sps_scaling_list_data_present_flag gates the explicit
1303            // scaling_list_data() structure (§7.3.4). When the inner
1304            // flag is 0 the default scaling lists (§7.4.5 Tables 7-5 /
1305            // 7-6) apply, so the SPS still parses.
1306            sps_scaling_list_data_present_flag = br.u1()? != 0;
1307            if sps_scaling_list_data_present_flag {
1308                scaling_list_data = Some(ScalingListData::parse(br)?);
1309            }
1310        }
1311
1312        let amp_enabled_flag = br.u1()? != 0;
1313        let sample_adaptive_offset_enabled_flag = br.u1()? != 0;
1314
1315        let pcm_enabled_flag = br.u1()? != 0;
1316        let pcm = if pcm_enabled_flag {
1317            let bit_depth_luma_minus1 = br.u(4)? as u8;
1318            let pcm_bit_depth_y = bit_depth_luma_minus1 as u32 + 1;
1319            let bit_depth_y = 8 + bit_depth_luma_minus8_raw;
1320            if pcm_bit_depth_y > bit_depth_y {
1321                return Err(SpsError::ValueOutOfRange {
1322                    field: "pcm_sample_bit_depth_luma_minus1",
1323                    got: bit_depth_luma_minus1 as u32,
1324                });
1325            }
1326            let bit_depth_chroma_minus1 = br.u(4)? as u8;
1327            let pcm_bit_depth_c = bit_depth_chroma_minus1 as u32 + 1;
1328            let bit_depth_c = 8 + bit_depth_chroma_minus8_raw;
1329            if pcm_bit_depth_c > bit_depth_c {
1330                return Err(SpsError::ValueOutOfRange {
1331                    field: "pcm_sample_bit_depth_chroma_minus1",
1332                    got: bit_depth_chroma_minus1 as u32,
1333                });
1334            }
1335            let log2_min_pcm_luma_coding_block_size_minus3 = br.ue()? as u8;
1336            let log2_diff_max_min_pcm_luma_coding_block_size = br.ue()? as u8;
1337            let loop_filter_disabled_flag = br.u1()? != 0;
1338            Some(PcmInfo {
1339                bit_depth_luma_minus1,
1340                bit_depth_chroma_minus1,
1341                log2_min_pcm_luma_coding_block_size_minus3,
1342                log2_diff_max_min_pcm_luma_coding_block_size,
1343                loop_filter_disabled_flag,
1344            })
1345        } else {
1346            None
1347        };
1348
1349        let num_short_term_ref_pic_sets_raw = br.ue()?;
1350        if num_short_term_ref_pic_sets_raw > HEVC_MAX_NUM_SHORT_TERM_RPS as u32 {
1351            return Err(SpsError::ValueOutOfRange {
1352                field: "num_short_term_ref_pic_sets",
1353                got: num_short_term_ref_pic_sets_raw,
1354            });
1355        }
1356        let num_short_term_ref_pic_sets = num_short_term_ref_pic_sets_raw;
1357        let mut short_term_ref_pic_sets = Vec::with_capacity(num_short_term_ref_pic_sets as usize);
1358        for st_rps_idx in 0..num_short_term_ref_pic_sets as usize {
1359            let prev = if st_rps_idx == 0 {
1360                None
1361            } else {
1362                short_term_ref_pic_sets.last()
1363            };
1364            let rps = ShortTermRefPicSet::parse(
1365                br,
1366                st_rps_idx as u32,
1367                num_short_term_ref_pic_sets,
1368                prev,
1369                &short_term_ref_pic_sets,
1370            )?;
1371            short_term_ref_pic_sets.push(rps);
1372        }
1373
1374        let long_term_ref_pics_present_flag = br.u1()? != 0;
1375        let mut num_long_term_ref_pics_sps = 0u32;
1376        let mut long_term_ref_pics = Vec::new();
1377        if long_term_ref_pics_present_flag {
1378            let raw = br.ue()?;
1379            if raw > HEVC_MAX_NUM_LONG_TERM_RPS as u32 {
1380                return Err(SpsError::ValueOutOfRange {
1381                    field: "num_long_term_ref_pics_sps",
1382                    got: raw,
1383                });
1384            }
1385            num_long_term_ref_pics_sps = raw;
1386            let poc_lsb_bits = log2_max_pic_order_cnt_lsb_minus4_raw as u8 + 4;
1387            long_term_ref_pics.reserve(num_long_term_ref_pics_sps as usize);
1388            for _ in 0..num_long_term_ref_pics_sps {
1389                let poc_lsb = br.u(poc_lsb_bits)?;
1390                let used = br.u1()? != 0;
1391                long_term_ref_pics.push(LongTermRefPicEntry {
1392                    poc_lsb,
1393                    used_by_curr_pic: used,
1394                });
1395            }
1396        }
1397
1398        let sps_temporal_mvp_enabled_flag = br.u1()? != 0;
1399        let strong_intra_smoothing_enabled_flag = br.u1()? != 0;
1400
1401        let vui_parameters_present_flag = br.u1()? != 0;
1402        // §E.2.1: the vui_parameters() body is decoded in full when
1403        // signalled, with the nested hrd_parameters( 1,
1404        // sps_max_sub_layers_minus1 ) call taking the SPS-level
1405        // maxNumSubLayersMinus1. Parsing then continues to
1406        // sps_extension_present_flag in both paths.
1407        let vui_parameters = if vui_parameters_present_flag {
1408            Some(VuiParameters::parse(br, max_sub_layers_minus1)?)
1409        } else {
1410            None
1411        };
1412
1413        let (
1414            sps_extension_present_flag,
1415            extension_flags,
1416            sps_range_extension,
1417            sps_scc_extension,
1418            opaque_tail,
1419        ) = if br.bits_left() == 0 {
1420            // The fixture corpus encoders sometimes elide the
1421            // sps_extension_present_flag if no extension is signalled
1422            // and the rbsp_trailing_bits happens to land on a byte
1423            // boundary; the field is still required, so a buffer with
1424            // no bits left here is a truncation.
1425            return Err(SpsError::Truncated);
1426        } else {
1427            let gate = br.u1()? != 0;
1428            if gate {
1429                // §7.3.2.2.1: when the gate is open, decode the eight
1430                // bits of typed extension flags first.
1431                let sps_range_extension_flag = br.u1()? != 0;
1432                let sps_multilayer_extension_flag = br.u1()? != 0;
1433                let sps_3d_extension_flag = br.u1()? != 0;
1434                let sps_scc_extension_flag = br.u1()? != 0;
1435                let sps_extension_4bits = br.u(4)? as u8;
1436                let flags = SpsExtensionFlags {
1437                    sps_range_extension_flag,
1438                    sps_multilayer_extension_flag,
1439                    sps_3d_extension_flag,
1440                    sps_scc_extension_flag,
1441                    sps_extension_4bits,
1442                };
1443                // §7.3.2.2.1: the range extension body (if signalled)
1444                // is the first to follow the eight typed flag bits, so
1445                // decode it in full.
1446                let range_ext = if flags.sps_range_extension_flag {
1447                    Some(SpsRangeExtension::parse(br)?)
1448                } else {
1449                    None
1450                };
1451                // §7.3.2.2.1 body order is range, multilayer, 3d, scc.
1452                // The SCC body can be decoded in place only when no
1453                // (still-opaque) multilayer / 3D body precedes it;
1454                // otherwise it stays inside the opaque tail.
1455                let scc_ext = if flags.scc_decodable_in_place() {
1456                    Some(SpsSccExtension::parse(
1457                        br,
1458                        chroma_format_idc,
1459                        8 + bit_depth_luma_minus8_raw as u8,
1460                        8 + bit_depth_chroma_minus8_raw as u8,
1461                    )?)
1462                } else {
1463                    None
1464                };
1465                // If any still-opaque body (a multilayer / 3D body, an
1466                // SCC body kept opaque by such a predecessor, or the
1467                // sps_extension_data_flag while-loop) follows, capture
1468                // the rest of the RBSP as an opaque tail starting at
1469                // the first un-decoded body's bit position. Otherwise
1470                // only rbsp_trailing_bits remains, consumed implicitly.
1471                let tail = if flags.has_opaque_body_after_decoded() {
1472                    Some(OpaqueTail::capture_at(br.bit_pos(), rbsp))
1473                } else {
1474                    None
1475                };
1476                (true, Some(flags), range_ext, scc_ext, tail)
1477            } else {
1478                // No extension present. Only the rbsp_trailing_bits
1479                // remain — a single `1` bit followed by zero-padding
1480                // to a byte boundary. We do not require the caller to
1481                // have validated it; surface nothing for the opaque tail.
1482                (false, None, None, None, None)
1483            }
1484        };
1485
1486        Ok(Self {
1487            vps_id,
1488            max_sub_layers_minus1,
1489            temporal_id_nesting_flag,
1490            ptl,
1491            sps_id,
1492            chroma_format_idc,
1493            separate_colour_plane_flag,
1494            pic_width_in_luma_samples,
1495            pic_height_in_luma_samples,
1496            conformance_window_flag,
1497            conformance_window,
1498            bit_depth_luma_minus8: bit_depth_luma_minus8_raw as u8,
1499            bit_depth_chroma_minus8: bit_depth_chroma_minus8_raw as u8,
1500            log2_max_pic_order_cnt_lsb_minus4: log2_max_pic_order_cnt_lsb_minus4_raw as u8,
1501            sub_layer_ordering_info_present_flag,
1502            sub_layer_ordering_info,
1503            log2_min_luma_coding_block_size_minus3,
1504            log2_diff_max_min_luma_coding_block_size,
1505            log2_min_luma_transform_block_size_minus2,
1506            log2_diff_max_min_luma_transform_block_size,
1507            max_transform_hierarchy_depth_inter,
1508            max_transform_hierarchy_depth_intra,
1509            scaling_list_enabled_flag,
1510            sps_scaling_list_data_present_flag,
1511            scaling_list_data,
1512            amp_enabled_flag,
1513            sample_adaptive_offset_enabled_flag,
1514            pcm_enabled_flag,
1515            pcm,
1516            num_short_term_ref_pic_sets,
1517            short_term_ref_pic_sets,
1518            long_term_ref_pics_present_flag,
1519            num_long_term_ref_pics_sps,
1520            long_term_ref_pics,
1521            sps_temporal_mvp_enabled_flag,
1522            strong_intra_smoothing_enabled_flag,
1523            vui_parameters_present_flag,
1524            vui_parameters,
1525            sps_extension_present_flag,
1526            extension_flags,
1527            sps_range_extension,
1528            sps_scc_extension,
1529            opaque_tail,
1530        })
1531    }
1532
1533    /// Convenience: derive the SPS-level `BitDepthY` (luma bit depth).
1534    pub fn bit_depth_luma(&self) -> u8 {
1535        8 + self.bit_depth_luma_minus8
1536    }
1537
1538    /// Derive the SPS-level `BitDepthC` (chroma bit depth).
1539    pub fn bit_depth_chroma(&self) -> u8 {
1540        8 + self.bit_depth_chroma_minus8
1541    }
1542
1543    /// Derive `MinCbLog2SizeY = log2_min_luma_coding_block_size_minus3 + 3`
1544    /// (§7.4.3.2.1).
1545    pub fn log2_min_cb_size(&self) -> u8 {
1546        self.log2_min_luma_coding_block_size_minus3 + 3
1547    }
1548
1549    /// Derive `CtbLog2SizeY = MinCbLog2SizeY
1550    /// + log2_diff_max_min_luma_coding_block_size` (§7.4.3.2.1).
1551    pub fn log2_ctb_size(&self) -> u8 {
1552        self.log2_min_cb_size() + self.log2_diff_max_min_luma_coding_block_size
1553    }
1554
1555    /// Derive `MinTbLog2SizeY = log2_min_luma_transform_block_size_minus2 + 2`
1556    /// (§7.4.3.2.1).
1557    pub fn log2_min_tb_size(&self) -> u8 {
1558        self.log2_min_luma_transform_block_size_minus2 + 2
1559    }
1560
1561    /// Derive `MaxPicOrderCntLsb = 1 << (log2_max_pic_order_cnt_lsb_minus4 + 4)`
1562    /// (§7.4.3.2.1).
1563    pub fn max_pic_order_cnt_lsb(&self) -> u32 {
1564        1u32 << (self.log2_max_pic_order_cnt_lsb_minus4 + 4)
1565    }
1566}
1567
1568impl OpaqueTail {
1569    /// Capture all RBSP bytes from the byte holding the bit at
1570    /// `bit_pos` (counted MSB-first from the start of `rbsp`) through
1571    /// end-of-buffer. Used by both the SPS extension tail and the
1572    /// [`crate::pps::PicParameterSet`] extension tail.
1573    pub fn capture_at(bit_pos: usize, rbsp: &[u8]) -> Self {
1574        let byte_index = bit_pos / 8;
1575        let bit_in_byte = (bit_pos % 8) as u8;
1576        Self {
1577            bytes: rbsp[byte_index..].to_vec(),
1578            start_bit_in_first_byte: bit_in_byte,
1579        }
1580    }
1581}
1582
1583impl ShortTermRefPicSet {
1584    /// Parse the in-line slice-header `st_ref_pic_set( num_short_term_ref_pic_sets )`
1585    /// per §7.3.6.1 / §7.3.7.
1586    ///
1587    /// This is the entry point the slice-header parser uses when
1588    /// `short_term_ref_pic_set_sps_flag == 0`: the picture's short-term
1589    /// RPS is constructed *inline* in the slice header at index
1590    /// `stRpsIdx == num_short_term_ref_pic_sets`, with the SPS's
1591    /// pre-existing list ([`SeqParameterSet::short_term_ref_pic_sets`])
1592    /// supplying `all_rps` for the §7.4.8 `RefRpsIdx` derivation.
1593    ///
1594    /// `br` must be positioned at the first bit of `st_ref_pic_set()`.
1595    /// On success the reader is advanced past the structure; on a
1596    /// truncation or range-check failure the reader state is undefined.
1597    pub fn parse_slice_inline(
1598        br: &mut BitReader<'_>,
1599        sps: &SeqParameterSet,
1600    ) -> Result<Self, SpsError> {
1601        Self::parse(
1602            br,
1603            sps.num_short_term_ref_pic_sets,
1604            sps.num_short_term_ref_pic_sets,
1605            sps.short_term_ref_pic_sets.last(),
1606            &sps.short_term_ref_pic_sets,
1607        )
1608    }
1609
1610    /// Parse one `st_ref_pic_set( stRpsIdx )` per §7.3.7.
1611    ///
1612    /// * `st_rps_idx` is `stRpsIdx`.
1613    /// * `num_short_term_ref_pic_sets` is the SPS-level count being
1614    ///   constructed (used to detect when `delta_idx_minus1` is signalled).
1615    /// * `prev` is the previously-parsed RPS, used when the
1616    ///   inter-RPS-prediction form is invoked without explicit
1617    ///   `delta_idx_minus1` (i.e. `stRpsIdx < num_short_term_ref_pic_sets`).
1618    /// * `all_rps` is the full list of RPSes parsed so far; the
1619    ///   §7.4.8 `RefRpsIdx` computation indexes into it.
1620    fn parse(
1621        br: &mut BitReader<'_>,
1622        st_rps_idx: u32,
1623        num_short_term_ref_pic_sets: u32,
1624        prev: Option<&ShortTermRefPicSet>,
1625        all_rps: &[ShortTermRefPicSet],
1626    ) -> Result<Self, SpsError> {
1627        let inter_ref_pic_set_prediction_flag = if st_rps_idx != 0 {
1628            br.u1()? != 0
1629        } else {
1630            false
1631        };
1632        if inter_ref_pic_set_prediction_flag {
1633            // delta_idx_minus1 is only signalled when the RPS being
1634            // constructed is the slice-header in-line RPS, i.e.
1635            // stRpsIdx == num_short_term_ref_pic_sets. For SPS-resident
1636            // entries the value is inferred to 0 per §7.4.8.
1637            let delta_idx_minus1 = if st_rps_idx == num_short_term_ref_pic_sets {
1638                br.ue()?
1639            } else {
1640                0
1641            };
1642            if delta_idx_minus1 >= st_rps_idx {
1643                return Err(SpsError::ValueOutOfRange {
1644                    field: "delta_idx_minus1",
1645                    got: delta_idx_minus1,
1646                });
1647            }
1648            let delta_rps_sign = br.u1()? != 0;
1649            let abs_delta_rps_minus1 = br.ue()?;
1650            if abs_delta_rps_minus1 > (1 << 15) - 1 {
1651                return Err(SpsError::ValueOutOfRange {
1652                    field: "abs_delta_rps_minus1",
1653                    got: abs_delta_rps_minus1,
1654                });
1655            }
1656            // RefRpsIdx = stRpsIdx − (delta_idx_minus1 + 1)
1657            let ref_rps_idx = (st_rps_idx as i64) - (delta_idx_minus1 as i64 + 1);
1658            let ref_rps = if ref_rps_idx >= 0 && (ref_rps_idx as usize) < all_rps.len() {
1659                Some(&all_rps[ref_rps_idx as usize])
1660            } else {
1661                // For SPS entries we expect ref_rps_idx in-range; the
1662                // only legal use of an out-of-range RefRpsIdx is when
1663                // st_rps_idx == num_short_term_ref_pic_sets, which is
1664                // the slice-header in-line case (handled elsewhere).
1665                prev
1666            };
1667            let num_delta_pocs = ref_rps.map(|r| r.num_delta_pocs()).unwrap_or(0);
1668            let entries = num_delta_pocs as usize + 1;
1669            let mut used_by_curr_pic_flag = Vec::with_capacity(entries);
1670            let mut use_delta_flag = Vec::with_capacity(entries);
1671            for _ in 0..entries {
1672                let used = br.u1()? != 0;
1673                used_by_curr_pic_flag.push(used);
1674                if !used {
1675                    let ud = br.u1()? != 0;
1676                    use_delta_flag.push(ud);
1677                } else {
1678                    // Per §7.4.8: when used_by_curr_pic_flag[j] is 1,
1679                    // use_delta_flag[j] is inferred to be 1.
1680                    use_delta_flag.push(true);
1681                }
1682            }
1683            Ok(Self {
1684                inter_ref_pic_set_prediction_flag,
1685                delta_idx_minus1,
1686                delta_rps_sign,
1687                abs_delta_rps_minus1,
1688                used_by_curr_pic_flag,
1689                use_delta_flag,
1690                num_negative_pics: 0,
1691                num_positive_pics: 0,
1692                delta_poc_s0_minus1: Vec::new(),
1693                used_by_curr_pic_s0_flag: Vec::new(),
1694                delta_poc_s1_minus1: Vec::new(),
1695                used_by_curr_pic_s1_flag: Vec::new(),
1696            })
1697        } else {
1698            let num_negative_pics = br.ue()?;
1699            if num_negative_pics > HEVC_MAX_RPS_PICS as u32 {
1700                return Err(SpsError::ValueOutOfRange {
1701                    field: "num_negative_pics",
1702                    got: num_negative_pics,
1703                });
1704            }
1705            let num_positive_pics = br.ue()?;
1706            if num_positive_pics > HEVC_MAX_RPS_PICS as u32 {
1707                return Err(SpsError::ValueOutOfRange {
1708                    field: "num_positive_pics",
1709                    got: num_positive_pics,
1710                });
1711            }
1712            let mut delta_poc_s0_minus1 = Vec::with_capacity(num_negative_pics as usize);
1713            let mut used_by_curr_pic_s0_flag = Vec::with_capacity(num_negative_pics as usize);
1714            for _ in 0..num_negative_pics {
1715                let dp = br.ue()?;
1716                if dp > (1 << 15) - 1 {
1717                    return Err(SpsError::ValueOutOfRange {
1718                        field: "delta_poc_s0_minus1",
1719                        got: dp,
1720                    });
1721                }
1722                delta_poc_s0_minus1.push(dp);
1723                used_by_curr_pic_s0_flag.push(br.u1()? != 0);
1724            }
1725            let mut delta_poc_s1_minus1 = Vec::with_capacity(num_positive_pics as usize);
1726            let mut used_by_curr_pic_s1_flag = Vec::with_capacity(num_positive_pics as usize);
1727            for _ in 0..num_positive_pics {
1728                let dp = br.ue()?;
1729                if dp > (1 << 15) - 1 {
1730                    return Err(SpsError::ValueOutOfRange {
1731                        field: "delta_poc_s1_minus1",
1732                        got: dp,
1733                    });
1734                }
1735                delta_poc_s1_minus1.push(dp);
1736                used_by_curr_pic_s1_flag.push(br.u1()? != 0);
1737            }
1738            Ok(Self {
1739                inter_ref_pic_set_prediction_flag,
1740                delta_idx_minus1: 0,
1741                delta_rps_sign: false,
1742                abs_delta_rps_minus1: 0,
1743                used_by_curr_pic_flag: Vec::new(),
1744                use_delta_flag: Vec::new(),
1745                num_negative_pics,
1746                num_positive_pics,
1747                delta_poc_s0_minus1,
1748                used_by_curr_pic_s0_flag,
1749                delta_poc_s1_minus1,
1750                used_by_curr_pic_s1_flag,
1751            })
1752        }
1753    }
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758    use super::*;
1759    use crate::nal::{collect_nal_units, strip_emulation_prevention};
1760
1761    /// Helper: convert a bit string (any non-`0`/`1` characters are
1762    /// ignored, useful for visual spacing) into a packed MSB-first
1763    /// byte vector with zero-padding up to the next byte boundary.
1764    fn bits_to_bytes(s: &str) -> Vec<u8> {
1765        let mut bits: Vec<u8> = Vec::new();
1766        for c in s.chars() {
1767            if c == '0' || c == '1' {
1768                bits.push((c as u8) - b'0');
1769            }
1770        }
1771        while bits.len() % 8 != 0 {
1772            bits.push(0);
1773        }
1774        let mut out = Vec::with_capacity(bits.len() / 8);
1775        for chunk in bits.chunks(8) {
1776            let mut b = 0u8;
1777            for &bit in chunk {
1778                b = (b << 1) | bit;
1779            }
1780            out.push(b);
1781        }
1782        out
1783    }
1784
1785    /// Header bits through the per-sub-layer ordering triple, with the
1786    /// `pic_width_in_luma_samples` / `pic_height_in_luma_samples`
1787    /// `ue(v)` bit strings supplied by the caller (the fixture default
1788    /// is 16 × 16 → `000010001` each). Everything else is fixed:
1789    /// `vps_id=0, max_sub_layers_minus1=0, nesting=1, a §7.3.3 PTL
1790    /// walk (profile_idc=1, level=30), sps_id=0, chroma_format_idc=1,
1791    /// conf_win=0, bit_depths=0, log2_max_poc_lsb_minus4=4,
1792    /// ordering_info present with single triple {0,0,0}`.
1793    fn synthesised_header_through_ordering(width_ue: &str, height_ue: &str) -> String {
1794        let mut s = String::new();
1795        s += "0000"; // vps_id
1796        s += "000"; // max_sub_layers_minus1
1797        s += "1"; // nesting flag
1798                  // profile_tier_level(1, 0)
1799        s += "00"; // profile_space
1800        s += "0"; // tier
1801        s += "00001"; // profile_idc
1802        for _ in 0..32 {
1803            s += "0";
1804        }
1805        s += "0000";
1806        for _ in 0..43 {
1807            s += "0";
1808        }
1809        s += "0";
1810        s += "00011110"; // level=30
1811                         // sps_id ue(v)=0
1812        s += "1";
1813        // chroma_format_idc ue(v)=1 → '010'
1814        s += "010";
1815        // width ue(v)
1816        s += width_ue;
1817        // height ue(v)
1818        s += height_ue;
1819        // conf_win = 0
1820        s += "0";
1821        // bd_luma=0, bd_chroma=0
1822        s += "1";
1823        s += "1";
1824        // log2_max_poc_lsb_minus4 = 4 → '00101'
1825        s += "00101";
1826        // ordering present = 1, single triple {0,0,0}
1827        s += "1";
1828        s += "1";
1829        s += "1";
1830        s += "1";
1831        s
1832    }
1833
1834    /// Prefix of bits that get every SPS hand-assembled fixture through
1835    /// the structural header up to (but not including) the round-4 tail.
1836    /// `chroma_format_idc=1, width=16, height=16, conf_win=0, bit_depths=0,
1837    /// log2_max_poc_lsb_minus4=4, max_sub_layers_minus1=0, ordering_info
1838    /// present with single triple {dpb=0, reorder=0, latency=0},
1839    /// log2_min_cb=0, log2_diff=1, log2_min_tb=0, log2_diff_tb=2,
1840    /// max_transform_depth_*=0, scaling_list=0, amp=0, sao=1`.
1841    ///
1842    /// This is the EXACT same prefix the round-3 tests used; the round-4
1843    /// tail tests then concatenate the tail bits they want to exercise.
1844    fn synthesised_prefix_bits() -> String {
1845        let mut s = synthesised_header_through_ordering("000010001", "000010001");
1846        // log2_min_cb_minus3 = 0
1847        s += "1";
1848        // log2_diff = 1 → '010'
1849        s += "010";
1850        // log2_min_tb_minus2 = 0
1851        s += "1";
1852        // log2_diff_tb = 2 → '011'
1853        s += "011";
1854        // max_transform_depth_{inter,intra} = 0
1855        s += "1";
1856        s += "1";
1857        // scaling_list_enabled = 0
1858        s += "0";
1859        // amp_enabled = 0
1860        s += "0";
1861        // sao_enabled = 1
1862        s += "1";
1863        s
1864    }
1865
1866    /// SPS RBSP body extracted from
1867    /// `docs/video/h265/fixtures/tiny-i-only-16x16-main/input.hevc`,
1868    /// after the Annex B start code and the two-byte NAL header have
1869    /// been removed and emulation-prevention bytes stripped. The wire
1870    /// SPS (NAL idx 1, type 33) was 38 bytes including the two-byte
1871    /// header; after §7.4.1.1 strip (3 escape bytes removed) the body
1872    /// is 32 bytes.
1873    const TINY_SPS_RBSP: &[u8] = &[
1874        0x01, 0x04, 0x08, 0x00, 0x00, 0x00, 0x9F, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x1E, 0xA0, 0x88,
1875        0x45, 0x96, 0xEA, 0xAF, 0x2B, 0xC0, 0x5A, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
1876        0x32, 0x10,
1877    ];
1878
1879    #[test]
1880    fn parses_tiny_fixture_sps() {
1881        let sps = SeqParameterSet::parse(TINY_SPS_RBSP).expect("SPS parse");
1882        // Trace cross-check (docs/video/h265/fixtures/tiny-i-only-16x16-main/trace.txt):
1883        //   SPS sps_id=0 vps_id=0 max_sub_layers=1 profile_idc=4 level_idc=30
1884        //       chroma_format_idc=1 bit_depth=8 bit_depth_chroma=8
1885        //       width=16 height=16 log2_ctb_size=4 log2_min_cb_size=3
1886        //       log2_min_tb_size=2 sao_enabled=1 amp_enabled=0 pcm_enabled=0
1887        //       scaling_list_enabled=0 long_term_ref_pics=0 temporal_mvp=1
1888        //       strong_intra_smoothing=1
1889        assert_eq!(sps.vps_id, 0);
1890        assert_eq!(sps.max_sub_layers_minus1, 0); // max_sub_layers = 1
1891        assert!(sps.temporal_id_nesting_flag);
1892        assert_eq!(sps.ptl.general_profile_idc, 4);
1893        assert_eq!(sps.ptl.general_level_idc, 30);
1894        assert_eq!(sps.sps_id, 0);
1895        assert_eq!(sps.chroma_format_idc, 1);
1896        assert!(!sps.separate_colour_plane_flag);
1897        assert_eq!(sps.pic_width_in_luma_samples, 16);
1898        assert_eq!(sps.pic_height_in_luma_samples, 16);
1899        assert!(!sps.conformance_window_flag);
1900        assert_eq!(sps.conformance_window, ConformanceWindow::default());
1901        assert_eq!(sps.bit_depth_luma_minus8, 0);
1902        assert_eq!(sps.bit_depth_chroma_minus8, 0);
1903        assert_eq!(sps.bit_depth_luma(), 8);
1904        assert_eq!(sps.bit_depth_chroma(), 8);
1905        assert_eq!(sps.log2_max_pic_order_cnt_lsb_minus4, 4); // MaxPicOrderCntLsb = 256
1906        assert_eq!(sps.max_pic_order_cnt_lsb(), 256);
1907        assert!(sps.sub_layer_ordering_info_present_flag);
1908        assert_eq!(
1909            sps.sub_layer_ordering_info[0].max_dec_pic_buffering_minus1,
1910            2
1911        );
1912        assert_eq!(sps.sub_layer_ordering_info[0].max_num_reorder_pics, 0);
1913        assert_eq!(sps.sub_layer_ordering_info[0].max_latency_increase_plus1, 1);
1914        assert_eq!(sps.log2_min_cb_size(), 3); // 8x8 minimum CU
1915        assert_eq!(sps.log2_ctb_size(), 4); // 16x16 CTU
1916        assert_eq!(sps.log2_min_tb_size(), 2); // 4x4 minimum TU
1917        assert_eq!(sps.max_transform_hierarchy_depth_inter, 0);
1918        assert_eq!(sps.max_transform_hierarchy_depth_intra, 0);
1919        assert!(!sps.scaling_list_enabled_flag);
1920        assert!(!sps.amp_enabled_flag);
1921        assert!(sps.sample_adaptive_offset_enabled_flag);
1922        // Round-4 tail.
1923        assert!(!sps.pcm_enabled_flag);
1924        assert!(sps.pcm.is_none());
1925        assert_eq!(sps.num_short_term_ref_pic_sets, 0);
1926        assert!(sps.short_term_ref_pic_sets.is_empty());
1927        assert!(!sps.long_term_ref_pics_present_flag);
1928        assert_eq!(sps.num_long_term_ref_pics_sps, 0);
1929        assert!(sps.long_term_ref_pics.is_empty());
1930        assert!(sps.sps_temporal_mvp_enabled_flag);
1931        assert!(sps.strong_intra_smoothing_enabled_flag);
1932        // The fixture's x265 CLI encode signals a §E.2.1 VUI body. It
1933        // decodes to a square (1:1) sample aspect ratio, an
1934        // unspecified-but-present video_signal_type, and a timing-info
1935        // block of vui_num_units_in_tick = 1 / vui_time_scale = 25
1936        // (25 fps) with neither HRD nor bitstream restriction. After
1937        // the VUI the SPS ends cleanly: sps_extension_present_flag == 0
1938        // and only the rbsp_trailing_bits() remain, so no opaque tail
1939        // is captured.
1940        assert!(sps.vui_parameters_present_flag);
1941        let vui = sps.vui_parameters.as_ref().expect("VUI body");
1942        assert!(vui.aspect_ratio_info_present_flag);
1943        assert_eq!(vui.aspect_ratio_idc, 1); // 1:1 square
1944        assert!(vui.sar_width.is_none());
1945        assert!(!vui.overscan_info_present_flag);
1946        assert!(vui.video_signal_type_present_flag);
1947        let vst = vui.video_signal_type.as_ref().expect("video signal type");
1948        assert_eq!(vst.video_format, 5); // unspecified
1949        assert!(!vst.video_full_range_flag);
1950        assert!(vst.colour_description.is_none());
1951        assert!(!vui.chroma_loc_info_present_flag);
1952        assert!(!vui.neutral_chroma_indication_flag);
1953        assert!(!vui.field_seq_flag);
1954        assert!(!vui.frame_field_info_present_flag);
1955        assert!(!vui.default_display_window_flag);
1956        assert!(vui.vui_timing_info_present_flag);
1957        let ti = vui.timing_info.as_ref().expect("timing info");
1958        assert_eq!(ti.num_units_in_tick, 1);
1959        assert_eq!(ti.time_scale, 25);
1960        assert!(!ti.poc_proportional_to_timing_flag);
1961        assert!(!ti.hrd_parameters_present_flag);
1962        assert!(!vui.bitstream_restriction_flag);
1963        // No extension, no opaque tail.
1964        assert!(!sps.sps_extension_present_flag);
1965        assert!(sps.opaque_tail.is_none());
1966    }
1967
1968    /// End-to-end: pull the SPS NAL out of the raw Annex B stream
1969    /// through the walker, then parse it. The fixture's full byte
1970    /// sequence is captured inline so the test does not depend on the
1971    /// `docs/` tree at run time.
1972    #[test]
1973    fn parses_tiny_fixture_sps_via_nal_walker() {
1974        // VPS NAL + SPS NAL only — the rest of the fixture (PPS / SEI
1975        // / slice) is unrelated to this test.
1976        let raw = &[
1977            // VPS: 00 00 00 01 40 01 ...
1978            0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0C, 0x01, 0xFF, 0xFF, 0x04, 0x08, 0x00, 0x00,
1979            0x03, 0x00, 0x9F, 0xA8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1E, 0xBA, 0x02, 0x40,
1980            // SPS: 00 00 00 01 42 01 ...
1981            0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9F,
1982            0xA8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1E, 0xA0, 0x88, 0x45, 0x96, 0xEA, 0xAF, 0x2B,
1983            0xC0, 0x5A, 0x02, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x32, 0x10,
1984        ];
1985        let units = collect_nal_units(raw).expect("walker");
1986        assert_eq!(units.len(), 2);
1987        assert_eq!(units[1].header.nal_unit_type, 33); // SPS_NUT
1988        let sps = SeqParameterSet::parse(&units[1].rbsp).expect("SPS parse");
1989        assert_eq!(sps.sps_id, 0);
1990        assert_eq!(sps.chroma_format_idc, 1);
1991        assert_eq!(sps.pic_width_in_luma_samples, 16);
1992        assert_eq!(sps.pic_height_in_luma_samples, 16);
1993        assert_eq!(sps.log2_ctb_size(), 4);
1994        assert!(sps.sample_adaptive_offset_enabled_flag);
1995        assert!(!sps.pcm_enabled_flag);
1996        assert_eq!(sps.num_short_term_ref_pic_sets, 0);
1997        assert!(!sps.long_term_ref_pics_present_flag);
1998        assert!(sps.sps_temporal_mvp_enabled_flag);
1999        assert!(sps.strong_intra_smoothing_enabled_flag);
2000        assert!(sps.vui_parameters_present_flag);
2001    }
2002
2003    #[test]
2004    fn strip_emulation_prevention_then_parse_matches_inline_decode() {
2005        // The wire SPS body (post NAL header, with the 3 emulation-
2006        // prevention escapes left in) must, after a §7.4.1.1 strip,
2007        // match TINY_SPS_RBSP exactly.
2008        let wire = &[
2009            0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9F, 0xA8, 0x00, 0x00, 0x03, 0x00, 0x00,
2010            0x1E, 0xA0, 0x88, 0x45, 0x96, 0xEA, 0xAF, 0x2B, 0xC0, 0x5A, 0x02, 0x00, 0x00, 0x03,
2011            0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x32, 0x10,
2012        ];
2013        let unesc = strip_emulation_prevention(wire);
2014        assert_eq!(unesc, TINY_SPS_RBSP);
2015        let a = SeqParameterSet::parse(&unesc).expect("SPS parse");
2016        let b = SeqParameterSet::parse(TINY_SPS_RBSP).expect("SPS parse");
2017        assert_eq!(a, b);
2018    }
2019
2020    #[test]
2021    fn rejects_truncated_rbsp() {
2022        // Cut the buffer just past the leading `vps_id / max_sub /
2023        // nesting` byte — well before profile_tier_level finishes.
2024        let err = SeqParameterSet::parse(&TINY_SPS_RBSP[..3]).unwrap_err();
2025        assert_eq!(err, SpsError::Truncated);
2026    }
2027
2028    /// Hand-assembled SPS exercising the `chroma_format_idc == 3`
2029    /// path (so `separate_colour_plane_flag` is signalled) plus the
2030    /// `conformance_window_flag == 1` four-`ue(v)` block. The remaining
2031    /// fields are kept at minimal values to make the bit string
2032    /// hand-traceable.
2033    #[test]
2034    fn parses_444_with_conformance_window() {
2035        let mut s = String::new();
2036        s += "0000"; // vps_id
2037        s += "000"; // max_sub_layers_minus1
2038        s += "1"; // nesting
2039                  // PTL(1,0)
2040        s += "00";
2041        s += "0";
2042        s += "00001";
2043        for _ in 0..32 {
2044            s += "0";
2045        }
2046        s += "0000";
2047        for _ in 0..43 {
2048            s += "0";
2049        }
2050        s += "0";
2051        s += "00011110";
2052        // sps_id=0
2053        s += "1";
2054        // chroma_format_idc=3 → '00100'
2055        s += "00100";
2056        // separate_colour_plane_flag=1
2057        s += "1";
2058        // width=16, height=16
2059        s += "000010001";
2060        s += "000010001";
2061        // conf_win=1, four ue(v)=0
2062        s += "1";
2063        s += "1";
2064        s += "1";
2065        s += "1";
2066        s += "1";
2067        // bd_luma=2 → '011'
2068        s += "011";
2069        s += "011";
2070        // log2_max_poc_lsb_minus4=4
2071        s += "00101";
2072        // ordering present, single triple of zeros
2073        s += "1";
2074        s += "1";
2075        s += "1";
2076        s += "1";
2077        // log2_min_cb=0
2078        s += "1";
2079        // log2_diff=1
2080        s += "010";
2081        s += "1";
2082        s += "011";
2083        s += "1";
2084        s += "1";
2085        // scaling=0
2086        s += "0";
2087        // amp=1
2088        s += "1";
2089        // sao=1
2090        s += "1";
2091        // pcm_enabled=0
2092        s += "0";
2093        // num_short_term_ref_pic_sets=0
2094        s += "1";
2095        // long_term_ref_pics_present=0
2096        s += "0";
2097        // temporal_mvp=1
2098        s += "1";
2099        // strong_intra_smoothing=0
2100        s += "0";
2101        // vui=0
2102        s += "0";
2103        // sps_extension_present=0
2104        s += "0";
2105        // rbsp trailing bits (stop bit then zero pad)
2106        s += "1";
2107
2108        let bytes = bits_to_bytes(&s);
2109        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2110        assert_eq!(sps.chroma_format_idc, 3);
2111        assert!(sps.separate_colour_plane_flag);
2112        assert_eq!(sps.bit_depth_luma(), 10);
2113        assert!(sps.amp_enabled_flag);
2114        assert!(sps.sample_adaptive_offset_enabled_flag);
2115        assert!(!sps.pcm_enabled_flag);
2116        assert_eq!(sps.num_short_term_ref_pic_sets, 0);
2117        assert!(!sps.long_term_ref_pics_present_flag);
2118        assert!(sps.sps_temporal_mvp_enabled_flag);
2119        assert!(!sps.strong_intra_smoothing_enabled_flag);
2120        assert!(!sps.vui_parameters_present_flag);
2121        assert!(!sps.sps_extension_present_flag);
2122        assert!(sps.opaque_tail.is_none());
2123    }
2124
2125    /// Hand-assembled SPS with two sub-layers and the ordering-info
2126    /// present flag set to 0 — the [0] sub-layer must inherit the
2127    /// [1] triple per §7.4.3.2.1.
2128    #[test]
2129    fn ordering_info_present_flag_zero_propagates() {
2130        let mut s = String::new();
2131        s += "0000"; // vps_id
2132        s += "001"; // max_sub_layers_minus1 = 1
2133        s += "1";
2134        // PTL(1, 1):
2135        s += "00";
2136        s += "0";
2137        s += "00001";
2138        for _ in 0..32 {
2139            s += "0";
2140        }
2141        s += "0000";
2142        for _ in 0..43 {
2143            s += "0";
2144        }
2145        s += "0";
2146        s += "00011110"; // level=30
2147                         // sub_layer_profile_present[0]/level_present[0] = 0,0
2148        s += "00";
2149        for _ in 0..14 {
2150            s += "0";
2151        }
2152        // sps_id=0
2153        s += "1";
2154        // chroma=1
2155        s += "010";
2156        // width=16, height=16
2157        s += "000010001";
2158        s += "000010001";
2159        // conf_win=0
2160        s += "0";
2161        // bd_luma=0
2162        s += "1";
2163        // bd_chroma=0
2164        s += "1";
2165        // log2_max_poc_lsb_minus4=4
2166        s += "00101";
2167        // sub_layer_ordering_info_present_flag = 0
2168        s += "0";
2169        // single triple at i=max_sub_layers_minus1 (=1): dpb=2 ('011'), reorder=0 ('1'), latency=0 ('1')
2170        s += "011";
2171        s += "1";
2172        s += "1";
2173        s += "1";
2174        s += "010";
2175        s += "1";
2176        s += "011";
2177        s += "1";
2178        s += "1";
2179        s += "0";
2180        s += "0";
2181        s += "1";
2182        // pcm_enabled=0
2183        s += "0";
2184        // num_short_term_ref_pic_sets=0
2185        s += "1";
2186        // long_term=0
2187        s += "0";
2188        // temporal_mvp=1
2189        s += "1";
2190        // strong_intra_smoothing=1
2191        s += "1";
2192        // vui=0
2193        s += "0";
2194        // sps_extension_present=0
2195        s += "0";
2196        // stop bit
2197        s += "1";
2198
2199        let bytes = bits_to_bytes(&s);
2200        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2201        assert!(!sps.sub_layer_ordering_info_present_flag);
2202        assert_eq!(sps.max_sub_layers_minus1, 1);
2203        assert_eq!(
2204            sps.sub_layer_ordering_info[1].max_dec_pic_buffering_minus1,
2205            2
2206        );
2207        // [0] inherited from [1]
2208        assert_eq!(
2209            sps.sub_layer_ordering_info[0].max_dec_pic_buffering_minus1,
2210            2
2211        );
2212        assert!(sps.strong_intra_smoothing_enabled_flag);
2213    }
2214
2215    /// SPS prefix bits identical to `synthesised_prefix_bits()` but
2216    /// stopping just *before* `scaling_list_enabled_flag` (i.e. after
2217    /// `max_transform_hierarchy_depth_intra`). Round 8 scaling-list
2218    /// tests append their own scaling_list block + the amp/sao bits +
2219    /// the SPS tail.
2220    fn synthesised_prefix_before_scaling_list() -> String {
2221        let mut s = String::new();
2222        s += "0000"; // vps_id
2223        s += "000"; // max_sub_layers_minus1
2224        s += "1"; // nesting flag
2225        s += "00"; // profile_space
2226        s += "0"; // tier
2227        s += "00001"; // profile_idc
2228        for _ in 0..32 {
2229            s += "0";
2230        }
2231        s += "0000";
2232        for _ in 0..43 {
2233            s += "0";
2234        }
2235        s += "0";
2236        s += "00011110"; // level=30
2237        s += "1"; // sps_id=0
2238        s += "010"; // chroma_format_idc=1
2239        s += "000010001"; // width=16
2240        s += "000010001"; // height=16
2241        s += "0"; // conf_win=0
2242        s += "1"; // bd_luma=0
2243        s += "1"; // bd_chroma=0
2244        s += "00101"; // log2_max_poc_lsb_minus4=4
2245        s += "1"; // ordering present=1
2246        s += "1"; // dpb=0
2247        s += "1"; // reorder=0
2248        s += "1"; // latency=0
2249        s += "1"; // log2_min_cb_minus3=0
2250        s += "010"; // log2_diff=1
2251        s += "1"; // log2_min_tb_minus2=0
2252        s += "011"; // log2_diff_tb=2
2253        s += "1"; // max_transform_depth_inter=0
2254        s += "1"; // max_transform_depth_intra=0
2255        s
2256    }
2257
2258    /// SPS tail bits from `amp_enabled` through the stop bit, matching
2259    /// the round-4 minimal tail (sao=1, pcm=0, num_short_term_rps=0,
2260    /// long_term=0, temporal_mvp=1, strong_intra_smoothing=1, vui=0,
2261    /// extension=0, stop=1).
2262    fn synthesised_tail_after_scaling_list() -> String {
2263        let mut s = String::new();
2264        s += "0"; // amp_enabled=0
2265        s += "1"; // sao_enabled=1
2266        s += "0"; // pcm_enabled=0
2267        s += "1"; // num_short_term_ref_pic_sets ue=0
2268        s += "0"; // long_term_ref_pics=0
2269        s += "1"; // temporal_mvp=1
2270        s += "1"; // strong_intra_smoothing=1
2271        s += "0"; // vui=0
2272        s += "0"; // sps_extension_present=0
2273        s += "1"; // stop bit
2274        s
2275    }
2276
2277    /// `scaling_list_enabled_flag == 1` with
2278    /// `sps_scaling_list_data_present_flag == 0`: per §7.4.5 the
2279    /// default scaling lists apply and the SPS parses end to end with
2280    /// no explicit [`ScalingListData`].
2281    #[test]
2282    fn scaling_list_enabled_default_lists() {
2283        let mut s = synthesised_prefix_before_scaling_list();
2284        s += "1"; // scaling_list_enabled = 1
2285        s += "0"; // sps_scaling_list_data_present_flag = 0
2286        s += &synthesised_tail_after_scaling_list();
2287        let bytes = bits_to_bytes(&s);
2288        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2289        assert!(sps.scaling_list_enabled_flag);
2290        assert!(!sps.sps_scaling_list_data_present_flag);
2291        assert!(sps.scaling_list_data.is_none());
2292    }
2293
2294    /// `scaling_list_enabled_flag == 1` with
2295    /// `sps_scaling_list_data_present_flag == 1`: the SPS carries an
2296    /// explicit `scaling_list_data()` (§7.3.4). Here every one of the
2297    /// 24 slots signals pred_mode=0, delta=0 (use the default list), so
2298    /// the parsed lists equal the §7.4.5 default tables.
2299    #[test]
2300    fn scaling_list_enabled_explicit_all_default() {
2301        let mut s = synthesised_prefix_before_scaling_list();
2302        s += "1"; // scaling_list_enabled = 1
2303        s += "1"; // sps_scaling_list_data_present_flag = 1
2304                  // scaling_list_data(): 24 slots, each pred_mode=0
2305                  // ('0') + scaling_list_pred_matrix_id_delta ue=0 ('1').
2306                  // sizeId 0/1/2 each 6 slots; sizeId 3 only 2 slots.
2307        for size_id in 0..4 {
2308            let step = if size_id == 3 { 3 } else { 1 };
2309            let mut m = 0;
2310            while m < 6 {
2311                s += "0"; // scaling_list_pred_mode_flag = 0
2312                s += "1"; // scaling_list_pred_matrix_id_delta ue = 0
2313                m += step;
2314            }
2315        }
2316        s += &synthesised_tail_after_scaling_list();
2317        let bytes = bits_to_bytes(&s);
2318        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2319        assert!(sps.scaling_list_enabled_flag);
2320        assert!(sps.sps_scaling_list_data_present_flag);
2321        let data = sps.scaling_list_data.expect("scaling_list_data present");
2322        // 4x4 default = all 16.
2323        assert_eq!(data.lists[0][0].coef, vec![16u16; 16]);
2324        // 8x8 inter default for matrixId 3.
2325        assert_eq!(data.lists[1][3].coef[63], 91);
2326    }
2327
2328    /// `chroma_format_idc` is u-Exp-Golomb so on-wire codeNum 4 would
2329    /// decode to a value of 4 — outside the legal 0..=3 range. The
2330    /// parser must reject it.
2331    #[test]
2332    fn rejects_chroma_format_idc_out_of_range() {
2333        let mut s = String::new();
2334        s += "0000";
2335        s += "000";
2336        s += "1";
2337        s += "00";
2338        s += "0";
2339        s += "00001";
2340        for _ in 0..32 {
2341            s += "0";
2342        }
2343        s += "0000";
2344        for _ in 0..43 {
2345            s += "0";
2346        }
2347        s += "0";
2348        s += "00011110";
2349        // sps_id=0
2350        s += "1";
2351        // chroma_format_idc ue=4 → '00101'
2352        s += "00101";
2353        let bytes = bits_to_bytes(&s);
2354        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2355        assert_eq!(
2356            err,
2357            SpsError::ValueOutOfRange {
2358                field: "chroma_format_idc",
2359                got: 4
2360            }
2361        );
2362    }
2363
2364    /// Fuzz regression (r282 `parse_annexb`): an SPS whose
2365    /// coding-block-size pair drives `CtbLog2SizeY` (§7.4.3.2.1
2366    /// eqs. 7-10 / 7-11) past the Annex A profile bound of 4..=6
2367    /// previously survived the parse and panicked downstream in every
2368    /// `CtbSizeY = 1 << CtbLog2SizeY` (eq. 7-13) re-derivation.
2369    #[test]
2370    fn rejects_ctb_log2_size_above_6() {
2371        let mut s = synthesised_header_through_ordering("000010001", "000010001");
2372        // log2_min_luma_coding_block_size_minus3 = 0 → MinCbLog2SizeY = 3
2373        s += "1";
2374        // log2_diff_max_min_luma_coding_block_size ue = 4 → CtbLog2SizeY = 7
2375        s += "00101";
2376        let bytes = bits_to_bytes(&s);
2377        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2378        assert_eq!(
2379            err,
2380            SpsError::ValueOutOfRange {
2381                field: "CtbLog2SizeY",
2382                got: 7
2383            }
2384        );
2385    }
2386
2387    /// Annex A also bounds `CtbLog2SizeY` from below (4): a
2388    /// 8×8-CTB SPS must be rejected.
2389    #[test]
2390    fn rejects_ctb_log2_size_below_4() {
2391        let mut s = synthesised_header_through_ordering("000010001", "000010001");
2392        // log2_min_cb_minus3 = 0, log2_diff = 0 → CtbLog2SizeY = 3
2393        s += "1";
2394        s += "1";
2395        let bytes = bits_to_bytes(&s);
2396        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2397        assert_eq!(
2398            err,
2399            SpsError::ValueOutOfRange {
2400                field: "CtbLog2SizeY",
2401                got: 3
2402            }
2403        );
2404    }
2405
2406    /// §7.4.3.2.1: "The CVS shall not contain data that result in
2407    /// MinTbLog2SizeY greater than or equal to MinCbLog2SizeY".
2408    #[test]
2409    fn rejects_min_tb_log2_size_reaching_min_cb() {
2410        let mut s = synthesised_header_through_ordering("000010001", "000010001");
2411        s += "1"; // MinCbLog2SizeY = 3
2412        s += "010"; // CtbLog2SizeY = 4
2413        s += "010"; // log2_min_tb_minus2 = 1 → MinTbLog2SizeY = 3 == MinCb
2414        let bytes = bits_to_bytes(&s);
2415        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2416        assert_eq!(
2417            err,
2418            SpsError::ValueOutOfRange {
2419                field: "log2_min_luma_transform_block_size_minus2",
2420                got: 1
2421            }
2422        );
2423    }
2424
2425    /// §7.4.3.2.1: "The CVS shall not contain data that result in
2426    /// MaxTbLog2SizeY greater than Min( CtbLog2SizeY, 5 )".
2427    #[test]
2428    fn rejects_max_tb_log2_size_above_cap() {
2429        let mut s = synthesised_header_through_ordering("000010001", "000010001");
2430        s += "1"; // MinCbLog2SizeY = 3
2431        s += "010"; // CtbLog2SizeY = 4 → cap = Min(4, 5) = 4
2432        s += "1"; // MinTbLog2SizeY = 2
2433        s += "00100"; // log2_diff_tb = 3 → MaxTbLog2SizeY = 5 > 4
2434        let bytes = bits_to_bytes(&s);
2435        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2436        assert_eq!(
2437            err,
2438            SpsError::ValueOutOfRange {
2439                field: "log2_diff_max_min_luma_transform_block_size",
2440                got: 3
2441            }
2442        );
2443    }
2444
2445    /// §7.4.3.2.1: both hierarchy depths "shall be in the range of 0
2446    /// to CtbLog2SizeY − MinTbLog2SizeY, inclusive" (= 2 here).
2447    #[test]
2448    fn rejects_transform_hierarchy_depth_above_cap() {
2449        let mut s = synthesised_header_through_ordering("000010001", "000010001");
2450        s += "1"; // MinCbLog2SizeY = 3
2451        s += "010"; // CtbLog2SizeY = 4
2452        s += "1"; // MinTbLog2SizeY = 2
2453        s += "011"; // log2_diff_tb = 2 → MaxTbLog2SizeY = 4 (legal)
2454        s += "00100"; // max_transform_hierarchy_depth_inter = 3 > 2
2455        let bytes = bits_to_bytes(&s);
2456        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2457        assert_eq!(
2458            err,
2459            SpsError::ValueOutOfRange {
2460                field: "max_transform_hierarchy_depth_inter",
2461                got: 3
2462            }
2463        );
2464    }
2465
2466    /// §A.4.1 item b): `pic_width_in_luma_samples` ≤
2467    /// `Sqrt( MaxLumaPs * 8 )` = 33 776 at the largest Table A.8
2468    /// level. An unbounded width previously let the
2469    /// PicWidthInCtbsY × PicHeightInCtbsY product (eq. 7-19 territory)
2470    /// overflow u32 downstream.
2471    #[test]
2472    fn rejects_oversized_pic_width() {
2473        // ue(33 777): codeNum + 1 = 33 778 needs 16 bits → 15-zero
2474        // prefix followed by the 16-bit value.
2475        let code: u32 = 33_778;
2476        let len = 32 - code.leading_zeros();
2477        let mut ue = "0".repeat(len as usize - 1);
2478        for i in (0..len).rev() {
2479            ue.push(if (code >> i) & 1 == 1 { '1' } else { '0' });
2480        }
2481        let s = synthesised_header_through_ordering(&ue, "000010001");
2482        let bytes = bits_to_bytes(&s);
2483        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2484        assert_eq!(
2485            err,
2486            SpsError::ValueOutOfRange {
2487                field: "pic_width_in_luma_samples",
2488                got: 33_777
2489            }
2490        );
2491    }
2492
2493    /// Hand-assembled SPS exercising the `pcm_enabled_flag == 1`
2494    /// branch (§7.3.2.2 PCM block).
2495    #[test]
2496    fn parses_pcm_enabled() {
2497        let mut s = synthesised_prefix_bits();
2498        // pcm_enabled_flag = 1
2499        s += "1";
2500        // pcm_sample_bit_depth_luma_minus1 = 7 (PcmBitDepthY = 8)
2501        s += "0111";
2502        // pcm_sample_bit_depth_chroma_minus1 = 7
2503        s += "0111";
2504        // log2_min_pcm_luma_coding_block_size_minus3 = 0 → '1'
2505        s += "1";
2506        // log2_diff_max_min_pcm_luma_coding_block_size = 0 → '1'
2507        s += "1";
2508        // pcm_loop_filter_disabled_flag = 1
2509        s += "1";
2510        // num_short_term_ref_pic_sets = 0
2511        s += "1";
2512        // long_term_ref_pics_present_flag = 0
2513        s += "0";
2514        // sps_temporal_mvp_enabled_flag = 1
2515        s += "1";
2516        // strong_intra_smoothing_enabled_flag = 1
2517        s += "1";
2518        // vui_parameters_present_flag = 0
2519        s += "0";
2520        // sps_extension_present_flag = 0
2521        s += "0";
2522        // stop bit
2523        s += "1";
2524        let bytes = bits_to_bytes(&s);
2525        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2526        assert!(sps.pcm_enabled_flag);
2527        let pcm = sps.pcm.expect("pcm block");
2528        assert_eq!(pcm.bit_depth_luma_minus1, 7);
2529        assert_eq!(pcm.bit_depth_chroma_minus1, 7);
2530        assert_eq!(pcm.log2_min_pcm_luma_coding_block_size_minus3, 0);
2531        assert_eq!(pcm.log2_diff_max_min_pcm_luma_coding_block_size, 0);
2532        assert!(pcm.loop_filter_disabled_flag);
2533        assert!(sps.sps_temporal_mvp_enabled_flag);
2534        assert!(sps.strong_intra_smoothing_enabled_flag);
2535    }
2536
2537    /// PCM bit depth above luma bit depth must be rejected per
2538    /// §7.4.3.2 / equation (7-25).
2539    #[test]
2540    fn rejects_pcm_bit_depth_exceeding_luma() {
2541        let mut s = synthesised_prefix_bits();
2542        s += "1"; // pcm_enabled_flag
2543                  // pcm_sample_bit_depth_luma_minus1 = 15 (PcmBitDepthY = 16)
2544                  // BitDepthY in the synthesised prefix is 8.
2545        s += "1111";
2546        let bytes = bits_to_bytes(&s);
2547        let err = SeqParameterSet::parse(&bytes).unwrap_err();
2548        assert_eq!(
2549            err,
2550            SpsError::ValueOutOfRange {
2551                field: "pcm_sample_bit_depth_luma_minus1",
2552                got: 15
2553            }
2554        );
2555    }
2556
2557    /// Hand-assembled SPS with one explicit short-term RPS (no
2558    /// inter-RPS-prediction): 1 negative pic, 0 positive pics.
2559    #[test]
2560    fn parses_one_short_term_rps_explicit() {
2561        let mut s = synthesised_prefix_bits();
2562        // pcm_enabled_flag = 0
2563        s += "0";
2564        // num_short_term_ref_pic_sets = 1 → ue(v) codeNum 1 → '010'
2565        s += "010";
2566        // st_ref_pic_set(0): inter_ref_pic_set_prediction_flag is NOT
2567        // signalled (st_rps_idx == 0); implicit 0.
2568        //   num_negative_pics = 1 → '010'
2569        s += "010";
2570        //   num_positive_pics = 0 → '1'
2571        s += "1";
2572        //   delta_poc_s0_minus1[0] = 0 → '1'
2573        s += "1";
2574        //   used_by_curr_pic_s0_flag[0] = 1
2575        s += "1";
2576        // long_term_ref_pics_present_flag = 0
2577        s += "0";
2578        // sps_temporal_mvp_enabled_flag = 1
2579        s += "1";
2580        // strong_intra_smoothing_enabled_flag = 0
2581        s += "0";
2582        // vui_parameters_present_flag = 0
2583        s += "0";
2584        // sps_extension_present_flag = 0
2585        s += "0";
2586        s += "1"; // stop bit
2587
2588        let bytes = bits_to_bytes(&s);
2589        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2590        assert_eq!(sps.num_short_term_ref_pic_sets, 1);
2591        let rps = &sps.short_term_ref_pic_sets[0];
2592        assert!(!rps.inter_ref_pic_set_prediction_flag);
2593        assert_eq!(rps.num_negative_pics, 1);
2594        assert_eq!(rps.num_positive_pics, 0);
2595        assert_eq!(rps.delta_poc_s0_minus1, vec![0]);
2596        assert_eq!(rps.used_by_curr_pic_s0_flag, vec![true]);
2597        assert!(rps.delta_poc_s1_minus1.is_empty());
2598        assert_eq!(rps.num_delta_pocs(), 1);
2599    }
2600
2601    /// Hand-assembled SPS with two short-term RPSes, the second one
2602    /// using inter-RPS-prediction relative to the first. Exercises
2603    /// the `for( j = 0; j <= NumDeltaPocs[RefRpsIdx]; j++ )` loop in
2604    /// §7.3.7 with the `use_delta_flag` inference of §7.4.8.
2605    #[test]
2606    fn parses_inter_rps_prediction() {
2607        let mut s = synthesised_prefix_bits();
2608        s += "0"; // pcm_enabled_flag
2609                  // num_short_term_ref_pic_sets = 2 → codeNum 2 → '011'
2610        s += "011";
2611        // st_ref_pic_set(0): explicit, num_negative_pics=1, num_positive=0,
2612        // delta_poc_s0_minus1[0]=0, used_by_curr_pic_s0_flag[0]=1
2613        s += "010"; // num_neg=1
2614        s += "1"; // num_pos=0
2615        s += "1"; // dp0=0
2616        s += "1"; // used=1
2617                  // st_ref_pic_set(1): inter_ref_pic_set_prediction_flag = 1
2618        s += "1";
2619        //   delta_idx_minus1 is NOT signalled (st_rps_idx=1, num=2; only
2620        //   signalled when st_rps_idx == num). Inferred to 0 →
2621        //   RefRpsIdx = 1 - (0+1) = 0.
2622        //   delta_rps_sign = 0
2623        s += "0";
2624        //   abs_delta_rps_minus1 = 0 → codeNum 0 → '1'
2625        s += "1";
2626        //   NumDeltaPocs[0] = 1, so loop runs j=0..1 (2 entries):
2627        //     j=0: used_by_curr_pic_flag = 1  → use_delta_flag inferred 1
2628        s += "1";
2629        //     j=1: used_by_curr_pic_flag = 0, use_delta_flag = 1
2630        s += "0";
2631        s += "1";
2632        // long_term=0, temporal_mvp=1, strong_intra_smoothing=1, vui=0, ext=0, stop
2633        s += "0";
2634        s += "1";
2635        s += "1";
2636        s += "0";
2637        s += "0";
2638        s += "1";
2639
2640        let bytes = bits_to_bytes(&s);
2641        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2642        assert_eq!(sps.num_short_term_ref_pic_sets, 2);
2643        let r1 = &sps.short_term_ref_pic_sets[1];
2644        assert!(r1.inter_ref_pic_set_prediction_flag);
2645        assert_eq!(r1.delta_idx_minus1, 0);
2646        assert!(!r1.delta_rps_sign);
2647        assert_eq!(r1.abs_delta_rps_minus1, 0);
2648        assert_eq!(r1.used_by_curr_pic_flag, vec![true, false]);
2649        assert_eq!(r1.use_delta_flag, vec![true, true]);
2650    }
2651
2652    /// Hand-assembled SPS exercising the long-term-ref-pic block.
2653    #[test]
2654    fn parses_long_term_ref_pics() {
2655        let mut s = synthesised_prefix_bits();
2656        s += "0"; // pcm_enabled_flag = 0
2657        s += "1"; // num_short_term_ref_pic_sets = 0
2658        s += "1"; // long_term_ref_pics_present_flag = 1
2659                  // num_long_term_ref_pics_sps = 2 → codeNum 2 → '011'
2660        s += "011";
2661        // log2_max_pic_order_cnt_lsb_minus4 = 4 in the synthesised
2662        // prefix, so lt_ref_pic_poc_lsb_sps[i] is 8 bits wide.
2663        //   i=0: poc_lsb = 0x10, used_by_curr_pic_lt_sps_flag = 1
2664        s += "00010000";
2665        s += "1";
2666        //   i=1: poc_lsb = 0x20, used = 0
2667        s += "00100000";
2668        s += "0";
2669        // temporal_mvp=1, strong_intra=0, vui=0, ext=0, stop
2670        s += "1";
2671        s += "0";
2672        s += "0";
2673        s += "0";
2674        s += "1";
2675
2676        let bytes = bits_to_bytes(&s);
2677        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2678        assert!(sps.long_term_ref_pics_present_flag);
2679        assert_eq!(sps.num_long_term_ref_pics_sps, 2);
2680        assert_eq!(sps.long_term_ref_pics.len(), 2);
2681        assert_eq!(sps.long_term_ref_pics[0].poc_lsb, 0x10);
2682        assert!(sps.long_term_ref_pics[0].used_by_curr_pic);
2683        assert_eq!(sps.long_term_ref_pics[1].poc_lsb, 0x20);
2684        assert!(!sps.long_term_ref_pics[1].used_by_curr_pic);
2685    }
2686
2687    /// SPS with `vui_parameters_present_flag == 1`: the parser now
2688    /// decodes the §E.2.1 `vui_parameters()` body in full and
2689    /// continues to `sps_extension_present_flag`. Here the VUI is the
2690    /// minimal all-flags-off body (ten `u(1)` flags), so no opaque
2691    /// tail is captured.
2692    #[test]
2693    fn decodes_vui_then_continues_to_extension_flag() {
2694        let mut s = synthesised_prefix_bits();
2695        s += "0"; // pcm_enabled_flag = 0
2696        s += "1"; // num_short_term_ref_pic_sets = 0
2697        s += "0"; // long_term = 0
2698        s += "1"; // temporal_mvp = 1
2699        s += "1"; // strong_intra_smoothing = 1
2700        s += "1"; // vui_parameters_present_flag = 1
2701                  // vui_parameters(): ten flags all 0 (minimal body)
2702        s += "0000000000";
2703        s += "0"; // sps_extension_present_flag = 0
2704        s += "1"; // rbsp stop bit
2705
2706        let bytes = bits_to_bytes(&s);
2707        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2708        assert!(sps.vui_parameters_present_flag);
2709        let vui = sps.vui_parameters.as_ref().expect("VUI body");
2710        assert!(!vui.aspect_ratio_info_present_flag);
2711        assert!(!vui.overscan_info_present_flag);
2712        assert!(!vui.video_signal_type_present_flag);
2713        assert!(!vui.chroma_loc_info_present_flag);
2714        assert!(!vui.vui_timing_info_present_flag);
2715        assert!(!vui.bitstream_restriction_flag);
2716        assert!(!sps.sps_extension_present_flag);
2717        assert!(sps.opaque_tail.is_none());
2718    }
2719
2720    /// SPS with `vui_parameters_present_flag == 1` whose VUI signals a
2721    /// timing-info block, followed by `sps_extension_present_flag == 1`
2722    /// — the extension body after the (now fully decoded) VUI is
2723    /// surfaced as an opaque tail.
2724    #[test]
2725    fn decodes_vui_then_captures_extension_tail() {
2726        let mut s = synthesised_prefix_bits();
2727        s += "0"; // pcm_enabled_flag = 0
2728        s += "1"; // num_short_term_ref_pic_sets = 0
2729        s += "0"; // long_term = 0
2730        s += "1"; // temporal_mvp = 1
2731        s += "1"; // strong_intra_smoothing = 1
2732        s += "1"; // vui_parameters_present_flag = 1
2733                  // vui_parameters():
2734        s += "0"; // aspect_ratio_info_present_flag = 0
2735        s += "0"; // overscan_info_present_flag = 0
2736        s += "0"; // video_signal_type_present_flag = 0
2737        s += "0"; // chroma_loc_info_present_flag = 0
2738        s += "0"; // neutral_chroma_indication_flag = 0
2739        s += "0"; // field_seq_flag = 0
2740        s += "0"; // frame_field_info_present_flag = 0
2741        s += "0"; // default_display_window_flag = 0
2742        s += "1"; // vui_timing_info_present_flag = 1
2743        s += "00000000000000000000000000000001"; // vui_num_units_in_tick = 1
2744        s += "00000000000000000000000000011001"; // vui_time_scale = 25
2745        s += "0"; // vui_poc_proportional_to_timing_flag = 0
2746        s += "0"; // vui_hrd_parameters_present_flag = 0
2747        s += "0"; // bitstream_restriction_flag = 0
2748                  // sps_extension_present_flag = 1, followed by the
2749                  // typed extension flag block. Set the range-extension
2750                  // flag so the nine `sps_range_extension()` flags are
2751                  // decoded; with no further body, no opaque tail follows.
2752        s += "1"; // sps_extension_present_flag
2753        s += "1"; // sps_range_extension_flag = 1
2754        s += "000"; // sps_multilayer / sps_3d / sps_scc = 0
2755        s += "0000"; // sps_extension_4bits = 0
2756        s += "101010101"; // sps_range_extension() nine flags
2757        s += "1"; // rbsp_trailing_bits stop bit
2758
2759        let bytes = bits_to_bytes(&s);
2760        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2761        let vui = sps.vui_parameters.as_ref().expect("VUI body");
2762        let ti = vui.timing_info.as_ref().expect("timing info");
2763        assert_eq!(ti.num_units_in_tick, 1);
2764        assert_eq!(ti.time_scale, 25);
2765        assert!(sps.sps_extension_present_flag);
2766        let flags = sps.extension_flags.expect("extension flag block");
2767        assert!(flags.sps_range_extension_flag);
2768        assert!(!flags.sps_multilayer_extension_flag);
2769        assert!(!flags.sps_3d_extension_flag);
2770        assert!(!flags.sps_scc_extension_flag);
2771        assert_eq!(flags.sps_extension_4bits, 0);
2772        assert!(flags.has_body());
2773        // The range-extension body is now decoded, not opaque: with no
2774        // multilayer/3d/scc/4bits body after it, no opaque tail remains.
2775        assert!(sps.opaque_tail.is_none());
2776        let re = sps.sps_range_extension.expect("range extension body");
2777        assert!(re.transform_skip_rotation_enabled_flag); // 1
2778        assert!(!re.transform_skip_context_enabled_flag); // 0
2779        assert!(re.implicit_rdpcm_enabled_flag); // 1
2780        assert!(!re.explicit_rdpcm_enabled_flag); // 0
2781        assert!(re.extended_precision_processing_flag); // 1
2782        assert!(!re.intra_smoothing_disabled_flag); // 0
2783        assert!(re.high_precision_offsets_enabled_flag); // 1
2784        assert!(!re.persistent_rice_adaptation_enabled_flag); // 0
2785        assert!(re.cabac_bypass_alignment_enabled_flag); // 1
2786    }
2787
2788    /// `sps_extension_present_flag == 1` with `sps_range_extension_flag
2789    /// == 1` decodes the typed flag block then the nine
2790    /// `sps_range_extension()` flags (§7.3.2.2.2) in bit-stream order.
2791    /// This is the RExt-profile entry point (§A.3.5).
2792    #[test]
2793    fn decodes_sps_range_extension_body() {
2794        let mut s = synthesised_prefix_bits();
2795        s += "0"; // pcm
2796        s += "1"; // num_short_term=0
2797        s += "0"; // long_term=0
2798        s += "1"; // temporal_mvp
2799        s += "1"; // strong_intra_smoothing
2800        s += "0"; // vui=0
2801        s += "1"; // sps_extension_present_flag = 1
2802        s += "1"; // sps_range_extension_flag = 1
2803        s += "000"; // sps_multilayer / sps_3d / sps_scc = 0
2804        s += "0000"; // sps_extension_4bits = 0
2805                     // sps_range_extension() nine `u(1)` flags, all set —
2806                     // every RExt tool enabled.
2807        s += "111111111";
2808        s += "1"; // rbsp_trailing_bits stop bit
2809        let bytes = bits_to_bytes(&s);
2810        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2811        assert!(!sps.vui_parameters_present_flag);
2812        assert!(sps.sps_extension_present_flag);
2813        let flags = sps.extension_flags.expect("extension flag block");
2814        assert!(flags.sps_range_extension_flag);
2815        assert_eq!(flags.sps_extension_4bits, 0);
2816        // No body after the range extension → no opaque tail.
2817        assert!(sps.opaque_tail.is_none());
2818        let re = sps.sps_range_extension.expect("range extension body");
2819        assert!(re.transform_skip_rotation_enabled_flag);
2820        assert!(re.transform_skip_context_enabled_flag);
2821        assert!(re.implicit_rdpcm_enabled_flag);
2822        assert!(re.explicit_rdpcm_enabled_flag);
2823        assert!(re.extended_precision_processing_flag);
2824        assert!(re.intra_smoothing_disabled_flag);
2825        assert!(re.high_precision_offsets_enabled_flag);
2826        assert!(re.persistent_rice_adaptation_enabled_flag);
2827        assert!(re.cabac_bypass_alignment_enabled_flag);
2828    }
2829
2830    /// `sps_range_extension_flag == 1` followed by `sps_scc_extension_flag
2831    /// == 1` (no multilayer/3D body between them): the nine
2832    /// range-extension flags AND the `sps_scc_extension()` body
2833    /// (§7.3.2.2.3) are both decoded in place — no opaque tail remains.
2834    /// Palette mode disabled, so only the three leading/trailing fields
2835    /// are present.
2836    #[test]
2837    fn decodes_range_extension_then_scc_body_no_palette() {
2838        let mut s = synthesised_prefix_bits();
2839        s += "0"; // pcm
2840        s += "1"; // num_short_term=0
2841        s += "0"; // long_term=0
2842        s += "1"; // temporal_mvp
2843        s += "1"; // strong_intra_smoothing
2844        s += "0"; // vui=0
2845        s += "1"; // sps_extension_present_flag = 1
2846        s += "1"; // sps_range_extension_flag = 1
2847        s += "00"; // sps_multilayer / sps_3d = 0
2848        s += "1"; // sps_scc_extension_flag = 1
2849        s += "0000"; // sps_extension_4bits = 0
2850        s += "000000000"; // sps_range_extension() nine flags, all 0
2851                          // sps_scc_extension():
2852        s += "1"; // sps_curr_pic_ref_enabled_flag = 1
2853        s += "0"; // palette_mode_enabled_flag = 0 (palette block absent)
2854        s += "10"; // motion_vector_resolution_control_idc = 2 (u(2))
2855        s += "1"; // intra_boundary_filtering_disabled_flag = 1
2856        s += "1"; // rbsp_trailing_bits stop bit
2857        let bytes = bits_to_bytes(&s);
2858        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2859        let flags = sps.extension_flags.expect("extension flag block");
2860        assert!(flags.sps_range_extension_flag);
2861        assert!(flags.sps_scc_extension_flag);
2862        let re = sps.sps_range_extension.expect("range extension body");
2863        assert_eq!(re, SpsRangeExtension::default());
2864        // Both bodies are decoded in place; nothing is left opaque.
2865        assert!(sps.opaque_tail.is_none());
2866        let scc = sps.sps_scc_extension.expect("scc extension body");
2867        assert!(scc.sps_curr_pic_ref_enabled_flag);
2868        assert!(!scc.palette_mode_enabled_flag);
2869        assert_eq!(scc.palette_max_size, 0);
2870        assert_eq!(scc.delta_palette_max_predictor_size, 0);
2871        assert!(!scc.sps_palette_predictor_initializers_present_flag);
2872        assert!(scc.sps_palette_predictor_initializer.is_empty());
2873        assert_eq!(scc.motion_vector_resolution_control_idc, 2);
2874        assert!(scc.intra_boundary_filtering_disabled_flag);
2875    }
2876
2877    /// `sps_scc_extension()` with `palette_mode_enabled_flag == 1` and
2878    /// palette predictor initializers present: the `palette_max_size`,
2879    /// `delta_palette_max_predictor_size`, and per-component initializer
2880    /// table (§7.3.2.2.3) are decoded, each `u(v)` sized by `BitDepthY` /
2881    /// `BitDepthC`. `chroma_format_idc == 1` here gives `numComps == 3`.
2882    #[test]
2883    fn decodes_scc_extension_palette_initializers() {
2884        let mut s = synthesised_prefix_bits();
2885        s += "0"; // pcm
2886        s += "1"; // num_short_term=0
2887        s += "0"; // long_term=0
2888        s += "1"; // temporal_mvp
2889        s += "1"; // strong_intra_smoothing
2890        s += "0"; // vui=0
2891        s += "1"; // sps_extension_present_flag = 1
2892        s += "0001"; // range/multilayer/3d = 0, scc = 1
2893        s += "0000"; // sps_extension_4bits = 0
2894                     // sps_scc_extension():
2895        s += "0"; // sps_curr_pic_ref_enabled_flag = 0
2896        s += "1"; // palette_mode_enabled_flag = 1
2897        s += "00100"; // palette_max_size = ue(3) → "00100"
2898        s += "010"; // delta_palette_max_predictor_size = ue(1) → "010"
2899        s += "1"; // sps_palette_predictor_initializers_present_flag = 1
2900        s += "010"; // sps_num_palette_predictor_initializers_minus1 = ue(1) → 1
2901                    // numComps = 3, num_entries = 2; default BitDepthY/C = 8.
2902                    // comp 0 (8-bit): 0x01, 0x02
2903        s += "00000001";
2904        s += "00000010";
2905        // comp 1 (8-bit): 0x03, 0x04
2906        s += "00000011";
2907        s += "00000100";
2908        // comp 2 (8-bit): 0x05, 0x06
2909        s += "00000101";
2910        s += "00000110";
2911        s += "10"; // motion_vector_resolution_control_idc = 2 (u(2))
2912        s += "0"; // intra_boundary_filtering_disabled_flag = 0
2913        s += "1"; // rbsp_trailing_bits stop bit
2914        let bytes = bits_to_bytes(&s);
2915        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
2916        assert!(sps.opaque_tail.is_none());
2917        let scc = sps.sps_scc_extension.expect("scc extension body");
2918        assert!(!scc.sps_curr_pic_ref_enabled_flag);
2919        assert!(scc.palette_mode_enabled_flag);
2920        assert_eq!(scc.palette_max_size, 3);
2921        assert_eq!(scc.delta_palette_max_predictor_size, 1);
2922        // PaletteMaxPredictorSize = 3 + 1 (eq. 7-35).
2923        assert_eq!(scc.palette_max_predictor_size(), 4);
2924        assert!(scc.sps_palette_predictor_initializers_present_flag);
2925        assert_eq!(scc.sps_num_palette_predictor_initializers_minus1, 1);
2926        assert_eq!(
2927            scc.sps_palette_predictor_initializer,
2928            vec![vec![1, 2], vec![3, 4], vec![5, 6]],
2929        );
2930        assert_eq!(scc.motion_vector_resolution_control_idc, 2);
2931        assert!(!scc.intra_boundary_filtering_disabled_flag);
2932    }
2933
2934    /// §7.4.3.2.3 / §A.3.7: `sps_num_palette_predictor_initializers_minus1`
2935    /// past the largest representable predictor (PaletteMaxPredictorSize
2936    /// <= 128) is rejected BEFORE any initializer allocation — a fuzzed
2937    /// count once drove a multi-GiB `Vec::with_capacity`.
2938    #[test]
2939    fn rejects_oversized_palette_predictor_initializer_count() {
2940        let mut s = synthesised_prefix_bits();
2941        s += "0"; // pcm
2942        s += "1"; // num_short_term=0
2943        s += "0"; // long_term=0
2944        s += "1"; // temporal_mvp
2945        s += "1"; // strong_intra_smoothing
2946        s += "0"; // vui=0
2947        s += "1"; // sps_extension_present_flag = 1
2948        s += "0001"; // range/multilayer/3d = 0, scc = 1
2949        s += "0000"; // sps_extension_4bits = 0
2950                     // sps_scc_extension():
2951        s += "0"; // sps_curr_pic_ref_enabled_flag = 0
2952        s += "1"; // palette_mode_enabled_flag = 1
2953        s += "00100"; // palette_max_size = 3
2954        s += "010"; // delta_palette_max_predictor_size = 1
2955        s += "1"; // sps_palette_predictor_initializers_present_flag = 1
2956                  // sps_num_palette_predictor_initializers_minus1 = ue(128):
2957                  // 129 in 8 bits = 10000001, prefix of 7 zeros.
2958        s += "0000000";
2959        s += "10000001";
2960        s += "1"; // (whatever follows is unreached)
2961        let bytes = bits_to_bytes(&s);
2962        let err = SeqParameterSet::parse(&bytes).expect_err("oversized initializer count");
2963        assert!(matches!(
2964            err,
2965            SpsError::ValueOutOfRange {
2966                field: "sps_num_palette_predictor_initializers_minus1",
2967                got: 128
2968            }
2969        ));
2970    }
2971
2972    /// §7.4.3.2.3: `motion_vector_resolution_control_idc == 3` is
2973    /// reserved and rejected as out-of-range.
2974    #[test]
2975    fn rejects_reserved_motion_vector_resolution_control_idc() {
2976        let mut s = synthesised_prefix_bits();
2977        s += "0"; // pcm
2978        s += "1"; // num_short_term=0
2979        s += "0"; // long_term=0
2980        s += "1"; // temporal_mvp
2981        s += "1"; // strong_intra_smoothing
2982        s += "0"; // vui=0
2983        s += "1"; // sps_extension_present_flag = 1
2984        s += "0001"; // range/multilayer/3d = 0, scc = 1
2985        s += "0000"; // sps_extension_4bits = 0
2986                     // sps_scc_extension():
2987        s += "0"; // sps_curr_pic_ref_enabled_flag = 0
2988        s += "0"; // palette_mode_enabled_flag = 0
2989        s += "11"; // motion_vector_resolution_control_idc = 3 (reserved)
2990        s += "0"; // intra_boundary_filtering_disabled_flag = 0
2991        s += "1"; // rbsp_trailing_bits stop bit
2992        let bytes = bits_to_bytes(&s);
2993        let err = SeqParameterSet::parse(&bytes).expect_err("reserved mvr idc");
2994        assert!(matches!(
2995            err,
2996            SpsError::ValueOutOfRange {
2997                field: "motion_vector_resolution_control_idc",
2998                got: 3
2999            }
3000        ));
3001    }
3002
3003    /// §7.4.3.2.3: when `palette_max_size == 0`, a non-zero
3004    /// `delta_palette_max_predictor_size` violates bitstream
3005    /// conformance and is rejected.
3006    #[test]
3007    fn rejects_delta_palette_when_max_size_zero() {
3008        let mut s = synthesised_prefix_bits();
3009        s += "0"; // pcm
3010        s += "1"; // num_short_term=0
3011        s += "0"; // long_term=0
3012        s += "1"; // temporal_mvp
3013        s += "1"; // strong_intra_smoothing
3014        s += "0"; // vui=0
3015        s += "1"; // sps_extension_present_flag = 1
3016        s += "0001"; // range/multilayer/3d = 0, scc = 1
3017        s += "0000"; // sps_extension_4bits = 0
3018                     // sps_scc_extension():
3019        s += "0"; // sps_curr_pic_ref_enabled_flag = 0
3020        s += "1"; // palette_mode_enabled_flag = 1
3021        s += "1"; // palette_max_size = 0 (ue)
3022        s += "010"; // delta_palette_max_predictor_size = 1 (ue) → illegal
3023        s += "0"; // sps_palette_predictor_initializers_present_flag = 0
3024        s += "00"; // motion_vector_resolution_control_idc = 0
3025        s += "0"; // intra_boundary_filtering_disabled_flag = 0
3026        s += "1"; // rbsp_trailing_bits stop bit
3027        let bytes = bits_to_bytes(&s);
3028        let err = SeqParameterSet::parse(&bytes).expect_err("delta vs max_size 0");
3029        assert!(matches!(
3030            err,
3031            SpsError::ValueOutOfRange {
3032                field: "delta_palette_max_predictor_size",
3033                got: 1
3034            }
3035        ));
3036    }
3037
3038    /// `sps_extension_present_flag == 1` with every typed extension
3039    /// flag (and `sps_extension_4bits`) equal to 0 decodes the
3040    /// flag block but consumes only `rbsp_trailing_bits()` afterwards
3041    /// — no opaque tail is surfaced because no extension body follows.
3042    #[test]
3043    fn decodes_extension_flag_block_without_bodies() {
3044        let mut s = synthesised_prefix_bits();
3045        s += "0"; // pcm
3046        s += "1"; // num_short_term=0
3047        s += "0"; // long_term=0
3048        s += "1"; // temporal_mvp
3049        s += "1"; // strong_intra_smoothing
3050        s += "0"; // vui=0
3051        s += "1"; // sps_extension_present_flag = 1
3052        s += "0000"; // four typed flags all 0
3053        s += "0000"; // sps_extension_4bits = 0
3054        s += "1"; // rbsp_trailing_bits stop bit
3055        let bytes = bits_to_bytes(&s);
3056        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3057        assert!(sps.sps_extension_present_flag);
3058        let flags = sps.extension_flags.expect("extension flag block");
3059        assert!(!flags.sps_range_extension_flag);
3060        assert!(!flags.sps_multilayer_extension_flag);
3061        assert!(!flags.sps_3d_extension_flag);
3062        assert!(!flags.sps_scc_extension_flag);
3063        assert_eq!(flags.sps_extension_4bits, 0);
3064        assert!(!flags.has_body());
3065        assert!(sps.opaque_tail.is_none());
3066    }
3067
3068    /// `sps_extension_present_flag == 1` with `sps_scc_extension_flag
3069    /// == 1` (and no preceding range/multilayer/3D body) selects the
3070    /// §A.3.7 Screen Content Coding profile family; the typed block
3071    /// decodes cleanly and the `sps_scc_extension()` body (§7.3.2.2.3)
3072    /// is decoded in place with no opaque tail. Palette mode disabled.
3073    #[test]
3074    fn decodes_scc_extension_body_no_range() {
3075        let mut s = synthesised_prefix_bits();
3076        s += "0"; // pcm
3077        s += "1"; // num_short_term=0
3078        s += "0"; // long_term=0
3079        s += "1"; // temporal_mvp
3080        s += "1"; // strong_intra_smoothing
3081        s += "0"; // vui=0
3082        s += "1"; // sps_extension_present_flag = 1
3083        s += "0"; // sps_range_extension_flag = 0
3084        s += "0"; // sps_multilayer_extension_flag = 0
3085        s += "0"; // sps_3d_extension_flag = 0
3086        s += "1"; // sps_scc_extension_flag = 1
3087        s += "0000"; // sps_extension_4bits = 0
3088                     // sps_scc_extension():
3089        s += "1"; // sps_curr_pic_ref_enabled_flag = 1
3090        s += "0"; // palette_mode_enabled_flag = 0
3091        s += "01"; // motion_vector_resolution_control_idc = 1 (u(2))
3092        s += "0"; // intra_boundary_filtering_disabled_flag = 0
3093        s += "1"; // rbsp_trailing_bits stop bit
3094        let bytes = bits_to_bytes(&s);
3095        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3096        let flags = sps.extension_flags.expect("extension flag block");
3097        assert!(!flags.sps_range_extension_flag);
3098        assert!(!flags.sps_multilayer_extension_flag);
3099        assert!(!flags.sps_3d_extension_flag);
3100        assert!(flags.sps_scc_extension_flag);
3101        assert_eq!(flags.sps_extension_4bits, 0);
3102        assert!(flags.has_body());
3103        assert!(sps.opaque_tail.is_none());
3104        let scc = sps.sps_scc_extension.expect("scc extension body");
3105        assert!(scc.sps_curr_pic_ref_enabled_flag);
3106        assert!(!scc.palette_mode_enabled_flag);
3107        assert_eq!(scc.motion_vector_resolution_control_idc, 1);
3108        assert!(!scc.intra_boundary_filtering_disabled_flag);
3109    }
3110
3111    /// When `sps_scc_extension_flag == 1` but a `sps_multilayer_extension()`
3112    /// body precedes it (§7.3.2.2.1 body order), the SCC body cannot be
3113    /// decoded in place and the whole multilayer-onward span — including
3114    /// the SCC body — stays in the opaque tail.
3115    #[test]
3116    fn scc_stays_opaque_behind_multilayer_body() {
3117        let mut s = synthesised_prefix_bits();
3118        s += "0"; // pcm
3119        s += "1"; // num_short_term=0
3120        s += "0"; // long_term=0
3121        s += "1"; // temporal_mvp
3122        s += "1"; // strong_intra_smoothing
3123        s += "0"; // vui=0
3124        s += "1"; // sps_extension_present_flag = 1
3125        s += "0"; // sps_range_extension_flag = 0
3126        s += "1"; // sps_multilayer_extension_flag = 1
3127        s += "0"; // sps_3d_extension_flag = 0
3128        s += "1"; // sps_scc_extension_flag = 1
3129        s += "0000"; // sps_extension_4bits = 0
3130        s += "11001100"; // opaque multilayer + scc span sentinel
3131        s += "1"; // rbsp_trailing_bits stop bit
3132        let bytes = bits_to_bytes(&s);
3133        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3134        let flags = sps.extension_flags.expect("extension flag block");
3135        assert!(flags.sps_multilayer_extension_flag);
3136        assert!(flags.sps_scc_extension_flag);
3137        // Multilayer body is still opaque, so the SCC body cannot be
3138        // decoded in place; both stay in the captured tail.
3139        assert!(sps.sps_scc_extension.is_none());
3140        assert!(sps.opaque_tail.is_some());
3141    }
3142
3143    /// `sps_extension_present_flag == 1` with the four typed
3144    /// extension flags all 0 but `sps_extension_4bits != 0` still
3145    /// surfaces an opaque tail — the §7.3.2.2.1
3146    /// `while( more_rbsp_data() ) sps_extension_data_flag` block is
3147    /// gated by `sps_extension_4bits` (the §7.4.3.2.1 decoder rule is
3148    /// to ignore the data flags but they must be skipped past
3149    /// rbsp_trailing_bits, so the bytes are surfaced as opaque).
3150    #[test]
3151    fn captures_extension_data_flag_tail_when_4bits_nonzero() {
3152        let mut s = synthesised_prefix_bits();
3153        s += "0"; // pcm
3154        s += "1"; // num_short_term=0
3155        s += "0"; // long_term=0
3156        s += "1"; // temporal_mvp
3157        s += "1"; // strong_intra_smoothing
3158        s += "0"; // vui=0
3159        s += "1"; // sps_extension_present_flag = 1
3160        s += "0000"; // four typed flags = 0
3161        s += "0001"; // sps_extension_4bits = 1 (reserved, non-zero)
3162        s += "0"; // a single sps_extension_data_flag value
3163        s += "1"; // rbsp_trailing_bits stop bit
3164        let bytes = bits_to_bytes(&s);
3165        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3166        let flags = sps.extension_flags.expect("extension flag block");
3167        assert_eq!(flags.sps_extension_4bits, 1);
3168        assert!(flags.has_body());
3169        assert!(sps.opaque_tail.is_some());
3170    }
3171
3172    /// When `sps_extension_present_flag == 0` the typed
3173    /// extension-flag block is absent and every flag is inferred to
3174    /// 0 per §7.4.3.2.1.
3175    #[test]
3176    fn extension_flags_absent_when_gate_zero() {
3177        let mut s = synthesised_prefix_bits();
3178        s += "0"; // pcm
3179        s += "1"; // num_short_term=0
3180        s += "0"; // long_term=0
3181        s += "1"; // temporal_mvp
3182        s += "1"; // strong_intra_smoothing
3183        s += "0"; // vui=0
3184        s += "0"; // sps_extension_present_flag = 0
3185        s += "1"; // rbsp_trailing_bits stop bit
3186        let bytes = bits_to_bytes(&s);
3187        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3188        assert!(!sps.sps_extension_present_flag);
3189        assert!(sps.extension_flags.is_none());
3190        assert!(sps.opaque_tail.is_none());
3191    }
3192
3193    /// SPS with both `vui` and `extension` flags off — no opaque
3194    /// tail is captured (only the rbsp_trailing_bits remain).
3195    #[test]
3196    fn no_opaque_tail_when_flags_clear() {
3197        let mut s = synthesised_prefix_bits();
3198        s += "0"; // pcm
3199        s += "1"; // num_short=0
3200        s += "0"; // long_term=0
3201        s += "1"; // temporal_mvp
3202        s += "1"; // strong_intra_smoothing
3203        s += "0"; // vui=0
3204        s += "0"; // sps_extension_present=0
3205        s += "1"; // stop bit
3206        let bytes = bits_to_bytes(&s);
3207        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3208        assert!(!sps.vui_parameters_present_flag);
3209        assert!(!sps.sps_extension_present_flag);
3210        assert!(sps.opaque_tail.is_none());
3211    }
3212
3213    /// `num_short_term_ref_pic_sets > 64` is illegal per §7.4.3.2.
3214    #[test]
3215    fn rejects_too_many_short_term_rps() {
3216        let mut s = synthesised_prefix_bits();
3217        s += "0"; // pcm
3218                  // num_short_term_ref_pic_sets = 65 → codeNum 65
3219                  // 65 in 0-th order Exp-Golomb: leadingZeroBits=6,
3220                  //   suffix = 65 - (2^6 - 1) = 2 = '000010'
3221                  // so the bit string is '000000 1 000010' = 13 bits.
3222        s += "0000001000010";
3223        let bytes = bits_to_bytes(&s);
3224        let err = SeqParameterSet::parse(&bytes).unwrap_err();
3225        assert!(matches!(
3226            err,
3227            SpsError::ValueOutOfRange {
3228                field: "num_short_term_ref_pic_sets",
3229                ..
3230            }
3231        ));
3232    }
3233
3234    /// §7.4.8 explicit-form materialisation: the cumulative recurrence
3235    /// for `DeltaPocS0[i]` (equation 7-69) and `DeltaPocS1[i]`
3236    /// (equation 7-70) starting from the equation-7-67 / 7-68 seeds.
3237    #[test]
3238    fn materialize_explicit_form_recurrence() {
3239        // Hand-assembled RPS: num_negative_pics = 3,
3240        // delta_poc_s0_minus1 = [0, 1, 0], used_by_curr_pic_s0_flag =
3241        // [true, false, true]; num_positive_pics = 2,
3242        // delta_poc_s1_minus1 = [1, 0], used_by_curr_pic_s1_flag =
3243        // [true, true]. Per §7.4.8:
3244        //   DeltaPocS0 = [-1, -3, -4]
3245        //   DeltaPocS1 = [ 2,  3]
3246        let rps = ShortTermRefPicSet {
3247            inter_ref_pic_set_prediction_flag: false,
3248            delta_idx_minus1: 0,
3249            delta_rps_sign: false,
3250            abs_delta_rps_minus1: 0,
3251            used_by_curr_pic_flag: Vec::new(),
3252            use_delta_flag: Vec::new(),
3253            num_negative_pics: 3,
3254            num_positive_pics: 2,
3255            delta_poc_s0_minus1: vec![0, 1, 0],
3256            used_by_curr_pic_s0_flag: vec![true, false, true],
3257            delta_poc_s1_minus1: vec![1, 0],
3258            used_by_curr_pic_s1_flag: vec![true, true],
3259        };
3260        let m = rps.materialize(None).expect("explicit materialise");
3261        assert_eq!(m.delta_poc_s0, vec![-1, -3, -4]);
3262        assert_eq!(m.used_by_curr_pic_s0, vec![true, false, true]);
3263        assert_eq!(m.delta_poc_s1, vec![2, 3]);
3264        assert_eq!(m.used_by_curr_pic_s1, vec![true, true]);
3265        assert_eq!(m.num_negative_pics(), 3);
3266        assert_eq!(m.num_positive_pics(), 2);
3267        assert_eq!(m.num_delta_pocs(), 5);
3268    }
3269
3270    /// §7.4.8 inter-RPS-prediction (equations 7-61 / 7-62): a tiny
3271    /// chain where the source has one negative POC at -1 and the
3272    /// derived RPS uses `deltaRps = +1` to shift it past zero, so the
3273    /// source's negative drops out and `deltaRps` itself lands as a
3274    /// positive entry gated by the wire flags. Matches the
3275    /// `parses_inter_rps_prediction` fixture above.
3276    #[test]
3277    fn materialize_inter_rps_prediction_matches_fixture() {
3278        let src = MaterializedShortTermRefPicSet {
3279            delta_poc_s0: vec![-1],
3280            used_by_curr_pic_s0: vec![true],
3281            delta_poc_s1: vec![],
3282            used_by_curr_pic_s1: vec![],
3283        };
3284        // The inter-form RPS from the existing
3285        // `parses_inter_rps_prediction` test: deltaRps = +1,
3286        // used_by_curr_pic_flag = [true, false], use_delta_flag = [true,
3287        // true].
3288        let inter = ShortTermRefPicSet {
3289            inter_ref_pic_set_prediction_flag: true,
3290            delta_idx_minus1: 0,
3291            delta_rps_sign: false,
3292            abs_delta_rps_minus1: 0,
3293            used_by_curr_pic_flag: vec![true, false],
3294            use_delta_flag: vec![true, true],
3295            num_negative_pics: 0,
3296            num_positive_pics: 0,
3297            delta_poc_s0_minus1: Vec::new(),
3298            used_by_curr_pic_s0_flag: Vec::new(),
3299            delta_poc_s1_minus1: Vec::new(),
3300            used_by_curr_pic_s1_flag: Vec::new(),
3301        };
3302        let m = inter
3303            .materialize(Some(&src))
3304            .expect("inter-RPS materialise");
3305        // Negative side (equation 7-61):
3306        //   * No source positives.
3307        //   * deltaRps = +1 ≥ 0, skip the self-term.
3308        //   * Source negative j=0: dPoc = -1 + 1 = 0, not < 0, skip.
3309        //   ⇒ NumNegativePics = 0.
3310        assert!(m.delta_poc_s0.is_empty());
3311        assert!(m.used_by_curr_pic_s0.is_empty());
3312        // Positive side (equation 7-62):
3313        //   * Source negative j=0 reverse: dPoc = -1 + 1 = 0, not > 0,
3314        //     skip.
3315        //   * deltaRps = +1 > 0 and use_delta_flag[NumDeltaPocs=1] =
3316        //     true ⇒ DeltaPocS1[0] = +1, UsedByCurrPicS1[0] =
3317        //     used_by_curr_pic_flag[1] = false.
3318        //   * No source positives.
3319        //   ⇒ NumPositivePics = 1.
3320        assert_eq!(m.delta_poc_s1, vec![1]);
3321        assert_eq!(m.used_by_curr_pic_s1, vec![false]);
3322    }
3323
3324    /// §7.4.8 inter-RPS-prediction with `deltaRps < 0`: a source
3325    /// positive POC at +1 with `deltaRps = -2` falls onto the negative
3326    /// side via the source-positives-reverse step of equation 7-61.
3327    #[test]
3328    fn materialize_inter_rps_prediction_negative_delta_rps() {
3329        // Source: one positive POC at +1 (no negatives).
3330        let src = MaterializedShortTermRefPicSet {
3331            delta_poc_s0: vec![],
3332            used_by_curr_pic_s0: vec![],
3333            delta_poc_s1: vec![1],
3334            used_by_curr_pic_s1: vec![true],
3335        };
3336        // Inter-form: deltaRps = -(1+1) = -2; arrays sized
3337        // NumDeltaPocs[src]+1 = 2.
3338        // used_by_curr_pic_flag indexed:
3339        //   * [0..NumNegativePics[src]) = none
3340        //   * [NumNegativePics, NumNegativePics + NumPositivePics) = [0..1) = j_src=0
3341        //   * [NumDeltaPocs] = trailing slot for `deltaRps` self-term
3342        // So used_by_curr_pic_flag = [true_for_src_pos0, true_for_self_term].
3343        let inter = ShortTermRefPicSet {
3344            inter_ref_pic_set_prediction_flag: true,
3345            delta_idx_minus1: 0,
3346            delta_rps_sign: true, // negative
3347            abs_delta_rps_minus1: 1,
3348            used_by_curr_pic_flag: vec![true, true],
3349            use_delta_flag: vec![true, true],
3350            num_negative_pics: 0,
3351            num_positive_pics: 0,
3352            delta_poc_s0_minus1: Vec::new(),
3353            used_by_curr_pic_s0_flag: Vec::new(),
3354            delta_poc_s1_minus1: Vec::new(),
3355            used_by_curr_pic_s1_flag: Vec::new(),
3356        };
3357        let m = inter
3358            .materialize(Some(&src))
3359            .expect("inter-RPS negative deltaRps");
3360        // Negative side (equation 7-61):
3361        //   * Source positive j=0 reverse: dPoc = 1 + (-2) = -1 < 0 and
3362        //     use_delta_flag[NumNeg=0 + 0] = use_delta_flag[0] = true
3363        //     ⇒ DeltaPocS0[0] = -1, UsedByCurrPicS0[0] =
3364        //       used_by_curr_pic_flag[0] = true.
3365        //   * deltaRps = -2 < 0 and use_delta_flag[NumDeltaPocs=1] =
3366        //     true ⇒ DeltaPocS0[1] = -2, UsedByCurrPicS0[1] =
3367        //       used_by_curr_pic_flag[1] = true.
3368        //   * No source negatives.
3369        assert_eq!(m.delta_poc_s0, vec![-1, -2]);
3370        assert_eq!(m.used_by_curr_pic_s0, vec![true, true]);
3371        // Positive side: source negatives reverse: none. deltaRps < 0,
3372        // skip self-term. Source positive j=0: dPoc = +1 + (-2) = -1,
3373        // not > 0, skip. ⇒ empty.
3374        assert!(m.delta_poc_s1.is_empty());
3375        assert!(m.used_by_curr_pic_s1.is_empty());
3376    }
3377
3378    /// [`ShortTermRefPicSet::materialize`] rejects the inter form
3379    /// without a source RPS.
3380    #[test]
3381    fn materialize_inter_rps_rejects_missing_source() {
3382        let inter = ShortTermRefPicSet {
3383            inter_ref_pic_set_prediction_flag: true,
3384            delta_idx_minus1: 0,
3385            delta_rps_sign: false,
3386            abs_delta_rps_minus1: 0,
3387            used_by_curr_pic_flag: vec![true],
3388            use_delta_flag: vec![true],
3389            ..Default::default()
3390        };
3391        assert_eq!(
3392            inter.materialize(None),
3393            Err(ShortTermRefPicSetMaterializeError::MissingSource),
3394        );
3395    }
3396
3397    /// [`ShortTermRefPicSet::materialize`] surfaces a per-position
3398    /// array-length mismatch against the source RPS's `NumDeltaPocs +
3399    /// 1`.
3400    #[test]
3401    fn materialize_inter_rps_rejects_length_mismatch() {
3402        let src = MaterializedShortTermRefPicSet {
3403            delta_poc_s0: vec![-1, -2],
3404            used_by_curr_pic_s0: vec![true, true],
3405            delta_poc_s1: vec![],
3406            used_by_curr_pic_s1: vec![],
3407        };
3408        // NumDeltaPocs[src] = 2, so the inter form expects arrays of
3409        // length 3. Pass 2-element arrays to provoke the mismatch.
3410        let inter = ShortTermRefPicSet {
3411            inter_ref_pic_set_prediction_flag: true,
3412            delta_idx_minus1: 0,
3413            delta_rps_sign: false,
3414            abs_delta_rps_minus1: 0,
3415            used_by_curr_pic_flag: vec![true, true],
3416            use_delta_flag: vec![true, true],
3417            ..Default::default()
3418        };
3419        assert!(matches!(
3420            inter.materialize(Some(&src)),
3421            Err(ShortTermRefPicSetMaterializeError::SourceLengthMismatch {
3422                expected: 3,
3423                got_used: 2,
3424                got_delta: 2,
3425            }),
3426        ));
3427    }
3428
3429    /// [`SeqParameterSet::materialize_short_term_ref_pic_sets`]
3430    /// chains explicit and inter-RPS-predicted entries through their
3431    /// `RefRpsIdx` lookups using the same fixture as
3432    /// `parses_inter_rps_prediction`.
3433    #[test]
3434    fn sps_materialize_chains_inter_rps_prediction() {
3435        let mut s = synthesised_prefix_bits();
3436        s += "0"; // pcm_enabled_flag
3437        s += "011"; // num_short_term_ref_pic_sets = 2
3438                    // st_ref_pic_set(0): explicit num_neg=1, num_pos=0,
3439                    // delta_poc_s0_minus1[0]=0, used_by_curr_pic_s0_flag=1
3440        s += "010";
3441        s += "1";
3442        s += "1";
3443        s += "1";
3444        // st_ref_pic_set(1): inter_ref_pic_set_prediction_flag = 1,
3445        // delta_rps_sign=0, abs_delta_rps_minus1=0 (deltaRps=+1),
3446        // used_by_curr_pic_flag = [1, 0], use_delta_flag[1] = 1.
3447        s += "1";
3448        s += "0";
3449        s += "1";
3450        s += "1"; // used_by_curr_pic_flag[0] = 1 (use_delta inferred)
3451        s += "0"; // used_by_curr_pic_flag[1] = 0
3452        s += "1"; // use_delta_flag[1] = 1
3453                  // long_term=0, temporal_mvp=1, strong_intra_smoothing=1,
3454                  // vui=0, ext=0, stop bit
3455        s += "0";
3456        s += "1";
3457        s += "1";
3458        s += "0";
3459        s += "0";
3460        s += "1";
3461
3462        let bytes = bits_to_bytes(&s);
3463        let sps = SeqParameterSet::parse(&bytes).expect("SPS parse");
3464        let m = sps
3465            .materialize_short_term_ref_pic_sets()
3466            .expect("materialize");
3467        assert_eq!(m.len(), 2);
3468        // Explicit source (idx 0): DeltaPocS0=[-1], UsedByCurrPicS0=[true].
3469        assert_eq!(m[0].delta_poc_s0, vec![-1]);
3470        assert_eq!(m[0].used_by_curr_pic_s0, vec![true]);
3471        assert_eq!(m[0].num_delta_pocs(), 1);
3472        // Inter-predicted (idx 1): NumNegativePics=0,
3473        // NumPositivePics=1 with DeltaPocS1=[+1] from the deltaRps
3474        // self-term (see `materialize_inter_rps_prediction_matches_fixture`).
3475        assert!(m[1].delta_poc_s0.is_empty());
3476        assert_eq!(m[1].delta_poc_s1, vec![1]);
3477        assert_eq!(m[1].used_by_curr_pic_s1, vec![false]);
3478    }
3479
3480    /// `num_long_term_ref_pics_sps > 32` is illegal per §7.4.3.2.
3481    #[test]
3482    fn rejects_too_many_long_term_rps() {
3483        let mut s = synthesised_prefix_bits();
3484        s += "0"; // pcm
3485        s += "1"; // num_short_term=0
3486        s += "1"; // long_term_ref_pics_present
3487                  // num_long_term_ref_pics_sps = 33: codeNum 33
3488                  // leadingZeroBits=5, suffix = 33 - (2^5 - 1) = 2 = '00010'
3489                  // bit string: '00000 1 00010' = 11 bits.
3490        s += "00000100010";
3491        let bytes = bits_to_bytes(&s);
3492        let err = SeqParameterSet::parse(&bytes).unwrap_err();
3493        assert!(matches!(
3494            err,
3495            SpsError::ValueOutOfRange {
3496                field: "num_long_term_ref_pics_sps",
3497                ..
3498            }
3499        ));
3500    }
3501}