Skip to main content

oxideav_h265/
slice.rs

1//! Slice segment header parser per ITU-T Rec. H.265 §7.3.6.1.
2//!
3//! Round-6 scope: parse the `slice_segment_header()` syntax structure
4//! (§7.3.6.1) for an independent slice segment, materialising every
5//! field that does **not** require decoded-picture-buffer state to
6//! interpret. The parse takes the activated SPS and PPS as context
7//! because several field widths and presence gates are derived from
8//! them (the `slice_segment_address` width is
9//! `Ceil( Log2( PicSizeInCtbsY ) )`, the `slice_pic_order_cnt_lsb`
10//! width is `log2_max_pic_order_cnt_lsb_minus4 + 4`, the SAO / MVP
11//! gates come from the SPS, and the tiles / entropy-sync entry-point
12//! block comes from the PPS).
13//!
14//! ## What this round materialises
15//!
16//! * `first_slice_segment_in_pic_flag`, `no_output_of_prior_pics_flag`
17//!   (IRAP only), `slice_pic_parameter_set_id`.
18//! * For non-first slice segments: `dependent_slice_segment_flag`
19//!   (only when `dependent_slice_segments_enabled_flag`) and
20//!   `slice_segment_address` (`u(v)`, width
21//!   `Ceil( Log2( PicSizeInCtbsY ) )`).
22//! * For independent slice segments (`!dependent_slice_segment_flag`):
23//!   the `slice_reserved_flag[]` block, `slice_type`,
24//!   `pic_output_flag` (only when `output_flag_present_flag`),
25//!   `colour_plane_id` (only when `separate_colour_plane_flag`),
26//!   `slice_temporal_mvp_enabled_flag` (only when
27//!   `sps_temporal_mvp_enabled_flag`), the SAO luma / chroma gates,
28//!   `slice_qp_delta` (`se(v)`), the chroma QP offsets, the
29//!   deblocking-filter override block, and
30//!   `slice_loop_filter_across_slices_enabled_flag`.
31//! * The entry-point-offset block (`num_entry_point_offsets`,
32//!   `offset_len_minus1`, `entry_point_offset_minus1[]`) when
33//!   `tiles_enabled_flag || entropy_coding_sync_enabled_flag`.
34//! * The slice-segment-header extension block when
35//!   `slice_segment_header_extension_present_flag`.
36//! * `byte_alignment()` consumed to the next byte boundary, so the
37//!   reported [`SliceSegmentHeader::byte_offset_to_slice_data`] points
38//!   at the first byte of `slice_segment_data()`.
39//!
40//! ## What this round defers (surfaced, not decoded)
41//!
42//! Three points need state this round does not carry:
43//!
44//! * The **non-IDR picture-order-count + reference-picture-set block**
45//!   (`slice_pic_order_cnt_lsb`, `short_term_ref_pic_set_sps_flag`,
46//!   the inline `st_ref_pic_set()`, the long-term block) needs the
47//!   SPS short-term-RPS parser to be re-entered for the in-line
48//!   `stRpsIdx == num_short_term_ref_pic_sets` case, which is not yet
49//!   exposed publicly. When the current NAL unit is **not** an IDR
50//!   (`nal_unit_type != IDR_W_RADL && != IDR_N_LP`), the parser stops
51//!   right after `colour_plane_id` and surfaces the remainder as
52//!   [`SliceSegmentHeader::opaque_tail`].
53//! * The **P / B reference-list / weighted-prediction sub-structures**
54//!   (`ref_pic_lists_modification()` §7.3.6.2 and `pred_weight_table()`
55//!   §7.3.6.3) need DPB-derived `NumPicTotalCurr` / `RefPicList`
56//!   values. When `slice_type` is P or B the parser materialises the
57//!   common P/B fields up to (but not including) the point where those
58//!   sub-structures would begin, then surfaces the remainder as the
59//!   opaque tail. The §7.3.6.2 syntax structure itself is implemented
60//!   as a standalone parser ([`RefPicListsModification::parse`]) so a
61//!   future round that wires up the §7.4.7.2 `NumPicTotalCurr`
62//!   derivation can decode the reference-picture-list-modification
63//!   block in place; the implicit `RefPicListTempX` derivation of
64//!   §8.3.4 stays the consumer's responsibility. When
65//!   `pps.lists_modification_present_flag == 0` the modification block
66//!   is statically absent (the §7.3.6.1 `if(... && NumPicTotalCurr > 1)`
67//!   short-circuit applies independent of any DPB state); the parser
68//!   in that case continues into the §7.3.6.1
69//!   `mvd_l1_zero_flag` / `cabac_init_flag` /
70//!   `collocated_from_l0_flag` / `collocated_ref_idx` block in-place
71//!   and surfaces those four fields, then defers at the weighted-pred
72//!   gate.
73//!
74//! Independent **I-slice IDR** segments — the dominant case for the
75//! intra-only fixtures this rebuild targets — are parsed end to end
76//! through `byte_alignment()`.
77
78use crate::bitreader::{BitReader, BitReaderError};
79use crate::pps::PicParameterSet;
80use crate::sps::{OpaqueTail, SeqParameterSet, ShortTermRefPicSet, SpsError};
81
82/// `nal_unit_type` value `BLA_W_LP` (Table 7-1). The IRAP range used by
83/// the `no_output_of_prior_pics_flag` gate is `BLA_W_LP..=RSV_IRAP_VCL23`.
84pub const BLA_W_LP: u8 = 16;
85/// `nal_unit_type` value `IDR_W_RADL` (Table 7-1).
86pub const IDR_W_RADL: u8 = 19;
87/// `nal_unit_type` value `IDR_N_LP` (Table 7-1).
88pub const IDR_N_LP: u8 = 20;
89/// `nal_unit_type` value `RSV_IRAP_VCL23` (Table 7-1) — the inclusive
90/// upper bound of the IRAP NAL-unit-type range.
91pub const RSV_IRAP_VCL23: u8 = 23;
92
93/// `slice_type` enumeration per Table 7-7.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SliceType {
96    /// B slice (`slice_type == 0`).
97    B,
98    /// P slice (`slice_type == 1`).
99    P,
100    /// I slice (`slice_type == 2`).
101    I,
102}
103
104impl SliceType {
105    /// Map the raw `ue(v)` `slice_type` value to the enum, rejecting
106    /// any value outside `0..=2`.
107    fn from_raw(v: u32) -> Result<Self, SliceError> {
108        match v {
109            0 => Ok(Self::B),
110            1 => Ok(Self::P),
111            2 => Ok(Self::I),
112            other => Err(SliceError::ValueOutOfRange {
113                field: "slice_type",
114                got: other as i64,
115            }),
116        }
117    }
118
119    /// True for P and B slices (the slice types that signal reference
120    /// lists, weighted prediction, etc.).
121    pub fn is_inter(self) -> bool {
122        matches!(self, Self::P | Self::B)
123    }
124}
125
126/// Errors that can arise while parsing a slice segment header.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum SliceError {
129    /// The RBSP ran out of bits before the header was fully parsed.
130    Truncated,
131    /// A syntax element's parsed value was outside the legal range
132    /// specified for it in §7.4.7.1.
133    ValueOutOfRange {
134        /// Name of the offending syntax element.
135        field: &'static str,
136        /// The (illegal) value as an `i64` (covers both `ue(v)` and
137        /// `se(v)` elements).
138        got: i64,
139    },
140    /// An unexpected bitstream-level error surfaced from the reader.
141    Bitstream(BitReaderError),
142    /// The in-line `st_ref_pic_set(num_short_term_ref_pic_sets)` parse
143    /// (invoked from §7.3.6.1 when `short_term_ref_pic_set_sps_flag == 0`)
144    /// failed.
145    InlineShortTermRpsParse(SpsError),
146}
147
148impl core::fmt::Display for SliceError {
149    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
150        match self {
151            Self::Truncated => f.write_str("slice segment header RBSP truncated"),
152            Self::ValueOutOfRange { field, got } => {
153                write!(f, "slice header syntax element {field} out of range: {got}")
154            }
155            Self::Bitstream(e) => write!(f, "bitstream error during slice header parse: {e}"),
156            Self::InlineShortTermRpsParse(e) => {
157                write!(f, "in-line slice-header st_ref_pic_set parse failed: {e}")
158            }
159        }
160    }
161}
162
163impl std::error::Error for SliceError {}
164
165impl From<BitReaderError> for SliceError {
166    fn from(e: BitReaderError) -> Self {
167        match e {
168            BitReaderError::EndOfBuffer => Self::Truncated,
169            other => Self::Bitstream(other),
170        }
171    }
172}
173
174impl From<SpsError> for SliceError {
175    fn from(e: SpsError) -> Self {
176        match e {
177            SpsError::Truncated => Self::Truncated,
178            SpsError::Bitstream(b) => Self::Bitstream(b),
179            other => Self::InlineShortTermRpsParse(other),
180        }
181    }
182}
183
184/// Deblocking-filter override block carried in the slice header
185/// (§7.3.6.1, gated by `deblocking_filter_override_flag`).
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub struct SliceDeblocking {
188    /// `slice_deblocking_filter_disabled_flag`. Inferred to
189    /// `pps_deblocking_filter_disabled_flag` when the override block is
190    /// absent (§7.4.7.1).
191    pub disabled_flag: bool,
192    /// `slice_beta_offset_div2` (`se(v)`, range −6..=6). Inferred to
193    /// `pps_beta_offset_div2` when absent.
194    pub beta_offset_div2: i8,
195    /// `slice_tc_offset_div2` (`se(v)`, range −6..=6). Inferred to
196    /// `pps_tc_offset_div2` when absent.
197    pub tc_offset_div2: i8,
198}
199
200/// One long-term reference picture entry signalled in the slice
201/// header (§7.3.6.1). For the first `num_long_term_sps` entries the
202/// `lt_idx_sps` indexes the SPS's long-term-ref-pic table; for the
203/// remaining `num_long_term_pics` entries the slice header carries
204/// the POC LSB and `used_by_curr_pic_lt_flag` directly.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct SliceLongTermRefPic {
207    /// Source of the entry: SPS table or in-slice signalling.
208    pub source: SliceLongTermRefPicSource,
209    /// `delta_poc_msb_present_flag[i]`.
210    pub delta_poc_msb_present_flag: bool,
211    /// `delta_poc_msb_cycle_lt[i]`. Inferred to 0 when
212    /// [`Self::delta_poc_msb_present_flag`] is false (§7.4.7.1).
213    pub delta_poc_msb_cycle_lt: u32,
214}
215
216/// Source of one [`SliceLongTermRefPic`] entry.
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum SliceLongTermRefPicSource {
219    /// First `num_long_term_sps` entries: `lt_idx_sps[i]` indexes the
220    /// SPS's long-term-ref-pic-poc table. The `u(v)` width is
221    /// `Ceil(Log2(num_long_term_ref_pics_sps))` bits; the value 0 is
222    /// inferred when `num_long_term_ref_pics_sps == 1`.
223    Sps {
224        /// `lt_idx_sps[i]` value (or 0 when inferred).
225        lt_idx_sps: u32,
226    },
227    /// Remaining `num_long_term_pics` entries: the POC LSB and
228    /// `used_by_curr_pic_lt_flag` are signalled directly in the slice
229    /// header.
230    InSlice {
231        /// `poc_lsb_lt[i]` (`u(v)`, width
232        /// `log2_max_pic_order_cnt_lsb_minus4 + 4`).
233        poc_lsb_lt: u32,
234        /// `used_by_curr_pic_lt_flag[i]`.
235        used_by_curr_pic_lt_flag: bool,
236    },
237}
238
239impl SliceLongTermRefPic {
240    /// Resolve `UsedByCurrPicLt[i]` for this entry per §7.4.7.1:
241    ///
242    /// > `UsedByCurrPicLt[ i ]` is set equal to
243    /// > `used_by_curr_pic_lt_sps_flag[ lt_idx_sps[ i ] ]` when the
244    /// > entry's source is the SPS table, and to
245    /// > `used_by_curr_pic_lt_flag[ i ]` when the entry is signalled
246    /// > directly in the slice header.
247    ///
248    /// Returns `None` when [`SliceLongTermRefPicSource::Sps`] points at
249    /// an index that is out of range of `sps.long_term_ref_pics`
250    /// (a bitstream-conformance failure — the SPS-resident table must
251    /// cover every `lt_idx_sps[i]` value).
252    pub fn used_by_curr_pic_lt(&self, sps: &SeqParameterSet) -> Option<bool> {
253        match self.source {
254            SliceLongTermRefPicSource::Sps { lt_idx_sps } => sps
255                .long_term_ref_pics
256                .get(lt_idx_sps as usize)
257                .map(|entry| entry.used_by_curr_pic),
258            SliceLongTermRefPicSource::InSlice {
259                used_by_curr_pic_lt_flag,
260                ..
261            } => Some(used_by_curr_pic_lt_flag),
262        }
263    }
264}
265
266/// Entry-point-offset block (§7.3.6.1, gated by
267/// `tiles_enabled_flag || entropy_coding_sync_enabled_flag`).
268///
269/// `num_entry_point_offsets` is the number of subsets of
270/// `slice_segment_data()` minus one; each
271/// `entry_point_offset_minus1[i] + 1` is the byte length of subset
272/// `i` (§7.4.7.1). The trailing subset (`num_entry_point_offsets`)
273/// runs to the end of `slice_segment_data()` and is therefore not
274/// encoded.
275///
276/// Per §7.4.7.1, the on-wire `num_entry_point_offsets` is bounded by
277/// the active partitioning:
278///
279/// * `tiles_enabled_flag == 1` and `entropy_coding_sync_enabled_flag
280///   == 0` → `0..=(NumTileColumns * NumTileRows − 1)`.
281/// * `tiles_enabled_flag == 0` and `entropy_coding_sync_enabled_flag
282///   == 1` → `0..=(PicHeightInCtbsY − 1)`.
283/// * Both flags set (the "tiles + WPP" combination) is constrained by
284///   §7.4.3.3.1 to never appear in a conforming stream; this parser
285///   accepts the wider of the two bounds in that pathological case
286///   rather than gate the parse on a flag combination the PPS parser
287///   already rejects.
288///
289/// Each `entry_point_offset_minus1[i]` is `offset_len_minus1 + 1` bits
290/// wide. `offset_len_minus1` itself is bounded to `0..=31` by
291/// §7.4.7.1, so the per-entry width is in `1..=32`.
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct EntryPointOffsets {
294    /// `num_entry_point_offsets` (`ue(v)`). The number of subsets of
295    /// slice-segment data is this value plus one.
296    pub num_entry_point_offsets: u32,
297    /// `offset_len_minus1` (`ue(v)`, range 0..=31). Each
298    /// `entry_point_offset_minus1[i]` is `offset_len_minus1 + 1` bits.
299    /// Only meaningful when `num_entry_point_offsets > 0`; left at 0
300    /// when no offsets are signalled.
301    pub offset_len_minus1: u8,
302    /// `entry_point_offset_minus1[i]` (`u(offset_len_minus1 + 1)`) for
303    /// `i = 0 .. num_entry_point_offsets`. Empty when
304    /// `num_entry_point_offsets == 0`.
305    pub entry_point_offset_minus1: Vec<u32>,
306}
307
308impl EntryPointOffsets {
309    /// Byte length of subset `i` per §7.4.7.1, i.e.
310    /// `entry_point_offset_minus1[i] + 1`. Returns `None` when `i` is
311    /// out of range.
312    pub fn subset_length(&self, i: usize) -> Option<u64> {
313        self.entry_point_offset_minus1
314            .get(i)
315            .map(|v| u64::from(*v) + 1)
316    }
317}
318
319/// Parsed `ref_pic_lists_modification()` syntax structure
320/// (ITU-T Rec. H.265 §7.3.6.2 / §7.4.7.2).
321///
322/// The structure is signalled in the slice header when
323/// `lists_modification_present_flag == 1 && NumPicTotalCurr > 1`
324/// (§7.3.6.1 gate). It carries a per-list "explicit list" override of
325/// the implicit `RefPicList0` / `RefPicList1` derivation of §8.3.4: the
326/// `list_entry_lX[i]` value is the index of the reference picture in
327/// `RefPicListTempX` to place at position `i` of `RefPicListX`. The
328/// `RefPicListTempX` derivation itself is part of §8.3.4 (DPB-driven)
329/// and is **not** performed by this parser — this struct surfaces only
330/// the on-wire syntax elements and applies the §7.4.7.2 inference and
331/// range checks.
332///
333/// Width of each `list_entry_lX[i]` is `Ceil( Log2( NumPicTotalCurr ) )`
334/// bits and the value must be in `0 ..= NumPicTotalCurr - 1`
335/// (§7.4.7.2). When `ref_pic_list_modification_flag_lX == 0` the
336/// corresponding entry list is empty; the implicit derivation of
337/// §8.3.4 applies (each `list_entry_lX[i]` is inferred to 0 per the
338/// §7.4.7.2 paragraph "When the syntax element list_entry_lX[i] is
339/// not present in the slice header, it is inferred to be equal to 0",
340/// but the inference is exercised by §8.3.4, not surfaced here).
341///
342/// The list-1 fields are present only when `slice_type == B`
343/// (§7.3.6.2 syntax). For a P slice the `list_entry_l1` vector is
344/// always empty.
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct RefPicListsModification {
347    /// `ref_pic_list_modification_flag_l0` (`u(1)`).
348    pub ref_pic_list_modification_flag_l0: bool,
349    /// `list_entry_l0[i]` for `i = 0 ..= num_ref_idx_l0_active_minus1`.
350    /// Empty when `ref_pic_list_modification_flag_l0 == 0`.
351    pub list_entry_l0: Vec<u32>,
352    /// `ref_pic_list_modification_flag_l1` (`u(1)`). `None` when the
353    /// slice is not a B slice (the field is not signalled).
354    pub ref_pic_list_modification_flag_l1: Option<bool>,
355    /// `list_entry_l1[i]` for `i = 0 ..= num_ref_idx_l1_active_minus1`.
356    /// Empty for P slices and when `ref_pic_list_modification_flag_l1
357    /// == 0`.
358    pub list_entry_l1: Vec<u32>,
359}
360
361impl RefPicListsModification {
362    /// Parse `ref_pic_lists_modification()` (§7.3.6.2) from the current
363    /// bit position of `br`.
364    ///
365    /// * `slice_type` — the active slice type. Per §7.3.6.2 the L1
366    ///   block (`ref_pic_list_modification_flag_l1` /
367    ///   `list_entry_l1[]`) is only signalled for B slices. For an
368    ///   I slice the structure is never present at all (the §7.3.6.1
369    ///   gate `lists_modification_present_flag && NumPicTotalCurr > 1`
370    ///   sits inside the inter-slice `slice_type != I` branch), so
371    ///   the parser rejects `SliceType::I` up front.
372    /// * `num_ref_idx_l0_active_minus1` — the *active* value (after
373    ///   the `num_ref_idx_active_override_flag` override, falling back
374    ///   to `num_ref_idx_l0_default_active_minus1` from the PPS). Per
375    ///   §7.4.7.1 the value is in `0 ..= 14`.
376    /// * `num_ref_idx_l1_active_minus1` — same, for L1; ignored for P
377    ///   slices.
378    /// * `num_pic_total_curr` — `NumPicTotalCurr` (§7.4.7.2 /
379    ///   equation 7-57). The caller derives it from the active RPS;
380    ///   the §7.3.6.1 gate guarantees `num_pic_total_curr > 1` at the
381    ///   point of call. This parser rejects `num_pic_total_curr <= 1`
382    ///   (the §7.3.6.1 gate would have prevented the call); a call
383    ///   with `num_pic_total_curr == 0` would also imply a bitstream
384    ///   conformance failure per §7.4.7.1 ("when the current picture
385    ///   contains a P or B slice, the value of NumPicTotalCurr shall
386    ///   not be equal to 0"). Each `list_entry_lX[i]` is read as
387    ///   `u(v)` of width `Ceil( Log2( num_pic_total_curr ) )` bits
388    ///   and range-checked against `num_pic_total_curr - 1`.
389    pub fn parse(
390        br: &mut BitReader<'_>,
391        slice_type: SliceType,
392        num_ref_idx_l0_active_minus1: u8,
393        num_ref_idx_l1_active_minus1: u8,
394        num_pic_total_curr: u32,
395    ) -> Result<Self, SliceError> {
396        if slice_type == SliceType::I {
397            return Err(SliceError::ValueOutOfRange {
398                field: "ref_pic_lists_modification/slice_type",
399                got: 2,
400            });
401        }
402        if num_pic_total_curr <= 1 {
403            return Err(SliceError::ValueOutOfRange {
404                field: "ref_pic_lists_modification/NumPicTotalCurr",
405                got: num_pic_total_curr as i64,
406            });
407        }
408        // §7.4.7.1 ranges `num_ref_idx_lX_active_minus1` at 0..=14;
409        // defensively cap the per-list loop length so a corrupted call
410        // can't drive an unbounded allocation. (The cap matches the
411        // spec maximum; a value above 14 would be rejected by the
412        // §7.4.7.1 slice-header parse before reaching here.)
413        if num_ref_idx_l0_active_minus1 > 14 {
414            return Err(SliceError::ValueOutOfRange {
415                field: "num_ref_idx_l0_active_minus1",
416                got: num_ref_idx_l0_active_minus1 as i64,
417            });
418        }
419        if slice_type == SliceType::B && num_ref_idx_l1_active_minus1 > 14 {
420            return Err(SliceError::ValueOutOfRange {
421                field: "num_ref_idx_l1_active_minus1",
422                got: num_ref_idx_l1_active_minus1 as i64,
423            });
424        }
425
426        let entry_bits = ceil_log2(num_pic_total_curr);
427        let max_entry = num_pic_total_curr - 1;
428
429        let ref_pic_list_modification_flag_l0 = br.u1()? != 0;
430        let mut list_entry_l0: Vec<u32> = Vec::new();
431        if ref_pic_list_modification_flag_l0 {
432            let n = num_ref_idx_l0_active_minus1 as u32 + 1;
433            list_entry_l0.reserve(n as usize);
434            for _ in 0..n {
435                let v = br.u(entry_bits)?;
436                if v > max_entry {
437                    return Err(SliceError::ValueOutOfRange {
438                        field: "list_entry_l0",
439                        got: v as i64,
440                    });
441                }
442                list_entry_l0.push(v);
443            }
444        }
445
446        let (ref_pic_list_modification_flag_l1, list_entry_l1) = if slice_type == SliceType::B {
447            let flag = br.u1()? != 0;
448            let mut entries: Vec<u32> = Vec::new();
449            if flag {
450                let n = num_ref_idx_l1_active_minus1 as u32 + 1;
451                entries.reserve(n as usize);
452                for _ in 0..n {
453                    let v = br.u(entry_bits)?;
454                    if v > max_entry {
455                        return Err(SliceError::ValueOutOfRange {
456                            field: "list_entry_l1",
457                            got: v as i64,
458                        });
459                    }
460                    entries.push(v);
461                }
462            }
463            (Some(flag), entries)
464        } else {
465            (None, Vec::new())
466        };
467
468        Ok(Self {
469            ref_pic_list_modification_flag_l0,
470            list_entry_l0,
471            ref_pic_list_modification_flag_l1,
472            list_entry_l1,
473        })
474    }
475}
476
477/// Inputs to the §7.4.7.2 `NumPicTotalCurr` derivation (equation 7-57).
478///
479/// `NumPicTotalCurr` counts the reference pictures in the current
480/// slice's RPS state that are flagged as *used by the current
481/// picture* — i.e. eligible for entry into `RefPicListTemp0` /
482/// `RefPicListTemp1`. The §7.3.6.1 gate
483/// `lists_modification_present_flag && NumPicTotalCurr > 1` consumes
484/// the derivation to decide whether the inter-slice header carries a
485/// `ref_pic_lists_modification()` block, and the per-entry width of
486/// that block's `list_entry_lX[i]` (`Ceil( Log2( NumPicTotalCurr ) )`,
487/// §7.4.7.2) consumes the value directly.
488///
489/// The four `UsedByCurrPic*` slices supplied by the caller are the
490/// resolved per-position state of the active RPS:
491///
492/// * `used_by_curr_pic_s0` — `UsedByCurrPicS0[ CurrRpsIdx ][ i ]` for
493///   `i = 0 .. NumNegativePics[ CurrRpsIdx ]`.
494/// * `used_by_curr_pic_s1` — `UsedByCurrPicS1[ CurrRpsIdx ][ i ]` for
495///   `i = 0 .. NumPositivePics[ CurrRpsIdx ]`.
496/// * `used_by_curr_pic_lt` — `UsedByCurrPicLt[ i ]` for
497///   `i = 0 .. num_long_term_sps + num_long_term_pics`. The §7.4.7.1
498///   selector ("SPS-resident → `used_by_curr_pic_lt_sps_flag[
499///   lt_idx_sps[ i ] ]`; in-slice → `used_by_curr_pic_lt_flag[ i ]`")
500///   is applied by the caller; [`SliceLongTermRefPic::used_by_curr_pic_lt`]
501///   does the per-entry resolution against the active SPS.
502///
503/// For the explicit (non-inter-RPS-predicted) short-term RPS form, the
504/// `S0` / `S1` slices come directly from
505/// [`ShortTermRefPicSet::used_by_curr_pic_s0_flag`] /
506/// [`ShortTermRefPicSet::used_by_curr_pic_s1_flag`] and the
507/// [`Self::from_explicit_short_term_rps`] builder is provided. For the
508/// inter-RPS-prediction form the §7.4.8 derivation (equations
509/// 7-58..7-66) must be run first; the result of that derivation is
510/// then handed to [`Self::from_used_flags`].
511///
512/// The remaining inputs:
513///
514/// * `pps_curr_pic_ref_enabled_flag` — §7.4.7.2 closing-clause flag,
515///   from the SCC extension of the active PPS. Inferred to `false`
516///   when the SCC PPS is not signalled (§7.4.3.3.1.4).
517/// * `nal_unit_type` — used only by the F.7.4.7.2 multilayer-extension
518///   variant of equation 7-57 (`F-56`): when the multilayer extension
519///   applies and the current picture is IDR (`IDR_W_RADL` /
520///   `IDR_N_LP`), the short-term and long-term loops are skipped
521///   entirely. For base §7.4.7.2 the value is unused because every
522///   IDR slice already has zero short-term and long-term entries.
523/// * `num_active_ref_layer_pics` — F.7.4.7.2 `NumActiveRefLayerPics`
524///   (the count of active inter-layer reference pictures for the
525///   current slice, §F.7.4.7.1). Set to `0` for base §7.4.7.2.
526///
527/// The `nal_unit_type` IDR gate and `num_active_ref_layer_pics`
528/// contribution are only applied when [`Self::multilayer_extension`]
529/// is `true` (forward-compat for the multilayer scaffold; left
530/// `false` by every base-profile call site).
531#[derive(Debug, Clone, PartialEq, Eq)]
532pub struct NumPicTotalCurrInputs<'a> {
533    /// Per-position `UsedByCurrPicS0[ CurrRpsIdx ][ i ]` flags.
534    pub used_by_curr_pic_s0: &'a [bool],
535    /// Per-position `UsedByCurrPicS1[ CurrRpsIdx ][ i ]` flags.
536    pub used_by_curr_pic_s1: &'a [bool],
537    /// Per-position `UsedByCurrPicLt[ i ]` flags, length
538    /// `num_long_term_sps + num_long_term_pics`.
539    pub used_by_curr_pic_lt: &'a [bool],
540    /// `pps_curr_pic_ref_enabled_flag` (§7.4.3.3.1.4 SCC PPS). Inferred
541    /// to `false` when not signalled.
542    pub pps_curr_pic_ref_enabled_flag: bool,
543    /// `nal_unit_type` of the slice's NAL unit (Table 7-1). Consumed
544    /// only when [`Self::multilayer_extension`] is `true`.
545    pub nal_unit_type: u8,
546    /// F.7.4.7.2 `NumActiveRefLayerPics` (the §F.7.4.7.1 inter-layer
547    /// active count). Consumed only when [`Self::multilayer_extension`]
548    /// is `true`.
549    pub num_active_ref_layer_pics: u32,
550    /// Forward-compat toggle: when `true`, equation `F-56` of
551    /// F.7.4.7.2 is applied instead of equation 7-57 of §7.4.7.2
552    /// (the short-term / long-term loops are skipped for IDR
553    /// `nal_unit_type`, and `NumActiveRefLayerPics` is added at the
554    /// end). Every base-profile call site leaves this `false`.
555    pub multilayer_extension: bool,
556}
557
558impl<'a> NumPicTotalCurrInputs<'a> {
559    /// Build the inputs from already-resolved per-position
560    /// `UsedByCurrPic*` slices. The caller is responsible for having
561    /// run the §7.4.8 inter-RPS-prediction derivation if the active
562    /// short-term RPS uses the predicted form.
563    pub fn from_used_flags(
564        used_by_curr_pic_s0: &'a [bool],
565        used_by_curr_pic_s1: &'a [bool],
566        used_by_curr_pic_lt: &'a [bool],
567    ) -> Self {
568        Self {
569            used_by_curr_pic_s0,
570            used_by_curr_pic_s1,
571            used_by_curr_pic_lt,
572            pps_curr_pic_ref_enabled_flag: false,
573            nal_unit_type: 0,
574            num_active_ref_layer_pics: 0,
575            multilayer_extension: false,
576        }
577    }
578
579    /// Build the inputs from an *explicit-form* short-term RPS, where
580    /// the `UsedByCurrPicS0` / `UsedByCurrPicS1` arrays are the
581    /// SPS-signalled `used_by_curr_pic_sX_flag` arrays themselves
582    /// (§7.4.8 equations 7-65 / 7-66). Returns `None` when the RPS
583    /// uses inter-prediction (`inter_ref_pic_set_prediction_flag ==
584    /// 1`) — the §7.4.8 derivation must be run first and the result
585    /// passed to [`Self::from_used_flags`].
586    pub fn from_explicit_short_term_rps(
587        curr_rps: &'a ShortTermRefPicSet,
588        used_by_curr_pic_lt: &'a [bool],
589    ) -> Option<Self> {
590        if curr_rps.inter_ref_pic_set_prediction_flag {
591            return None;
592        }
593        Some(Self::from_used_flags(
594            &curr_rps.used_by_curr_pic_s0_flag,
595            &curr_rps.used_by_curr_pic_s1_flag,
596            used_by_curr_pic_lt,
597        ))
598    }
599
600    /// Set [`Self::pps_curr_pic_ref_enabled_flag`] (builder).
601    pub fn with_pps_curr_pic_ref_enabled(mut self, flag: bool) -> Self {
602        self.pps_curr_pic_ref_enabled_flag = flag;
603        self
604    }
605
606    /// Set the multilayer-extension trio (builder).
607    pub fn with_multilayer_extension(
608        mut self,
609        nal_unit_type: u8,
610        num_active_ref_layer_pics: u32,
611    ) -> Self {
612        self.multilayer_extension = true;
613        self.nal_unit_type = nal_unit_type;
614        self.num_active_ref_layer_pics = num_active_ref_layer_pics;
615        self
616    }
617
618    /// Compute `NumPicTotalCurr` per equation 7-57 (base §7.4.7.2) or
619    /// equation `F-56` (F.7.4.7.2 multilayer extension), depending on
620    /// [`Self::multilayer_extension`].
621    ///
622    /// The base equation 7-57:
623    ///
624    /// ```text
625    /// NumPicTotalCurr = 0
626    /// for i in 0..NumNegativePics[CurrRpsIdx]:
627    ///     if UsedByCurrPicS0[CurrRpsIdx][i]:                       NumPicTotalCurr++
628    /// for i in 0..NumPositivePics[CurrRpsIdx]:
629    ///     if UsedByCurrPicS1[CurrRpsIdx][i]:                       NumPicTotalCurr++
630    /// for i in 0..(num_long_term_sps + num_long_term_pics):
631    ///     if UsedByCurrPicLt[i]:                                   NumPicTotalCurr++
632    /// if pps_curr_pic_ref_enabled_flag:                            NumPicTotalCurr++
633    /// ```
634    ///
635    /// The multilayer variant `F-56` skips the short-term and
636    /// long-term loops entirely for IDR `nal_unit_type` values
637    /// (`IDR_W_RADL` / `IDR_N_LP`), then adds `NumActiveRefLayerPics`
638    /// after the `pps_curr_pic_ref_enabled_flag` step.
639    pub fn compute(&self) -> u32 {
640        let is_idr = self.nal_unit_type == IDR_W_RADL || self.nal_unit_type == IDR_N_LP;
641        let skip_temporal_loops = self.multilayer_extension && is_idr;
642
643        let mut n: u32 = 0;
644        if !skip_temporal_loops {
645            n += self.used_by_curr_pic_s0.iter().filter(|&&v| v).count() as u32;
646            n += self.used_by_curr_pic_s1.iter().filter(|&&v| v).count() as u32;
647            n += self.used_by_curr_pic_lt.iter().filter(|&&v| v).count() as u32;
648        }
649        if self.pps_curr_pic_ref_enabled_flag {
650            n += 1;
651        }
652        if self.multilayer_extension {
653            n += self.num_active_ref_layer_pics;
654        }
655        n
656    }
657}
658
659/// Per-list weighted-prediction entry for one reference picture in
660/// [`PredWeightTable`] (one entry per `i = 0 ..= num_ref_idx_lX_active_minus1`).
661///
662/// Fields are the raw §7.3.6.3 syntax elements, kept in unresolved form
663/// so the caller can both audit the on-wire bits and compute the
664/// §7.4.7.3 derived variables `LumaWeightLX[i]`,
665/// `ChromaWeightLX[i][j]`, `ChromaOffsetLX[i][j]` through the
666/// helper methods on [`PredWeightTable`] (which also apply the
667/// §7.4.7.3 inference rules for the absent fields).
668#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
669pub struct PredWeightEntry {
670    /// `luma_weight_lX_flag[i]` (`u(1)`). Inferred to `false` when the
671    /// §7.3.6.3 outer gate (`pic_layer_id != nuh_layer_id ||
672    /// PicOrderCnt(RefPicListX[i]) != PicOrderCnt(CurrPic)`) is `false`
673    /// for this `i` — for a base-profile single-layer slice the gate is
674    /// always `true`, so this flag is always signalled.
675    pub luma_weight_flag: bool,
676    /// `chroma_weight_lX_flag[i]` (`u(1)`). Absent (inferred `false`)
677    /// when `ChromaArrayType == 0` or when the outer gate is `false`
678    /// for this `i`.
679    pub chroma_weight_flag: bool,
680    /// `delta_luma_weight_lX[i]` (`se(v)`, range −128..=127). Inferred
681    /// to `0` when [`Self::luma_weight_flag`] is `false`.
682    pub delta_luma_weight: i32,
683    /// `luma_offset_lX[i]` (`se(v)`, range
684    /// `−WpOffsetHalfRangeY ..= WpOffsetHalfRangeY − 1`). Inferred to
685    /// `0` when [`Self::luma_weight_flag`] is `false`.
686    pub luma_offset: i32,
687    /// `delta_chroma_weight_lX[i][j]` (`se(v)`, range −128..=127) for
688    /// `j = 0 (Cb), 1 (Cr)`. Both inferred to `0` when
689    /// [`Self::chroma_weight_flag`] is `false`.
690    pub delta_chroma_weight: [i32; 2],
691    /// `delta_chroma_offset_lX[i][j]` (`se(v)`, range
692    /// `−4 * WpOffsetHalfRangeC ..= 4 * WpOffsetHalfRangeC − 1`) for
693    /// `j = 0 (Cb), 1 (Cr)`. Both inferred to `0` when
694    /// [`Self::chroma_weight_flag`] is `false`.
695    pub delta_chroma_offset: [i32; 2],
696}
697
698/// Parsed `pred_weight_table()` syntax structure (ITU-T Rec. H.265
699/// §7.3.6.3 / §7.4.7.3).
700///
701/// The structure is signalled in the slice header when
702/// `(weighted_pred_flag && slice_type == P) ||
703/// (weighted_bipred_flag && slice_type == B)` (§7.3.6.1 gate). It
704/// carries per-reference weighting factors and additive offsets that
705/// §8.5.3.3.4.3 applies to the inter-prediction samples produced from
706/// each `RefPicListX[i]`.
707///
708/// ### Outer §7.3.6.3 gate
709///
710/// Each `luma_weight_lX_flag[i]` and `chroma_weight_lX_flag[i]` syntax
711/// element is wrapped in a conditional:
712///
713/// ```text
714///   if( pic_layer_id( RefPicListX[ i ] ) != nuh_layer_id ||
715///       PicOrderCnt( RefPicListX[ i ] ) != PicOrderCnt( CurrPic ) )
716///       luma_weight_lX_flag[ i ]   u(1)
717/// ```
718///
719/// — the flag is only signalled when the reference is a *different
720/// picture* (i.e. either an inter-layer reference or a temporal
721/// reference). For a base-profile single-layer slice every active
722/// reference is temporal, so the gate is universally `true` and every
723/// flag is signalled. For inter-layer / SCC self-reference cases the
724/// gate is `false` for some `i`, and the parser must skip the
725/// corresponding flag bit and infer it to `0` (§7.4.7.3 "When
726/// luma_weight_lX_flag[ i ] is not present, it is inferred to be equal
727/// to 0").
728///
729/// The caller resolves the DPB-driven gate and passes the per-i
730/// boolean decisions through [`PredWeightTableInputs::signal_luma_l0`]
731/// / [`PredWeightTableInputs::signal_chroma_l0`] /
732/// [`PredWeightTableInputs::signal_luma_l1`] /
733/// [`PredWeightTableInputs::signal_chroma_l1`]; the default
734/// [`PredWeightTableInputs::base_profile`] constructor leaves them all
735/// `true` (the base-profile case).
736///
737/// ### Derived variables
738///
739/// Per §7.4.7.3 the on-wire deltas combine with the per-list
740/// `..._log2_weight_denom` to produce the actual weighting factors and
741/// offsets the §8.5.3.3.4.3 inter-prediction process consumes:
742///
743/// * `ChromaLog2WeightDenom = luma_log2_weight_denom +
744///   delta_chroma_log2_weight_denom` (range 0..=7).
745/// * `LumaWeightLX[i] = (1 << luma_log2_weight_denom) +
746///   delta_luma_weight_lX[i]` when the luma flag is set, else inferred
747///   to `1 << luma_log2_weight_denom`.
748/// * `ChromaWeightLX[i][j] = (1 << ChromaLog2WeightDenom) +
749///   delta_chroma_weight_lX[i][j]` when the chroma flag is set, else
750///   inferred to `1 << ChromaLog2WeightDenom`.
751/// * `ChromaOffsetLX[i][j]` per equation 7-58 (a clipped expression
752///   parameterised by `WpOffsetHalfRangeC` and `ChromaLog2WeightDenom`).
753///
754/// The accessor methods on this struct apply those derivations.
755///
756/// ### Conformance check
757///
758/// §7.4.7.3 closes with the `sumWeightLXFlags` cap: for a P slice
759/// `sumWeightL0Flags ≤ 24`; for a B slice
760/// `sumWeightL0Flags + sumWeightL1Flags ≤ 24` where each
761/// `sumWeightLXFlags = Σ ( luma_weight_lX_flag[i] +
762/// 2 * chroma_weight_lX_flag[i] )`. The parser computes this sum and
763/// enforces the cap.
764///
765/// ### Range checks
766///
767/// * `luma_log2_weight_denom` ∈ 0..=7.
768/// * `luma_log2_weight_denom + delta_chroma_log2_weight_denom` ∈ 0..=7
769///   (the variable `ChromaLog2WeightDenom`).
770/// * `delta_luma_weight_lX[i]` ∈ −128..=127 when the luma flag is set.
771/// * `luma_offset_lX[i]` ∈ `−WpOffsetHalfRangeY ..= WpOffsetHalfRangeY − 1`
772///   when the luma flag is set.
773/// * `delta_chroma_weight_lX[i][j]` ∈ −128..=127 when the chroma flag
774///   is set.
775/// * `delta_chroma_offset_lX[i][j]` ∈
776///   `−4 * WpOffsetHalfRangeC ..= 4 * WpOffsetHalfRangeC − 1` when the
777///   chroma flag is set.
778#[derive(Debug, Clone, PartialEq, Eq)]
779pub struct PredWeightTable {
780    /// `luma_log2_weight_denom` (`ue(v)`, range 0..=7).
781    pub luma_log2_weight_denom: u8,
782    /// `delta_chroma_log2_weight_denom` (`se(v)`). Absent (and
783    /// inferred to 0 per §7.4.7.3) when `ChromaArrayType == 0`.
784    pub delta_chroma_log2_weight_denom: i32,
785    /// L0 per-reference entries, length
786    /// `num_ref_idx_l0_active_minus1 + 1`.
787    pub entries_l0: Vec<PredWeightEntry>,
788    /// L1 per-reference entries, length
789    /// `num_ref_idx_l1_active_minus1 + 1` for B slices. Empty for P
790    /// slices (the §7.3.6.3 `if( slice_type == B )` gate suppresses
791    /// every L1 syntax element).
792    pub entries_l1: Vec<PredWeightEntry>,
793}
794
795/// Inputs to [`PredWeightTable::parse`].
796///
797/// Carries every value the parser needs to derive field widths,
798/// presence gates, range bounds and the per-i §7.3.6.3 outer-gate
799/// decisions. The [`Self::base_profile`] constructor covers the common
800/// case (single-layer base profile, `high_precision_offsets_enabled_flag
801/// == 0`, every per-i gate `true`); the other setters carry the
802/// extension-specific knobs.
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub struct PredWeightTableInputs<'a> {
805    /// Active `slice_type` (the L1 syntax block is suppressed for P).
806    pub slice_type: SliceType,
807    /// Active `num_ref_idx_l0_active_minus1` after the
808    /// `num_ref_idx_active_override_flag` override (range 0..=14 per
809    /// §7.4.7.1).
810    pub num_ref_idx_l0_active_minus1: u8,
811    /// Active `num_ref_idx_l1_active_minus1`. Ignored for P slices.
812    pub num_ref_idx_l1_active_minus1: u8,
813    /// `ChromaArrayType` per §7.4.2.2. When `0` (monochrome or
814    /// separate-colour-plane) the entire chroma sub-block is absent.
815    pub chroma_array_type: u8,
816    /// `high_precision_offsets_enabled_flag` from the SPS range
817    /// extension (§7.4.3.2.2 / equations 7-33 / 7-34). Inferred to
818    /// `false` when the SPS range extension is not signalled.
819    pub high_precision_offsets_enabled_flag: bool,
820    /// `BitDepthY` from the SPS (§7.4.3.2.1), used by
821    /// `WpOffsetHalfRangeY` when [`Self::high_precision_offsets_enabled_flag`]
822    /// is `true`. Ignored otherwise (`WpOffsetHalfRangeY = 128`).
823    pub bit_depth_y: u8,
824    /// `BitDepthC` from the SPS (§7.4.3.2.1), used by
825    /// `WpOffsetHalfRangeC` when [`Self::high_precision_offsets_enabled_flag`]
826    /// is `true`. Ignored otherwise (`WpOffsetHalfRangeC = 128`).
827    pub bit_depth_c: u8,
828    /// Per-i outer-gate decision for `luma_weight_l0_flag[i]`. When
829    /// `None`, every position is treated as gated `true`
830    /// (base-profile case). When `Some(slice)`, length must equal
831    /// `num_ref_idx_l0_active_minus1 + 1` and `slice[i] == false`
832    /// suppresses the corresponding flag bit (inferred to `0`).
833    pub signal_luma_l0: Option<&'a [bool]>,
834    /// Same as [`Self::signal_luma_l0`] for `chroma_weight_l0_flag[i]`.
835    pub signal_chroma_l0: Option<&'a [bool]>,
836    /// Same as [`Self::signal_luma_l0`] for `luma_weight_l1_flag[i]`
837    /// (B slices only).
838    pub signal_luma_l1: Option<&'a [bool]>,
839    /// Same as [`Self::signal_luma_l0`] for `chroma_weight_l1_flag[i]`
840    /// (B slices only).
841    pub signal_chroma_l1: Option<&'a [bool]>,
842}
843
844impl<'a> PredWeightTableInputs<'a> {
845    /// Base-profile single-layer constructor: every per-i §7.3.6.3
846    /// outer-gate decision is `true`, `high_precision_offsets_enabled_flag
847    /// == false`. The caller supplies only the slice-type, the active
848    /// ref-list cardinalities, and the activated SPS's
849    /// `ChromaArrayType` + bit depths.
850    pub fn base_profile(
851        slice_type: SliceType,
852        num_ref_idx_l0_active_minus1: u8,
853        num_ref_idx_l1_active_minus1: u8,
854        chroma_array_type: u8,
855        bit_depth_y: u8,
856        bit_depth_c: u8,
857    ) -> Self {
858        Self {
859            slice_type,
860            num_ref_idx_l0_active_minus1,
861            num_ref_idx_l1_active_minus1,
862            chroma_array_type,
863            high_precision_offsets_enabled_flag: false,
864            bit_depth_y,
865            bit_depth_c,
866            signal_luma_l0: None,
867            signal_chroma_l0: None,
868            signal_luma_l1: None,
869            signal_chroma_l1: None,
870        }
871    }
872
873    /// `WpOffsetHalfRangeY` per equation 7-33.
874    fn wp_offset_half_range_y(&self) -> i32 {
875        let shift = if self.high_precision_offsets_enabled_flag {
876            (self.bit_depth_y as i32) - 1
877        } else {
878            7
879        };
880        1i32 << shift
881    }
882
883    /// `WpOffsetHalfRangeC` per equation 7-34.
884    fn wp_offset_half_range_c(&self) -> i32 {
885        let shift = if self.high_precision_offsets_enabled_flag {
886            (self.bit_depth_c as i32) - 1
887        } else {
888            7
889        };
890        1i32 << shift
891    }
892}
893
894impl PredWeightTable {
895    /// Parse `pred_weight_table()` (§7.3.6.3) from the current bit
896    /// position of `br`. See [`PredWeightTableInputs`] for the per-call
897    /// inputs and the base-profile constructor.
898    ///
899    /// The parser:
900    ///
901    /// 1. Reads `luma_log2_weight_denom` (`ue(v)`, range 0..=7).
902    /// 2. When `chroma_array_type != 0`, reads
903    ///    `delta_chroma_log2_weight_denom` (`se(v)`) and validates the
904    ///    derived `ChromaLog2WeightDenom` ∈ 0..=7.
905    /// 3. Reads the L0 luma-flag pass, applying the per-i outer-gate
906    ///    decision from [`PredWeightTableInputs::signal_luma_l0`].
907    /// 4. When `chroma_array_type != 0`, reads the L0 chroma-flag
908    ///    pass with the matching gate slice.
909    /// 5. Reads the L0 per-reference delta block: for each `i` where
910    ///    the flag is set, reads `delta_luma_weight_l0[i]` +
911    ///    `luma_offset_l0[i]`; when the chroma flag is set, reads
912    ///    `delta_chroma_weight_l0[i][j]` + `delta_chroma_offset_l0[i][j]`
913    ///    for `j ∈ {0, 1}`.
914    /// 6. For B slices, mirrors steps 3–5 for L1.
915    /// 7. Validates the §7.4.7.3 `sumWeightLXFlags ≤ 24` cap.
916    ///
917    /// Each delta is range-checked per §7.4.7.3; range failures
918    /// surface as [`SliceError::ValueOutOfRange`].
919    pub fn parse(
920        br: &mut BitReader<'_>,
921        inputs: &PredWeightTableInputs<'_>,
922    ) -> Result<Self, SliceError> {
923        if inputs.slice_type == SliceType::I {
924            return Err(SliceError::ValueOutOfRange {
925                field: "pred_weight_table/slice_type",
926                got: 2,
927            });
928        }
929        if inputs.num_ref_idx_l0_active_minus1 > 14 {
930            return Err(SliceError::ValueOutOfRange {
931                field: "num_ref_idx_l0_active_minus1",
932                got: inputs.num_ref_idx_l0_active_minus1 as i64,
933            });
934        }
935        if inputs.slice_type == SliceType::B && inputs.num_ref_idx_l1_active_minus1 > 14 {
936            return Err(SliceError::ValueOutOfRange {
937                field: "num_ref_idx_l1_active_minus1",
938                got: inputs.num_ref_idx_l1_active_minus1 as i64,
939            });
940        }
941
942        let n_l0 = inputs.num_ref_idx_l0_active_minus1 as usize + 1;
943        let n_l1 = if inputs.slice_type == SliceType::B {
944            inputs.num_ref_idx_l1_active_minus1 as usize + 1
945        } else {
946            0
947        };
948        validate_signal_slice("signal_luma_l0", inputs.signal_luma_l0, n_l0)?;
949        validate_signal_slice("signal_chroma_l0", inputs.signal_chroma_l0, n_l0)?;
950        validate_signal_slice("signal_luma_l1", inputs.signal_luma_l1, n_l1)?;
951        validate_signal_slice("signal_chroma_l1", inputs.signal_chroma_l1, n_l1)?;
952
953        let chroma_present = inputs.chroma_array_type != 0;
954
955        let luma_log2_weight_denom_u = br.ue()?;
956        if luma_log2_weight_denom_u > 7 {
957            return Err(SliceError::ValueOutOfRange {
958                field: "luma_log2_weight_denom",
959                got: luma_log2_weight_denom_u as i64,
960            });
961        }
962        let luma_log2_weight_denom = luma_log2_weight_denom_u as u8;
963
964        let delta_chroma_log2_weight_denom: i32 = if chroma_present {
965            let v = br.se()?;
966            let chroma_denom = luma_log2_weight_denom as i32 + v;
967            if !(0..=7).contains(&chroma_denom) {
968                return Err(SliceError::ValueOutOfRange {
969                    field: "ChromaLog2WeightDenom",
970                    got: chroma_denom as i64,
971                });
972            }
973            v
974        } else {
975            0
976        };
977
978        // L0: parse the two flag passes (luma, then chroma when
979        // chroma is present), then the per-reference delta block.
980        let entries_l0 = parse_pred_weight_list(
981            br,
982            n_l0,
983            chroma_present,
984            inputs.signal_luma_l0,
985            inputs.signal_chroma_l0,
986            inputs.wp_offset_half_range_y(),
987            inputs.wp_offset_half_range_c(),
988            "l0",
989        )?;
990
991        // L1: B slices only.
992        let entries_l1 = if inputs.slice_type == SliceType::B {
993            parse_pred_weight_list(
994                br,
995                n_l1,
996                chroma_present,
997                inputs.signal_luma_l1,
998                inputs.signal_chroma_l1,
999                inputs.wp_offset_half_range_y(),
1000                inputs.wp_offset_half_range_c(),
1001                "l1",
1002            )?
1003        } else {
1004            Vec::new()
1005        };
1006
1007        // §7.4.7.3 sumWeightLXFlags cap: ≤ 24 per list contribution.
1008        let sum_l0 = sum_weight_flags(&entries_l0);
1009        if inputs.slice_type == SliceType::P && sum_l0 > 24 {
1010            return Err(SliceError::ValueOutOfRange {
1011                field: "sumWeightL0Flags",
1012                got: sum_l0 as i64,
1013            });
1014        }
1015        if inputs.slice_type == SliceType::B {
1016            let sum_l1 = sum_weight_flags(&entries_l1);
1017            if sum_l0 + sum_l1 > 24 {
1018                return Err(SliceError::ValueOutOfRange {
1019                    field: "sumWeightL0Flags+sumWeightL1Flags",
1020                    got: (sum_l0 + sum_l1) as i64,
1021                });
1022            }
1023        }
1024
1025        Ok(Self {
1026            luma_log2_weight_denom,
1027            delta_chroma_log2_weight_denom,
1028            entries_l0,
1029            entries_l1,
1030        })
1031    }
1032
1033    /// `ChromaLog2WeightDenom = luma_log2_weight_denom +
1034    /// delta_chroma_log2_weight_denom` per §7.4.7.3. Returns `0` when
1035    /// the chroma sub-block was absent (`ChromaArrayType == 0`); the
1036    /// derivation is moot in that case.
1037    pub fn chroma_log2_weight_denom(&self) -> u8 {
1038        // The parser's range check on `ChromaLog2WeightDenom ∈ 0..=7`
1039        // guarantees the sum fits in a `u8`.
1040        (self.luma_log2_weight_denom as i32 + self.delta_chroma_log2_weight_denom) as u8
1041    }
1042
1043    /// `LumaWeightL0[i]` per §7.4.7.3: `(1 << luma_log2_weight_denom)
1044    /// + delta_luma_weight_l0[i]` when the flag is set, else
1045    /// inferred to `1 << luma_log2_weight_denom`.
1046    pub fn luma_weight_l0(&self, i: usize) -> Option<i32> {
1047        self.entries_l0
1048            .get(i)
1049            .map(|e| self.luma_weight_value(e.luma_weight_flag, e.delta_luma_weight))
1050    }
1051
1052    /// `LumaWeightL1[i]` per §7.4.7.3.
1053    pub fn luma_weight_l1(&self, i: usize) -> Option<i32> {
1054        self.entries_l1
1055            .get(i)
1056            .map(|e| self.luma_weight_value(e.luma_weight_flag, e.delta_luma_weight))
1057    }
1058
1059    /// `ChromaWeightL0[i][j]` per §7.4.7.3.
1060    pub fn chroma_weight_l0(&self, i: usize, j: usize) -> Option<i32> {
1061        let e = self.entries_l0.get(i)?;
1062        let v = *e.delta_chroma_weight.get(j)?;
1063        Some(self.chroma_weight_value(e.chroma_weight_flag, v))
1064    }
1065
1066    /// `ChromaWeightL1[i][j]` per §7.4.7.3.
1067    pub fn chroma_weight_l1(&self, i: usize, j: usize) -> Option<i32> {
1068        let e = self.entries_l1.get(i)?;
1069        let v = *e.delta_chroma_weight.get(j)?;
1070        Some(self.chroma_weight_value(e.chroma_weight_flag, v))
1071    }
1072
1073    /// `ChromaOffsetL0[i][j]` per §7.4.7.3 equation 7-58.
1074    pub fn chroma_offset_l0(&self, i: usize, j: usize, wp_offset_half_range_c: i32) -> Option<i32> {
1075        let e = self.entries_l0.get(i)?;
1076        if !e.chroma_weight_flag {
1077            return Some(0);
1078        }
1079        let delta_off = *e.delta_chroma_offset.get(j)?;
1080        let chroma_w = self.chroma_weight_value(true, *e.delta_chroma_weight.get(j)?);
1081        Some(chroma_offset_eq_7_58(
1082            wp_offset_half_range_c,
1083            delta_off,
1084            chroma_w,
1085            self.chroma_log2_weight_denom(),
1086        ))
1087    }
1088
1089    /// `ChromaOffsetL1[i][j]` per §7.4.7.3 equation 7-58.
1090    pub fn chroma_offset_l1(&self, i: usize, j: usize, wp_offset_half_range_c: i32) -> Option<i32> {
1091        let e = self.entries_l1.get(i)?;
1092        if !e.chroma_weight_flag {
1093            return Some(0);
1094        }
1095        let delta_off = *e.delta_chroma_offset.get(j)?;
1096        let chroma_w = self.chroma_weight_value(true, *e.delta_chroma_weight.get(j)?);
1097        Some(chroma_offset_eq_7_58(
1098            wp_offset_half_range_c,
1099            delta_off,
1100            chroma_w,
1101            self.chroma_log2_weight_denom(),
1102        ))
1103    }
1104
1105    fn luma_weight_value(&self, flag: bool, delta: i32) -> i32 {
1106        let base = 1i32 << self.luma_log2_weight_denom;
1107        if flag {
1108            base + delta
1109        } else {
1110            base
1111        }
1112    }
1113
1114    fn chroma_weight_value(&self, flag: bool, delta: i32) -> i32 {
1115        let base = 1i32 << self.chroma_log2_weight_denom();
1116        if flag {
1117            base + delta
1118        } else {
1119            base
1120        }
1121    }
1122}
1123
1124/// Verify the caller-supplied per-i gate slice has the expected length.
1125fn validate_signal_slice(
1126    field: &'static str,
1127    slice: Option<&[bool]>,
1128    expected: usize,
1129) -> Result<(), SliceError> {
1130    match slice {
1131        None => Ok(()),
1132        Some(s) if s.len() == expected => Ok(()),
1133        Some(s) => Err(SliceError::ValueOutOfRange {
1134            field,
1135            got: s.len() as i64,
1136        }),
1137    }
1138}
1139
1140/// Parse one per-list (L0 or L1) sub-block of §7.3.6.3.
1141#[allow(clippy::too_many_arguments)]
1142fn parse_pred_weight_list(
1143    br: &mut BitReader<'_>,
1144    n: usize,
1145    chroma_present: bool,
1146    signal_luma: Option<&[bool]>,
1147    signal_chroma: Option<&[bool]>,
1148    wp_off_half_y: i32,
1149    wp_off_half_c: i32,
1150    list_tag: &'static str,
1151) -> Result<Vec<PredWeightEntry>, SliceError> {
1152    let mut entries: Vec<PredWeightEntry> = (0..n).map(|_| PredWeightEntry::default()).collect();
1153
1154    // Luma flag pass.
1155    for (i, e) in entries.iter_mut().enumerate() {
1156        let signalled = signal_luma.map(|s| s[i]).unwrap_or(true);
1157        e.luma_weight_flag = if signalled { br.u1()? != 0 } else { false };
1158    }
1159
1160    // Chroma flag pass — present only when ChromaArrayType != 0.
1161    if chroma_present {
1162        for (i, e) in entries.iter_mut().enumerate() {
1163            let signalled = signal_chroma.map(|s| s[i]).unwrap_or(true);
1164            e.chroma_weight_flag = if signalled { br.u1()? != 0 } else { false };
1165        }
1166    }
1167
1168    // Per-reference delta block.
1169    for (i, e) in entries.iter_mut().enumerate() {
1170        if e.luma_weight_flag {
1171            let d = br.se()?;
1172            if !(-128..=127).contains(&d) {
1173                return Err(SliceError::ValueOutOfRange {
1174                    field: if list_tag == "l0" {
1175                        "delta_luma_weight_l0"
1176                    } else {
1177                        "delta_luma_weight_l1"
1178                    },
1179                    got: d as i64,
1180                });
1181            }
1182            e.delta_luma_weight = d;
1183
1184            let off = br.se()?;
1185            if off < -wp_off_half_y || off > wp_off_half_y - 1 {
1186                return Err(SliceError::ValueOutOfRange {
1187                    field: if list_tag == "l0" {
1188                        "luma_offset_l0"
1189                    } else {
1190                        "luma_offset_l1"
1191                    },
1192                    got: off as i64,
1193                });
1194            }
1195            e.luma_offset = off;
1196        }
1197        if e.chroma_weight_flag {
1198            for j in 0..2 {
1199                let d = br.se()?;
1200                if !(-128..=127).contains(&d) {
1201                    return Err(SliceError::ValueOutOfRange {
1202                        field: if list_tag == "l0" {
1203                            "delta_chroma_weight_l0"
1204                        } else {
1205                            "delta_chroma_weight_l1"
1206                        },
1207                        got: d as i64,
1208                    });
1209                }
1210                e.delta_chroma_weight[j] = d;
1211
1212                let off = br.se()?;
1213                if off < -4 * wp_off_half_c || off > 4 * wp_off_half_c - 1 {
1214                    return Err(SliceError::ValueOutOfRange {
1215                        field: if list_tag == "l0" {
1216                            "delta_chroma_offset_l0"
1217                        } else {
1218                            "delta_chroma_offset_l1"
1219                        },
1220                        got: off as i64,
1221                    });
1222                }
1223                e.delta_chroma_offset[j] = off;
1224            }
1225        }
1226        let _ = i; // silence the unused-`i` when iter_mut().enumerate() is mixed with explicit loop
1227    }
1228
1229    Ok(entries)
1230}
1231
1232/// §7.4.7.3 closing summand:
1233/// `sumWeightLXFlags = Σ luma_weight_lX_flag[i] + 2 * chroma_weight_lX_flag[i]`.
1234fn sum_weight_flags(entries: &[PredWeightEntry]) -> u32 {
1235    entries
1236        .iter()
1237        .map(|e| u32::from(e.luma_weight_flag) + 2 * u32::from(e.chroma_weight_flag))
1238        .sum()
1239}
1240
1241/// §7.4.7.3 equation 7-58 for `ChromaOffsetLX[i][j]`. Extracted as a
1242/// free function so [`PredWeightTable::chroma_offset_l0`] /
1243/// [`PredWeightTable::chroma_offset_l1`] share the implementation.
1244fn chroma_offset_eq_7_58(
1245    wp_off_half_c: i32,
1246    delta_chroma_offset: i32,
1247    chroma_weight: i32,
1248    chroma_log2_weight_denom: u8,
1249) -> i32 {
1250    let raw = wp_off_half_c + delta_chroma_offset
1251        - ((wp_off_half_c * chroma_weight) >> chroma_log2_weight_denom);
1252    raw.clamp(-wp_off_half_c, wp_off_half_c - 1)
1253}
1254
1255/// Parsed slice segment header per §7.3.6.1.
1256///
1257/// Fields that this round defers (the non-IDR POC/RPS block, the P/B
1258/// reference-list / weighted-prediction sub-structures) are absent from
1259/// the materialised struct; when one of those points is reached the
1260/// remainder of the header is surfaced via [`Self::opaque_tail`] and
1261/// the corresponding `Option` fields stay `None`.
1262#[derive(Debug, Clone, PartialEq, Eq)]
1263pub struct SliceSegmentHeader {
1264    /// `first_slice_segment_in_pic_flag`.
1265    pub first_slice_segment_in_pic_flag: bool,
1266    /// `no_output_of_prior_pics_flag`. `None` when not present (the NAL
1267    /// unit is not an IRAP picture).
1268    pub no_output_of_prior_pics_flag: Option<bool>,
1269    /// `slice_pic_parameter_set_id` (`ue(v)`, range 0..=63).
1270    pub slice_pic_parameter_set_id: u8,
1271    /// `dependent_slice_segment_flag`. Inferred to false when the slice
1272    /// is the first segment of the picture or when
1273    /// `dependent_slice_segments_enabled_flag` is 0 (§7.4.7.1).
1274    pub dependent_slice_segment_flag: bool,
1275    /// `slice_segment_address` (`u(v)`). Inferred to 0 when not present
1276    /// (the first slice segment of the picture).
1277    pub slice_segment_address: u32,
1278    /// `slice_reserved_flag[]` — `num_extra_slice_header_bits` raw
1279    /// flags. Decoders ignore the value; carried for completeness.
1280    /// Empty for dependent slice segments and when the count is 0.
1281    pub slice_reserved_flags: Vec<bool>,
1282    /// `slice_type` per Table 7-7. `None` for dependent slice segments
1283    /// (the value is inherited from the associated independent slice
1284    /// segment, which this struct does not resolve).
1285    pub slice_type: Option<SliceType>,
1286    /// `pic_output_flag`. Inferred to true when not present (§7.4.7.1).
1287    pub pic_output_flag: bool,
1288    /// `colour_plane_id` (`u(2)`). `None` when not present
1289    /// (`separate_colour_plane_flag == 0`).
1290    pub colour_plane_id: Option<u8>,
1291    /// `slice_pic_order_cnt_lsb` (`u(v)`, width
1292    /// `log2_max_pic_order_cnt_lsb_minus4 + 4` bits). `None` when the
1293    /// current NAL unit is an IDR — IDR pictures have no slice POC LSB
1294    /// (the POC is reset to 0 per §8.3.1) — or when the parser stopped
1295    /// at the deferred P/B body before reaching this point.
1296    pub slice_pic_order_cnt_lsb: Option<u32>,
1297    /// `short_term_ref_pic_set_sps_flag`. `None` for IDR slices and
1298    /// for headers that stopped before this point.
1299    pub short_term_ref_pic_set_sps_flag: Option<bool>,
1300    /// In-line `st_ref_pic_set(num_short_term_ref_pic_sets)` parsed from
1301    /// the slice header itself (only when
1302    /// `short_term_ref_pic_set_sps_flag == 0`).
1303    pub inline_short_term_ref_pic_set: Option<ShortTermRefPicSet>,
1304    /// `short_term_ref_pic_set_idx` (`u(v)`, width
1305    /// `Ceil(Log2(num_short_term_ref_pic_sets))`). `None` when the SPS
1306    /// in-line form is used, when `num_short_term_ref_pic_sets <= 1`
1307    /// (the value is inferred to 0), or for IDR slices.
1308    pub short_term_ref_pic_set_idx: Option<u32>,
1309    /// `num_long_term_sps` (`ue(v)`). `None` when the long-term-ref-pic
1310    /// block is absent (no `long_term_ref_pics_present_flag` on the SPS
1311    /// or IDR slice); 0 (with the SPS gate satisfied but
1312    /// `num_long_term_ref_pics_sps == 0`).
1313    pub num_long_term_sps: Option<u32>,
1314    /// `num_long_term_pics` (`ue(v)`). `None` when the long-term-ref-pic
1315    /// block is absent.
1316    pub num_long_term_pics: Option<u32>,
1317    /// Per-entry long-term ref pic block (§7.3.6.1), length
1318    /// `num_long_term_sps + num_long_term_pics`. Empty when the block
1319    /// is absent.
1320    pub long_term_ref_pics: Vec<SliceLongTermRefPic>,
1321    /// `slice_temporal_mvp_enabled_flag`. Inferred to false when not
1322    /// present (`sps_temporal_mvp_enabled_flag == 0`) (§7.4.7.1).
1323    pub slice_temporal_mvp_enabled_flag: bool,
1324    /// `slice_sao_luma_flag`. Inferred to false when not present
1325    /// (`sample_adaptive_offset_enabled_flag == 0`).
1326    pub slice_sao_luma_flag: bool,
1327    /// `slice_sao_chroma_flag`. Inferred to false when not present.
1328    pub slice_sao_chroma_flag: bool,
1329    /// `num_ref_idx_active_override_flag` (§7.3.6.1). `None` for I
1330    /// slices (the field is absent — `slice_type == 2`) and for
1331    /// dependent slice segments. Read as `u(1)` immediately after the
1332    /// SAO block for P / B slices.
1333    pub num_ref_idx_active_override_flag: Option<bool>,
1334    /// `num_ref_idx_l0_active_minus1` (§7.3.6.1, range 0..=14). For P
1335    /// / B slices, signalled when `num_ref_idx_active_override_flag ==
1336    /// 1` and otherwise inferred to `pps.num_ref_idx_l0_default_active_
1337    /// minus1` per §7.4.7.1. `None` when the slice is I or the parser
1338    /// stopped before reaching this point.
1339    pub num_ref_idx_l0_active_minus1: Option<u8>,
1340    /// `num_ref_idx_l1_active_minus1` (§7.3.6.1, range 0..=14). For B
1341    /// slices, signalled when `num_ref_idx_active_override_flag == 1`
1342    /// and otherwise inferred to `pps.num_ref_idx_l1_default_active_
1343    /// minus1` per §7.4.7.1. `None` when the slice is not B or the
1344    /// parser stopped before reaching this point.
1345    pub num_ref_idx_l1_active_minus1: Option<u8>,
1346    /// `mvd_l1_zero_flag` (§7.3.6.1). `u(1)`, present only for B slices.
1347    /// `None` for I / P slices, dependent slice segments, and headers
1348    /// whose parse stopped before the inter-slice mvd block (either at
1349    /// the `ref_pic_lists_modification()` gate when
1350    /// `pps.lists_modification_present_flag == 1`, or at any earlier
1351    /// deferral point).
1352    pub mvd_l1_zero_flag: Option<bool>,
1353    /// `cabac_init_flag` (§7.3.6.1). `u(1)`, present only when
1354    /// `pps.cabac_init_present_flag == 1`; inferred to `false`
1355    /// otherwise per §7.4.7.1. `None` for I slices, dependent slice
1356    /// segments, and headers whose parse stopped before the inter-slice
1357    /// cabac-init point.
1358    pub cabac_init_flag: Option<bool>,
1359    /// `collocated_from_l0_flag` (§7.3.6.1). `u(1)`, present only when
1360    /// `slice_temporal_mvp_enabled_flag == 1 && slice_type == B`.
1361    /// Inferred to `true` when absent (per §7.4.7.1, "When
1362    /// `collocated_from_l0_flag` is not present, it is inferred to be
1363    /// equal to 1"). `None` when the slice is I, when the parse
1364    /// stopped before this point, or when
1365    /// `slice_temporal_mvp_enabled_flag == 0` (the field has no
1366    /// meaning).
1367    pub collocated_from_l0_flag: Option<bool>,
1368    /// `collocated_ref_idx` (§7.3.6.1). `ue(v)`, present only when
1369    /// `slice_temporal_mvp_enabled_flag == 1` and the relevant active
1370    /// list has more than one entry (specifically:
1371    /// `(collocated_from_l0_flag && num_ref_idx_l0_active_minus1 > 0)
1372    /// || (!collocated_from_l0_flag && num_ref_idx_l1_active_minus1 >
1373    /// 0)`). Inferred to `0` when absent per §7.4.7.1. `None` when the
1374    /// slice is I, when the parse stopped before this point, or when
1375    /// `slice_temporal_mvp_enabled_flag == 0`.
1376    pub collocated_ref_idx: Option<u32>,
1377    /// `five_minus_max_num_merge_cand` (§7.3.6.1). `ue(v)`, present only
1378    /// for P/B slices, immediately after the optional
1379    /// `pred_weight_table()`. §7.4.7.1: the derived
1380    /// `MaxNumMergeCand = 5 - five_minus_max_num_merge_cand` shall be in
1381    /// the range 1..=5 — i.e. the wire value is in 0..=4. `None` for I
1382    /// slices, dependent slice segments, and headers whose parse stopped
1383    /// before this point (currently: the parse defers at the
1384    /// `pred_weight_table()` gate when either
1385    /// `pps.weighted_pred_flag && slice_type == P` or
1386    /// `pps.weighted_bipred_flag && slice_type == B` is true).
1387    pub five_minus_max_num_merge_cand: Option<u32>,
1388    /// `use_integer_mv_flag` (§7.3.6.1) — present for P/B slices when
1389    /// the SPS SCC `motion_vector_resolution_control_idc == 2`;
1390    /// otherwise inferred equal to
1391    /// `motion_vector_resolution_control_idc` (§7.4.7.1). When 1,
1392    /// motion vectors of this slice referring to pictures other than
1393    /// the current picture use integer resolution (eqs 8-98..8-101 /
1394    /// 8-124..8-125).
1395    pub use_integer_mv_flag: bool,
1396    /// Decoded `pred_weight_table()` (§7.3.6.3) when the §7.3.6.1 outer
1397    /// gate is statically present
1398    /// (`(pps.weighted_pred_flag && slice_type == P) ||
1399    /// (pps.weighted_bipred_flag && slice_type == B)`). `None` when the
1400    /// outer gate is statically absent (the table is not signalled), for
1401    /// I slices, for dependent slice segments, and for headers whose
1402    /// parse stopped before this point.
1403    ///
1404    /// The in-place call uses the base-profile single-layer assumption
1405    /// for every per-i §7.3.6.3 outer-gate decision (universally `true`)
1406    /// — see [`PredWeightTableInputs::base_profile`]. Single-layer base
1407    /// profile is the only configuration this crate currently surfaces;
1408    /// the multilayer Annex F / SCC self-reference cases need the SPS
1409    /// range / multilayer / SCC extensions and the DPB to be wired up,
1410    /// at which point the in-place call site here will be widened to
1411    /// thread per-i gate decisions through.
1412    pub pred_weight_table: Option<PredWeightTable>,
1413    /// `slice_qp_delta` (`se(v)`). `None` when the parser stopped before
1414    /// reaching it (a deferred non-IDR or P/B body).
1415    pub slice_qp_delta: Option<i32>,
1416    /// `slice_cb_qp_offset` (`se(v)`, range −12..=12). Inferred to 0
1417    /// when not present (`pps_slice_chroma_qp_offsets_present_flag == 0`).
1418    pub slice_cb_qp_offset: i8,
1419    /// `slice_cr_qp_offset` (`se(v)`, range −12..=12). Inferred to 0
1420    /// when not present.
1421    pub slice_cr_qp_offset: i8,
1422    /// `slice_act_y_qp_offset` (`se(v)`, §7.3.6.1). Present only when
1423    /// `pps_slice_act_qp_offsets_present_flag` (the SCC PPS body);
1424    /// inferred to 0 otherwise. §7.4.7.1 bounds
1425    /// `PpsActQpOffsetY + slice_act_y_qp_offset` to −12..=12.
1426    pub slice_act_y_qp_offset: i32,
1427    /// `slice_act_cb_qp_offset` (`se(v)`, §7.3.6.1). Present only when
1428    /// `pps_slice_act_qp_offsets_present_flag`; inferred to 0 otherwise.
1429    pub slice_act_cb_qp_offset: i32,
1430    /// `slice_act_cr_qp_offset` (`se(v)`, §7.3.6.1). Present only when
1431    /// `pps_slice_act_qp_offsets_present_flag`; inferred to 0 otherwise.
1432    pub slice_act_cr_qp_offset: i32,
1433    /// `cu_chroma_qp_offset_enabled_flag` (`u(1)`, §7.3.6.1). Present
1434    /// only when the range-extension `chroma_qp_offset_list_enabled_flag`
1435    /// is set; inferred to 0 otherwise.
1436    pub cu_chroma_qp_offset_enabled_flag: bool,
1437    /// Deblocking-filter values, carrying the §7.4.7.1 inferred
1438    /// defaults when the slice override block is absent. `None` when
1439    /// the parser stopped before this point.
1440    pub deblocking: Option<SliceDeblocking>,
1441    /// `slice_loop_filter_across_slices_enabled_flag`. Inferred to
1442    /// `pps_loop_filter_across_slices_enabled_flag` when not present.
1443    /// `None` when the parser stopped before this point.
1444    pub slice_loop_filter_across_slices_enabled_flag: Option<bool>,
1445    /// Entry-point-offset block. `None` when neither tiles nor
1446    /// entropy-coding-sync are enabled (the block is absent), or when
1447    /// the parser stopped before this point.
1448    pub entry_point_offsets: Option<EntryPointOffsets>,
1449    /// `slice_segment_header_extension_length` (`ue(v)`). `None` when
1450    /// `slice_segment_header_extension_present_flag == 0` or the parser
1451    /// stopped before this point.
1452    pub slice_segment_header_extension_length: Option<u32>,
1453    /// Byte offset, from the start of the RBSP, of the first byte of
1454    /// `slice_segment_data()` — i.e. the position immediately after
1455    /// `byte_alignment()`. `None` when the header was not parsed all
1456    /// the way to `byte_alignment()` (a deferred body).
1457    pub byte_offset_to_slice_data: Option<usize>,
1458    /// Decoded `ref_pic_lists_modification()` (§7.3.6.2) when the
1459    /// §7.3.6.1 outer gate
1460    /// (`pps.lists_modification_present_flag == 1 && NumPicTotalCurr > 1`)
1461    /// is statically present. `None` when the gate is statically
1462    /// absent (`pps.lists_modification_present_flag == 0`,
1463    /// `NumPicTotalCurr <= 1`, an I slice, a dependent slice segment,
1464    /// or a header whose parse stopped before this point — including
1465    /// the inter-RPS-predicted SPS-form short-term RPS case, where
1466    /// the per-position `UsedByCurrPicS0` / `UsedByCurrPicS1` flags
1467    /// needed for the §7.4.7.2 `NumPicTotalCurr` derivation can only
1468    /// be resolved by running the §7.4.8 inter-RPS-prediction step;
1469    /// the parser defers in that case and surfaces an
1470    /// [`Self::opaque_tail`] starting at the `ref_pic_lists_modification()`
1471    /// bit position).
1472    pub ref_pic_lists_modification: Option<RefPicListsModification>,
1473    /// Opaque suffix of the slice-header RBSP. Populated when the
1474    /// parser reaches a deferred body (the non-IDR POC/RPS block or a
1475    /// P/B reference-list / weighted-prediction sub-structure); carries
1476    /// the still-unparsed RBSP bytes and the start-bit offset. `None`
1477    /// when the header was parsed to completion.
1478    pub opaque_tail: Option<OpaqueTail>,
1479}
1480
1481impl SliceSegmentHeader {
1482    /// Parse `slice_segment_header()` from the first bit of the
1483    /// (already-unescaped) slice-segment-layer RBSP body — i.e. after
1484    /// the two-byte NAL header has been removed (see
1485    /// [`crate::nal::NalUnit`]).
1486    ///
1487    /// * `nal_unit_type` is the value from the NAL header; it gates
1488    ///   both `no_output_of_prior_pics_flag` (IRAP range) and the
1489    ///   non-IDR POC/RPS block.
1490    /// * `sps` is the activated SPS, `pps` the activated PPS. The
1491    ///   caller resolves `slice_pic_parameter_set_id` to the right PPS
1492    ///   and that PPS's `pps_seq_parameter_set_id` to the right SPS;
1493    ///   this parser uses the supplied pair for the field widths and
1494    ///   presence gates.
1495    pub fn parse(
1496        rbsp: &[u8],
1497        nal_unit_type: u8,
1498        sps: &SeqParameterSet,
1499        pps: &PicParameterSet,
1500    ) -> Result<Self, SliceError> {
1501        let mut br = BitReader::new(rbsp);
1502
1503        let first_slice_segment_in_pic_flag = br.u1()? != 0;
1504
1505        let no_output_of_prior_pics_flag = if (BLA_W_LP..=RSV_IRAP_VCL23).contains(&nal_unit_type) {
1506            Some(br.u1()? != 0)
1507        } else {
1508            None
1509        };
1510
1511        let slice_pic_parameter_set_id = br.ue()?;
1512        if slice_pic_parameter_set_id > 63 {
1513            return Err(SliceError::ValueOutOfRange {
1514                field: "slice_pic_parameter_set_id",
1515                got: slice_pic_parameter_set_id as i64,
1516            });
1517        }
1518        let slice_pic_parameter_set_id = slice_pic_parameter_set_id as u8;
1519
1520        // §7.3.6.1: dependent_slice_segment_flag / slice_segment_address
1521        // only appear for non-first slice segments.
1522        let mut dependent_slice_segment_flag = false;
1523        let mut slice_segment_address = 0u32;
1524        if !first_slice_segment_in_pic_flag {
1525            if pps.dependent_slice_segments_enabled_flag {
1526                dependent_slice_segment_flag = br.u1()? != 0;
1527            }
1528            // slice_segment_address width is Ceil( Log2( PicSizeInCtbsY ) )
1529            // bits (§7.4.7.1).
1530            let addr_bits = ceil_log2(pic_size_in_ctbs_y(sps));
1531            slice_segment_address = br.u(addr_bits)?;
1532            if slice_segment_address >= pic_size_in_ctbs_y(sps) {
1533                return Err(SliceError::ValueOutOfRange {
1534                    field: "slice_segment_address",
1535                    got: slice_segment_address as i64,
1536                });
1537            }
1538        }
1539
1540        // Defaults / inferences (§7.4.7.1).
1541        let mut slice_reserved_flags = Vec::new();
1542        let mut slice_type = None;
1543        let mut pic_output_flag = true;
1544        let mut colour_plane_id = None;
1545        let mut slice_pic_order_cnt_lsb: Option<u32> = None;
1546        let mut short_term_ref_pic_set_sps_flag: Option<bool> = None;
1547        let mut inline_short_term_ref_pic_set: Option<ShortTermRefPicSet> = None;
1548        let mut short_term_ref_pic_set_idx: Option<u32> = None;
1549        let mut num_long_term_sps: Option<u32> = None;
1550        let mut num_long_term_pics: Option<u32> = None;
1551        let mut long_term_ref_pics: Vec<SliceLongTermRefPic> = Vec::new();
1552        let mut slice_temporal_mvp_enabled_flag = false;
1553
1554        if !dependent_slice_segment_flag {
1555            for _ in 0..pps.num_extra_slice_header_bits {
1556                slice_reserved_flags.push(br.u1()? != 0);
1557            }
1558            let st = SliceType::from_raw(br.ue()?)?;
1559            slice_type = Some(st);
1560
1561            if pps.output_flag_present_flag {
1562                pic_output_flag = br.u1()? != 0;
1563            }
1564
1565            if sps.separate_colour_plane_flag {
1566                let id = br.u(2)? as u8;
1567                colour_plane_id = Some(id);
1568            }
1569
1570            // Non-IDR POC + reference-picture-set block (§7.3.6.1).
1571            let is_idr = nal_unit_type == IDR_W_RADL || nal_unit_type == IDR_N_LP;
1572            if !is_idr {
1573                // slice_pic_order_cnt_lsb u(v), width log2_max_poc_lsb_minus4+4.
1574                let poc_lsb_bits = sps.log2_max_pic_order_cnt_lsb_minus4 + 4;
1575                let poc_lsb = br.u(poc_lsb_bits)?;
1576                if poc_lsb >= sps.max_pic_order_cnt_lsb() {
1577                    return Err(SliceError::ValueOutOfRange {
1578                        field: "slice_pic_order_cnt_lsb",
1579                        got: poc_lsb as i64,
1580                    });
1581                }
1582                slice_pic_order_cnt_lsb = Some(poc_lsb);
1583
1584                // short_term_ref_pic_set_sps_flag u(1).
1585                let st_sps_flag = br.u1()? != 0;
1586                short_term_ref_pic_set_sps_flag = Some(st_sps_flag);
1587                if st_sps_flag && sps.num_short_term_ref_pic_sets == 0 {
1588                    // §7.4.7.1: when num_short_term_ref_pic_sets == 0,
1589                    // short_term_ref_pic_set_sps_flag shall be 0.
1590                    return Err(SliceError::ValueOutOfRange {
1591                        field: "short_term_ref_pic_set_sps_flag",
1592                        got: 1,
1593                    });
1594                }
1595
1596                if !st_sps_flag {
1597                    let inline = ShortTermRefPicSet::parse_slice_inline(&mut br, sps)?;
1598                    inline_short_term_ref_pic_set = Some(inline);
1599                } else if sps.num_short_term_ref_pic_sets > 1 {
1600                    let idx_bits = ceil_log2(sps.num_short_term_ref_pic_sets);
1601                    let idx = br.u(idx_bits)?;
1602                    if idx >= sps.num_short_term_ref_pic_sets {
1603                        return Err(SliceError::ValueOutOfRange {
1604                            field: "short_term_ref_pic_set_idx",
1605                            got: idx as i64,
1606                        });
1607                    }
1608                    short_term_ref_pic_set_idx = Some(idx);
1609                }
1610                // else: short_term_ref_pic_set_idx is inferred to 0
1611                // (and left as None in the struct to signal "absent").
1612
1613                if sps.long_term_ref_pics_present_flag {
1614                    let (nl_sps, nl_pics, entries) = parse_long_term_ref_pic_block(&mut br, sps)?;
1615                    num_long_term_sps = Some(nl_sps);
1616                    num_long_term_pics = Some(nl_pics);
1617                    long_term_ref_pics = entries;
1618                }
1619                // §7.3.6.1: slice_temporal_mvp_enabled_flag is signalled
1620                // inside the non-IDR block — an IDR picture never
1621                // carries it and §7.4.7.1 infers it to 0.
1622                if sps.sps_temporal_mvp_enabled_flag {
1623                    slice_temporal_mvp_enabled_flag = br.u1()? != 0;
1624                }
1625            }
1626        }
1627
1628        // SAO block (§7.3.6.1) — inside the !dependent gate: a dependent
1629        // slice segment inherits the SAO flags from the associated
1630        // independent slice segment and does not re-signal them.
1631        let mut slice_sao_luma_flag = false;
1632        let mut slice_sao_chroma_flag = false;
1633        if !dependent_slice_segment_flag && sps.sample_adaptive_offset_enabled_flag {
1634            slice_sao_luma_flag = br.u1()? != 0;
1635            if chroma_array_type(sps) != 0 {
1636                slice_sao_chroma_flag = br.u1()? != 0;
1637            }
1638        }
1639
1640        // The remaining body lives inside the !dependent gate. For a
1641        // dependent slice segment the header ends after the SAO block,
1642        // before byte_alignment() (the rest of the header is inherited).
1643        if dependent_slice_segment_flag {
1644            // §7.3.6.1: the entry-point block and the header-extension
1645            // block sit OUTSIDE the !dependent gate — a dependent slice
1646            // segment signals its own substream entry points.
1647            let entry_point_offsets = parse_entry_point_offsets(&mut br, sps, pps)?;
1648            let slice_segment_header_extension_length = parse_header_extension(&mut br, pps)?;
1649            let byte_offset = consume_byte_alignment(&mut br)?;
1650            return Ok(Self {
1651                first_slice_segment_in_pic_flag,
1652                no_output_of_prior_pics_flag,
1653                slice_pic_parameter_set_id,
1654                dependent_slice_segment_flag,
1655                slice_segment_address,
1656                slice_reserved_flags,
1657                slice_type,
1658                pic_output_flag,
1659                colour_plane_id,
1660                slice_pic_order_cnt_lsb: None,
1661                short_term_ref_pic_set_sps_flag: None,
1662                inline_short_term_ref_pic_set: None,
1663                short_term_ref_pic_set_idx: None,
1664                num_long_term_sps: None,
1665                num_long_term_pics: None,
1666                long_term_ref_pics: Vec::new(),
1667                slice_temporal_mvp_enabled_flag,
1668                slice_sao_luma_flag,
1669                slice_sao_chroma_flag,
1670                num_ref_idx_active_override_flag: None,
1671                num_ref_idx_l0_active_minus1: None,
1672                num_ref_idx_l1_active_minus1: None,
1673                mvd_l1_zero_flag: None,
1674                cabac_init_flag: None,
1675                collocated_from_l0_flag: None,
1676                collocated_ref_idx: None,
1677                five_minus_max_num_merge_cand: None,
1678                use_integer_mv_flag: false,
1679                pred_weight_table: None,
1680                slice_qp_delta: None,
1681                slice_cb_qp_offset: 0,
1682                slice_cr_qp_offset: 0,
1683                slice_act_y_qp_offset: 0,
1684                slice_act_cb_qp_offset: 0,
1685                slice_act_cr_qp_offset: 0,
1686                cu_chroma_qp_offset_enabled_flag: false,
1687                deblocking: None,
1688                slice_loop_filter_across_slices_enabled_flag: None,
1689                entry_point_offsets,
1690                slice_segment_header_extension_length,
1691                byte_offset_to_slice_data: Some(byte_offset),
1692                ref_pic_lists_modification: None,
1693                opaque_tail: None,
1694            });
1695        }
1696
1697        // §7.3.6.1: for P / B slices, the SAO block is immediately
1698        // followed by the `num_ref_idx_active_override_flag` /
1699        // `num_ref_idx_lX_active_minus1` block. This is the in-place
1700        // prerequisite for the later `ref_pic_lists_modification()`
1701        // call (its `list_entry_lX[]` loop indexes `0..=
1702        // num_ref_idx_lX_active_minus1`). The §7.4.7.1 inference rule
1703        // fills both `num_ref_idx_lX_active_minus1` values from the PPS
1704        // defaults when the override flag is 0; both values are capped
1705        // at 14.
1706        let st = slice_type.expect("independent slice has a slice_type");
1707        let (
1708            num_ref_idx_active_override_flag,
1709            num_ref_idx_l0_active_minus1,
1710            num_ref_idx_l1_active_minus1,
1711        ) = if st.is_inter() {
1712            let override_flag = br.u1()? != 0;
1713            let (n0, n1) = if override_flag {
1714                let n0 = br.ue()?;
1715                if n0 > 14 {
1716                    return Err(SliceError::ValueOutOfRange {
1717                        field: "num_ref_idx_l0_active_minus1",
1718                        got: n0 as i64,
1719                    });
1720                }
1721                let n1 = if matches!(st, SliceType::B) {
1722                    let v = br.ue()?;
1723                    if v > 14 {
1724                        return Err(SliceError::ValueOutOfRange {
1725                            field: "num_ref_idx_l1_active_minus1",
1726                            got: v as i64,
1727                        });
1728                    }
1729                    Some(v as u8)
1730                } else {
1731                    None
1732                };
1733                (n0 as u8, n1)
1734            } else {
1735                // §7.4.7.1 inference defaults from the PPS.
1736                let n1 = if matches!(st, SliceType::B) {
1737                    Some(pps.num_ref_idx_l1_default_active_minus1)
1738                } else {
1739                    None
1740                };
1741                (pps.num_ref_idx_l0_default_active_minus1, n1)
1742            };
1743            (Some(override_flag), Some(n0), n1)
1744        } else {
1745            (None, None, None)
1746        };
1747
1748        // §7.3.6.1 inter-slice continuation: after the
1749        // `num_ref_idx_active_override_flag` block, the spec emits
1750        //   if( lists_modification_present_flag && NumPicTotalCurr > 1 )
1751        //       ref_pic_lists_modification( )
1752        //   if( slice_type == B )           mvd_l1_zero_flag      u(1)
1753        //   if( cabac_init_present_flag )   cabac_init_flag       u(1)
1754        //   if( slice_temporal_mvp_enabled_flag ) {
1755        //       if( slice_type == B )       collocated_from_l0_flag  u(1)   (else inferred 1, §7.4.7.1)
1756        //       if( ( collocated_from_l0_flag && num_ref_idx_l0_active_minus1 > 0 ) ||
1757        //           ( !collocated_from_l0_flag && num_ref_idx_l1_active_minus1 > 0 ) )
1758        //           collocated_ref_idx     ue(v)                          (else inferred 0)
1759        //   }
1760        // followed by the weighted-pred-table gate.
1761        //
1762        // The `ref_pic_lists_modification()` gate consumes the
1763        // §7.4.7.2 `NumPicTotalCurr` derivation (equation 7-57). When
1764        // `pps.lists_modification_present_flag == 0` the
1765        // `if(... && NumPicTotalCurr > 1)` short-circuit applies
1766        // without needing the derivation, so the bit stream advances
1767        // straight to `mvd_l1_zero_flag`. When the flag is 1 we attempt
1768        // to derive `NumPicTotalCurr` from the resolved slice-header
1769        // state (active short-term RPS + long-term entries):
1770        //
1771        // * For the inline-form short-term RPS
1772        //   (`short_term_ref_pic_set_sps_flag == 0`) the on-wire form
1773        //   per §7.4.8 has `inter_ref_pic_set_prediction_flag == 0`
1774        //   when `stRpsIdx == num_short_term_ref_pic_sets` (the
1775        //   slice-inline index): the `used_by_curr_pic_s{0,1}_flag`
1776        //   arrays come directly from the inline RPS. The §7.4.8 form
1777        //   *is* allowed at the slice-inline index when the SPS has
1778        //   `num_short_term_ref_pic_sets > 0`; in that case the parser
1779        //   defers (the derivation requires walking the source RPS
1780        //   chain and is out of scope here).
1781        // * For the SPS-form (`short_term_ref_pic_set_sps_flag == 1`)
1782        //   the active RPS is `sps.short_term_ref_pic_sets[idx]`. When
1783        //   that RPS uses the explicit form the per-position
1784        //   `used_by_curr_pic_sX_flag` arrays are usable directly; when
1785        //   it uses inter-prediction the §7.4.8 derivation must be run
1786        //   first and the parser defers.
1787        //
1788        // The §F.7.4.7.2 multilayer-extension variant and the
1789        // SCC `pps_curr_pic_ref_enabled_flag` closing-clause are wired
1790        // through [`NumPicTotalCurrInputs`] but the base-profile call
1791        // site here leaves both at their `false` defaults (the PPS
1792        // SCC extension is not yet surfaced; multilayer extension is
1793        // forwarded via the long-term-ref builder).
1794        //
1795        // For IDR slices the entire non-IDR POC/RPS block is absent
1796        // and `inline_short_term_ref_pic_set` / `long_term_ref_pics`
1797        // are empty: `NumPicTotalCurr` is `0` and the gate is
1798        // statically false.
1799        let (ref_pic_lists_modification, num_pic_total_curr_resolved) = if st.is_inter()
1800            && pps.lists_modification_present_flag
1801        {
1802            match resolve_active_short_term_rps(
1803                sps,
1804                short_term_ref_pic_set_sps_flag,
1805                inline_short_term_ref_pic_set.as_ref(),
1806                short_term_ref_pic_set_idx,
1807            ) {
1808                ActiveShortTermRps::Materialized(m) => {
1809                    let lt_used = collect_used_by_curr_pic_lt(&long_term_ref_pics, sps);
1810                    let inputs = NumPicTotalCurrInputs::from_used_flags(
1811                        &m.used_by_curr_pic_s0,
1812                        &m.used_by_curr_pic_s1,
1813                        &lt_used,
1814                    );
1815                    let npc = inputs.compute();
1816                    if npc > 1 {
1817                        let l0_active =
1818                            num_ref_idx_l0_active_minus1.ok_or(SliceError::ValueOutOfRange {
1819                                field: "num_ref_idx_l0_active_minus1",
1820                                got: -1,
1821                            })?;
1822                        let l1_active = num_ref_idx_l1_active_minus1.unwrap_or(0);
1823                        let rplm =
1824                            RefPicListsModification::parse(&mut br, st, l0_active, l1_active, npc)?;
1825                        (Some(rplm), Some(npc))
1826                    } else {
1827                        // `NumPicTotalCurr <= 1` — the §7.3.6.1
1828                        // gate is statically false; the structure
1829                        // is not signalled and we continue at
1830                        // `mvd_l1_zero_flag`.
1831                        (None, Some(npc))
1832                    }
1833                }
1834                ActiveShortTermRps::Empty => {
1835                    // IDR or no RPS picked: `NumPicTotalCurr == 0`,
1836                    // gate is statically false.
1837                    (None, Some(0))
1838                }
1839                ActiveShortTermRps::MaterializeFailed => {
1840                    // §7.4.8 derivation could not run (malformed
1841                    // `RefRpsIdx` chain or array-length mismatch);
1842                    // defer to opaque tail so the caller can salvage
1843                    // the rest of the bitstream.
1844                    return Ok(Self {
1845                        first_slice_segment_in_pic_flag,
1846                        no_output_of_prior_pics_flag,
1847                        slice_pic_parameter_set_id,
1848                        dependent_slice_segment_flag,
1849                        slice_segment_address,
1850                        slice_reserved_flags,
1851                        slice_type,
1852                        pic_output_flag,
1853                        colour_plane_id,
1854                        slice_pic_order_cnt_lsb,
1855                        short_term_ref_pic_set_sps_flag,
1856                        inline_short_term_ref_pic_set,
1857                        short_term_ref_pic_set_idx,
1858                        num_long_term_sps,
1859                        num_long_term_pics,
1860                        long_term_ref_pics,
1861                        slice_temporal_mvp_enabled_flag,
1862                        slice_sao_luma_flag,
1863                        slice_sao_chroma_flag,
1864                        num_ref_idx_active_override_flag,
1865                        num_ref_idx_l0_active_minus1,
1866                        num_ref_idx_l1_active_minus1,
1867                        mvd_l1_zero_flag: None,
1868                        cabac_init_flag: None,
1869                        collocated_from_l0_flag: None,
1870                        collocated_ref_idx: None,
1871                        five_minus_max_num_merge_cand: None,
1872                        use_integer_mv_flag: false,
1873                        pred_weight_table: None,
1874                        slice_qp_delta: None,
1875                        slice_cb_qp_offset: 0,
1876                        slice_cr_qp_offset: 0,
1877                        slice_act_y_qp_offset: 0,
1878                        slice_act_cb_qp_offset: 0,
1879                        slice_act_cr_qp_offset: 0,
1880                        cu_chroma_qp_offset_enabled_flag: false,
1881                        deblocking: None,
1882                        slice_loop_filter_across_slices_enabled_flag: None,
1883                        entry_point_offsets: None,
1884                        slice_segment_header_extension_length: None,
1885                        byte_offset_to_slice_data: None,
1886                        ref_pic_lists_modification: None,
1887                        opaque_tail: Some(OpaqueTail::capture_at(br.bit_pos(), rbsp)),
1888                    });
1889                }
1890            }
1891        } else {
1892            (None, None)
1893        };
1894        // `num_pic_total_curr_resolved` is currently only used to gate
1895        // the in-place RPLM parse above; future rounds may surface it
1896        // on the slice header (the §8.3.4 implicit reference-list
1897        // derivation needs the same value).
1898        let _ = num_pic_total_curr_resolved;
1899
1900        // §7.3.6.1 inter-slice mvd / cabac-init / collocated block —
1901        // reached only when `slice_type` is P or B and the
1902        // `ref_pic_lists_modification()` block is statically absent
1903        // (`pps.lists_modification_present_flag == 0`, which makes the
1904        // §7.3.6.1 outer `if(... && NumPicTotalCurr > 1)` false
1905        // unconditionally). For an I slice the entire block is absent
1906        // and the four fields stay `None`.
1907        let (mvd_l1_zero_flag, cabac_init_flag, collocated_from_l0_flag, collocated_ref_idx) =
1908            if st.is_inter() {
1909                // §7.3.6.1 `if( slice_type == B ) mvd_l1_zero_flag u(1)`.
1910                let mvd_l1_zero = if matches!(st, SliceType::B) {
1911                    Some(br.u1()? != 0)
1912                } else {
1913                    None
1914                };
1915
1916                // §7.3.6.1 `if( cabac_init_present_flag ) cabac_init_flag
1917                // u(1)`. §7.4.7.1: inferred to 0 when absent.
1918                let cabac_init = if pps.cabac_init_present_flag {
1919                    Some(br.u1()? != 0)
1920                } else {
1921                    Some(false)
1922                };
1923
1924                // §7.3.6.1 temporal-MVP block.
1925                let (coll_from_l0, coll_ref_idx) = if slice_temporal_mvp_enabled_flag {
1926                    // `if( slice_type == B ) collocated_from_l0_flag u(1)`,
1927                    // else §7.4.7.1 inference to 1.
1928                    let from_l0 = if matches!(st, SliceType::B) {
1929                        br.u1()? != 0
1930                    } else {
1931                        true
1932                    };
1933
1934                    // §7.3.6.1: `collocated_ref_idx` is present iff the
1935                    // active list (selected by `collocated_from_l0_flag`)
1936                    // has more than one entry. §7.4.7.1: inferred to 0
1937                    // when absent. Both `num_ref_idx_lX_active_minus1`
1938                    // values are `Some(_)` at this point (the override
1939                    // block populated them for every inter slice, and L1
1940                    // is populated for B slices).
1941                    let n0 = num_ref_idx_l0_active_minus1.expect("L0 active populated for inter");
1942                    let needs_ref_idx = if from_l0 {
1943                        n0 > 0
1944                    } else {
1945                        // !from_l0 implies slice_type == B (an I/P slice
1946                        // takes the inferred `true` branch). For a B slice
1947                        // L1 is signalled by the override block.
1948                        let n1 = num_ref_idx_l1_active_minus1.expect("L1 active populated for B");
1949                        n1 > 0
1950                    };
1951                    let ref_idx = if needs_ref_idx {
1952                        let raw = br.ue()?;
1953                        let max = if from_l0 {
1954                            n0 as u32
1955                        } else {
1956                            num_ref_idx_l1_active_minus1.unwrap() as u32
1957                        };
1958                        if raw > max {
1959                            return Err(SliceError::ValueOutOfRange {
1960                                field: "collocated_ref_idx",
1961                                got: raw as i64,
1962                            });
1963                        }
1964                        raw
1965                    } else {
1966                        0
1967                    };
1968
1969                    (Some(from_l0), Some(ref_idx))
1970                } else {
1971                    (None, None)
1972                };
1973
1974                (mvd_l1_zero, cabac_init, coll_from_l0, coll_ref_idx)
1975            } else {
1976                (None, None, None, None)
1977            };
1978
1979        // §7.3.6.1 P / B `pred_weight_table()` gate. The table is
1980        // signalled iff either `(weighted_pred_flag && slice_type == P)`
1981        // or `(weighted_bipred_flag && slice_type == B)`. When the gate
1982        // is statically absent the parser walks straight past it into
1983        // the merge-candidate block; when it is present the standalone
1984        // [`PredWeightTable::parse`] is invoked in place with the
1985        // base-profile single-layer assumption (every per-i §7.3.6.3
1986        // outer gate `true` — see [`PredWeightTableInputs::base_profile`]).
1987        // This is the only single-layer configuration this crate
1988        // currently surfaces: the SPS range / multilayer / SCC
1989        // extensions are not yet wired through, so the bit-depth
1990        // arguments fall back to the SPS `BitDepthY` / `BitDepthC`
1991        // (`WpOffsetHalfRangeY` = `WpOffsetHalfRangeC` = 128 per §7.4.7.3
1992        // because `high_precision_offsets_enabled_flag` defaults to 0).
1993        // The per-i outer-gate decisions, when needed for the
1994        // multilayer-extension / SCC self-reference cases, will be
1995        // threaded through here once those extensions are surfaced.
1996        let weighted_pred_table_present = (pps.weighted_pred_flag && matches!(st, SliceType::P))
1997            || (pps.weighted_bipred_flag && matches!(st, SliceType::B));
1998        let pred_weight_table = if weighted_pred_table_present {
1999            // Resolve the active L0 / L1 cardinalities the table parser
2000            // needs. Both have been populated by the override block
2001            // above (§7.4.7.1 inference fills L0 for P/B and L1 for B
2002            // when override == 0).
2003            let n0 = num_ref_idx_l0_active_minus1.expect("L0 active populated for inter");
2004            let n1 = if matches!(st, SliceType::B) {
2005                num_ref_idx_l1_active_minus1.expect("L1 active populated for B")
2006            } else {
2007                0
2008            };
2009            let inputs = PredWeightTableInputs::base_profile(
2010                st,
2011                n0,
2012                n1,
2013                chroma_array_type(sps),
2014                sps.bit_depth_luma(),
2015                sps.bit_depth_chroma(),
2016            );
2017            Some(PredWeightTable::parse(&mut br, &inputs)?)
2018        } else {
2019            None
2020        };
2021
2022        // §7.3.6.1 `five_minus_max_num_merge_cand` (ue(v)), signalled
2023        // for every inter slice immediately after the (optional)
2024        // pred_weight_table(). §7.4.7.1 derives
2025        // `MaxNumMergeCand = 5 - five_minus_max_num_merge_cand`, with
2026        // the conformance constraint `1 <= MaxNumMergeCand <= 5` —
2027        // i.e. the wire value lies in 0..=4. The SCC
2028        // `use_integer_mv_flag` (gated on
2029        // `motion_vector_resolution_control_idc == 2`) is statically
2030        // absent because the PPS SCC extension is not surfaced by this
2031        // crate yet (§7.4.7.1: when not present,
2032        // `motion_vector_resolution_control_idc` is inferred to 0).
2033        let five_minus_max_num_merge_cand = if st.is_inter() {
2034            let v = br.ue()?;
2035            if v > 4 {
2036                return Err(SliceError::ValueOutOfRange {
2037                    field: "five_minus_max_num_merge_cand",
2038                    got: v as i64,
2039                });
2040            }
2041            Some(v)
2042        } else {
2043            None
2044        };
2045
2046        // §7.3.6.1: use_integer_mv_flag is present for inter slices
2047        // when motion_vector_resolution_control_idc == 2; otherwise
2048        // §7.4.7.1 infers it equal to the idc value.
2049        let mv_res_idc = sps
2050            .sps_scc_extension
2051            .as_ref()
2052            .map_or(0, |s| s.motion_vector_resolution_control_idc);
2053        let use_integer_mv_flag = if st.is_inter() && mv_res_idc == 2 {
2054            br.u1()? != 0
2055        } else {
2056            mv_res_idc == 1
2057        };
2058
2059        // Slice QP / chroma QP / deblocking / loop-filter / entry-points
2060        // tail (§7.3.6.1) — shared by I, P and B independent slice
2061        // segments.
2062        let slice_qp_delta = br.se()?;
2063
2064        let mut slice_cb_qp_offset = 0i8;
2065        let mut slice_cr_qp_offset = 0i8;
2066        if pps.pps_slice_chroma_qp_offsets_present_flag {
2067            slice_cb_qp_offset = parse_qp_offset(&mut br, "slice_cb_qp_offset")?;
2068            slice_cr_qp_offset = parse_qp_offset(&mut br, "slice_cr_qp_offset")?;
2069        }
2070
2071        // SCC adaptive-colour-transform per-slice QP offsets (§7.3.6.1),
2072        // present only when the SCC PPS body set
2073        // `pps_slice_act_qp_offsets_present_flag`. §7.4.7.1 bounds the
2074        // sum `PpsActQpOffset{Y,Cb,Cr} + slice_act_{y,cb,cr}_qp_offset`
2075        // to −12..=12; the per-element offsets themselves are se(v) with
2076        // no independent bound, so the conformance check is applied to
2077        // the combined value using the PPS-level offsets.
2078        let mut slice_act_y_qp_offset = 0i32;
2079        let mut slice_act_cb_qp_offset = 0i32;
2080        let mut slice_act_cr_qp_offset = 0i32;
2081        let pps_slice_act_qp_offsets_present_flag = pps
2082            .pps_scc_extension
2083            .as_ref()
2084            .map(|scc| scc.pps_slice_act_qp_offsets_present_flag)
2085            .unwrap_or(false);
2086        if pps_slice_act_qp_offsets_present_flag {
2087            // The presence of the offsets implies a decoded SCC body.
2088            let scc = pps
2089                .pps_scc_extension
2090                .as_ref()
2091                .expect("pps_slice_act_qp_offsets_present_flag implies SCC body");
2092            slice_act_y_qp_offset = parse_slice_act_qp_offset(
2093                &mut br,
2094                "slice_act_y_qp_offset",
2095                scc.pps_act_qp_offset_y(),
2096            )?;
2097            slice_act_cb_qp_offset = parse_slice_act_qp_offset(
2098                &mut br,
2099                "slice_act_cb_qp_offset",
2100                scc.pps_act_qp_offset_cb(),
2101            )?;
2102            slice_act_cr_qp_offset = parse_slice_act_qp_offset(
2103                &mut br,
2104                "slice_act_cr_qp_offset",
2105                scc.pps_act_qp_offset_cr(),
2106            )?;
2107        }
2108
2109        // `cu_chroma_qp_offset_enabled_flag` (§7.3.6.1), present only
2110        // when the range-extension `chroma_qp_offset_list_enabled_flag`
2111        // is set; inferred to 0 otherwise (§7.4.7.1).
2112        let chroma_qp_offset_list_enabled_flag = pps
2113            .pps_range_extension
2114            .as_ref()
2115            .map(|re| re.chroma_qp_offset_list_enabled_flag)
2116            .unwrap_or(false);
2117        let cu_chroma_qp_offset_enabled_flag = if chroma_qp_offset_list_enabled_flag {
2118            br.u1()? != 0
2119        } else {
2120            false
2121        };
2122
2123        // Deblocking override (§7.3.6.1).
2124        let deblocking = parse_slice_deblocking(&mut br, pps)?;
2125
2126        // slice_loop_filter_across_slices_enabled_flag gate (§7.3.6.1).
2127        let slice_loop_filter_across_slices_enabled_flag = if pps
2128            .pps_loop_filter_across_slices_enabled_flag
2129            && (slice_sao_luma_flag || slice_sao_chroma_flag || !deblocking.disabled_flag)
2130        {
2131            br.u1()? != 0
2132        } else {
2133            pps.pps_loop_filter_across_slices_enabled_flag
2134        };
2135
2136        // Entry-point-offset block (§7.3.6.1). §7.4.7.1 bounds
2137        // `num_entry_point_offsets` by the active partitioning: the
2138        // tile count when only `tiles_enabled_flag == 1`,
2139        // `PicHeightInCtbsY` when only
2140        // `entropy_coding_sync_enabled_flag == 1`, and
2141        // `NumTileColumns * PicHeightInCtbsY` when both are 1
2142        // (wavefronts inside every tile). Each
2143        // `entry_point_offset_minus1[i]` is `offset_len_minus1 + 1`
2144        // bits wide and is read into [`EntryPointOffsets::
2145        // entry_point_offset_minus1`] (a per-index `Vec<u32>`); the
2146        // byte length of subset `i` follows as
2147        // `entry_point_offset_minus1[i] + 1` (§7.4.7.1) and is exposed
2148        // via [`EntryPointOffsets::subset_length`].
2149        let entry_point_offsets = parse_entry_point_offsets(&mut br, sps, pps)?;
2150
2151        // Slice-segment-header extension block (§7.3.6.1).
2152        let slice_segment_header_extension_length = parse_header_extension(&mut br, pps)?;
2153
2154        let byte_offset = consume_byte_alignment(&mut br)?;
2155
2156        Ok(Self {
2157            first_slice_segment_in_pic_flag,
2158            no_output_of_prior_pics_flag,
2159            slice_pic_parameter_set_id,
2160            dependent_slice_segment_flag,
2161            slice_segment_address,
2162            slice_reserved_flags,
2163            slice_type,
2164            pic_output_flag,
2165            colour_plane_id,
2166            slice_pic_order_cnt_lsb,
2167            short_term_ref_pic_set_sps_flag,
2168            inline_short_term_ref_pic_set,
2169            short_term_ref_pic_set_idx,
2170            num_long_term_sps,
2171            num_long_term_pics,
2172            long_term_ref_pics,
2173            slice_temporal_mvp_enabled_flag,
2174            slice_sao_luma_flag,
2175            slice_sao_chroma_flag,
2176            num_ref_idx_active_override_flag,
2177            num_ref_idx_l0_active_minus1,
2178            num_ref_idx_l1_active_minus1,
2179            mvd_l1_zero_flag,
2180            cabac_init_flag,
2181            collocated_from_l0_flag,
2182            collocated_ref_idx,
2183            five_minus_max_num_merge_cand,
2184            use_integer_mv_flag,
2185            pred_weight_table,
2186            slice_qp_delta: Some(slice_qp_delta),
2187            slice_cb_qp_offset,
2188            slice_cr_qp_offset,
2189            slice_act_y_qp_offset,
2190            slice_act_cb_qp_offset,
2191            slice_act_cr_qp_offset,
2192            cu_chroma_qp_offset_enabled_flag,
2193            deblocking: Some(deblocking),
2194            slice_loop_filter_across_slices_enabled_flag: Some(
2195                slice_loop_filter_across_slices_enabled_flag,
2196            ),
2197            entry_point_offsets,
2198            slice_segment_header_extension_length,
2199            byte_offset_to_slice_data: Some(byte_offset),
2200            ref_pic_lists_modification,
2201            opaque_tail: None,
2202        })
2203    }
2204
2205    /// `SliceQpY = 26 + init_qp_minus26 + slice_qp_delta` (equation
2206    /// 7-54). Returns `None` when `slice_qp_delta` was not parsed (a
2207    /// deferred body); `pps` supplies `init_qp_minus26`.
2208    pub fn slice_qp_y(&self, pps: &PicParameterSet) -> Option<i32> {
2209        self.slice_qp_delta.map(|d| 26 + pps.init_qp_minus26 + d)
2210    }
2211
2212    /// `MaxNumMergeCand` per §7.4.7.1 equation 7-53:
2213    /// `MaxNumMergeCand = 5 - five_minus_max_num_merge_cand`. Returns
2214    /// `None` for I slices (the field is absent) and for headers whose
2215    /// parse stopped before the merge-candidate block. The derived
2216    /// value is guaranteed to lie in 1..=5 — `Self::parse` rejects an
2217    /// out-of-range wire value at decode time.
2218    pub fn max_num_merge_cand(&self) -> Option<u8> {
2219        self.five_minus_max_num_merge_cand
2220            .map(|v| 5u8.saturating_sub(v as u8))
2221    }
2222}
2223
2224/// `ChromaArrayType` per §7.4.2.2: equal to `chroma_format_idc` unless
2225/// `separate_colour_plane_flag == 1`, in which case it is 0.
2226fn chroma_array_type(sps: &SeqParameterSet) -> u8 {
2227    if sps.separate_colour_plane_flag {
2228        0
2229    } else {
2230        sps.chroma_format_idc
2231    }
2232}
2233
2234/// `PicSizeInCtbsY = PicWidthInCtbsY * PicHeightInCtbsY`
2235/// (equations 7-15/7-17/7-19). `CtbSizeY = 1 << CtbLog2SizeY` and the
2236/// per-dimension counts use ceiling division.
2237fn pic_size_in_ctbs_y(sps: &SeqParameterSet) -> u32 {
2238    let ctb_size = 1u32 << sps.log2_ctb_size();
2239    let width_in_ctbs = sps.pic_width_in_luma_samples.div_ceil(ctb_size);
2240    let height_in_ctbs = sps.pic_height_in_luma_samples.div_ceil(ctb_size);
2241    width_in_ctbs * height_in_ctbs
2242}
2243
2244/// `PicHeightInCtbsY` per equation 7-19: the picture height in CTBs,
2245/// i.e. `Ceil(pic_height_in_luma_samples / CtbSizeY)`.
2246fn pic_height_in_ctbs_y(sps: &SeqParameterSet) -> u32 {
2247    let ctb_size = 1u32 << sps.log2_ctb_size();
2248    sps.pic_height_in_luma_samples.div_ceil(ctb_size)
2249}
2250
2251/// §7.3.6.1 — the entry-point-offset block (`num_entry_point_offsets`,
2252/// `offset_len_minus1`, `entry_point_offset_minus1[i]`), present when
2253/// tiles or entropy-coding sync are enabled. Signalled by BOTH
2254/// independent and dependent slice segments.
2255fn parse_entry_point_offsets(
2256    br: &mut BitReader<'_>,
2257    sps: &SeqParameterSet,
2258    pps: &PicParameterSet,
2259) -> Result<Option<EntryPointOffsets>, SliceError> {
2260    if !(pps.tiles_enabled_flag || pps.entropy_coding_sync_enabled_flag) {
2261        return Ok(None);
2262    }
2263    let num_entry_point_offsets = br.ue()?;
2264    let max_num_entry_point_offsets = num_entry_point_offsets_upper_bound(sps, pps);
2265    if num_entry_point_offsets > max_num_entry_point_offsets {
2266        return Err(SliceError::ValueOutOfRange {
2267            field: "num_entry_point_offsets",
2268            got: num_entry_point_offsets as i64,
2269        });
2270    }
2271    let (offset_len_minus1, entry_point_offset_minus1) = if num_entry_point_offsets > 0 {
2272        let v = br.ue()?;
2273        if v > 31 {
2274            return Err(SliceError::ValueOutOfRange {
2275                field: "offset_len_minus1",
2276                got: v as i64,
2277            });
2278        }
2279        let len = v as u8;
2280        let bits = len + 1;
2281        let mut offsets = Vec::with_capacity(num_entry_point_offsets as usize);
2282        for _ in 0..num_entry_point_offsets {
2283            offsets.push(br.u(bits)?);
2284        }
2285        (len, offsets)
2286    } else {
2287        (0, Vec::new())
2288    };
2289    Ok(Some(EntryPointOffsets {
2290        num_entry_point_offsets,
2291        offset_len_minus1,
2292        entry_point_offset_minus1,
2293    }))
2294}
2295
2296/// §7.3.6.1 — the slice-segment-header extension block (length +
2297/// skipped payload bytes), present for BOTH independent and dependent
2298/// slice segments when the PPS signals it.
2299fn parse_header_extension(
2300    br: &mut BitReader<'_>,
2301    pps: &PicParameterSet,
2302) -> Result<Option<u32>, SliceError> {
2303    if !pps.slice_segment_header_extension_present_flag {
2304        return Ok(None);
2305    }
2306    let len = br.ue()?;
2307    for _ in 0..len {
2308        br.skip(8)?;
2309    }
2310    Ok(Some(len))
2311}
2312
2313/// §7.4.7.1 upper bound on the slice header's
2314/// `num_entry_point_offsets` for the active PPS partitioning. The
2315/// caller has already gated on `tiles_enabled_flag ||
2316/// entropy_coding_sync_enabled_flag`.
2317fn num_entry_point_offsets_upper_bound(sps: &SeqParameterSet, pps: &PicParameterSet) -> u32 {
2318    // §7.4.7.1, three-way constraint:
2319    //   * tiles == 0, sync == 1  ⇒ 0 .. PicHeightInCtbsY − 1
2320    //   * tiles == 1, sync == 0  ⇒ 0 .. cols * rows − 1
2321    //   * tiles == 1, sync == 1  ⇒ 0 .. cols * PicHeightInCtbsY − 1
2322    // (wavefront rows counted per tile COLUMN — every tile row of a
2323    // column contributes its CTB rows, summing to PicHeightInCtbsY).
2324    // Arithmetic in u64 keeps the parser defensive against a
2325    // pathological PPS even though no conforming level overflows u32.
2326    let bound = if pps.tiles_enabled_flag {
2327        let cols = u64::from(pps.tiles.num_tile_columns_minus1) + 1;
2328        if pps.entropy_coding_sync_enabled_flag {
2329            cols.saturating_mul(u64::from(pic_height_in_ctbs_y(sps)))
2330                .saturating_sub(1)
2331        } else {
2332            let rows = u64::from(pps.tiles.num_tile_rows_minus1) + 1;
2333            cols.saturating_mul(rows).saturating_sub(1)
2334        }
2335    } else {
2336        u64::from(pic_height_in_ctbs_y(sps)).saturating_sub(1)
2337    };
2338    u32::try_from(bound).unwrap_or(u32::MAX)
2339}
2340
2341/// `Ceil( Log2( n ) )` — the §7.4.7.1 width formula for
2342/// `slice_segment_address`. For `n <= 1` the width is 0 bits (a
2343/// single-CTB picture has no address to signal).
2344fn ceil_log2(n: u32) -> u8 {
2345    if n <= 1 {
2346        0
2347    } else {
2348        // Ceil(Log2(n)) = bit-width of (n - 1).
2349        (32 - (n - 1).leading_zeros()) as u8
2350    }
2351}
2352
2353/// Parse one `se(v)` chroma QP offset, range-checked to −12..=12.
2354fn parse_qp_offset(br: &mut BitReader<'_>, field: &'static str) -> Result<i8, SliceError> {
2355    let v = br.se()?;
2356    if !(-12..=12).contains(&v) {
2357        return Err(SliceError::ValueOutOfRange {
2358            field,
2359            got: v as i64,
2360        });
2361    }
2362    Ok(v as i8)
2363}
2364
2365/// Parse a `slice_act_*_qp_offset` (`se(v)`, §7.3.6.1) and enforce the
2366/// §7.4.7.1 conformance bound on its sum with the PPS-level offset:
2367/// `pps_act_qp_offset + slice_act_qp_offset` must lie in −12..=12.
2368fn parse_slice_act_qp_offset(
2369    br: &mut BitReader<'_>,
2370    field: &'static str,
2371    pps_act_qp_offset: i32,
2372) -> Result<i32, SliceError> {
2373    let v = br.se()?;
2374    let sum = pps_act_qp_offset + v;
2375    if !(-12..=12).contains(&sum) {
2376        return Err(SliceError::ValueOutOfRange {
2377            field,
2378            got: sum as i64,
2379        });
2380    }
2381    Ok(v)
2382}
2383
2384/// Parse the deblocking-filter override block of §7.3.6.1, applying the
2385/// §7.4.7.1 inference rules for the absent fields.
2386fn parse_slice_deblocking(
2387    br: &mut BitReader<'_>,
2388    pps: &PicParameterSet,
2389) -> Result<SliceDeblocking, SliceError> {
2390    // deblocking_filter_override_flag only present when
2391    // deblocking_filter_override_enabled_flag (PPS).
2392    let override_flag = if pps.deblocking.override_enabled_flag {
2393        br.u1()? != 0
2394    } else {
2395        false
2396    };
2397
2398    if !override_flag {
2399        // Inferred from the PPS (§7.4.7.1).
2400        return Ok(SliceDeblocking {
2401            disabled_flag: pps.deblocking.disabled_flag,
2402            beta_offset_div2: pps.deblocking.beta_offset_div2,
2403            tc_offset_div2: pps.deblocking.tc_offset_div2,
2404        });
2405    }
2406
2407    let disabled_flag = br.u1()? != 0;
2408    let (beta, tc) = if !disabled_flag {
2409        let beta = br.se()?;
2410        if !(-6..=6).contains(&beta) {
2411            return Err(SliceError::ValueOutOfRange {
2412                field: "slice_beta_offset_div2",
2413                got: beta as i64,
2414            });
2415        }
2416        let tc = br.se()?;
2417        if !(-6..=6).contains(&tc) {
2418            return Err(SliceError::ValueOutOfRange {
2419                field: "slice_tc_offset_div2",
2420                got: tc as i64,
2421            });
2422        }
2423        (beta as i8, tc as i8)
2424    } else {
2425        // When deblocking is disabled the offsets are not signalled and
2426        // are inferred to 0 (their effect is moot when disabled).
2427        (0, 0)
2428    };
2429
2430    Ok(SliceDeblocking {
2431        disabled_flag,
2432        beta_offset_div2: beta,
2433        tc_offset_div2: tc,
2434    })
2435}
2436
2437/// Parse the long-term-ref-pic block of §7.3.6.1 (the body gated by
2438/// `long_term_ref_pics_present_flag` on the SPS), returning the parsed
2439/// `(num_long_term_sps, num_long_term_pics, entries)` triple.
2440///
2441/// The block:
2442///
2443/// ```text
2444///   if( num_long_term_ref_pics_sps > 0 )
2445///       num_long_term_sps  ue(v)
2446///   num_long_term_pics      ue(v)
2447///   for( i = 0; i < num_long_term_sps + num_long_term_pics; i++ ) {
2448///       if( i < num_long_term_sps ) {
2449///           if( num_long_term_ref_pics_sps > 1 )
2450///               lt_idx_sps[i]   u(v) — Ceil(Log2(num_long_term_ref_pics_sps))
2451///       } else {
2452///           poc_lsb_lt[i]                    u(v) — log2_max_poc_lsb_minus4+4
2453///           used_by_curr_pic_lt_flag[i]      u(1)
2454///       }
2455///       delta_poc_msb_present_flag[i]        u(1)
2456///       if( delta_poc_msb_present_flag[i] )
2457///           delta_poc_msb_cycle_lt[i]        ue(v)
2458///   }
2459/// ```
2460fn parse_long_term_ref_pic_block(
2461    br: &mut BitReader<'_>,
2462    sps: &SeqParameterSet,
2463) -> Result<(u32, u32, Vec<SliceLongTermRefPic>), SliceError> {
2464    let num_long_term_sps = if sps.num_long_term_ref_pics_sps > 0 {
2465        let v = br.ue()?;
2466        if v > sps.num_long_term_ref_pics_sps {
2467            return Err(SliceError::ValueOutOfRange {
2468                field: "num_long_term_sps",
2469                got: v as i64,
2470            });
2471        }
2472        v
2473    } else {
2474        0
2475    };
2476    let num_long_term_pics = br.ue()?;
2477    // §7.4.7.1 bounds num_long_term_pics by the SPS DPB capacity; we
2478    // apply a defensive sanity ceiling instead of computing the full
2479    // DPB-derived bound (which needs RPS counts not yet wired through
2480    // here). A pathological encoder could otherwise drive an unbounded
2481    // allocation.
2482    if num_long_term_pics > HEVC_MAX_LONG_TERM_PICS_IN_SLICE as u32 {
2483        return Err(SliceError::ValueOutOfRange {
2484            field: "num_long_term_pics",
2485            got: num_long_term_pics as i64,
2486        });
2487    }
2488    let total = num_long_term_sps + num_long_term_pics;
2489    let lt_idx_bits = if sps.num_long_term_ref_pics_sps > 1 {
2490        ceil_log2(sps.num_long_term_ref_pics_sps)
2491    } else {
2492        0
2493    };
2494    let poc_lsb_bits = sps.log2_max_pic_order_cnt_lsb_minus4 + 4;
2495
2496    let mut entries = Vec::with_capacity(total as usize);
2497    for i in 0..total {
2498        let source = if i < num_long_term_sps {
2499            let lt_idx_sps = if lt_idx_bits > 0 {
2500                let v = br.u(lt_idx_bits)?;
2501                if v >= sps.num_long_term_ref_pics_sps {
2502                    return Err(SliceError::ValueOutOfRange {
2503                        field: "lt_idx_sps",
2504                        got: v as i64,
2505                    });
2506                }
2507                v
2508            } else {
2509                0
2510            };
2511            SliceLongTermRefPicSource::Sps { lt_idx_sps }
2512        } else {
2513            let poc_lsb_lt = br.u(poc_lsb_bits)?;
2514            let used_by_curr_pic_lt_flag = br.u1()? != 0;
2515            SliceLongTermRefPicSource::InSlice {
2516                poc_lsb_lt,
2517                used_by_curr_pic_lt_flag,
2518            }
2519        };
2520        let delta_poc_msb_present_flag = br.u1()? != 0;
2521        let delta_poc_msb_cycle_lt = if delta_poc_msb_present_flag {
2522            br.ue()?
2523        } else {
2524            0
2525        };
2526        entries.push(SliceLongTermRefPic {
2527            source,
2528            delta_poc_msb_present_flag,
2529            delta_poc_msb_cycle_lt,
2530        });
2531    }
2532    Ok((num_long_term_sps, num_long_term_pics, entries))
2533}
2534
2535/// Resolution of the active short-term RPS for the in-place
2536/// `NumPicTotalCurr` derivation at the §7.3.6.1
2537/// `ref_pic_lists_modification()` gate. The result is always the
2538/// post-§7.4.8 materialised form (explicit or inter-predicted both
2539/// produce the same shape).
2540enum ActiveShortTermRps {
2541    /// The active short-term RPS has been resolved to its post-§7.4.8
2542    /// form. The contained `UsedByCurrPicS{0,1}` arrays are the
2543    /// per-position flags consumed by equation 7-57.
2544    Materialized(crate::sps::MaterializedShortTermRefPicSet),
2545    /// The slice has no active short-term RPS (an IDR slice, where the
2546    /// non-IDR POC/RPS block is absent). `NumPicTotalCurr` is `0`.
2547    Empty,
2548    /// Materialisation of the active RPS failed — for instance because
2549    /// the inter-RPS-prediction `used_by_curr_pic_flag` /
2550    /// `use_delta_flag` arrays did not match the source RPS's
2551    /// `NumDeltaPocs[RefRpsIdx] + 1`. The slice parser surfaces this
2552    /// as a deferred opaque tail so the caller can investigate without
2553    /// the parse aborting.
2554    MaterializeFailed,
2555}
2556
2557/// Resolve the active short-term RPS for the current slice given the
2558/// already-parsed §7.3.6.1 RPS gate state, running the §7.4.8
2559/// derivation against the SPS list when needed. See §7.4.8 for the
2560/// `stRpsIdx` selection.
2561fn resolve_active_short_term_rps(
2562    sps: &SeqParameterSet,
2563    short_term_ref_pic_set_sps_flag: Option<bool>,
2564    inline_rps: Option<&ShortTermRefPicSet>,
2565    short_term_ref_pic_set_idx: Option<u32>,
2566) -> ActiveShortTermRps {
2567    // Materialise the SPS list once; we may need it both as a source
2568    // for the slice-inline inter-RPS-prediction and as the active RPS
2569    // for the SPS form. The list is short (cap
2570    // `HEVC_MAX_NUM_SHORT_TERM_RPS = 64`) so this is inexpensive
2571    // relative to a frame decode.
2572    let sps_materialised = match sps.materialize_short_term_ref_pic_sets() {
2573        Ok(v) => v,
2574        Err(_) => return ActiveShortTermRps::MaterializeFailed,
2575    };
2576    match short_term_ref_pic_set_sps_flag {
2577        None => ActiveShortTermRps::Empty,
2578        Some(false) => match inline_rps {
2579            None => ActiveShortTermRps::Empty,
2580            Some(rps) => {
2581                let source = if rps.inter_ref_pic_set_prediction_flag {
2582                    // For the slice-inline form `stRpsIdx ==
2583                    // num_short_term_ref_pic_sets` and the source is
2584                    // `RefRpsIdx = num_short_term_ref_pic_sets -
2585                    // (delta_idx_minus1 + 1)` per equation 7-59.
2586                    let st_rps_idx = sps.num_short_term_ref_pic_sets as i64;
2587                    let ref_rps_idx = st_rps_idx - (rps.delta_idx_minus1 as i64 + 1);
2588                    if ref_rps_idx < 0 {
2589                        return ActiveShortTermRps::MaterializeFailed;
2590                    }
2591                    sps_materialised.get(ref_rps_idx as usize)
2592                } else {
2593                    None
2594                };
2595                match rps.materialize(source) {
2596                    Ok(m) => ActiveShortTermRps::Materialized(m),
2597                    Err(_) => ActiveShortTermRps::MaterializeFailed,
2598                }
2599            }
2600        },
2601        Some(true) => {
2602            // §7.4.7.1: when not signalled (because
2603            // `num_short_term_ref_pic_sets <= 1`), the index is
2604            // inferred to 0.
2605            let idx = short_term_ref_pic_set_idx.unwrap_or(0) as usize;
2606            match sps_materialised.into_iter().nth(idx) {
2607                None => ActiveShortTermRps::Empty,
2608                Some(m) => ActiveShortTermRps::Materialized(m),
2609            }
2610        }
2611    }
2612}
2613
2614/// Resolve the per-entry `UsedByCurrPicLt[i]` flags for the active
2615/// long-term-ref-pic block, per §7.4.7.1. SPS-resident entries pick
2616/// `used_by_curr_pic_lt_sps_flag[lt_idx_sps[i]]`; in-slice entries
2617/// carry the flag directly on the wire.
2618///
2619/// Returns `false` for any out-of-range SPS lookup (the slice parser
2620/// rejects `lt_idx_sps >= num_long_term_ref_pics_sps` already; the
2621/// fallback is defensive).
2622fn collect_used_by_curr_pic_lt(
2623    entries: &[SliceLongTermRefPic],
2624    sps: &SeqParameterSet,
2625) -> Vec<bool> {
2626    entries
2627        .iter()
2628        .map(|e| e.used_by_curr_pic_lt(sps).unwrap_or(false))
2629        .collect()
2630}
2631
2632/// Defensive upper bound on `num_long_term_pics` (§7.4.7.1 bounds the
2633/// value by `sps_max_dec_pic_buffering_minus1[TemporalId] − …`; an
2634/// HEVC DPB is bounded by `MaxDpbSize` which is bounded by
2635/// `sps_max_dec_pic_buffering_minus1` ≤ 15 per §7.4.3.2.1).
2636const HEVC_MAX_LONG_TERM_PICS_IN_SLICE: usize = 16;
2637
2638/// Consume `byte_alignment()` (§7.3.2.4): one `alignment_bit_equal_to_one`
2639/// followed by `alignment_bit_equal_to_zero` bits until the cursor is on
2640/// a byte boundary. Returns the byte offset (from the start of the RBSP)
2641/// of the first byte that follows.
2642fn consume_byte_alignment(br: &mut BitReader<'_>) -> Result<usize, SliceError> {
2643    // alignment_bit_equal_to_one.
2644    let _ = br.u1()?;
2645    while br.bit_pos() % 8 != 0 {
2646        let _ = br.u1()?;
2647    }
2648    Ok(br.bit_pos() / 8)
2649}
2650
2651#[cfg(test)]
2652mod tests {
2653    use super::*;
2654    use crate::sps::LongTermRefPicEntry;
2655
2656    /// Build a minimal SPS for slice-header parsing context. Only the
2657    /// fields the slice parser reads are populated meaningfully; the
2658    /// rest carry defaults that do not affect the header parse.
2659    #[allow(clippy::too_many_arguments)]
2660    fn ctx_sps(
2661        chroma_format_idc: u8,
2662        separate_colour_plane_flag: bool,
2663        sao: bool,
2664        mvp: bool,
2665        width: u32,
2666        height: u32,
2667        log2_diff_max_min_cb: u8,
2668        log2_min_cb_minus3: u8,
2669        log2_max_poc_lsb_minus4: u8,
2670    ) -> SeqParameterSet {
2671        // Hand-assemble the smallest valid SPS RBSP that decodes to the
2672        // requested gate values, by parsing the tiny fixture's SPS and
2673        // patching the relevant fields. The slice parser only consults
2674        // chroma_format_idc, separate_colour_plane_flag,
2675        // sample_adaptive_offset_enabled_flag,
2676        // sps_temporal_mvp_enabled_flag, the CTB / picture-size
2677        // derivations, and log2_max_pic_order_cnt_lsb_minus4, so a
2678        // patched struct is sufficient for these unit tests.
2679        let mut sps = SeqParameterSet::parse(TINY_SPS_RBSP).expect("tiny SPS");
2680        sps.chroma_format_idc = chroma_format_idc;
2681        sps.separate_colour_plane_flag = separate_colour_plane_flag;
2682        sps.sample_adaptive_offset_enabled_flag = sao;
2683        sps.sps_temporal_mvp_enabled_flag = mvp;
2684        sps.pic_width_in_luma_samples = width;
2685        sps.pic_height_in_luma_samples = height;
2686        sps.log2_diff_max_min_luma_coding_block_size = log2_diff_max_min_cb;
2687        sps.log2_min_luma_coding_block_size_minus3 = log2_min_cb_minus3;
2688        sps.log2_max_pic_order_cnt_lsb_minus4 = log2_max_poc_lsb_minus4;
2689        sps
2690    }
2691
2692    /// SPS RBSP body from the tiny fixture (see `sps.rs` tests).
2693    const TINY_SPS_RBSP: &[u8] = &[
2694        0x01, 0x04, 0x08, 0x00, 0x00, 0x00, 0x9F, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x1E, 0xA0, 0x88,
2695        0x45, 0x96, 0xEA, 0xAF, 0x2B, 0xC0, 0x5A, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
2696        0x32, 0x10,
2697    ];
2698    /// PPS RBSP body from the tiny fixture (see `pps.rs` tests).
2699    const TINY_PPS_RBSP: &[u8] = &[0xC1, 0x73, 0xC0, 0x89];
2700
2701    #[test]
2702    fn slice_type_table_7_7() {
2703        assert_eq!(SliceType::from_raw(0).unwrap(), SliceType::B);
2704        assert_eq!(SliceType::from_raw(1).unwrap(), SliceType::P);
2705        assert_eq!(SliceType::from_raw(2).unwrap(), SliceType::I);
2706        assert!(SliceType::from_raw(3).is_err());
2707        assert!(!SliceType::I.is_inter());
2708        assert!(SliceType::P.is_inter());
2709        assert!(SliceType::B.is_inter());
2710    }
2711
2712    #[test]
2713    fn ceil_log2_widths() {
2714        // §7.4.7.1: slice_segment_address width = Ceil(Log2(N)).
2715        assert_eq!(ceil_log2(1), 0);
2716        assert_eq!(ceil_log2(2), 1);
2717        assert_eq!(ceil_log2(3), 2);
2718        assert_eq!(ceil_log2(4), 2);
2719        assert_eq!(ceil_log2(5), 3);
2720        assert_eq!(ceil_log2(8), 3);
2721        assert_eq!(ceil_log2(9), 4);
2722    }
2723
2724    /// Hand-assembled minimal independent I-slice IDR header. With the
2725    /// fixture SPS/PPS gates (sao=1, mvp=1, chroma=1, single CTB,
2726    /// loop-filter-across-slices=1) the bit layout is:
2727    ///   first(1)=1 no_output(1)=0 pps_id ue=1 (->0)
2728    ///   slice_type ue=011 (->2,I) mvp(1)=0 sao_l(1)=1 sao_c(1)=0
2729    ///   qp_delta se=011 (->-1) lf_across(1)=1
2730    ///   byte_alignment: 1 then zeros.
2731    /// We assemble those bits ourselves so the parse is fully
2732    /// controlled (the tiny fixture's own slice trace is internally
2733    /// inconsistent — see the module-level note / docs gap).
2734    #[test]
2735    fn parses_hand_assembled_i_idr_header() {
2736        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
2737        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
2738        // pps from the fixture has loop_filter_across_slices=1,
2739        // tiles=0, ecs=0, deblock-ctrl-present=0, chroma-qp-off=0.
2740
2741        // Build the bit string.
2742        let bits = concat_bits(&[
2743            (1, 1),     // first_slice_segment_in_pic_flag
2744            (0, 1),     // no_output_of_prior_pics_flag (IRAP)
2745            (0b1, 1),   // pps_id ue(v) '1' -> 0
2746            (0b011, 3), // slice_type ue(v) '011' -> 2 (I)
2747            // slice_temporal_mvp_enabled_flag: absent for an IDR — the
2748            // §7.3.6.1 flag sits inside the non-IDR block.
2749            (1, 1),     // slice_sao_luma_flag
2750            (0, 1),     // slice_sao_chroma_flag (chroma!=0)
2751            (0b011, 3), // slice_qp_delta se(v) '011' -> -1
2752            (1, 1),     // slice_loop_filter_across_slices_enabled_flag
2753            (1, 1),     // byte_alignment: alignment_bit_equal_to_one
2754        ]);
2755        let rbsp = pack_bits(&bits);
2756
2757        let sh = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect("slice header");
2758        assert!(sh.first_slice_segment_in_pic_flag);
2759        assert_eq!(sh.no_output_of_prior_pics_flag, Some(false));
2760        assert_eq!(sh.slice_pic_parameter_set_id, 0);
2761        assert!(!sh.dependent_slice_segment_flag);
2762        assert_eq!(sh.slice_segment_address, 0);
2763        assert_eq!(sh.slice_type, Some(SliceType::I));
2764        assert!(!sh.slice_temporal_mvp_enabled_flag);
2765        assert!(sh.slice_sao_luma_flag);
2766        assert!(!sh.slice_sao_chroma_flag);
2767        assert_eq!(sh.slice_qp_delta, Some(-1));
2768        assert_eq!(sh.slice_loop_filter_across_slices_enabled_flag, Some(true));
2769        assert!(sh.opaque_tail.is_none());
2770        // init_qp from the fixture PPS is 26, so SliceQpY = 26 + -1 = 25.
2771        assert_eq!(sh.slice_qp_y(&pps), Some(25));
2772        // The header must byte-align: with 1+1+1+3+1+1+3+1=12 bits
2773        // before alignment, alignment consumes bits 12..16 (one '1'
2774        // plus zero pad), so slice data begins at byte 2.
2775        assert_eq!(sh.byte_offset_to_slice_data, Some(2));
2776    }
2777
2778    /// I-slice whose PPS carries an SCC body with
2779    /// `pps_slice_act_qp_offsets_present_flag == 1`: the three
2780    /// `slice_act_{y,cb,cr}_qp_offset` se(v) fields (§7.3.6.1) are
2781    /// parsed after `slice_qp_delta`, with the §7.4.7.1 sum bound
2782    /// enforced against the PPS-level offsets.
2783    #[test]
2784    fn parses_slice_act_qp_offsets_when_pps_signals_them() {
2785        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
2786        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
2787        pps.pps_scc_extension = Some(crate::pps::PpsSccExtension {
2788            pps_slice_act_qp_offsets_present_flag: true,
2789            ..Default::default()
2790        });
2791        let bits = concat_bits(&[
2792            (1, 1),     // first_slice_segment_in_pic_flag
2793            (0, 1),     // no_output_of_prior_pics_flag (IRAP)
2794            (0b1, 1),   // pps_id ue -> 0
2795            (0b011, 3), // slice_type ue -> I
2796            // slice_temporal_mvp_enabled_flag: absent for an IDR.
2797            (1, 1),     // slice_sao_luma_flag
2798            (0, 1),     // slice_sao_chroma_flag
2799            (0b011, 3), // slice_qp_delta se '011' -> -1
2800            // pps_slice_chroma_qp_offsets_present_flag = 0 → absent.
2801            // slice_act_*_qp_offset (PpsActQpOffset* all 0 here):
2802            (0b1, 1),   // slice_act_y_qp_offset se '1' -> 0
2803            (0b010, 3), // slice_act_cb_qp_offset se '010' -> +1
2804            (0b011, 3), // slice_act_cr_qp_offset se '011' -> -1
2805            // chroma_qp_offset_list_enabled_flag = 0 →
2806            // cu_chroma_qp_offset_enabled_flag absent.
2807            (1, 1), // slice_loop_filter_across_slices_enabled_flag
2808            (1, 1), // byte_alignment '1'
2809        ]);
2810        let rbsp = pack_bits(&bits);
2811        let sh = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect("slice header");
2812        assert_eq!(sh.slice_type, Some(SliceType::I));
2813        assert_eq!(sh.slice_qp_delta, Some(-1));
2814        assert_eq!(sh.slice_act_y_qp_offset, 0);
2815        assert_eq!(sh.slice_act_cb_qp_offset, 1);
2816        assert_eq!(sh.slice_act_cr_qp_offset, -1);
2817        assert!(!sh.cu_chroma_qp_offset_enabled_flag);
2818        assert!(sh.opaque_tail.is_none());
2819        assert!(sh.byte_offset_to_slice_data.is_some());
2820    }
2821
2822    /// I-slice whose PPS range-extension sets
2823    /// `chroma_qp_offset_list_enabled_flag == 1`: the
2824    /// `cu_chroma_qp_offset_enabled_flag` u(1) (§7.3.6.1) is parsed
2825    /// after the QP-offset block.
2826    #[test]
2827    fn parses_cu_chroma_qp_offset_enabled_flag_when_list_enabled() {
2828        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
2829        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
2830        pps.pps_range_extension = Some(crate::pps::PpsRangeExtension {
2831            chroma_qp_offset_list_enabled_flag: true,
2832            ..Default::default()
2833        });
2834        let bits = concat_bits(&[
2835            (1, 1),     // first_slice_segment_in_pic_flag
2836            (0, 1),     // no_output_of_prior_pics_flag
2837            (0b1, 1),   // pps_id ue -> 0
2838            (0b011, 3), // slice_type ue -> I
2839            // slice_temporal_mvp_enabled_flag: absent for an IDR.
2840            (1, 1),     // slice_sao_luma_flag
2841            (0, 1),     // slice_sao_chroma_flag
2842            (0b011, 3), // slice_qp_delta se -> -1
2843            // act offsets absent (pps_slice_act_qp_offsets_present_flag 0)
2844            (1, 1), // cu_chroma_qp_offset_enabled_flag = 1
2845            (1, 1), // slice_loop_filter_across_slices_enabled_flag
2846            (1, 1), // byte_alignment '1'
2847        ]);
2848        let rbsp = pack_bits(&bits);
2849        let sh = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect("slice header");
2850        assert_eq!(sh.slice_type, Some(SliceType::I));
2851        assert!(sh.cu_chroma_qp_offset_enabled_flag);
2852        assert!(sh.opaque_tail.is_none());
2853    }
2854
2855    /// §7.4.7.1: a `slice_act_y_qp_offset` whose sum with the PPS-level
2856    /// `PpsActQpOffsetY` falls outside −12..=12 is rejected.
2857    #[test]
2858    fn rejects_out_of_range_slice_act_qp_offset_sum() {
2859        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
2860        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
2861        // PpsActQpOffsetY = +10 (pps_act_y_qp_offset_plus5 = 15).
2862        pps.pps_scc_extension = Some(crate::pps::PpsSccExtension {
2863            pps_slice_act_qp_offsets_present_flag: true,
2864            pps_act_y_qp_offset_plus5: 15,
2865            ..Default::default()
2866        });
2867        let bits = concat_bits(&[
2868            (1, 1),     // first_slice_segment_in_pic_flag
2869            (0, 1),     // no_output_of_prior_pics_flag
2870            (0b1, 1),   // pps_id ue -> 0
2871            (0b011, 3), // slice_type ue -> I
2872            // slice_temporal_mvp_enabled_flag: absent for an IDR.
2873            (1, 1),     // slice_sao_luma_flag
2874            (0, 1),     // slice_sao_chroma_flag
2875            (0b011, 3), // slice_qp_delta se -> -1
2876            // slice_act_y_qp_offset = +5 (se '0001010') → sum 10+5 = 15 > 12
2877            (0b0001010, 7),
2878        ]);
2879        let rbsp = pack_bits(&bits);
2880        let err =
2881            SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect_err("act sum range");
2882        assert!(matches!(
2883            err,
2884            SliceError::ValueOutOfRange {
2885                field: "slice_act_y_qp_offset",
2886                got: 15
2887            }
2888        ));
2889    }
2890
2891    /// Non-IDR **I-slice** (CRA, type 21 — in the IRAP range so
2892    /// `no_output_of_prior_pics_flag` is present): the POC + RPS block
2893    /// is now parsed inline (round 105), so the parser reaches
2894    /// `byte_alignment()` with no opaque tail.
2895    #[test]
2896    fn parses_non_idr_i_slice_cra_with_inline_zero_rps() {
2897        // Tiny SPS context: num_short_term_ref_pic_sets = 0, so
2898        // short_term_ref_pic_set_sps_flag must be 0 and the in-line RPS
2899        // (stRpsIdx = 0, no inter-RPS-prediction signal) reads
2900        // num_negative_pics + num_positive_pics, both 0.
2901        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
2902        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
2903
2904        let bits = concat_bits(&[
2905            (1, 1),     // first_slice_segment_in_pic_flag
2906            (0, 1),     // no_output_of_prior_pics_flag (CRA = 21 ∈ IRAP range)
2907            (0b1, 1),   // pps_id ue '1' -> 0
2908            (0b011, 3), // slice_type ue '011' -> 2 (I)
2909            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
2910            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
2911            // in-line st_ref_pic_set( num_short_term_ref_pic_sets = 0 ):
2912            // stRpsIdx == 0 so inter_ref_pic_set_prediction_flag absent.
2913            (0b1, 1), // num_negative_pics ue '1' -> 0
2914            (0b1, 1), // num_positive_pics ue '1' -> 0
2915            (0, 1),   // slice_temporal_mvp_enabled_flag = 0
2916            (1, 1),   // slice_sao_luma_flag = 1
2917            (0, 1),   // slice_sao_chroma_flag = 0
2918            (0b1, 1), // slice_qp_delta se '1' -> 0
2919            // pps.lf_across=1 and sao_luma OR !deblock.disabled => present.
2920            // PPS deblocking override_enabled_flag=0, disabled_flag=0 by
2921            // inference => !disabled_flag is true => gate fires.
2922            (1, 1), // slice_loop_filter_across_slices_enabled_flag = 1
2923            (1, 1), // byte_alignment one-bit
2924        ]);
2925        let rbsp = pack_bits(&bits);
2926        // CRA_NUT (NAL type 21) is in BLA_W_LP..=RSV_IRAP_VCL23 (16..=23).
2927        let sh = SliceSegmentHeader::parse(&rbsp, 21, &sps, &pps).expect("slice header");
2928        assert!(sh.first_slice_segment_in_pic_flag);
2929        assert_eq!(sh.no_output_of_prior_pics_flag, Some(false));
2930        assert_eq!(sh.slice_type, Some(SliceType::I));
2931        assert_eq!(sh.slice_pic_order_cnt_lsb, Some(0));
2932        assert_eq!(sh.short_term_ref_pic_set_sps_flag, Some(false));
2933        let inline = sh
2934            .inline_short_term_ref_pic_set
2935            .as_ref()
2936            .expect("inline ST RPS");
2937        assert!(!inline.inter_ref_pic_set_prediction_flag);
2938        assert_eq!(inline.num_negative_pics, 0);
2939        assert_eq!(inline.num_positive_pics, 0);
2940        assert!(sh.short_term_ref_pic_set_idx.is_none());
2941        assert_eq!(sh.num_long_term_sps, None); // SPS gate off
2942        assert_eq!(sh.num_long_term_pics, None);
2943        assert!(!sh.slice_temporal_mvp_enabled_flag);
2944        assert!(sh.slice_sao_luma_flag);
2945        assert!(!sh.slice_sao_chroma_flag);
2946        assert_eq!(sh.slice_qp_delta, Some(0));
2947        // Tail consumed, byte_alignment reached.
2948        assert!(sh.opaque_tail.is_none());
2949        assert_eq!(sh.byte_offset_to_slice_data, Some(3));
2950    }
2951
2952    /// Non-IDR I-slice using the SPS-resident ST RPS (the SPS has
2953    /// `num_short_term_ref_pic_sets == 1`, so
2954    /// `short_term_ref_pic_set_idx` is **absent** — its value is
2955    /// inferred to 0 — and the slice header reads no extra bits for
2956    /// the RPS).
2957    #[test]
2958    fn parses_non_idr_i_slice_with_sps_rps_single_entry() {
2959        let mut sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
2960        // Forge an SPS with one short-term-RPS entry so the gate path
2961        // matches the §7.3.6.1 "else if( num_short_term_ref_pic_sets > 1 )"
2962        // branch FALSE path: short_term_ref_pic_set_idx absent.
2963        sps.num_short_term_ref_pic_sets = 1;
2964        sps.short_term_ref_pic_sets = vec![ShortTermRefPicSet {
2965            inter_ref_pic_set_prediction_flag: false,
2966            num_negative_pics: 0,
2967            num_positive_pics: 0,
2968            ..Default::default()
2969        }];
2970        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
2971
2972        let bits = concat_bits(&[
2973            (1, 1),     // first
2974            (0, 1),     // no_output (IRAP)
2975            (0b1, 1),   // pps_id ue '1' -> 0
2976            (0b011, 3), // slice_type ue '011' -> 2 (I)
2977            (0, 8),     // slice_pic_order_cnt_lsb = 0
2978            (1, 1),     // short_term_ref_pic_set_sps_flag = 1
2979            // num_short_term_ref_pic_sets == 1 so short_term_ref_pic_set_idx
2980            // is NOT signalled (inferred 0).
2981            (0, 1),   // slice_temporal_mvp_enabled_flag = 0
2982            (1, 1),   // sao_luma = 1
2983            (0, 1),   // sao_chroma = 0
2984            (0b1, 1), // slice_qp_delta se '1' -> 0
2985            (1, 1),   // lf_across_slices = 1
2986            (1, 1),   // byte_alignment one bit
2987        ]);
2988        let rbsp = pack_bits(&bits);
2989        let sh = SliceSegmentHeader::parse(&rbsp, 21, &sps, &pps).expect("slice header");
2990        assert_eq!(sh.short_term_ref_pic_set_sps_flag, Some(true));
2991        assert!(sh.inline_short_term_ref_pic_set.is_none());
2992        assert!(sh.short_term_ref_pic_set_idx.is_none());
2993        assert_eq!(sh.slice_qp_delta, Some(0));
2994        assert!(sh.opaque_tail.is_none());
2995    }
2996
2997    /// Non-IDR I-slice using the SPS-resident ST RPS with multiple
2998    /// entries: `short_term_ref_pic_set_idx` is signalled `u(v)` with
2999    /// width `Ceil(Log2(num_short_term_ref_pic_sets))`.
3000    #[test]
3001    fn parses_non_idr_i_slice_with_sps_rps_idx_signalled() {
3002        let mut sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3003        sps.num_short_term_ref_pic_sets = 3; // idx width = 2 bits
3004        sps.short_term_ref_pic_sets = vec![
3005            ShortTermRefPicSet::default(),
3006            ShortTermRefPicSet::default(),
3007            ShortTermRefPicSet::default(),
3008        ];
3009        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3010
3011        let bits = concat_bits(&[
3012            (1, 1),
3013            (0, 1),
3014            (0b1, 1),
3015            (0b011, 3),
3016            (0, 8),    // poc_lsb = 0
3017            (1, 1),    // short_term_ref_pic_set_sps_flag = 1
3018            (0b10, 2), // short_term_ref_pic_set_idx u(2) = 2
3019            (0, 1),    // mvp = 0
3020            (1, 1),    // sao_luma
3021            (0, 1),    // sao_chroma
3022            (0b1, 1),  // slice_qp_delta = 0
3023            (1, 1),    // lf_across
3024            (1, 1),    // byte_alignment
3025        ]);
3026        let rbsp = pack_bits(&bits);
3027        let sh = SliceSegmentHeader::parse(&rbsp, 21, &sps, &pps).expect("slice header");
3028        assert_eq!(sh.short_term_ref_pic_set_idx, Some(2));
3029        assert!(sh.inline_short_term_ref_pic_set.is_none());
3030        assert!(sh.opaque_tail.is_none());
3031    }
3032
3033    /// Long-term-ref-pic block: SPS has
3034    /// `long_term_ref_pics_present_flag=1, num_long_term_ref_pics_sps=2`.
3035    /// The slice header carries one SPS-indexed entry plus one in-slice
3036    /// entry, each with `delta_poc_msb_present_flag` and the cycle.
3037    #[test]
3038    fn parses_non_idr_i_slice_with_long_term_block() {
3039        use crate::sps::LongTermRefPicEntry;
3040        let mut sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3041        sps.long_term_ref_pics_present_flag = true;
3042        sps.num_long_term_ref_pics_sps = 2; // lt_idx_sps width = 1 bit
3043        sps.long_term_ref_pics = vec![
3044            LongTermRefPicEntry {
3045                poc_lsb: 0,
3046                used_by_curr_pic: true,
3047            },
3048            LongTermRefPicEntry {
3049                poc_lsb: 4,
3050                used_by_curr_pic: false,
3051            },
3052        ];
3053        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3054
3055        let bits = concat_bits(&[
3056            (1, 1), // first
3057            (0, 1), // no_output (IRAP)
3058            (0b1, 1),
3059            (0b011, 3), // I slice
3060            (0, 8),     // poc_lsb
3061            (0, 1),     // st_sps_flag = 0 (num_st_rps=0)
3062            (0b1, 1),   // num_negative_pics ue '1' -> 0
3063            (0b1, 1),   // num_positive_pics ue '1' -> 0
3064            // Long-term block:
3065            (0b010, 3), // num_long_term_sps ue '010' -> 1
3066            (0b010, 3), // num_long_term_pics ue '010' -> 1
3067            // Entry 0: SPS-indexed (i < num_long_term_sps)
3068            (1, 1), // lt_idx_sps[0] u(1) = 1
3069            (0, 1), // delta_poc_msb_present_flag[0] = 0
3070            // Entry 1: in-slice (i >= num_long_term_sps)
3071            (0b1010, 8), // poc_lsb_lt[1] u(8) = 0xAA bit pattern... use 10
3072            (1, 1),      // used_by_curr_pic_lt_flag[1] = 1
3073            (1, 1),      // delta_poc_msb_present_flag[1] = 1
3074            (0b010, 3),  // delta_poc_msb_cycle_lt[1] ue '010' -> 1
3075            (0, 1),      // slice_temporal_mvp_enabled_flag = 0
3076            (1, 1),      // sao_luma
3077            (0, 1),      // sao_chroma
3078            (0b1, 1),    // slice_qp_delta se -> 0
3079            (1, 1),      // lf_across
3080            (1, 1),      // byte_alignment
3081        ]);
3082        let rbsp = pack_bits(&bits);
3083        let sh = SliceSegmentHeader::parse(&rbsp, 21, &sps, &pps).expect("slice header");
3084        assert_eq!(sh.num_long_term_sps, Some(1));
3085        assert_eq!(sh.num_long_term_pics, Some(1));
3086        assert_eq!(sh.long_term_ref_pics.len(), 2);
3087        match sh.long_term_ref_pics[0].source {
3088            SliceLongTermRefPicSource::Sps { lt_idx_sps } => assert_eq!(lt_idx_sps, 1),
3089            other => panic!("entry 0 should be SPS-indexed: {other:?}"),
3090        }
3091        assert!(!sh.long_term_ref_pics[0].delta_poc_msb_present_flag);
3092        match sh.long_term_ref_pics[1].source {
3093            SliceLongTermRefPicSource::InSlice {
3094                poc_lsb_lt,
3095                used_by_curr_pic_lt_flag,
3096            } => {
3097                assert_eq!(poc_lsb_lt, 0b1010); // 8-bit u(v) field carrying 10
3098                assert!(used_by_curr_pic_lt_flag);
3099            }
3100            other => panic!("entry 1 should be in-slice: {other:?}"),
3101        }
3102        assert!(sh.long_term_ref_pics[1].delta_poc_msb_present_flag);
3103        assert_eq!(sh.long_term_ref_pics[1].delta_poc_msb_cycle_lt, 1);
3104        assert!(sh.opaque_tail.is_none());
3105    }
3106
3107    /// The §7.4.7.1 cross-check: when `num_short_term_ref_pic_sets ==
3108    /// 0`, signalling `short_term_ref_pic_set_sps_flag == 1` is illegal.
3109    #[test]
3110    fn rejects_st_sps_flag_when_no_sps_rps() {
3111        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3112        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3113        let bits = concat_bits(&[
3114            (1, 1),
3115            (0, 1),
3116            (0b1, 1),
3117            (0b011, 3), // I
3118            (0, 8),     // poc_lsb
3119            (1, 1),     // short_term_ref_pic_set_sps_flag = 1 (illegal: num_st_rps=0)
3120        ]);
3121        let rbsp = pack_bits(&bits);
3122        let err = SliceSegmentHeader::parse(&rbsp, 21, &sps, &pps).unwrap_err();
3123        assert_eq!(
3124            err,
3125            SliceError::ValueOutOfRange {
3126                field: "short_term_ref_pic_set_sps_flag",
3127                got: 1
3128            }
3129        );
3130    }
3131
3132    /// IDR P-slice with `num_ref_idx_active_override_flag == 0` and
3133    /// `pps.weighted_pred_flag == 1`: the parser reads the override
3134    /// flag, infers `num_ref_idx_l0_active_minus1` from the PPS
3135    /// default, traverses the §7.3.6.1 mvd / cabac-init / collocated
3136    /// block (with `mvd` absent for P, `cabac_init_flag` inferred
3137    /// `false` per §7.4.7.1, and the collocated block absent because
3138    /// `slice_temporal_mvp_enabled_flag == 0`), and then decodes
3139    /// `pred_weight_table()` in place (the §7.3.6.3 gate is statically
3140    /// present because `weighted_pred_flag && slice_type == P`).
3141    /// `ctx_sps` keeps `chroma_format_idc == 1` (4:2:0) so the chroma
3142    /// sub-block is present, and the PPS default
3143    /// `num_ref_idx_l0_default_active_minus1 == 0` (single L0 entry)
3144    /// keeps the per-i loop to one iteration. The minimal table payload
3145    /// here sets every flag off so the L0 entry's weight / offset
3146    /// remain at their §7.4.7.3 inferred defaults; the parser then
3147    /// walks the full inter-slice tail through `byte_alignment()`.
3148    #[test]
3149    fn parses_pb_with_weighted_pred_table_in_place() {
3150        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3151        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3152        pps.weighted_pred_flag = true;
3153        let bits = concat_bits(&[
3154            (1, 1),     // first
3155            (0, 1),     // no_output (IDR is IRAP)
3156            (0b1, 1),   // pps_id -> 0
3157            (0b010, 3), // slice_type -> P
3158            (1, 1),     // sao_luma
3159            (1, 1),     // sao_chroma
3160            (0, 1),     // num_ref_idx_active_override_flag = 0
3161            // §7.3.6.3 pred_weight_table() — minimal "all flags 0" body.
3162            (0b1, 1), // luma_log2_weight_denom ue -> 0
3163            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3164            (0, 1),   // luma_weight_l0_flag[0] = 0
3165            (0, 1),   // chroma_weight_l0_flag[0] = 0
3166            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3167            (0b1, 1), // slice_qp_delta se -> 0
3168            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3169            (1, 1),   // byte_alignment '1'
3170        ]);
3171        let rbsp = pack_bits(&bits);
3172        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3173        assert_eq!(sh.slice_type, Some(SliceType::P));
3174        assert!(sh.slice_sao_luma_flag);
3175        assert!(sh.slice_sao_chroma_flag);
3176        // §7.4.7.1 inference: P slice with override == 0 picks up the
3177        // PPS default for L0 and leaves L1 absent.
3178        assert_eq!(sh.num_ref_idx_active_override_flag, Some(false));
3179        assert_eq!(
3180            sh.num_ref_idx_l0_active_minus1,
3181            Some(pps.num_ref_idx_l0_default_active_minus1)
3182        );
3183        assert_eq!(sh.num_ref_idx_l1_active_minus1, None);
3184        // §7.3.6.1 mvd / cabac-init / collocated walk for this gate
3185        // combination: mvd absent (P), cabac inferred false, collocated
3186        // absent (mvp off).
3187        assert_eq!(sh.mvd_l1_zero_flag, None);
3188        assert_eq!(sh.cabac_init_flag, Some(false));
3189        assert_eq!(sh.collocated_from_l0_flag, None);
3190        assert_eq!(sh.collocated_ref_idx, None);
3191        // pred_weight_table parsed in place — single L0 entry, all
3192        // flags off.
3193        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3194        assert_eq!(pwt.luma_log2_weight_denom, 0);
3195        assert_eq!(pwt.delta_chroma_log2_weight_denom, 0);
3196        assert_eq!(pwt.entries_l0.len(), 1);
3197        assert!(!pwt.entries_l0[0].luma_weight_flag);
3198        assert!(!pwt.entries_l0[0].chroma_weight_flag);
3199        // P slice → L1 block is empty.
3200        assert!(pwt.entries_l1.is_empty());
3201        // Parser walked the rest of the tail past byte_alignment.
3202        assert_eq!(sh.five_minus_max_num_merge_cand, Some(0));
3203        assert_eq!(sh.max_num_merge_cand(), Some(5));
3204        assert_eq!(sh.slice_qp_delta, Some(0));
3205        assert!(sh.opaque_tail.is_none());
3206        assert!(sh.byte_offset_to_slice_data.is_some());
3207    }
3208
3209    /// IDR P-slice with `num_ref_idx_active_override_flag == 1` and an
3210    /// explicitly signalled `num_ref_idx_l0_active_minus1 == 1`. P
3211    /// slices never signal L1; verify the parser materialises the
3212    /// override flag and the explicit L0 value, leaves L1 absent,
3213    /// decodes a two-entry `pred_weight_table()` in place (the §7.3.6.3
3214    /// gate is statically present here via `pps.weighted_pred_flag =
3215    /// true`), and walks the rest of the inter-slice tail through
3216    /// `byte_alignment()`.
3217    #[test]
3218    fn parses_pb_override_with_explicit_l0() {
3219        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3220        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3221        pps.weighted_pred_flag = true;
3222        let bits = concat_bits(&[
3223            (1, 1),     // first
3224            (0, 1),     // no_output (IDR)
3225            (0b1, 1),   // pps_id -> 0
3226            (0b010, 3), // slice_type -> P
3227            (1, 1),     // sao_luma
3228            (1, 1),     // sao_chroma
3229            (1, 1),     // num_ref_idx_active_override_flag = 1
3230            (0b010, 3), // num_ref_idx_l0_active_minus1 ue(v) -> 1
3231            // pred_weight_table() — 2 L0 entries, all flags 0.
3232            (0b1, 1), // luma_log2_weight_denom ue -> 0
3233            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3234            (0, 1),   // luma_weight_l0_flag[0]
3235            (0, 1),   // chroma_weight_l0_flag[0]
3236            (0, 1),   // luma_weight_l0_flag[1]
3237            (0, 1),   // chroma_weight_l0_flag[1]
3238            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3239            (0b1, 1), // slice_qp_delta se -> 0
3240            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3241            (1, 1),   // byte_alignment '1'
3242        ]);
3243        let rbsp = pack_bits(&bits);
3244        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3245        assert_eq!(sh.num_ref_idx_active_override_flag, Some(true));
3246        assert_eq!(sh.num_ref_idx_l0_active_minus1, Some(1));
3247        assert_eq!(sh.num_ref_idx_l1_active_minus1, None);
3248        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3249        assert_eq!(pwt.entries_l0.len(), 2);
3250        assert!(pwt.entries_l1.is_empty());
3251        assert!(sh.opaque_tail.is_none());
3252    }
3253
3254    /// IDR B-slice with `num_ref_idx_active_override_flag == 1`:
3255    /// verifies that both `num_ref_idx_l0_active_minus1` and
3256    /// `num_ref_idx_l1_active_minus1` are read for a B slice, the
3257    /// `mvd_l1_zero_flag` is consumed, and the in-place
3258    /// `pred_weight_table()` body (gate statically present via
3259    /// `pps.weighted_bipred_flag = true`) decodes both L0 and L1
3260    /// per-entry flag passes; the parser then walks the rest of the
3261    /// inter-slice tail through `byte_alignment()`.
3262    #[test]
3263    fn parses_b_slice_override_with_both_lists() {
3264        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3265        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3266        pps.weighted_bipred_flag = true;
3267        let bits = concat_bits(&[
3268            (1, 1),     // first
3269            (0, 1),     // no_output (IDR)
3270            (0b1, 1),   // pps_id -> 0
3271            (0b1, 1),   // slice_type ue(v) -> 0 (B)
3272            (1, 1),     // sao_luma
3273            (1, 1),     // sao_chroma
3274            (1, 1),     // num_ref_idx_active_override_flag = 1
3275            (0b011, 3), // num_ref_idx_l0_active_minus1 ue(v) -> 2
3276            (0b010, 3), // num_ref_idx_l1_active_minus1 ue(v) -> 1
3277            (1, 1),     // mvd_l1_zero_flag = 1
3278            // pred_weight_table() — L0 has 3 entries, L1 has 2 entries.
3279            (0b1, 1), // luma_log2_weight_denom ue -> 0
3280            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3281            (0, 1),   // luma_weight_l0_flag[0]
3282            (0, 1),   // chroma_weight_l0_flag[0]
3283            (0, 1),   // luma_weight_l0_flag[1]
3284            (0, 1),   // chroma_weight_l0_flag[1]
3285            (0, 1),   // luma_weight_l0_flag[2]
3286            (0, 1),   // chroma_weight_l0_flag[2]
3287            (0, 1),   // luma_weight_l1_flag[0]
3288            (0, 1),   // chroma_weight_l1_flag[0]
3289            (0, 1),   // luma_weight_l1_flag[1]
3290            (0, 1),   // chroma_weight_l1_flag[1]
3291            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3292            (0b1, 1), // slice_qp_delta se -> 0
3293            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3294            (1, 1),   // byte_alignment '1'
3295        ]);
3296        let rbsp = pack_bits(&bits);
3297        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3298        assert_eq!(sh.slice_type, Some(SliceType::B));
3299        assert_eq!(sh.num_ref_idx_active_override_flag, Some(true));
3300        assert_eq!(sh.num_ref_idx_l0_active_minus1, Some(2));
3301        assert_eq!(sh.num_ref_idx_l1_active_minus1, Some(1));
3302        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3303        assert_eq!(pwt.entries_l0.len(), 3);
3304        assert_eq!(pwt.entries_l1.len(), 2);
3305        assert!(sh.opaque_tail.is_none());
3306    }
3307
3308    /// IDR B-slice with `num_ref_idx_active_override_flag == 0`: §7.4.7.1
3309    /// must infer BOTH L0 and L1 defaults from the PPS. The parser
3310    /// reads through the mvd / cabac / collocated block, decodes the
3311    /// in-place `pred_weight_table()` (gate present via
3312    /// `pps.weighted_bipred_flag = true`) with a single L0 / L1 entry
3313    /// each (the PPS defaults), then walks the rest of the tail to
3314    /// `byte_alignment()`.
3315    #[test]
3316    fn b_slice_override_zero_infers_both_defaults() {
3317        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3318        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3319        pps.weighted_bipred_flag = true;
3320        let bits = concat_bits(&[
3321            (1, 1),   // first
3322            (0, 1),   // no_output (IDR)
3323            (0b1, 1), // pps_id -> 0
3324            (0b1, 1), // slice_type -> B
3325            (1, 1),   // sao_luma
3326            (1, 1),   // sao_chroma
3327            (0, 1),   // num_ref_idx_active_override_flag = 0
3328            (1, 1),   // mvd_l1_zero_flag = 1
3329            // pred_weight_table() — 1 L0 + 1 L1 entry (defaults), all
3330            // flags 0.
3331            (0b1, 1), // luma_log2_weight_denom ue -> 0
3332            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3333            (0, 1),   // luma_weight_l0_flag[0]
3334            (0, 1),   // chroma_weight_l0_flag[0]
3335            (0, 1),   // luma_weight_l1_flag[0]
3336            (0, 1),   // chroma_weight_l1_flag[0]
3337            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3338            (0b1, 1), // slice_qp_delta se -> 0
3339            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3340            (1, 1),   // byte_alignment '1'
3341        ]);
3342        let rbsp = pack_bits(&bits);
3343        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3344        assert_eq!(sh.slice_type, Some(SliceType::B));
3345        assert_eq!(sh.num_ref_idx_active_override_flag, Some(false));
3346        assert_eq!(
3347            sh.num_ref_idx_l0_active_minus1,
3348            Some(pps.num_ref_idx_l0_default_active_minus1)
3349        );
3350        assert_eq!(
3351            sh.num_ref_idx_l1_active_minus1,
3352            Some(pps.num_ref_idx_l1_default_active_minus1)
3353        );
3354        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3355        assert_eq!(pwt.entries_l0.len(), 1);
3356        assert_eq!(pwt.entries_l1.len(), 1);
3357        assert!(sh.opaque_tail.is_none());
3358    }
3359
3360    /// IDR B-slice with `pps.lists_modification_present_flag == 0`
3361    /// (default for `TINY_PPS_RBSP`): the §7.3.6.1 mvd / cabac-init /
3362    /// collocated block is walked in-place. With
3363    /// `pps.cabac_init_present_flag == 0` (default) the cabac-init bit
3364    /// is absent (inferred `false` per §7.4.7.1); with
3365    /// `slice_temporal_mvp_enabled_flag == 0` the collocated block is
3366    /// absent. `mvd_l1_zero_flag` is the only bit consumed past the
3367    /// override block.
3368    #[test]
3369    fn parses_b_slice_mvd_l1_zero_walk_no_mvp() {
3370        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3371        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3372        pps.weighted_bipred_flag = true;
3373        let bits = concat_bits(&[
3374            (1, 1),     // first
3375            (0, 1),     // no_output (IDR)
3376            (0b1, 1),   // pps_id -> 0
3377            (0b1, 1),   // slice_type -> B
3378            (1, 1),     // sao_luma
3379            (1, 1),     // sao_chroma
3380            (1, 1),     // num_ref_idx_active_override_flag = 1
3381            (0b010, 3), // num_ref_idx_l0_active_minus1 ue -> 1
3382            (0b010, 3), // num_ref_idx_l1_active_minus1 ue -> 1
3383            (1, 1),     // mvd_l1_zero_flag = 1
3384            // pred_weight_table() — 2 L0 + 2 L1 entries, all flags 0.
3385            (0b1, 1), // luma_log2_weight_denom ue -> 0
3386            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3387            (0, 1),   // luma_weight_l0_flag[0]
3388            (0, 1),   // chroma_weight_l0_flag[0]
3389            (0, 1),   // luma_weight_l0_flag[1]
3390            (0, 1),   // chroma_weight_l0_flag[1]
3391            (0, 1),   // luma_weight_l1_flag[0]
3392            (0, 1),   // chroma_weight_l1_flag[0]
3393            (0, 1),   // luma_weight_l1_flag[1]
3394            (0, 1),   // chroma_weight_l1_flag[1]
3395            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3396            (0b1, 1), // slice_qp_delta se -> 0
3397            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3398            (1, 1),   // byte_alignment '1'
3399        ]);
3400        let rbsp = pack_bits(&bits);
3401        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3402        assert_eq!(sh.mvd_l1_zero_flag, Some(true));
3403        // cabac_init_present_flag == 0 → inferred 0 per §7.4.7.1.
3404        assert_eq!(sh.cabac_init_flag, Some(false));
3405        // mvp off → entire collocated block absent.
3406        assert_eq!(sh.collocated_from_l0_flag, None);
3407        assert_eq!(sh.collocated_ref_idx, None);
3408        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3409        assert_eq!(pwt.entries_l0.len(), 2);
3410        assert_eq!(pwt.entries_l1.len(), 2);
3411        assert!(sh.opaque_tail.is_none());
3412    }
3413
3414    /// IDR P-slice with `pps.cabac_init_present_flag == 1`: the cabac-
3415    /// init bit is signalled (P slice still walks the gate, even though
3416    /// `mvd_l1_zero_flag` is absent). With `mvp == 0` the collocated
3417    /// block is absent.
3418    #[test]
3419    fn parses_p_slice_cabac_init_walk() {
3420        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3421        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3422        pps.cabac_init_present_flag = true;
3423        pps.weighted_pred_flag = true;
3424        let bits = concat_bits(&[
3425            (1, 1),     // first
3426            (0, 1),     // no_output (IDR)
3427            (0b1, 1),   // pps_id -> 0
3428            (0b010, 3), // slice_type -> P
3429            (1, 1),     // sao_luma
3430            (1, 1),     // sao_chroma
3431            (0, 1),     // num_ref_idx_active_override_flag = 0
3432            (1, 1),     // cabac_init_flag = 1
3433            // pred_weight_table() — 1 L0 entry, all flags 0.
3434            (0b1, 1), // luma_log2_weight_denom ue -> 0
3435            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3436            (0, 1),   // luma_weight_l0_flag[0]
3437            (0, 1),   // chroma_weight_l0_flag[0]
3438            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3439            (0b1, 1), // slice_qp_delta se -> 0
3440            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3441            (1, 1),   // byte_alignment '1'
3442        ]);
3443        let rbsp = pack_bits(&bits);
3444        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3445        // P slice → mvd absent.
3446        assert_eq!(sh.mvd_l1_zero_flag, None);
3447        assert_eq!(sh.cabac_init_flag, Some(true));
3448        // mvp off → entire collocated block absent.
3449        assert_eq!(sh.collocated_from_l0_flag, None);
3450        assert_eq!(sh.collocated_ref_idx, None);
3451        assert!(sh.pred_weight_table.is_some());
3452        assert!(sh.opaque_tail.is_none());
3453    }
3454
3455    /// Non-IDR (TRAIL_R) P-slice with `mvp == 1` and
3456    /// `num_ref_idx_l0_active_minus1 == 0` (single L0 entry): §7.4.7.1
3457    /// infers `collocated_from_l0_flag = 1` (no bit consumed since
3458    /// slice_type != B) and the `collocated_ref_idx` field is absent
3459    /// (only signalled when the active list has more than one entry).
3460    /// `slice_temporal_mvp_enabled_flag` sits inside the §7.3.6.1
3461    /// non-IDR block, after the POC + RPS fields.
3462    #[test]
3463    fn parses_p_slice_temporal_mvp_single_ref_collocated_inferred() {
3464        // mvp = true via the ctx_sps argument.
3465        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3466        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3467        pps.weighted_pred_flag = true;
3468        let bits = concat_bits(&[
3469            (1, 1),     // first
3470            (0b1, 1),   // pps_id -> 0
3471            (0b010, 3), // slice_type -> P
3472            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3473            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
3474            (0b010, 3), // num_negative_pics ue -> 1
3475            (0b1, 1),   // num_positive_pics ue -> 0
3476            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
3477            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
3478            (1, 1),     // slice_temporal_mvp_enabled_flag = 1
3479            (1, 1),     // sao_luma
3480            (1, 1),     // sao_chroma
3481            (1, 1),     // num_ref_idx_active_override_flag = 1
3482            (0b1, 1),   // num_ref_idx_l0_active_minus1 ue -> 0
3483            // pred_weight_table() — 1 L0 entry.
3484            (0b1, 1), // luma_log2_weight_denom ue -> 0
3485            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3486            (0, 1),   // luma_weight_l0_flag[0]
3487            (0, 1),   // chroma_weight_l0_flag[0]
3488            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3489            (0b1, 1), // slice_qp_delta se -> 0
3490            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3491            (1, 1),   // byte_alignment '1'
3492        ]);
3493        let rbsp = pack_bits(&bits);
3494        // TRAIL_R (NAL type 1) — a non-IDR picture.
3495        let sh = SliceSegmentHeader::parse(&rbsp, 1, &sps, &pps).expect("slice header");
3496        assert!(sh.slice_temporal_mvp_enabled_flag);
3497        // P slice + mvp on: §7.4.7.1 infers collocated_from_l0 = 1.
3498        assert_eq!(sh.collocated_from_l0_flag, Some(true));
3499        // L0 has a single entry (active_minus1 == 0) → ref_idx absent,
3500        // inferred to 0.
3501        assert_eq!(sh.collocated_ref_idx, Some(0));
3502        assert!(sh.pred_weight_table.is_some());
3503        assert!(sh.opaque_tail.is_none());
3504    }
3505
3506    /// Non-IDR (TRAIL_R) P-slice with `mvp == 1` and
3507    /// `num_ref_idx_l0_active_minus1 == 2` (three L0 entries):
3508    /// `collocated_from_l0_flag` is inferred to 1 (P slice) and
3509    /// `collocated_ref_idx` is signalled `ue(v)`.
3510    #[test]
3511    fn parses_p_slice_temporal_mvp_collocated_ref_idx_signalled() {
3512        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3513        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3514        pps.weighted_pred_flag = true;
3515        let bits = concat_bits(&[
3516            (1, 1),     // first
3517            (0b1, 1),   // pps_id -> 0
3518            (0b010, 3), // slice_type -> P
3519            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3520            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
3521            (0b010, 3), // num_negative_pics ue -> 1
3522            (0b1, 1),   // num_positive_pics ue -> 0
3523            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
3524            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
3525            (1, 1),     // slice_temporal_mvp_enabled_flag = 1
3526            (1, 1),     // sao_luma
3527            (1, 1),     // sao_chroma
3528            (1, 1),     // num_ref_idx_active_override_flag = 1
3529            (0b011, 3), // num_ref_idx_l0_active_minus1 ue -> 2
3530            (0b010, 3), // collocated_ref_idx ue -> 1
3531            // pred_weight_table() — 3 L0 entries.
3532            (0b1, 1), // luma_log2_weight_denom ue -> 0
3533            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3534            (0, 1),   // luma_weight_l0_flag[0]
3535            (0, 1),   // chroma_weight_l0_flag[0]
3536            (0, 1),   // luma_weight_l0_flag[1]
3537            (0, 1),   // chroma_weight_l0_flag[1]
3538            (0, 1),   // luma_weight_l0_flag[2]
3539            (0, 1),   // chroma_weight_l0_flag[2]
3540            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3541            (0b1, 1), // slice_qp_delta se -> 0
3542            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3543            (1, 1),   // byte_alignment '1'
3544        ]);
3545        let rbsp = pack_bits(&bits);
3546        // TRAIL_R (NAL type 1) — a non-IDR picture.
3547        let sh = SliceSegmentHeader::parse(&rbsp, 1, &sps, &pps).expect("slice header");
3548        assert_eq!(sh.num_ref_idx_l0_active_minus1, Some(2));
3549        // P slice + mvp on: §7.4.7.1 infers collocated_from_l0 = 1.
3550        assert_eq!(sh.collocated_from_l0_flag, Some(true));
3551        assert_eq!(sh.collocated_ref_idx, Some(1));
3552        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3553        assert_eq!(pwt.entries_l0.len(), 3);
3554        assert!(sh.opaque_tail.is_none());
3555    }
3556
3557    /// Non-IDR (TRAIL_R) B-slice with `mvp == 1`,
3558    /// `collocated_from_l0_flag = 0` (L1 path), and an L1 with more
3559    /// than one entry: `collocated_ref_idx` indexes L1.
3560    #[test]
3561    fn parses_b_slice_temporal_mvp_collocated_from_l1() {
3562        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3563        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3564        pps.weighted_bipred_flag = true;
3565        let bits = concat_bits(&[
3566            (1, 1),     // first
3567            (0b1, 1),   // pps_id -> 0
3568            (0b1, 1),   // slice_type -> B
3569            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3570            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
3571            (0b010, 3), // num_negative_pics ue -> 1
3572            (0b1, 1),   // num_positive_pics ue -> 0
3573            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
3574            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
3575            (1, 1),     // slice_temporal_mvp_enabled_flag = 1
3576            (1, 1),     // sao_luma
3577            (1, 1),     // sao_chroma
3578            (1, 1),     // num_ref_idx_active_override_flag = 1
3579            (0b1, 1),   // num_ref_idx_l0_active_minus1 ue -> 0
3580            (0b010, 3), // num_ref_idx_l1_active_minus1 ue -> 1
3581            (0, 1),     // mvd_l1_zero_flag = 0
3582            (0, 1),     // collocated_from_l0_flag = 0
3583            (0b010, 3), // collocated_ref_idx ue -> 1
3584            // pred_weight_table() — 1 L0 + 2 L1 entries, all flags 0.
3585            (0b1, 1), // luma_log2_weight_denom ue -> 0
3586            (0b1, 1), // delta_chroma_log2_weight_denom se -> 0
3587            (0, 1),   // luma_weight_l0_flag[0]
3588            (0, 1),   // chroma_weight_l0_flag[0]
3589            (0, 1),   // luma_weight_l1_flag[0]
3590            (0, 1),   // chroma_weight_l1_flag[0]
3591            (0, 1),   // luma_weight_l1_flag[1]
3592            (0, 1),   // chroma_weight_l1_flag[1]
3593            (0b1, 1), // five_minus_max_num_merge_cand ue -> 0
3594            (0b1, 1), // slice_qp_delta se -> 0
3595            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
3596            (1, 1),   // byte_alignment '1'
3597        ]);
3598        let rbsp = pack_bits(&bits);
3599        // TRAIL_R (NAL type 1) — a non-IDR picture.
3600        let sh = SliceSegmentHeader::parse(&rbsp, 1, &sps, &pps).expect("slice header");
3601        assert_eq!(sh.mvd_l1_zero_flag, Some(false));
3602        assert_eq!(sh.collocated_from_l0_flag, Some(false));
3603        // L1 active_minus1 == 1 → 2 entries; ref_idx = 1 indexes the
3604        // second entry, in range.
3605        assert_eq!(sh.collocated_ref_idx, Some(1));
3606        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
3607        assert_eq!(pwt.entries_l0.len(), 1);
3608        assert_eq!(pwt.entries_l1.len(), 2);
3609        assert!(sh.opaque_tail.is_none());
3610    }
3611
3612    /// `collocated_ref_idx` overflow check: a value > the active
3613    /// `num_ref_idx_lX_active_minus1` is a §7.4.7.1 range failure.
3614    #[test]
3615    fn rejects_collocated_ref_idx_above_active_minus1() {
3616        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
3617        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3618        let bits = concat_bits(&[
3619            (1, 1),     // first
3620            (0b1, 1),   // pps_id -> 0
3621            (0b010, 3), // slice_type -> P
3622            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3623            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
3624            (0b010, 3), // num_negative_pics ue -> 1
3625            (0b1, 1),   // num_positive_pics ue -> 0
3626            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
3627            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
3628            (1, 1),     // mvp = 1
3629            (1, 1),     // sao_luma
3630            (1, 1),     // sao_chroma
3631            (1, 1),     // override = 1
3632            (0b010, 3), // L0 active_minus1 = 1 (-> 2 entries, valid range 0..=1)
3633            (0b011, 3), // collocated_ref_idx ue -> 2 (out of range)
3634        ]);
3635        let rbsp = pack_bits(&bits);
3636        // TRAIL_R (NAL type 1) — a non-IDR picture.
3637        let err = SliceSegmentHeader::parse(&rbsp, 1, &sps, &pps).unwrap_err();
3638        assert_eq!(
3639            err,
3640            SliceError::ValueOutOfRange {
3641                field: "collocated_ref_idx",
3642                got: 2,
3643            }
3644        );
3645    }
3646
3647    /// An IDR P-slice with `pps.lists_modification_present_flag == 1`:
3648    /// because the §7.3.6.1 non-IDR POC/RPS block is absent for IDR
3649    /// slices, the active short-term RPS is empty and the §7.4.7.2
3650    /// `NumPicTotalCurr` is `0`. The §7.3.6.1 outer gate
3651    /// (`... && NumPicTotalCurr > 1`) is therefore statically false
3652    /// and `ref_pic_lists_modification()` is not signalled; the parser
3653    /// continues straight into the mvd / cabac-init / collocated block
3654    /// (all absent for this P slice + no-MVP / no-cabac-init /
3655    /// no-temporal-MVP configuration) and walks the rest of the tail
3656    /// through `byte_alignment()`.
3657    #[test]
3658    fn skips_rplm_when_num_pic_total_curr_is_zero_idr() {
3659        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3660        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3661        pps.lists_modification_present_flag = true;
3662        let bits = concat_bits(&[
3663            (1, 1),     // first
3664            (0, 1),     // no_output (IDR)
3665            (0b1, 1),   // pps_id -> 0
3666            (0b010, 3), // slice_type -> P
3667            (1, 1),     // sao_luma
3668            (1, 1),     // sao_chroma
3669            (0, 1),     // num_ref_idx_active_override_flag = 0
3670            // RPLM gate statically false (NumPicTotalCurr == 0) — no
3671            // bits consumed; parser walks into the mvd / cabac-init /
3672            // collocated block. With the current configuration that
3673            // block is fully absent (P slice, no cabac_init, no MVP).
3674            (0b010, 3), // five_minus_max_num_merge_cand ue -> 1
3675            (0b1, 1),   // slice_qp_delta se -> 0
3676            (1, 1),     // slice_loop_filter_across_slices_enabled_flag
3677            (1, 1),     // byte_alignment '1'
3678        ]);
3679        let rbsp = pack_bits(&bits);
3680        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3681        assert_eq!(sh.num_ref_idx_active_override_flag, Some(false));
3682        assert_eq!(sh.ref_pic_lists_modification, None);
3683        assert_eq!(sh.mvd_l1_zero_flag, None);
3684        assert_eq!(sh.cabac_init_flag, Some(false));
3685        assert_eq!(sh.collocated_from_l0_flag, None);
3686        assert_eq!(sh.collocated_ref_idx, None);
3687        assert_eq!(sh.five_minus_max_num_merge_cand, Some(1));
3688        assert!(sh.opaque_tail.is_none());
3689        assert!(sh.byte_offset_to_slice_data.is_some());
3690    }
3691
3692    /// Non-IDR P-slice with `pps.lists_modification_present_flag == 1`
3693    /// and an inline short-term RPS in explicit form carrying two
3694    /// `used_by_curr_pic_s0_flag` entries set to 1: §7.4.7.2 equation
3695    /// 7-57 gives `NumPicTotalCurr == 2`, the §7.3.6.1 outer gate is
3696    /// statically present, and `ref_pic_lists_modification()` parses
3697    /// in place. The per-entry width of `list_entry_l0` is
3698    /// `Ceil(Log2(2)) = 1` bit (§7.4.7.2). After RPLM the parser walks
3699    /// the rest of the inter tail through `byte_alignment()`.
3700    #[test]
3701    fn parses_rplm_in_place_with_explicit_inline_rps_npc_two() {
3702        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3703        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3704        pps.lists_modification_present_flag = true;
3705        let bits = concat_bits(&[
3706            (1, 1),     // first
3707            (0b1, 1),   // pps_id ue -> 0
3708            (0b010, 3), // slice_type ue -> P
3709            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3710            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
3711            // Inline st_ref_pic_set: stRpsIdx == num_short_term_ref_pic_sets
3712            // == 0, so inter_ref_pic_set_prediction_flag is absent.
3713            (0b011, 3), // num_negative_pics ue '011' -> 2
3714            (0b1, 1),   // num_positive_pics ue '1' -> 0
3715            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
3716            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
3717            (0b1, 1),   // delta_poc_s0_minus1[1] ue -> 0
3718            (1, 1),     // used_by_curr_pic_s0_flag[1] = 1
3719            (1, 1),     // sao_luma
3720            (1, 1),     // sao_chroma
3721            (0, 1),     // num_ref_idx_active_override_flag = 0
3722            //   → infers L0 active_minus1 = pps.num_ref_idx_l0_default
3723            // RPLM gate statically present (NumPicTotalCurr == 2 > 1):
3724            (1, 1), // ref_pic_list_modification_flag_l0 = 1
3725            // list_entry_l0[0 .. l0_active_minus1] — each
3726            // `Ceil(Log2(2)) = 1` bit, range 0..=1. The PPS default
3727            // active_minus1 from TINY_PPS_RBSP picks the loop count.
3728            // We feed enough bits for the largest plausible default
3729            // and check that the slice parser caps the count from the
3730            // resolved override-or-default value.
3731            (0, 1),     // list_entry_l0[0] = 0
3732            (0b010, 3), // five_minus_max_num_merge_cand ue -> 1
3733            (0b1, 1),   // slice_qp_delta se -> 0
3734            (1, 1),     // slice_loop_filter_across_slices_enabled_flag
3735            (1, 1),     // byte_alignment '1'
3736        ]);
3737        let rbsp = pack_bits(&bits);
3738        // TRAIL_N (Table 7-1 value 0) is non-IDR / non-IRAP.
3739        let sh = SliceSegmentHeader::parse(&rbsp, 0, &sps, &pps).expect("slice header");
3740        assert_eq!(sh.slice_type, Some(SliceType::P));
3741        assert_eq!(sh.short_term_ref_pic_set_sps_flag, Some(false));
3742        let inline = sh
3743            .inline_short_term_ref_pic_set
3744            .as_ref()
3745            .expect("inline ST RPS");
3746        assert_eq!(inline.num_negative_pics, 2);
3747        assert_eq!(inline.used_by_curr_pic_s0_flag, vec![true, true]);
3748        let rplm = sh
3749            .ref_pic_lists_modification
3750            .as_ref()
3751            .expect("RPLM parsed in place");
3752        assert!(rplm.ref_pic_list_modification_flag_l0);
3753        // L0 active_minus1 == 0 (PPS TINY default) → 1 entry.
3754        assert_eq!(rplm.list_entry_l0, vec![0]);
3755        // P slice → L1 fields are not signalled.
3756        assert_eq!(rplm.ref_pic_list_modification_flag_l1, None);
3757        assert!(rplm.list_entry_l1.is_empty());
3758        // Full inter tail walked, no opaque suffix.
3759        assert!(sh.opaque_tail.is_none());
3760        assert!(sh.byte_offset_to_slice_data.is_some());
3761    }
3762
3763    /// Non-IDR P-slice with `pps.lists_modification_present_flag == 1`
3764    /// and an inline short-term RPS in explicit form carrying *one*
3765    /// `used_by_curr_pic_s0_flag` entry: equation 7-57 gives
3766    /// `NumPicTotalCurr == 1`, the §7.3.6.1 outer gate is statically
3767    /// false, `ref_pic_lists_modification()` is not signalled, and the
3768    /// parser continues straight into the rest of the inter tail.
3769    #[test]
3770    fn skips_rplm_when_num_pic_total_curr_is_one() {
3771        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3772        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3773        pps.lists_modification_present_flag = true;
3774        let bits = concat_bits(&[
3775            (1, 1),     // first
3776            (0b1, 1),   // pps_id ue -> 0
3777            (0b010, 3), // slice_type ue -> P
3778            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3779            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
3780            (0b010, 3), // num_negative_pics ue '010' -> 1
3781            (0b1, 1),   // num_positive_pics ue '1' -> 0
3782            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
3783            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
3784            (1, 1),     // sao_luma
3785            (1, 1),     // sao_chroma
3786            (0, 1),     // num_ref_idx_active_override_flag = 0
3787            // No RPLM (gate statically false at NPC == 1).
3788            (0b010, 3), // five_minus_max_num_merge_cand ue -> 1
3789            (0b1, 1),   // slice_qp_delta se -> 0
3790            (1, 1),     // slice_loop_filter_across_slices_enabled_flag
3791            (1, 1),     // byte_alignment '1'
3792        ]);
3793        let rbsp = pack_bits(&bits);
3794        let sh = SliceSegmentHeader::parse(&rbsp, 0, &sps, &pps).expect("slice header");
3795        assert_eq!(sh.slice_type, Some(SliceType::P));
3796        assert_eq!(sh.ref_pic_lists_modification, None);
3797        assert_eq!(sh.five_minus_max_num_merge_cand, Some(1));
3798        assert!(sh.opaque_tail.is_none());
3799        assert!(sh.byte_offset_to_slice_data.is_some());
3800    }
3801
3802    /// Non-IDR P-slice with `pps.lists_modification_present_flag == 1`
3803    /// using an SPS-resident short-term RPS whose
3804    /// `inter_ref_pic_set_prediction_flag == 1` with malformed
3805    /// per-position arrays (lengths do not match the source's
3806    /// `NumDeltaPocs[RefRpsIdx] + 1`). The §7.4.8 materialiser rejects
3807    /// the chain and the parser surfaces an opaque tail starting at the
3808    /// `ref_pic_lists_modification()` bit so the caller can inspect the
3809    /// bitstream.
3810    #[test]
3811    fn defers_rplm_when_active_st_rps_uses_inter_prediction() {
3812        let mut sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3813        sps.num_short_term_ref_pic_sets = 2;
3814        sps.short_term_ref_pic_sets = vec![
3815            ShortTermRefPicSet {
3816                inter_ref_pic_set_prediction_flag: false,
3817                num_negative_pics: 1,
3818                num_positive_pics: 0,
3819                delta_poc_s0_minus1: vec![0],
3820                used_by_curr_pic_s0_flag: vec![true],
3821                ..Default::default()
3822            },
3823            ShortTermRefPicSet {
3824                // The picked RPS is in inter-prediction form but the
3825                // arrays are empty (length 0 ≠ source's NumDeltaPocs+1
3826                // = 2) — materialisation fails and the parse defers.
3827                inter_ref_pic_set_prediction_flag: true,
3828                ..Default::default()
3829            },
3830        ];
3831        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3832        pps.lists_modification_present_flag = true;
3833        let bits = concat_bits(&[
3834            (1, 1),     // first
3835            (0b1, 1),   // pps_id ue -> 0
3836            (0b010, 3), // slice_type ue -> P
3837            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3838            (1, 1),     // short_term_ref_pic_set_sps_flag = 1
3839            // num_short_term_ref_pic_sets == 2 → idx field width = 1 bit.
3840            (1, 1), // short_term_ref_pic_set_idx u(1) = 1 (the inter-predicted RPS)
3841            (1, 1), // sao_luma
3842            (1, 1), // sao_chroma
3843            (0, 1), // num_ref_idx_active_override_flag = 0
3844            // (Deferral begins here; remaining bits are opaque.)
3845            (1, 1),
3846        ]);
3847        let rbsp = pack_bits(&bits);
3848        let sh = SliceSegmentHeader::parse(&rbsp, 0, &sps, &pps).expect("slice header");
3849        assert_eq!(sh.slice_type, Some(SliceType::P));
3850        assert_eq!(sh.short_term_ref_pic_set_sps_flag, Some(true));
3851        assert_eq!(sh.short_term_ref_pic_set_idx, Some(1));
3852        assert!(sh.ref_pic_lists_modification.is_none());
3853        assert_eq!(sh.mvd_l1_zero_flag, None);
3854        assert_eq!(sh.cabac_init_flag, None);
3855        assert!(sh.opaque_tail.is_some());
3856        assert!(sh.byte_offset_to_slice_data.is_none());
3857    }
3858
3859    /// Non-IDR P-slice with `pps.lists_modification_present_flag == 1`
3860    /// using an SPS-resident short-term RPS whose
3861    /// `inter_ref_pic_set_prediction_flag == 1` and well-formed
3862    /// per-position arrays. The §7.4.8 materialiser succeeds: the
3863    /// derived RPS has `NumPicTotalCurr == 1` (single positive POC
3864    /// gated `false`) so the §7.3.6.1 outer gate is statically false
3865    /// and the parser walks the inter-slice tail to `byte_alignment()`
3866    /// without surfacing an opaque tail. Closes the prior §7.4.8
3867    /// deferral point for this configuration.
3868    #[test]
3869    fn parses_p_slice_with_sps_inter_predicted_rps_npc_le_1() {
3870        let mut sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3871        sps.num_short_term_ref_pic_sets = 2;
3872        // Same fixture used by sps::tests::materialize_inter_rps_prediction_matches_fixture:
3873        //   * Source (idx 0): explicit, num_neg=1, num_pos=0,
3874        //     delta_poc_s0_minus1=[0], used_by_curr_pic_s0_flag=[true].
3875        //   * Inter (idx 1): delta_rps_sign=false,
3876        //     abs_delta_rps_minus1=0 (deltaRps=+1),
3877        //     used_by_curr_pic_flag=[true,false], use_delta_flag=[true,true].
3878        // Derived (idx 1): DeltaPocS1=[+1], UsedByCurrPicS1=[false]
3879        // ⇒ NumPicTotalCurr = 0 (the lone positive's used flag is
3880        // false), gate is statically false.
3881        sps.short_term_ref_pic_sets = vec![
3882            ShortTermRefPicSet {
3883                inter_ref_pic_set_prediction_flag: false,
3884                num_negative_pics: 1,
3885                num_positive_pics: 0,
3886                delta_poc_s0_minus1: vec![0],
3887                used_by_curr_pic_s0_flag: vec![true],
3888                ..Default::default()
3889            },
3890            ShortTermRefPicSet {
3891                inter_ref_pic_set_prediction_flag: true,
3892                delta_idx_minus1: 0,
3893                delta_rps_sign: false,
3894                abs_delta_rps_minus1: 0,
3895                used_by_curr_pic_flag: vec![true, false],
3896                use_delta_flag: vec![true, true],
3897                ..Default::default()
3898            },
3899        ];
3900        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3901        pps.lists_modification_present_flag = true;
3902        let bits = concat_bits(&[
3903            (1, 1),     // first_slice_segment_in_pic_flag
3904            (0b1, 1),   // pps_id ue -> 0
3905            (0b010, 3), // slice_type ue -> P
3906            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
3907            (1, 1),     // short_term_ref_pic_set_sps_flag = 1
3908            // num_short_term_ref_pic_sets == 2 → idx field width = 1 bit.
3909            (1, 1), // short_term_ref_pic_set_idx u(1) = 1 (the inter-predicted RPS)
3910            (1, 1), // sao_luma
3911            (1, 1), // sao_chroma
3912            (0, 1), // num_ref_idx_active_override_flag = 0
3913            // (NumPicTotalCurr == 0 → ref_pic_lists_modification gate
3914            //  is statically absent; the parser walks straight to
3915            //  `mvd_l1_zero_flag`, which is absent for P, then the
3916            //  inferred `cabac_init_flag = false`, then the absent
3917            //  collocated block, then five_minus_max_num_merge_cand.)
3918            (0b010, 3), // five_minus_max_num_merge_cand ue -> 1
3919            (0b1, 1),   // slice_qp_delta se -> 0
3920            (1, 1),     // slice_loop_filter_across_slices_enabled_flag = 1
3921            (1, 1),     // byte_alignment '1' bit (rest zero-padded)
3922        ]);
3923        let rbsp = pack_bits(&bits);
3924        let sh = SliceSegmentHeader::parse(&rbsp, 0, &sps, &pps).expect("slice header");
3925        assert_eq!(sh.slice_type, Some(SliceType::P));
3926        assert_eq!(sh.short_term_ref_pic_set_sps_flag, Some(true));
3927        assert_eq!(sh.short_term_ref_pic_set_idx, Some(1));
3928        // §7.4.8 materialisation succeeded, NumPicTotalCurr == 0 ⇒
3929        // gate statically false, no RPLM signalled.
3930        assert!(sh.ref_pic_lists_modification.is_none());
3931        // The parser walked through to byte_alignment().
3932        assert!(sh.opaque_tail.is_none());
3933        assert_eq!(sh.five_minus_max_num_merge_cand, Some(1));
3934        assert_eq!(sh.slice_qp_delta, Some(0));
3935        assert!(sh.byte_offset_to_slice_data.is_some());
3936    }
3937
3938    /// IDR P-slice that walks the full inter-slice tail through
3939    /// `byte_alignment()` because the §7.3.6.3 `pred_weight_table()`
3940    /// gate is statically absent (`pps.weighted_pred_flag == 0`,
3941    /// inherited from `TINY_PPS_RBSP`). After the override block, the
3942    /// parser walks past `mvd_l1_zero_flag` (absent for P) and
3943    /// `cabac_init_flag` (absent + inferred `false` because
3944    /// `pps.cabac_init_present_flag == 0`), past the collocated block
3945    /// (absent because `slice_temporal_mvp_enabled_flag == 0`), reads
3946    /// `five_minus_max_num_merge_cand`, then the I-slice-shared tail
3947    /// (`slice_qp_delta` + chroma QP + deblocking + loop-filter +
3948    /// entry-points + extension + byte_alignment). The `use_integer_mv_flag`
3949    /// SCC bit is statically absent because the PPS SCC extension is
3950    /// not surfaced (motion_vector_resolution_control_idc inferred 0).
3951    #[test]
3952    fn parses_p_slice_full_inter_tail_no_weighted_pred() {
3953        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
3954        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
3955        // Sanity: TINY_PPS keeps the weighted-pred gate statically off.
3956        assert!(!pps.weighted_pred_flag);
3957        assert!(!pps.weighted_bipred_flag);
3958        assert!(!pps.cabac_init_present_flag);
3959        assert!(!pps.lists_modification_present_flag);
3960        let bits = concat_bits(&[
3961            (1, 1),     // first
3962            (0, 1),     // no_output (IDR)
3963            (0b1, 1),   // pps_id ue -> 0
3964            (0b010, 3), // slice_type ue -> P
3965            (1, 1),     // sao_luma
3966            (1, 1),     // sao_chroma
3967            (0, 1),     // num_ref_idx_active_override_flag = 0
3968            // (no mvd / cabac_init / collocated bits — all inferred)
3969            (0b010, 3), // five_minus_max_num_merge_cand ue -> 1
3970            (0b1, 1),   // slice_qp_delta se -> 0
3971            (1, 1),     // slice_loop_filter_across_slices_enabled_flag = 1
3972            (1, 1),     // byte_alignment '1' bit (rest are 0 to byte boundary)
3973        ]);
3974        let rbsp = pack_bits(&bits);
3975        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
3976        assert_eq!(sh.slice_type, Some(SliceType::P));
3977        // §7.4.7.1 inference: P override == 0 → L0 from PPS default.
3978        assert_eq!(
3979            sh.num_ref_idx_l0_active_minus1,
3980            Some(pps.num_ref_idx_l0_default_active_minus1)
3981        );
3982        assert_eq!(sh.num_ref_idx_l1_active_minus1, None);
3983        assert_eq!(sh.mvd_l1_zero_flag, None);
3984        assert_eq!(sh.cabac_init_flag, Some(false));
3985        assert_eq!(sh.collocated_from_l0_flag, None);
3986        assert_eq!(sh.collocated_ref_idx, None);
3987        // five_minus_max_num_merge_cand = 1 → MaxNumMergeCand = 4.
3988        assert_eq!(sh.five_minus_max_num_merge_cand, Some(1));
3989        assert_eq!(sh.max_num_merge_cand(), Some(4));
3990        assert_eq!(sh.slice_qp_delta, Some(0));
3991        assert_eq!(sh.slice_loop_filter_across_slices_enabled_flag, Some(true));
3992        // Tail walked to byte_alignment; no opaque suffix.
3993        assert!(sh.opaque_tail.is_none());
3994        assert!(sh.byte_offset_to_slice_data.is_some());
3995    }
3996
3997    /// Non-IDR (TRAIL_R) B-slice walking the full inter-slice tail
3998    /// through `byte_alignment()` with `pps.weighted_bipred_flag == 0`:
3999    /// exercises the B-only `mvd_l1_zero_flag` bit and the temporal-MVP
4000    /// `collocated_from_l0_flag` signalling, then walks straight to
4001    /// `five_minus_max_num_merge_cand` and the shared I-slice tail.
4002    #[test]
4003    fn parses_b_slice_full_inter_tail_with_mvp() {
4004        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
4005        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4006        let bits = concat_bits(&[
4007            (1, 1),     // first
4008            (0b1, 1),   // pps_id ue -> 0
4009            (0b1, 1),   // slice_type ue -> B
4010            (0, 8),     // slice_pic_order_cnt_lsb u(8) = 0
4011            (0, 1),     // short_term_ref_pic_set_sps_flag = 0
4012            (0b010, 3), // num_negative_pics ue -> 1
4013            (0b1, 1),   // num_positive_pics ue -> 0
4014            (0b1, 1),   // delta_poc_s0_minus1[0] ue -> 0
4015            (1, 1),     // used_by_curr_pic_s0_flag[0] = 1
4016            (1, 1),     // slice_temporal_mvp_enabled_flag = 1
4017            (1, 1),     // sao_luma
4018            (1, 1),     // sao_chroma
4019            (1, 1),     // num_ref_idx_active_override_flag = 1
4020            (0b010, 3), // num_ref_idx_l0_active_minus1 ue -> 1
4021            (0b1, 1),   // num_ref_idx_l1_active_minus1 ue -> 0
4022            (0, 1),     // mvd_l1_zero_flag = 0
4023            // cabac_init_flag absent (cabac_init_present_flag == 0).
4024            (1, 1),     // collocated_from_l0_flag = 1
4025            (0b010, 3), // collocated_ref_idx ue -> 1 (L0 has 2 entries, in range)
4026            (0b1, 1),   // five_minus_max_num_merge_cand ue -> 0
4027            (0b011, 3), // slice_qp_delta se -> -1
4028            (1, 1),     // slice_loop_filter_across_slices_enabled_flag = 1
4029            (1, 1),     // byte_alignment '1'
4030        ]);
4031        let rbsp = pack_bits(&bits);
4032        // TRAIL_R (NAL type 1) — a non-IDR picture.
4033        let sh = SliceSegmentHeader::parse(&rbsp, 1, &sps, &pps).expect("slice header");
4034        assert_eq!(sh.slice_type, Some(SliceType::B));
4035        assert!(sh.slice_temporal_mvp_enabled_flag);
4036        assert_eq!(sh.mvd_l1_zero_flag, Some(false));
4037        assert_eq!(sh.collocated_from_l0_flag, Some(true));
4038        assert_eq!(sh.collocated_ref_idx, Some(1));
4039        assert_eq!(sh.five_minus_max_num_merge_cand, Some(0));
4040        assert_eq!(sh.max_num_merge_cand(), Some(5));
4041        assert_eq!(sh.slice_qp_delta, Some(-1));
4042        assert!(sh.opaque_tail.is_none());
4043    }
4044
4045    /// IDR P-slice with `pps.weighted_pred_flag == 1` and a non-trivial
4046    /// `pred_weight_table()` body (single L0 entry, `luma_log2_weight_denom
4047    /// == 2`, `luma_weight_l0_flag == 1`, `delta_luma_weight_l0[0] == 5`,
4048    /// `luma_offset_l0[0] == 0`; chroma flag off). Verifies the in-place
4049    /// PWT decode resolves the §7.4.7.3 derived `LumaWeightL0[0] = (1 <<
4050    /// 2) + 5 = 9` correctly and the parser continues to walk the rest
4051    /// of the inter-slice tail through `byte_alignment()`.
4052    #[test]
4053    fn parses_p_slice_in_place_pwt_resolves_luma_weight() {
4054        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
4055        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4056        pps.weighted_pred_flag = true;
4057        let mut fields: Vec<(u32, u8)> = vec![
4058            (1, 1),     // first
4059            (0, 1),     // no_output (IDR)
4060            (0b1, 1),   // pps_id ue -> 0
4061            (0b010, 3), // slice_type ue -> P
4062            (1, 1),     // sao_luma
4063            (1, 1),     // sao_chroma
4064            (0, 1),     // num_ref_idx_active_override_flag = 0 → L0 = PPS default (0)
4065        ];
4066        // pred_weight_table()
4067        fields.push(ue_codeword(2)); // luma_log2_weight_denom = 2
4068        fields.push(se_codeword(0)); // delta_chroma_log2_weight_denom = 0
4069        fields.push((1, 1)); // luma_weight_l0_flag[0]
4070        fields.push((0, 1)); // chroma_weight_l0_flag[0]
4071        fields.push(se_codeword(5)); // delta_luma_weight_l0[0]
4072        fields.push(se_codeword(0)); // luma_offset_l0[0]
4073                                     // Inter-slice tail
4074        fields.push(ue_codeword(0)); // five_minus_max_num_merge_cand
4075        fields.push(se_codeword(0)); // slice_qp_delta
4076        fields.push((1, 1)); // slice_loop_filter_across_slices_enabled_flag
4077        fields.push((1, 1)); // byte_alignment '1'
4078        let bits = concat_bits(&fields);
4079        let rbsp = pack_bits(&bits);
4080        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
4081        assert_eq!(sh.slice_type, Some(SliceType::P));
4082        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
4083        assert_eq!(pwt.luma_log2_weight_denom, 2);
4084        assert_eq!(pwt.entries_l0.len(), 1);
4085        assert!(pwt.entries_l0[0].luma_weight_flag);
4086        assert!(!pwt.entries_l0[0].chroma_weight_flag);
4087        assert_eq!(pwt.entries_l0[0].delta_luma_weight, 5);
4088        // §7.4.7.3 derived LumaWeightL0[0] = (1 << 2) + 5 = 9.
4089        assert_eq!(pwt.luma_weight_l0(0), Some(9));
4090        // The chroma flag is off → derived ChromaWeightL0[0][j] = 1 <<
4091        // ChromaLog2WeightDenom = 1 << 2 = 4 (inferred form).
4092        assert_eq!(pwt.chroma_weight_l0(0, 0), Some(4));
4093        // ChromaOffsetL0[0][j] inferred to 0 when the chroma flag is off.
4094        assert_eq!(pwt.chroma_offset_l0(0, 0, 128), Some(0));
4095        // Header walked to byte_alignment.
4096        assert!(sh.opaque_tail.is_none());
4097        assert!(sh.byte_offset_to_slice_data.is_some());
4098    }
4099
4100    /// IDR B-slice with `pps.weighted_bipred_flag == 1` and a
4101    /// `pred_weight_table()` body that carries an L1 chroma weight +
4102    /// offset delta. Verifies the in-place call site populates both L0
4103    /// and L1 entries, the chroma sub-block on L1 is parsed, and the
4104    /// derived `ChromaWeightL1` / `ChromaOffsetL1` resolve correctly.
4105    #[test]
4106    fn parses_b_slice_in_place_pwt_resolves_l1_chroma() {
4107        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
4108        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4109        pps.weighted_bipred_flag = true;
4110        let mut fields: Vec<(u32, u8)> = vec![
4111            (1, 1),   // first
4112            (0, 1),   // no_output (IDR)
4113            (0b1, 1), // pps_id ue -> 0
4114            (0b1, 1), // slice_type ue -> B
4115            (1, 1),   // sao_luma
4116            (1, 1),   // sao_chroma
4117            (0, 1),   // num_ref_idx_active_override_flag = 0
4118            (1, 1),   // mvd_l1_zero_flag = 1
4119        ];
4120        // pred_weight_table() — 1 L0 + 1 L1 entry.
4121        fields.push(ue_codeword(1)); // luma_log2_weight_denom = 1
4122        fields.push(se_codeword(1)); // delta_chroma_log2_weight_denom = 1 → ChromaLog2WeightDenom = 2
4123        fields.push((0, 1)); // luma_weight_l0_flag[0]
4124        fields.push((0, 1)); // chroma_weight_l0_flag[0]
4125        fields.push((0, 1)); // luma_weight_l1_flag[0]
4126        fields.push((1, 1)); // chroma_weight_l1_flag[0] = 1
4127        fields.push(se_codeword(2)); // delta_chroma_weight_l1[0][0] = 2
4128        fields.push(se_codeword(-1)); // delta_chroma_offset_l1[0][0] = -1
4129        fields.push(se_codeword(-3)); // delta_chroma_weight_l1[0][1] = -3
4130        fields.push(se_codeword(0)); // delta_chroma_offset_l1[0][1] = 0
4131                                     // Inter-slice tail
4132        fields.push(ue_codeword(0)); // five_minus_max_num_merge_cand
4133        fields.push(se_codeword(0)); // slice_qp_delta
4134        fields.push((1, 1)); // slice_loop_filter_across_slices_enabled_flag
4135        fields.push((1, 1)); // byte_alignment '1'
4136        let bits = concat_bits(&fields);
4137        let rbsp = pack_bits(&bits);
4138        let sh = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).expect("slice header");
4139        assert_eq!(sh.slice_type, Some(SliceType::B));
4140        let pwt = sh.pred_weight_table.as_ref().expect("PWT decoded in place");
4141        assert_eq!(pwt.luma_log2_weight_denom, 1);
4142        assert_eq!(pwt.chroma_log2_weight_denom(), 2);
4143        assert_eq!(pwt.entries_l0.len(), 1);
4144        assert_eq!(pwt.entries_l1.len(), 1);
4145        assert!(!pwt.entries_l1[0].luma_weight_flag);
4146        assert!(pwt.entries_l1[0].chroma_weight_flag);
4147        assert_eq!(pwt.entries_l1[0].delta_chroma_weight, [2, -3]);
4148        assert_eq!(pwt.entries_l1[0].delta_chroma_offset, [-1, 0]);
4149        // §7.4.7.3 derived ChromaWeightL1[0][j] = (1 << 2) + delta:
4150        //   j=0: 4 + 2 = 6;  j=1: 4 + (-3) = 1.
4151        assert_eq!(pwt.chroma_weight_l1(0, 0), Some(6));
4152        assert_eq!(pwt.chroma_weight_l1(0, 1), Some(1));
4153        // Equation 7-58 with WpOffsetHalfRangeC = 128 (base-profile bit
4154        // depths, high_precision_offsets_enabled_flag = 0):
4155        //   j=0: 128 + (-1) - ((128 * 6) >> 2) = 127 - 192 = -65.
4156        //   j=1: 128 + 0    - ((128 * 1) >> 2) = 128 - 32  =  96.
4157        assert_eq!(pwt.chroma_offset_l1(0, 0, 128), Some(-65));
4158        assert_eq!(pwt.chroma_offset_l1(0, 1, 128), Some(96));
4159        // L0 chroma flag was off → inferred ChromaWeightL0[0][j] = 4.
4160        assert_eq!(pwt.chroma_weight_l0(0, 0), Some(4));
4161        assert!(sh.opaque_tail.is_none());
4162    }
4163
4164    /// A `pred_weight_table()` body that violates the §7.4.7.3 range
4165    /// bound on `delta_luma_weight_l0` (out-of-range value 128) surfaces
4166    /// from the in-place call site as the same
4167    /// [`SliceError::ValueOutOfRange`] the standalone parser raises —
4168    /// the failure must propagate from `SliceSegmentHeader::parse`.
4169    #[test]
4170    fn in_place_pwt_propagates_range_error() {
4171        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
4172        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4173        pps.weighted_pred_flag = true;
4174        let mut fields: Vec<(u32, u8)> = vec![
4175            (1, 1),     // first
4176            (0, 1),     // no_output (IDR)
4177            (0b1, 1),   // pps_id ue -> 0
4178            (0b010, 3), // slice_type ue -> P
4179            (1, 1),     // sao_luma
4180            (1, 1),     // sao_chroma
4181            (0, 1),     // num_ref_idx_active_override_flag = 0 → L0 default = 0
4182        ];
4183        fields.push(ue_codeword(0)); // luma_log2_weight_denom = 0
4184        fields.push(se_codeword(0)); // delta_chroma_log2_weight_denom = 0
4185        fields.push((1, 1)); // luma_weight_l0_flag[0]
4186        fields.push((0, 1)); // chroma_weight_l0_flag[0]
4187        fields.push(se_codeword(128)); // delta_luma_weight_l0[0] = 128 (out of range)
4188        let bits = concat_bits(&fields);
4189        let rbsp = pack_bits(&bits);
4190        let err = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).unwrap_err();
4191        assert_eq!(
4192            err,
4193            SliceError::ValueOutOfRange {
4194                field: "delta_luma_weight_l0",
4195                got: 128,
4196            }
4197        );
4198    }
4199
4200    /// `five_minus_max_num_merge_cand > 4` is a §7.4.7.1 conformance
4201    /// violation (the derived `MaxNumMergeCand` would fall below 1).
4202    /// Encode wire value 5 as ue(v) = `0b00110` (5 bits).
4203    #[test]
4204    fn rejects_five_minus_max_num_merge_cand_above_4() {
4205        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
4206        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4207        let bits = concat_bits(&[
4208            (1, 1),     // first
4209            (0, 1),     // no_output (IDR)
4210            (0b1, 1),   // pps_id -> 0
4211            (0b010, 3), // slice_type -> P
4212            (1, 1),     // sao_luma
4213            (1, 1),     // sao_chroma
4214            (0, 1),     // num_ref_idx_active_override_flag = 0
4215            // mvd / cabac / collocated all inferred absent.
4216            ue_codeword(5), // five_minus_max_num_merge_cand = 5 (illegal)
4217        ]);
4218        let rbsp = pack_bits(&bits);
4219        let err = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).unwrap_err();
4220        assert_eq!(
4221            err,
4222            SliceError::ValueOutOfRange {
4223                field: "five_minus_max_num_merge_cand",
4224                got: 5,
4225            }
4226        );
4227    }
4228
4229    /// `num_ref_idx_l0_active_minus1 > 14` is a range failure (§7.4.7.1).
4230    /// Encode value 15 as ue(v) = `0b000010000` (9 bits).
4231    #[test]
4232    fn rejects_num_ref_idx_l0_active_minus1_above_14() {
4233        let sps = ctx_sps(1, false, true, false, 16, 16, 1, 0, 4);
4234        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4235        let bits = concat_bits(&[
4236            (1, 1),          // first
4237            (0, 1),          // no_output (IDR)
4238            (0b1, 1),        // pps_id -> 0
4239            (0b010, 3),      // slice_type -> P
4240            (1, 1),          // sao_luma
4241            (1, 1),          // sao_chroma
4242            (1, 1),          // num_ref_idx_active_override_flag = 1
4243            ue_codeword(15), // num_ref_idx_l0_active_minus1 = 15 -> illegal
4244        ]);
4245        let rbsp = pack_bits(&bits);
4246        let err = SliceSegmentHeader::parse(&rbsp, IDR_W_RADL, &sps, &pps).unwrap_err();
4247        assert_eq!(
4248            err,
4249            SliceError::ValueOutOfRange {
4250                field: "num_ref_idx_l0_active_minus1",
4251                got: 15,
4252            }
4253        );
4254    }
4255
4256    /// Non-first dependent slice segment: dependent flag + address are
4257    /// read, then the body ends after SAO (no slice_type etc.).
4258    #[test]
4259    fn parses_dependent_slice_segment() {
4260        // 4-CTB picture (2x2) so slice_segment_address is 2 bits wide.
4261        // PPS must have dependent_slice_segments_enabled_flag; the
4262        // fixture PPS has it 0, so patch a parsed PPS.
4263        let sps = ctx_sps(1, false, true, true, 32, 32, 1, 0, 4); // 32/16=2 -> 2x2=4 CTBs
4264        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4265        pps.dependent_slice_segments_enabled_flag = true;
4266        // first=0, dependent=1, slice_segment_address u(2)=2, then the
4267        // header ends: every remaining §7.3.6.1 field (slice_type, SAO
4268        // flags, QP, …) lives inside the !dependent gate and is
4269        // inherited from the associated independent slice segment. The
4270        // header goes straight to byte_alignment().
4271        let bits = concat_bits(&[
4272            (0, 1),    // first_slice_segment_in_pic_flag = 0
4273            (0b1, 1),  // pps_id ue -> 0
4274            (1, 1),    // dependent_slice_segment_flag
4275            (0b10, 2), // slice_segment_address = 2
4276            (1, 1),    // byte_alignment one bit
4277        ]);
4278        let rbsp = pack_bits(&bits);
4279        // Use a non-IRAP type so no_output is absent and the layout
4280        // above matches.
4281        let sh =
4282            SliceSegmentHeader::parse(&rbsp, 0 /* TRAIL_N */, &sps, &pps).expect("slice header");
4283        assert!(!sh.first_slice_segment_in_pic_flag);
4284        assert!(sh.dependent_slice_segment_flag);
4285        assert_eq!(sh.slice_segment_address, 2);
4286        assert_eq!(sh.slice_type, None);
4287        // SAO flags are not signalled in a dependent segment — they are
4288        // inherited by the caller; the struct leaves them false.
4289        assert!(!sh.slice_sao_luma_flag);
4290        assert!(!sh.slice_sao_chroma_flag);
4291        assert!(sh.opaque_tail.is_none());
4292        assert_eq!(sh.byte_offset_to_slice_data, Some(1));
4293    }
4294
4295    #[test]
4296    fn rejects_truncated_rbsp() {
4297        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
4298        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4299        // Only one byte; first bit reads ok but ue() for pps_id runs off
4300        // a short enough buffer eventually. Use an empty buffer to force
4301        // truncation on the very first read.
4302        let err = SliceSegmentHeader::parse(&[], IDR_N_LP, &sps, &pps).unwrap_err();
4303        assert_eq!(err, SliceError::Truncated);
4304    }
4305
4306    #[test]
4307    fn rejects_slice_type_out_of_range() {
4308        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
4309        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4310        // slice_type ue(v) = 3 ('00100') is out of Table 7-7 range.
4311        let bits = concat_bits(&[
4312            (1, 1),       // first
4313            (0, 1),       // no_output (IDR)
4314            (0b1, 1),     // pps_id -> 0
4315            (0b00100, 5), // slice_type ue -> 3 (illegal)
4316        ]);
4317        let rbsp = pack_bits(&bits);
4318        let err = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).unwrap_err();
4319        assert_eq!(
4320            err,
4321            SliceError::ValueOutOfRange {
4322                field: "slice_type",
4323                got: 3
4324            }
4325        );
4326    }
4327
4328    #[test]
4329    fn end_to_end_via_nal_walker() {
4330        use crate::nal::collect_nal_units;
4331        // Build an Annex B stream carrying a single IDR_N_LP slice NAL
4332        // whose RBSP is the hand-assembled I-slice header from
4333        // `parses_hand_assembled_i_idr_header`.
4334        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
4335        let pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
4336        let bits = concat_bits(&[
4337            (1, 1),     // first_slice_segment_in_pic_flag
4338            (0, 1),     // no_output_of_prior_pics_flag (IRAP)
4339            (0b1, 1),   // pps_id ue -> 0
4340            (0b011, 3), // slice_type ue -> I
4341            (1, 1),     // slice_sao_luma_flag
4342            (0, 1),     // slice_sao_chroma_flag
4343            (0b011, 3), // slice_qp_delta se -> -1
4344            (1, 1),     // slice_loop_filter_across_slices_enabled_flag
4345            (1, 1),     // byte_alignment one bit
4346        ]);
4347        let body = pack_bits(&bits);
4348        // NAL header for IDR_N_LP (type 20), layer 0, temporal_id 0.
4349        let b0 = (IDR_N_LP & 0x3F) << 1;
4350        let b1 = 0x01; // temporal_id_plus1 = 1
4351        let mut stream = vec![0x00, 0x00, 0x01, b0, b1];
4352        stream.extend_from_slice(&body);
4353        let units = collect_nal_units(&stream).expect("walker");
4354        assert_eq!(units.len(), 1);
4355        let u = &units[0];
4356        assert_eq!(u.header.nal_unit_type, IDR_N_LP);
4357        let sh = SliceSegmentHeader::parse(&u.rbsp, u.header.nal_unit_type, &sps, &pps)
4358            .expect("slice header");
4359        assert_eq!(sh.slice_type, Some(SliceType::I));
4360        assert_eq!(sh.slice_qp_delta, Some(-1));
4361        assert_eq!(sh.slice_qp_y(&pps), Some(25));
4362    }
4363
4364    // --- §7.3.6.2 ref_pic_lists_modification() ---
4365
4366    /// Hand-build a `ref_pic_lists_modification()` RBSP and parse it for
4367    /// a P slice with the modification flag set and a single L0 entry.
4368    /// `NumPicTotalCurr == 2` so the per-entry width is
4369    /// `Ceil(Log2(2)) == 1` bit. With `num_ref_idx_l0_active_minus1 ==
4370    /// 0` the loop reads exactly one `list_entry_l0`. The bits are:
4371    ///   `ref_pic_list_modification_flag_l0` (1) = 1
4372    ///   `list_entry_l0[0]` (u(1)) = 1
4373    /// (B-only L1 fields are absent for a P slice.)
4374    #[test]
4375    fn parses_p_slice_l0_only_modification() {
4376        let bits = concat_bits(&[(1, 1), (1, 1)]);
4377        let rbsp = pack_bits(&bits);
4378        let mut br = BitReader::new(&rbsp);
4379        let m = RefPicListsModification::parse(
4380            &mut br,
4381            SliceType::P,
4382            0, /* num_ref_idx_l0_active_minus1 */
4383            0, /* num_ref_idx_l1_active_minus1 (ignored for P) */
4384            2, /* NumPicTotalCurr */
4385        )
4386        .expect("ref_pic_lists_modification");
4387
4388        assert!(m.ref_pic_list_modification_flag_l0);
4389        assert_eq!(m.list_entry_l0, vec![1]);
4390        // P slice: list-1 fields absent.
4391        assert!(m.ref_pic_list_modification_flag_l1.is_none());
4392        assert!(m.list_entry_l1.is_empty());
4393        // Exactly 2 bits consumed.
4394        assert_eq!(br.bit_pos(), 2);
4395    }
4396
4397    /// B slice with both list-0 and list-1 modifications active.
4398    /// `NumPicTotalCurr == 4` so the per-entry width is
4399    /// `Ceil(Log2(4)) == 2` bits. With both `num_ref_idx_lX_active_minus1
4400    /// == 1` each list contributes 2 entries.
4401    ///   flag_l0 (1)=1
4402    ///   list_entry_l0[0] u(2)=0b10=2
4403    ///   list_entry_l0[1] u(2)=0b00=0
4404    ///   flag_l1 (1)=1
4405    ///   list_entry_l1[0] u(2)=0b01=1
4406    ///   list_entry_l1[1] u(2)=0b11=3
4407    /// Total = 1 + 2 + 2 + 1 + 2 + 2 = 10 bits.
4408    #[test]
4409    fn parses_b_slice_both_lists_modification() {
4410        let bits = concat_bits(&[
4411            (1, 1),    // ref_pic_list_modification_flag_l0
4412            (0b10, 2), // list_entry_l0[0] = 2
4413            (0b00, 2), // list_entry_l0[1] = 0
4414            (1, 1),    // ref_pic_list_modification_flag_l1
4415            (0b01, 2), // list_entry_l1[0] = 1
4416            (0b11, 2), // list_entry_l1[1] = 3
4417        ]);
4418        let rbsp = pack_bits(&bits);
4419        let mut br = BitReader::new(&rbsp);
4420        let m = RefPicListsModification::parse(&mut br, SliceType::B, 1, 1, 4).expect("parse");
4421
4422        assert!(m.ref_pic_list_modification_flag_l0);
4423        assert_eq!(m.list_entry_l0, vec![2, 0]);
4424        assert_eq!(m.ref_pic_list_modification_flag_l1, Some(true));
4425        assert_eq!(m.list_entry_l1, vec![1, 3]);
4426        assert_eq!(br.bit_pos(), 10);
4427    }
4428
4429    /// B slice where the L0 modification flag is 0 (implicit
4430    /// derivation): the L0 entry list is empty, and the L1 flag is
4431    /// read immediately after. `NumPicTotalCurr == 3` so per-entry
4432    /// width is `Ceil(Log2(3)) == 2` bits.
4433    ///   flag_l0 (1)=0
4434    ///   flag_l1 (1)=1
4435    ///   list_entry_l1[0] u(2)=0b10=2
4436    /// 4 bits total; the parser must NOT have consumed any L0 entries.
4437    #[test]
4438    fn parses_b_slice_l0_implicit_l1_explicit() {
4439        let bits = concat_bits(&[(0, 1), (1, 1), (0b10, 2)]);
4440        let rbsp = pack_bits(&bits);
4441        let mut br = BitReader::new(&rbsp);
4442        let m = RefPicListsModification::parse(&mut br, SliceType::B, 0, 0, 3).expect("parse");
4443
4444        assert!(!m.ref_pic_list_modification_flag_l0);
4445        assert!(m.list_entry_l0.is_empty());
4446        assert_eq!(m.ref_pic_list_modification_flag_l1, Some(true));
4447        assert_eq!(m.list_entry_l1, vec![2]);
4448        assert_eq!(br.bit_pos(), 4);
4449    }
4450
4451    /// Both flags zero: a minimal degenerate case where the entire
4452    /// structure is two bits and produces empty entry lists.
4453    #[test]
4454    fn parses_b_slice_both_flags_zero() {
4455        let bits = concat_bits(&[(0, 1), (0, 1)]);
4456        let rbsp = pack_bits(&bits);
4457        let mut br = BitReader::new(&rbsp);
4458        let m = RefPicListsModification::parse(&mut br, SliceType::B, 0, 0, 2).expect("parse");
4459
4460        assert!(!m.ref_pic_list_modification_flag_l0);
4461        assert!(m.list_entry_l0.is_empty());
4462        assert_eq!(m.ref_pic_list_modification_flag_l1, Some(false));
4463        assert!(m.list_entry_l1.is_empty());
4464        assert_eq!(br.bit_pos(), 2);
4465    }
4466
4467    /// P slice with `flag_l0 == 0`: exactly one bit consumed, no list
4468    /// entries, and no L1 fields present.
4469    #[test]
4470    fn parses_p_slice_l0_implicit() {
4471        let bits = concat_bits(&[(0, 1)]);
4472        let rbsp = pack_bits(&bits);
4473        let mut br = BitReader::new(&rbsp);
4474        let m = RefPicListsModification::parse(&mut br, SliceType::P, 14, 14, 5).expect("parse");
4475
4476        assert!(!m.ref_pic_list_modification_flag_l0);
4477        assert!(m.list_entry_l0.is_empty());
4478        assert!(m.ref_pic_list_modification_flag_l1.is_none());
4479        assert!(m.list_entry_l1.is_empty());
4480        assert_eq!(br.bit_pos(), 1);
4481    }
4482
4483    /// §7.4.7.2 range check: `list_entry_l0[i]` must be
4484    /// `< NumPicTotalCurr`. With `NumPicTotalCurr == 3` the per-entry
4485    /// width is 2 bits, so the value `3` (0b11) is legally encodable
4486    /// but is rejected by the range check.
4487    #[test]
4488    fn list_entry_l0_value_must_be_less_than_num_pic_total_curr() {
4489        let bits = concat_bits(&[
4490            (1, 1),    // flag_l0
4491            (0b11, 2), // list_entry_l0[0] = 3 — illegal: must be <= 2
4492        ]);
4493        let rbsp = pack_bits(&bits);
4494        let mut br = BitReader::new(&rbsp);
4495        let err = RefPicListsModification::parse(&mut br, SliceType::P, 0, 0, 3)
4496            .expect_err("out-of-range entry must error");
4497        assert_eq!(
4498            err,
4499            SliceError::ValueOutOfRange {
4500                field: "list_entry_l0",
4501                got: 3,
4502            }
4503        );
4504    }
4505
4506    /// §7.4.7.2 range check for L1: same rule, exercised through the
4507    /// B-slice branch.
4508    #[test]
4509    fn list_entry_l1_value_must_be_less_than_num_pic_total_curr() {
4510        // NumPicTotalCurr=2 → entry width=1 bit, max value=1. Send a
4511        // legal L0 (flag=0) then an explicit L1 (flag=1, entry=1 OK,
4512        // then we cannot encode 2 in 1 bit so use a 3-curr setup
4513        // instead to exercise the range check.)
4514        // Use NumPicTotalCurr=3 (2-bit entries) so 0b11=3 is illegal.
4515        let bits = concat_bits(&[
4516            (0, 1),    // flag_l0
4517            (1, 1),    // flag_l1
4518            (0b11, 2), // list_entry_l1[0] = 3 — illegal
4519        ]);
4520        let rbsp = pack_bits(&bits);
4521        let mut br = BitReader::new(&rbsp);
4522        let err = RefPicListsModification::parse(&mut br, SliceType::B, 0, 0, 3)
4523            .expect_err("out-of-range L1 entry must error");
4524        assert_eq!(
4525            err,
4526            SliceError::ValueOutOfRange {
4527                field: "list_entry_l1",
4528                got: 3,
4529            }
4530        );
4531    }
4532
4533    /// The §7.3.6.2 structure is only signalled for inter slices. The
4534    /// parser rejects an I-slice call up front rather than reading any
4535    /// bits (the bitreader position must stay at 0).
4536    #[test]
4537    fn rejects_i_slice_call() {
4538        let rbsp = [0xFFu8; 4];
4539        let mut br = BitReader::new(&rbsp);
4540        let err = RefPicListsModification::parse(&mut br, SliceType::I, 0, 0, 2)
4541            .expect_err("I-slice call must error");
4542        assert_eq!(
4543            err,
4544            SliceError::ValueOutOfRange {
4545                field: "ref_pic_lists_modification/slice_type",
4546                got: 2,
4547            }
4548        );
4549        assert_eq!(br.bit_pos(), 0);
4550    }
4551
4552    /// The §7.3.6.1 gate guarantees `NumPicTotalCurr > 1`. The parser
4553    /// rejects a call with `NumPicTotalCurr <= 1` (a defensive
4554    /// pre-condition).
4555    #[test]
4556    fn rejects_num_pic_total_curr_le_1() {
4557        let rbsp = [0xFFu8; 4];
4558        let mut br = BitReader::new(&rbsp);
4559        let err = RefPicListsModification::parse(&mut br, SliceType::P, 0, 0, 1)
4560            .expect_err("NumPicTotalCurr <= 1 must error");
4561        assert_eq!(
4562            err,
4563            SliceError::ValueOutOfRange {
4564                field: "ref_pic_lists_modification/NumPicTotalCurr",
4565                got: 1,
4566            }
4567        );
4568        assert_eq!(br.bit_pos(), 0);
4569
4570        let mut br2 = BitReader::new(&rbsp);
4571        let err2 = RefPicListsModification::parse(&mut br2, SliceType::B, 0, 0, 0)
4572            .expect_err("NumPicTotalCurr == 0 must error");
4573        assert_eq!(
4574            err2,
4575            SliceError::ValueOutOfRange {
4576                field: "ref_pic_lists_modification/NumPicTotalCurr",
4577                got: 0,
4578            }
4579        );
4580    }
4581
4582    /// `num_ref_idx_l0_active_minus1` is constrained to 0..=14 by
4583    /// §7.4.7.1. The parser rejects a call that violates that bound,
4584    /// matching the precondition documented on
4585    /// [`RefPicListsModification::parse`].
4586    #[test]
4587    fn rejects_num_ref_idx_l0_out_of_range() {
4588        let rbsp = [0xFFu8; 4];
4589        let mut br = BitReader::new(&rbsp);
4590        let err = RefPicListsModification::parse(&mut br, SliceType::P, 15, 0, 2)
4591            .expect_err("num_ref_idx_l0_active_minus1 > 14 must error");
4592        assert_eq!(
4593            err,
4594            SliceError::ValueOutOfRange {
4595                field: "num_ref_idx_l0_active_minus1",
4596                got: 15,
4597            }
4598        );
4599    }
4600
4601    /// Same check for L1, exercised through the B-slice branch (the
4602    /// L1 bound is only validated for B slices).
4603    #[test]
4604    fn rejects_num_ref_idx_l1_out_of_range() {
4605        let rbsp = [0xFFu8; 4];
4606        let mut br = BitReader::new(&rbsp);
4607        let err = RefPicListsModification::parse(&mut br, SliceType::B, 0, 15, 2)
4608            .expect_err("num_ref_idx_l1_active_minus1 > 14 must error");
4609        assert_eq!(
4610            err,
4611            SliceError::ValueOutOfRange {
4612                field: "num_ref_idx_l1_active_minus1",
4613                got: 15,
4614            }
4615        );
4616    }
4617
4618    /// Maximum-active-index case: 15 entries per list (the §7.4.7.1
4619    /// cap, `num_ref_idx_lX_active_minus1 == 14`), with
4620    /// `NumPicTotalCurr == 8` (3-bit entries). Bit accounting:
4621    ///   flag_l0 (1) + 15 * 3 = 46 bits for the L0 portion.
4622    /// The P slice has no L1 fields, so the test verifies the parser
4623    /// reads exactly 46 bits.
4624    #[test]
4625    fn max_active_minus1_p_slice_l0() {
4626        let mut fields: Vec<(u32, u8)> = vec![(1, 1)]; // flag_l0 = 1
4627        for i in 0..15u32 {
4628            // entry value = i mod 8 ∈ 0..=7, fits in 3 bits, in range.
4629            fields.push((i % 8, 3));
4630        }
4631        let bits = concat_bits(&fields);
4632        let rbsp = pack_bits(&bits);
4633        let mut br = BitReader::new(&rbsp);
4634        let m = RefPicListsModification::parse(&mut br, SliceType::P, 14, 0, 8).expect("parse");
4635        assert!(m.ref_pic_list_modification_flag_l0);
4636        assert_eq!(m.list_entry_l0.len(), 15);
4637        for (i, &v) in m.list_entry_l0.iter().enumerate() {
4638            assert_eq!(v, (i as u32) % 8);
4639        }
4640        assert_eq!(br.bit_pos(), 1 + 15 * 3);
4641    }
4642
4643    /// `Ceil(Log2(N))` width: confirm the per-entry width matches the
4644    /// §7.4.7.2 formula for a representative set of `NumPicTotalCurr`
4645    /// values by reading exactly the expected bit count from a flag=1
4646    /// L0 with a single entry.
4647    #[test]
4648    fn entry_width_matches_ceil_log2() {
4649        // (num_pic_total_curr, expected_bits_per_entry)
4650        let cases: &[(u32, u8)] = &[(2, 1), (3, 2), (4, 2), (5, 3), (8, 3), (9, 4), (16, 4)];
4651        for &(curr, w) in cases {
4652            // bit layout: flag_l0=1 then list_entry_l0[0]=0
4653            let mut bits: Vec<u8> = vec![1];
4654            bits.resize(1 + w as usize, 0);
4655            let rbsp = pack_bits(&bits);
4656            let mut br = BitReader::new(&rbsp);
4657            let m =
4658                RefPicListsModification::parse(&mut br, SliceType::P, 0, 0, curr).expect("parse");
4659            assert_eq!(m.list_entry_l0, vec![0]);
4660            assert_eq!(br.bit_pos(), 1 + w as usize, "curr={curr} width={w}");
4661        }
4662    }
4663
4664    /// Truncated RBSP: the parser surfaces [`SliceError::Truncated`]
4665    /// if the buffer runs out mid-element.
4666    #[test]
4667    fn truncated_buffer_surfaces_truncated_error() {
4668        // flag_l0=1 declared but no bits remain for list_entry_l0[0].
4669        let bits: Vec<u8> = vec![1];
4670        let rbsp = pack_bits(&bits); // one byte: 0b1000_0000
4671                                     // Restrict the reader to the first bit only.
4672        let buf = &rbsp[..0]; // zero bytes; even flag_l0 fails
4673        let mut br = BitReader::new(buf);
4674        let err = RefPicListsModification::parse(&mut br, SliceType::P, 0, 0, 4)
4675            .expect_err("empty buffer must error");
4676        assert_eq!(err, SliceError::Truncated);
4677    }
4678
4679    // --- §7.4.7.2 NumPicTotalCurr derivation ---
4680
4681    /// Equation 7-57 with only short-term entries: two negative pics
4682    /// both used (`UsedByCurrPicS0 = [1, 1]`), one positive pic not
4683    /// used (`UsedByCurrPicS1 = [0]`), and no long-term entries.
4684    /// Expected: `NumPicTotalCurr = 2`.
4685    #[test]
4686    fn num_pic_total_curr_short_term_only() {
4687        let s0 = [true, true];
4688        let s1 = [false];
4689        let lt: [bool; 0] = [];
4690        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt);
4691        assert_eq!(inputs.compute(), 2);
4692    }
4693
4694    /// Equation 7-57 with a mix of S0, S1 and long-term entries
4695    /// flagged "used by current pic". Hand-derived: 2 S0 ones (3
4696    /// flags, 2 set) + 1 S1 one (2 flags, 1 set) + 2 LT ones (3 flags,
4697    /// 2 set) = 5.
4698    #[test]
4699    fn num_pic_total_curr_mixed_short_and_long_term() {
4700        let s0 = [true, false, true];
4701        let s1 = [true, false];
4702        let lt = [true, false, true];
4703        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt);
4704        assert_eq!(inputs.compute(), 5);
4705    }
4706
4707    /// Equation 7-57 with `pps_curr_pic_ref_enabled_flag == 1` adding
4708    /// the final `NumPicTotalCurr++`. With zero short-term and zero
4709    /// long-term contributions, the value is exactly 1 (the IBC /
4710    /// self-reference case the SCC PPS flag enables).
4711    #[test]
4712    fn num_pic_total_curr_curr_pic_ref_only() {
4713        let s0: [bool; 0] = [];
4714        let s1: [bool; 0] = [];
4715        let lt: [bool; 0] = [];
4716        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt)
4717            .with_pps_curr_pic_ref_enabled(true);
4718        assert_eq!(inputs.compute(), 1);
4719    }
4720
4721    /// Equation 7-57 with every contributing source: 1 S0 + 1 S1 +
4722    /// 1 LT + `pps_curr_pic_ref_enabled_flag` = 4.
4723    #[test]
4724    fn num_pic_total_curr_all_contributors() {
4725        let s0 = [true];
4726        let s1 = [true];
4727        let lt = [true];
4728        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt)
4729            .with_pps_curr_pic_ref_enabled(true);
4730        assert_eq!(inputs.compute(), 4);
4731    }
4732
4733    /// Empty short-term RPS + empty long-term + no SCC self-ref =
4734    /// `NumPicTotalCurr == 0`. The §7.4.7.1 conformance rule "when
4735    /// the current picture contains a P or B slice, the value of
4736    /// NumPicTotalCurr shall not be equal to 0" is the consumer's
4737    /// responsibility — this primitive returns the literal
4738    /// equation-7-57 value.
4739    #[test]
4740    fn num_pic_total_curr_zero_when_nothing_contributes() {
4741        let s0: [bool; 0] = [];
4742        let s1: [bool; 0] = [];
4743        let lt: [bool; 0] = [];
4744        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt);
4745        assert_eq!(inputs.compute(), 0);
4746    }
4747
4748    /// Build a short-term RPS in *explicit* form with three negative
4749    /// and two positive pics, then derive `NumPicTotalCurr` via
4750    /// [`NumPicTotalCurrInputs::from_explicit_short_term_rps`].
4751    /// Negative used flags `[1, 0, 1]` + positive `[1, 1]` + no LT =
4752    /// 2 + 2 + 0 = 4.
4753    #[test]
4754    fn num_pic_total_curr_from_explicit_rps_builder() {
4755        let rps = ShortTermRefPicSet {
4756            inter_ref_pic_set_prediction_flag: false,
4757            num_negative_pics: 3,
4758            num_positive_pics: 2,
4759            delta_poc_s0_minus1: vec![0, 1, 2],
4760            used_by_curr_pic_s0_flag: vec![true, false, true],
4761            delta_poc_s1_minus1: vec![0, 1],
4762            used_by_curr_pic_s1_flag: vec![true, true],
4763            ..Default::default()
4764        };
4765        let lt: [bool; 0] = [];
4766        let inputs = NumPicTotalCurrInputs::from_explicit_short_term_rps(&rps, &lt)
4767            .expect("explicit RPS yields builder");
4768        assert_eq!(inputs.compute(), 4);
4769    }
4770
4771    /// The explicit-RPS builder refuses an inter-RPS-predicted RPS:
4772    /// the §7.4.8 derivation (equations 7-58..7-66) must run first to
4773    /// resolve the per-position `UsedByCurrPicSX` arrays, and the
4774    /// result fed through [`NumPicTotalCurrInputs::from_used_flags`].
4775    #[test]
4776    fn num_pic_total_curr_from_explicit_rps_rejects_inter_prediction() {
4777        let rps = ShortTermRefPicSet {
4778            inter_ref_pic_set_prediction_flag: true,
4779            ..Default::default()
4780        };
4781        let lt: [bool; 0] = [];
4782        assert!(NumPicTotalCurrInputs::from_explicit_short_term_rps(&rps, &lt).is_none());
4783    }
4784
4785    /// §7.4.7.1 / §7.4.7.2 long-term resolution:
4786    /// [`SliceLongTermRefPic::used_by_curr_pic_lt`] reads the SPS
4787    /// table when the entry is `Sps { lt_idx_sps }`, and the in-slice
4788    /// flag when the entry is `InSlice { used_by_curr_pic_lt_flag }`.
4789    /// Construct an SPS with two LT entries (`[used=1, used=0]`) and
4790    /// verify the SPS lookup; then verify the in-slice form.
4791    #[test]
4792    fn used_by_curr_pic_lt_resolves_sps_table_and_in_slice() {
4793        let mut sps = ctx_sps(1, false, false, false, 16, 16, 0, 0, 4);
4794        sps.long_term_ref_pics_present_flag = true;
4795        sps.num_long_term_ref_pics_sps = 2;
4796        sps.long_term_ref_pics = vec![
4797            LongTermRefPicEntry {
4798                poc_lsb: 0,
4799                used_by_curr_pic: true,
4800            },
4801            LongTermRefPicEntry {
4802                poc_lsb: 1,
4803                used_by_curr_pic: false,
4804            },
4805        ];
4806
4807        let sps_entry_used = SliceLongTermRefPic {
4808            source: SliceLongTermRefPicSource::Sps { lt_idx_sps: 0 },
4809            delta_poc_msb_present_flag: false,
4810            delta_poc_msb_cycle_lt: 0,
4811        };
4812        assert_eq!(sps_entry_used.used_by_curr_pic_lt(&sps), Some(true));
4813
4814        let sps_entry_unused = SliceLongTermRefPic {
4815            source: SliceLongTermRefPicSource::Sps { lt_idx_sps: 1 },
4816            delta_poc_msb_present_flag: false,
4817            delta_poc_msb_cycle_lt: 0,
4818        };
4819        assert_eq!(sps_entry_unused.used_by_curr_pic_lt(&sps), Some(false));
4820
4821        let in_slice_used = SliceLongTermRefPic {
4822            source: SliceLongTermRefPicSource::InSlice {
4823                poc_lsb_lt: 7,
4824                used_by_curr_pic_lt_flag: true,
4825            },
4826            delta_poc_msb_present_flag: false,
4827            delta_poc_msb_cycle_lt: 0,
4828        };
4829        assert_eq!(in_slice_used.used_by_curr_pic_lt(&sps), Some(true));
4830
4831        // Out-of-range SPS index surfaces `None`.
4832        let sps_oob = SliceLongTermRefPic {
4833            source: SliceLongTermRefPicSource::Sps { lt_idx_sps: 99 },
4834            delta_poc_msb_present_flag: false,
4835            delta_poc_msb_cycle_lt: 0,
4836        };
4837        assert_eq!(sps_oob.used_by_curr_pic_lt(&sps), None);
4838    }
4839
4840    /// End-to-end: build the long-term ref list a §7.3.6.1 slice
4841    /// header would carry (one SPS-resident `used == 1` entry + one
4842    /// in-slice `used == 0` entry + one in-slice `used == 1` entry),
4843    /// resolve each entry's `UsedByCurrPicLt[i]`, and feed the bool
4844    /// vector through equation 7-57. With empty short-term sets the
4845    /// result is 2.
4846    #[test]
4847    fn num_pic_total_curr_from_resolved_slice_long_term_list() {
4848        let mut sps = ctx_sps(1, false, false, false, 16, 16, 0, 0, 4);
4849        sps.long_term_ref_pics_present_flag = true;
4850        sps.num_long_term_ref_pics_sps = 1;
4851        sps.long_term_ref_pics = vec![LongTermRefPicEntry {
4852            poc_lsb: 0,
4853            used_by_curr_pic: true,
4854        }];
4855
4856        let slice_lt = [
4857            SliceLongTermRefPic {
4858                source: SliceLongTermRefPicSource::Sps { lt_idx_sps: 0 },
4859                delta_poc_msb_present_flag: false,
4860                delta_poc_msb_cycle_lt: 0,
4861            },
4862            SliceLongTermRefPic {
4863                source: SliceLongTermRefPicSource::InSlice {
4864                    poc_lsb_lt: 4,
4865                    used_by_curr_pic_lt_flag: false,
4866                },
4867                delta_poc_msb_present_flag: false,
4868                delta_poc_msb_cycle_lt: 0,
4869            },
4870            SliceLongTermRefPic {
4871                source: SliceLongTermRefPicSource::InSlice {
4872                    poc_lsb_lt: 8,
4873                    used_by_curr_pic_lt_flag: true,
4874                },
4875                delta_poc_msb_present_flag: false,
4876                delta_poc_msb_cycle_lt: 0,
4877            },
4878        ];
4879        let used_lt: Vec<bool> = slice_lt
4880            .iter()
4881            .map(|e| e.used_by_curr_pic_lt(&sps).expect("in-range"))
4882            .collect();
4883        let s0: [bool; 0] = [];
4884        let s1: [bool; 0] = [];
4885        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &used_lt);
4886        assert_eq!(inputs.compute(), 2);
4887    }
4888
4889    /// F.7.4.7.2 multilayer-extension form (equation `F-56`): when
4890    /// the slice's `nal_unit_type` is IDR, the short-term and
4891    /// long-term loops are SKIPPED entirely, so the count starts at 0
4892    /// and only `pps_curr_pic_ref_enabled_flag` + the
4893    /// `NumActiveRefLayerPics` summand contribute. Feed
4894    /// `used_by_curr_pic_*` flags that would each contribute 1 under
4895    /// equation 7-57 — they must be ignored.
4896    #[test]
4897    fn num_pic_total_curr_multilayer_skips_temporal_loops_for_idr() {
4898        let s0 = [true];
4899        let s1 = [true];
4900        let lt = [true];
4901        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt)
4902            .with_multilayer_extension(IDR_W_RADL, 3);
4903        // Skipped loops contribute 0; pps_curr_pic_ref_enabled = false;
4904        // NumActiveRefLayerPics = 3.
4905        assert_eq!(inputs.compute(), 3);
4906
4907        // Same inputs but flipping the SCC self-ref flag: +1 = 4.
4908        let inputs = NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt)
4909            .with_pps_curr_pic_ref_enabled(true)
4910            .with_multilayer_extension(IDR_N_LP, 3);
4911        assert_eq!(inputs.compute(), 4);
4912    }
4913
4914    /// F.7.4.7.2 multilayer-extension form for a *non-IDR* slice: the
4915    /// short-term and long-term loops are NOT skipped, so the count
4916    /// matches the base-spec 7-57 result plus
4917    /// `NumActiveRefLayerPics`.
4918    /// 1 (S0) + 1 (S1) + 1 (LT) + 2 (NumActiveRefLayerPics) = 5.
4919    #[test]
4920    fn num_pic_total_curr_multilayer_keeps_loops_for_non_idr() {
4921        let s0 = [true];
4922        let s1 = [true];
4923        let lt = [true];
4924        // TRAIL_N (Table 7-1 value 0) is not IDR.
4925        let inputs =
4926            NumPicTotalCurrInputs::from_used_flags(&s0, &s1, &lt).with_multilayer_extension(0, 2);
4927        assert_eq!(inputs.compute(), 5);
4928    }
4929
4930    /// §7.3.6.1 gate sanity: `NumPicTotalCurr > 1` is the condition
4931    /// under which the slice header signals `ref_pic_lists_modification()`.
4932    /// Compose an explicit-form RPS that would yield exactly 1 (one
4933    /// `UsedByCurrPicS0` flag set, nothing else) and confirm
4934    /// `NumPicTotalCurr == 1` — the §7.3.6.1 gate would not fire.
4935    /// Then compose one that yields 2 (two flags set) and confirm the
4936    /// gate would fire. This is a derivation cross-check, not a
4937    /// parser invocation.
4938    #[test]
4939    fn num_pic_total_curr_drives_section_7_3_6_1_gate() {
4940        let rps_one = ShortTermRefPicSet {
4941            inter_ref_pic_set_prediction_flag: false,
4942            num_negative_pics: 1,
4943            num_positive_pics: 0,
4944            delta_poc_s0_minus1: vec![0],
4945            used_by_curr_pic_s0_flag: vec![true],
4946            ..Default::default()
4947        };
4948        let lt: [bool; 0] = [];
4949        let inputs = NumPicTotalCurrInputs::from_explicit_short_term_rps(&rps_one, &lt).unwrap();
4950        assert_eq!(inputs.compute(), 1, "gate should not fire at 1");
4951
4952        let rps_two = ShortTermRefPicSet {
4953            inter_ref_pic_set_prediction_flag: false,
4954            num_negative_pics: 2,
4955            num_positive_pics: 0,
4956            delta_poc_s0_minus1: vec![0, 1],
4957            used_by_curr_pic_s0_flag: vec![true, true],
4958            ..Default::default()
4959        };
4960        let inputs = NumPicTotalCurrInputs::from_explicit_short_term_rps(&rps_two, &lt).unwrap();
4961        assert_eq!(inputs.compute(), 2, "gate fires at 2");
4962    }
4963
4964    // --- §7.3.6.3 pred_weight_table() ---
4965
4966    /// Build the `ue(v)` codeword for a value `v` (Table 9-3). Returns
4967    /// `(value, width_in_bits)` ready for [`concat_bits`].
4968    fn ue_codeword(v: u32) -> (u32, u8) {
4969        let plus1 = v + 1;
4970        let m = 32 - plus1.leading_zeros() - 1; // floor(log2(v+1))
4971        let width = (2 * m + 1) as u8;
4972        (plus1, width)
4973    }
4974
4975    /// `se(v)` codeword: map signed to unsigned per the §9.2.1 inverse
4976    /// of Table 9-3 (`codeNum = 2*|v| - (v > 0 ? 1 : 0)`), then encode
4977    /// the result as an `ue(v)`.
4978    fn se_codeword(v: i32) -> (u32, u8) {
4979        let code_num: u32 = if v <= 0 {
4980            (2 * (-v)) as u32
4981        } else {
4982            (2 * v - 1) as u32
4983        };
4984        ue_codeword(code_num)
4985    }
4986
4987    /// Minimal monochrome P-slice case (`ChromaArrayType == 0` so no
4988    /// chroma fields are signalled): one reference, `luma_weight_l0_flag
4989    /// == 1`, `delta_luma_weight_l0[0] == 5`, `luma_offset_l0[0] == 0`.
4990    ///
4991    /// Bit layout:
4992    /// ```text
4993    ///   luma_log2_weight_denom              ue(v) = 2     -> 0b011  (3 bits)
4994    ///   luma_weight_l0_flag[0]              u(1)  = 1     -> 0b1    (1 bit)
4995    ///   delta_luma_weight_l0[0]             se(v) = 5     -> codeNum=9, ue(v)=0b0001010 (7 bits)
4996    ///   luma_offset_l0[0]                   se(v) = 0     -> codeNum=0, ue(v)=0b1       (1 bit)
4997    /// ```
4998    /// total = 3 + 1 + 7 + 1 = 12 bits.
4999    #[test]
5000    fn parses_monochrome_p_slice_single_ref() {
5001        let mut fields = vec![ue_codeword(2)]; // luma_log2_weight_denom
5002        fields.push((1, 1)); // luma_weight_l0_flag[0]
5003        fields.push(se_codeword(5)); // delta_luma_weight_l0[0]
5004        fields.push(se_codeword(0)); // luma_offset_l0[0]
5005        let bits = concat_bits(&fields);
5006        let rbsp = pack_bits(&bits);
5007        let mut br = BitReader::new(&rbsp);
5008        let inputs = PredWeightTableInputs::base_profile(
5009            SliceType::P,
5010            0,
5011            0,
5012            /*ChromaArrayType*/ 0,
5013            8,
5014            8,
5015        );
5016        let pwt = PredWeightTable::parse(&mut br, &inputs).expect("parse");
5017
5018        assert_eq!(pwt.luma_log2_weight_denom, 2);
5019        assert_eq!(pwt.delta_chroma_log2_weight_denom, 0); // absent → inferred 0
5020        assert_eq!(pwt.entries_l0.len(), 1);
5021        assert!(pwt.entries_l0[0].luma_weight_flag);
5022        assert!(!pwt.entries_l0[0].chroma_weight_flag); // monochrome
5023        assert_eq!(pwt.entries_l0[0].delta_luma_weight, 5);
5024        assert_eq!(pwt.entries_l0[0].luma_offset, 0);
5025        assert!(pwt.entries_l1.is_empty());
5026        // 12 bits consumed.
5027        assert_eq!(br.bit_pos(), 12);
5028        // Derived: LumaWeightL0[0] = (1 << 2) + 5 = 9
5029        assert_eq!(pwt.luma_weight_l0(0), Some(9));
5030    }
5031
5032    /// P-slice with `ChromaArrayType == 1` (4:2:0), one reference,
5033    /// every flag = 1. Verifies the chroma sub-block parses and the
5034    /// derived `ChromaLog2WeightDenom`, `ChromaWeightL0[0][j]` and
5035    /// `ChromaOffsetL0[0][j]` resolve correctly.
5036    ///
5037    /// Bit layout:
5038    /// ```text
5039    ///   luma_log2_weight_denom              ue(v) = 1   -> 0b010
5040    ///   delta_chroma_log2_weight_denom      se(v) = 1   -> codeNum=1, ue(v)=0b010
5041    ///   luma_weight_l0_flag[0]              u(1)  = 1
5042    ///   chroma_weight_l0_flag[0]            u(1)  = 1
5043    ///   delta_luma_weight_l0[0]             se(v) = -3  -> codeNum=6, ue(v)=0b00111
5044    ///   luma_offset_l0[0]                   se(v) = 7   -> codeNum=13, ue(v)=0b0001110
5045    ///   delta_chroma_weight_l0[0][0]        se(v) = 0   -> 0b1
5046    ///   delta_chroma_offset_l0[0][0]        se(v) = 2   -> codeNum=3, ue(v)=0b00100
5047    ///   delta_chroma_weight_l0[0][1]        se(v) = 0   -> 0b1
5048    ///   delta_chroma_offset_l0[0][1]        se(v) = -1  -> codeNum=2, ue(v)=0b011
5049    /// ```
5050    #[test]
5051    fn parses_p_slice_420_single_ref_with_chroma() {
5052        let mut fields = vec![ue_codeword(1)];
5053        fields.push(se_codeword(1));
5054        fields.push((1, 1)); // luma_weight_l0_flag
5055        fields.push((1, 1)); // chroma_weight_l0_flag
5056        fields.push(se_codeword(-3));
5057        fields.push(se_codeword(7));
5058        fields.push(se_codeword(0));
5059        fields.push(se_codeword(2));
5060        fields.push(se_codeword(0));
5061        fields.push(se_codeword(-1));
5062        let bits = concat_bits(&fields);
5063        let rbsp = pack_bits(&bits);
5064        let mut br = BitReader::new(&rbsp);
5065        let inputs = PredWeightTableInputs::base_profile(SliceType::P, 0, 0, 1, 8, 8);
5066        let pwt = PredWeightTable::parse(&mut br, &inputs).expect("parse");
5067
5068        assert_eq!(pwt.luma_log2_weight_denom, 1);
5069        assert_eq!(pwt.delta_chroma_log2_weight_denom, 1);
5070        assert_eq!(pwt.chroma_log2_weight_denom(), 2);
5071        assert_eq!(pwt.entries_l0[0].delta_luma_weight, -3);
5072        assert_eq!(pwt.entries_l0[0].luma_offset, 7);
5073        assert_eq!(pwt.entries_l0[0].delta_chroma_weight, [0, 0]);
5074        assert_eq!(pwt.entries_l0[0].delta_chroma_offset, [2, -1]);
5075        // Derived: LumaWeightL0[0] = (1 << 1) + (-3) = -1
5076        assert_eq!(pwt.luma_weight_l0(0), Some(-1));
5077        // ChromaWeightL0[0][j] = (1 << 2) + 0 = 4 for j ∈ {0, 1}
5078        assert_eq!(pwt.chroma_weight_l0(0, 0), Some(4));
5079        assert_eq!(pwt.chroma_weight_l0(0, 1), Some(4));
5080        // Equation 7-58 with WpOffsetHalfRangeC = 128:
5081        //   raw_j0 = 128 + 2 - ((128 * 4) >> 2) = 130 - 128 = 2
5082        //   raw_j1 = 128 + (-1) - 128 = -1
5083        assert_eq!(pwt.chroma_offset_l0(0, 0, 128), Some(2));
5084        assert_eq!(pwt.chroma_offset_l0(0, 1, 128), Some(-1));
5085    }
5086
5087    /// B-slice with `ChromaArrayType == 1`, one ref per list, all flags
5088    /// off (the minimal-content B case). Verifies the L1 block is
5089    /// reached and the chroma flag pass is read on both lists; absent
5090    /// deltas remain at 0; derived `LumaWeightLX[0]` inferred to
5091    /// `1 << luma_log2_weight_denom`.
5092    ///
5093    /// Bit layout (denoms then four flag bits, all 0; L1 mirrors L0):
5094    /// ```text
5095    ///   luma_log2_weight_denom              ue(v) = 0  -> 0b1
5096    ///   delta_chroma_log2_weight_denom      se(v) = 0  -> 0b1
5097    ///   luma_weight_l0_flag[0]              u(1)  = 0
5098    ///   chroma_weight_l0_flag[0]            u(1)  = 0
5099    ///   luma_weight_l1_flag[0]              u(1)  = 0
5100    ///   chroma_weight_l1_flag[0]            u(1)  = 0
5101    /// ```
5102    /// 2 bits denoms + 4 bits flags = 6 bits.
5103    #[test]
5104    fn parses_b_slice_all_flags_zero() {
5105        let mut fields = vec![ue_codeword(0), se_codeword(0)];
5106        fields.push((0, 1));
5107        fields.push((0, 1));
5108        fields.push((0, 1));
5109        fields.push((0, 1));
5110        let bits = concat_bits(&fields);
5111        let rbsp = pack_bits(&bits);
5112        let mut br = BitReader::new(&rbsp);
5113        let inputs = PredWeightTableInputs::base_profile(SliceType::B, 0, 0, 1, 8, 8);
5114        let pwt = PredWeightTable::parse(&mut br, &inputs).expect("parse");
5115
5116        assert_eq!(pwt.entries_l0.len(), 1);
5117        assert_eq!(pwt.entries_l1.len(), 1);
5118        assert!(!pwt.entries_l0[0].luma_weight_flag);
5119        assert!(!pwt.entries_l1[0].chroma_weight_flag);
5120        // No deltas were signalled; absent values inferred to 0.
5121        assert_eq!(pwt.entries_l0[0].delta_luma_weight, 0);
5122        assert_eq!(pwt.entries_l1[0].delta_chroma_offset, [0, 0]);
5123        // Derived LumaWeightLX[0] = (1 << 0) + 0 = 1 (inferred form).
5124        assert_eq!(pwt.luma_weight_l0(0), Some(1));
5125        assert_eq!(pwt.luma_weight_l1(0), Some(1));
5126        // ChromaOffsetLX[i][j] inferred to 0 when chroma_weight_flag == 0.
5127        assert_eq!(pwt.chroma_offset_l0(0, 0, 128), Some(0));
5128        assert_eq!(pwt.chroma_offset_l1(0, 1, 128), Some(0));
5129        assert_eq!(br.bit_pos(), 6);
5130    }
5131
5132    /// `luma_log2_weight_denom > 7` is a range failure (§7.4.7.3).
5133    /// `ue(v)` value 8 encodes as `0b0001001` (7 bits).
5134    #[test]
5135    fn rejects_luma_log2_weight_denom_above_7() {
5136        let bits = concat_bits(&[ue_codeword(8)]);
5137        let rbsp = pack_bits(&bits);
5138        let mut br = BitReader::new(&rbsp);
5139        let inputs = PredWeightTableInputs::base_profile(SliceType::P, 0, 0, 0, 8, 8);
5140        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5141        assert_eq!(
5142            err,
5143            SliceError::ValueOutOfRange {
5144                field: "luma_log2_weight_denom",
5145                got: 8,
5146            }
5147        );
5148    }
5149
5150    /// `ChromaLog2WeightDenom = luma_log2_weight_denom +
5151    /// delta_chroma_log2_weight_denom` ∈ 0..=7 (§7.4.7.3). Encode
5152    /// `luma_log2_weight_denom == 3, delta_chroma_log2_weight_denom ==
5153    /// 5` → derived = 8, must error.
5154    #[test]
5155    fn rejects_derived_chroma_log2_weight_denom_above_7() {
5156        let bits = concat_bits(&[ue_codeword(3), se_codeword(5)]);
5157        let rbsp = pack_bits(&bits);
5158        let mut br = BitReader::new(&rbsp);
5159        let inputs = PredWeightTableInputs::base_profile(SliceType::P, 0, 0, 1, 8, 8);
5160        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5161        assert_eq!(
5162            err,
5163            SliceError::ValueOutOfRange {
5164                field: "ChromaLog2WeightDenom",
5165                got: 8,
5166            }
5167        );
5168    }
5169
5170    /// `delta_luma_weight_l0[i]` ∉ −128..=127 must error (§7.4.7.3).
5171    /// `se(v)` value 128 encodes via codeNum = 255 → ue(v) is 15 bits
5172    /// long; pack it and verify the parser surfaces ValueOutOfRange.
5173    #[test]
5174    fn rejects_delta_luma_weight_l0_above_127() {
5175        let mut fields = vec![ue_codeword(0)];
5176        fields.push((1, 1)); // luma_weight_l0_flag = 1
5177        fields.push(se_codeword(128)); // out of range
5178        let bits = concat_bits(&fields);
5179        let rbsp = pack_bits(&bits);
5180        let mut br = BitReader::new(&rbsp);
5181        let inputs = PredWeightTableInputs::base_profile(SliceType::P, 0, 0, 0, 8, 8);
5182        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5183        assert_eq!(
5184            err,
5185            SliceError::ValueOutOfRange {
5186                field: "delta_luma_weight_l0",
5187                got: 128,
5188            }
5189        );
5190    }
5191
5192    /// `luma_offset_l0[i]` ∈ `−128..=127` for base profile (8 bits, no
5193    /// high-precision offsets). Encode value 128 → range failure.
5194    #[test]
5195    fn rejects_luma_offset_l0_above_127_at_8_bit() {
5196        let mut fields = vec![ue_codeword(0)];
5197        fields.push((1, 1)); // flag = 1
5198        fields.push(se_codeword(0)); // delta_luma_weight = 0 (in range)
5199        fields.push(se_codeword(128)); // luma_offset = 128 out of range
5200        let bits = concat_bits(&fields);
5201        let rbsp = pack_bits(&bits);
5202        let mut br = BitReader::new(&rbsp);
5203        let inputs = PredWeightTableInputs::base_profile(SliceType::P, 0, 0, 0, 8, 8);
5204        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5205        assert_eq!(
5206            err,
5207            SliceError::ValueOutOfRange {
5208                field: "luma_offset_l0",
5209                got: 128,
5210            }
5211        );
5212    }
5213
5214    /// `high_precision_offsets_enabled_flag == true` widens
5215    /// `WpOffsetHalfRangeY` from `1 << 7 = 128` to
5216    /// `1 << (BitDepthY - 1)`. With `BitDepthY == 10` the new range is
5217    /// `−512..=511`. Encode `luma_offset_l0 == 200` (was out-of-range
5218    /// at 8-bit, in-range at 10-bit high-precision) and verify parse
5219    /// succeeds.
5220    #[test]
5221    fn accepts_luma_offset_in_high_precision_range() {
5222        let mut fields = vec![ue_codeword(0)];
5223        fields.push((1, 1));
5224        fields.push(se_codeword(0));
5225        fields.push(se_codeword(200));
5226        let bits = concat_bits(&fields);
5227        let rbsp = pack_bits(&bits);
5228        let mut br = BitReader::new(&rbsp);
5229        let inputs = PredWeightTableInputs {
5230            slice_type: SliceType::P,
5231            num_ref_idx_l0_active_minus1: 0,
5232            num_ref_idx_l1_active_minus1: 0,
5233            chroma_array_type: 0,
5234            high_precision_offsets_enabled_flag: true,
5235            bit_depth_y: 10,
5236            bit_depth_c: 10,
5237            signal_luma_l0: None,
5238            signal_chroma_l0: None,
5239            signal_luma_l1: None,
5240            signal_chroma_l1: None,
5241        };
5242        let pwt = PredWeightTable::parse(&mut br, &inputs).expect("parse");
5243        assert_eq!(pwt.entries_l0[0].luma_offset, 200);
5244    }
5245
5246    /// §7.3.6.3 outer gate: when the caller passes
5247    /// `signal_luma_l0[i] == false`, the corresponding flag bit is NOT
5248    /// consumed from the bitstream and the parsed flag is inferred to
5249    /// `false`. Build a two-ref P slice with `signal_luma_l0 = [false,
5250    /// true]`: only one luma-flag bit is present (the second), and
5251    /// the parser must read exactly the right bits.
5252    ///
5253    /// Bit layout (monochrome, n_l0=2):
5254    /// ```text
5255    ///   luma_log2_weight_denom            ue(v) = 0  -> 0b1     (1)
5256    ///   luma_weight_l0_flag[1]            u(1)  = 1            (1)
5257    ///   delta_luma_weight_l0[1]           se(v) = 4  -> codeNum=7, ue(v)=0b0001000 (7)
5258    ///   luma_offset_l0[1]                 se(v) = 0  -> 0b1     (1)
5259    /// ```
5260    /// Total = 10 bits.
5261    #[test]
5262    fn outer_gate_suppresses_per_i_flag_bits() {
5263        let mut fields = vec![ue_codeword(0)];
5264        fields.push((1, 1)); // luma_weight_l0_flag[1]
5265        fields.push(se_codeword(4)); // delta_luma_weight_l0[1]
5266        fields.push(se_codeword(0));
5267        let bits = concat_bits(&fields);
5268        let rbsp = pack_bits(&bits);
5269        let mut br = BitReader::new(&rbsp);
5270
5271        let gate_l0 = [false, true];
5272        let inputs = PredWeightTableInputs {
5273            slice_type: SliceType::P,
5274            num_ref_idx_l0_active_minus1: 1,
5275            num_ref_idx_l1_active_minus1: 0,
5276            chroma_array_type: 0,
5277            high_precision_offsets_enabled_flag: false,
5278            bit_depth_y: 8,
5279            bit_depth_c: 8,
5280            signal_luma_l0: Some(&gate_l0),
5281            signal_chroma_l0: None,
5282            signal_luma_l1: None,
5283            signal_chroma_l1: None,
5284        };
5285        let pwt = PredWeightTable::parse(&mut br, &inputs).expect("parse");
5286        assert_eq!(pwt.entries_l0.len(), 2);
5287        assert!(!pwt.entries_l0[0].luma_weight_flag); // gated off → inferred 0
5288        assert!(pwt.entries_l0[1].luma_weight_flag);
5289        assert_eq!(pwt.entries_l0[1].delta_luma_weight, 4);
5290        // Position 0's deltas remain at the inferred default (0).
5291        assert_eq!(pwt.entries_l0[0].delta_luma_weight, 0);
5292        assert_eq!(pwt.entries_l0[0].luma_offset, 0);
5293        // Bit accounting matches the expected layout.
5294        assert_eq!(br.bit_pos(), 10);
5295    }
5296
5297    /// Per-i gate slice length mismatch is a precondition failure.
5298    #[test]
5299    fn rejects_signal_slice_length_mismatch() {
5300        let bits = concat_bits(&[ue_codeword(0)]);
5301        let rbsp = pack_bits(&bits);
5302        let mut br = BitReader::new(&rbsp);
5303        let gate_too_short = [true]; // num_ref_idx_l0_active_minus1 + 1 == 2
5304        let inputs = PredWeightTableInputs {
5305            slice_type: SliceType::P,
5306            num_ref_idx_l0_active_minus1: 1,
5307            num_ref_idx_l1_active_minus1: 0,
5308            chroma_array_type: 0,
5309            high_precision_offsets_enabled_flag: false,
5310            bit_depth_y: 8,
5311            bit_depth_c: 8,
5312            signal_luma_l0: Some(&gate_too_short),
5313            signal_chroma_l0: None,
5314            signal_luma_l1: None,
5315            signal_chroma_l1: None,
5316        };
5317        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5318        assert_eq!(
5319            err,
5320            SliceError::ValueOutOfRange {
5321                field: "signal_luma_l0",
5322                got: 1,
5323            }
5324        );
5325    }
5326
5327    /// Rejects an I-slice call (the §7.3.6.1 gate
5328    /// `weighted_pred_flag && slice_type == P` /
5329    /// `weighted_bipred_flag && slice_type == B` excludes I slices).
5330    #[test]
5331    fn rejects_i_slice_call_pwt() {
5332        let rbsp = [0xFFu8; 4];
5333        let mut br = BitReader::new(&rbsp);
5334        let inputs = PredWeightTableInputs::base_profile(SliceType::I, 0, 0, 0, 8, 8);
5335        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5336        assert_eq!(
5337            err,
5338            SliceError::ValueOutOfRange {
5339                field: "pred_weight_table/slice_type",
5340                got: 2,
5341            }
5342        );
5343        assert_eq!(br.bit_pos(), 0);
5344    }
5345
5346    /// §7.4.7.3 conformance: for a P slice, `sumWeightL0Flags ≤ 24`.
5347    /// Each entry contributes up to 3 (luma=1, chroma=2), so 9 entries
5348    /// with both flags set sum to 27, breaching the cap. Build the
5349    /// minimal P-slice case with 9 entries (`num_ref_idx_l0_active_minus1
5350    /// = 8`) and verify the parser rejects.
5351    #[test]
5352    fn rejects_sum_weight_l0_above_24() {
5353        let mut fields = vec![ue_codeword(0), se_codeword(0)];
5354        let n = 9usize;
5355        // 9 luma_weight_l0_flag bits, all 1
5356        for _ in 0..n {
5357            fields.push((1, 1));
5358        }
5359        // 9 chroma_weight_l0_flag bits, all 1
5360        for _ in 0..n {
5361            fields.push((1, 1));
5362        }
5363        // Per-entry deltas (luma + chroma): delta=0, offset=0
5364        for _ in 0..n {
5365            fields.push(se_codeword(0)); // delta_luma_weight
5366            fields.push(se_codeword(0)); // luma_offset
5367            for _ in 0..2 {
5368                fields.push(se_codeword(0)); // delta_chroma_weight
5369                fields.push(se_codeword(0)); // delta_chroma_offset
5370            }
5371        }
5372        let bits = concat_bits(&fields);
5373        let rbsp = pack_bits(&bits);
5374        let mut br = BitReader::new(&rbsp);
5375        let inputs = PredWeightTableInputs::base_profile(SliceType::P, (n - 1) as u8, 0, 1, 8, 8);
5376        let err = PredWeightTable::parse(&mut br, &inputs).expect_err("must error");
5377        assert_eq!(
5378            err,
5379            SliceError::ValueOutOfRange {
5380                field: "sumWeightL0Flags",
5381                got: 27,
5382            }
5383        );
5384    }
5385
5386    /// I-slice with WPP enabled (`entropy_coding_sync_enabled_flag ==
5387    /// 1`): the §7.3.6.1 entry-point block is signalled with
5388    /// `num_entry_point_offsets = 2`, `offset_len_minus1 = 3` (each
5389    /// entry is `u(4)`), and per-row byte offsets `{6, 9}`. Verify
5390    /// the parser captures the offsets verbatim and exposes the
5391    /// per-subset byte length via [`EntryPointOffsets::subset_length`]
5392    /// (`entry_point_offset_minus1[i] + 1`, §7.4.7.1).
5393    #[test]
5394    fn parses_wpp_entry_point_offsets_in_place() {
5395        // Tall enough picture for two WPP entry points: 3 CTU rows.
5396        // With CTB size = 16 (`log2_min_cb_minus3 = 0`,
5397        // `log2_diff_max_min_cb = 1`) and `pic_height = 48`,
5398        // `PicHeightInCtbsY = 3`, so the upper bound on
5399        // `num_entry_point_offsets` is 2.
5400        let sps = ctx_sps(1, false, true, true, 16, 48, 1, 0, 4);
5401        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
5402        pps.entropy_coding_sync_enabled_flag = true;
5403
5404        let bits = concat_bits(&[
5405            (1, 1),     // first_slice_segment_in_pic_flag
5406            (0, 1),     // no_output (IRAP)
5407            (0b1, 1),   // pps_id ue -> 0
5408            (0b011, 3), // slice_type ue -> I
5409            // slice_temporal_mvp_enabled_flag: absent for an IDR.
5410            (1, 1),   // sao_luma
5411            (0, 1),   // sao_chroma
5412            (0b1, 1), // slice_qp_delta se -> 0
5413            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
5414            // Entry-point block.
5415            (0b011, 3),   // num_entry_point_offsets ue -> 2
5416            (0b00100, 5), // offset_len_minus1 ue -> 3 (entries are u(4))
5417            (6, 4),       // entry_point_offset_minus1[0]
5418            (9, 4),       // entry_point_offset_minus1[1]
5419            (1, 1),       // byte_alignment '1'
5420        ]);
5421        let rbsp = pack_bits(&bits);
5422        let sh = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect("slice header");
5423        let eps = sh.entry_point_offsets.expect("entry-point block present");
5424        assert_eq!(eps.num_entry_point_offsets, 2);
5425        assert_eq!(eps.offset_len_minus1, 3);
5426        assert_eq!(eps.entry_point_offset_minus1, vec![6, 9]);
5427        // §7.4.7.1 subset length is `entry_point_offset_minus1[i] + 1`.
5428        assert_eq!(eps.subset_length(0), Some(7));
5429        assert_eq!(eps.subset_length(1), Some(10));
5430        assert_eq!(eps.subset_length(2), None);
5431        assert!(sh.byte_offset_to_slice_data.is_some());
5432    }
5433
5434    /// Tiles enabled with a single tile (`num_tile_columns_minus1 ==
5435    /// 0`, `num_tile_rows_minus1 == 0`): the §7.4.7.1 upper bound on
5436    /// `num_entry_point_offsets` is `1 * 1 − 1 == 0`, so the block is
5437    /// present (the gate fires on `tiles_enabled_flag`) but
5438    /// `num_entry_point_offsets` must be 0 and the `offset_len_minus1`
5439    /// / per-entry loop are skipped. Verify the parser materialises an
5440    /// empty vec and reports a bare gate.
5441    #[test]
5442    fn parses_tiles_block_with_single_tile_no_offsets() {
5443        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
5444        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
5445        pps.tiles_enabled_flag = true;
5446        // tiles inferred default (cols=0, rows=0 minus1 → 1×1).
5447
5448        let bits = concat_bits(&[
5449            (1, 1),     // first_slice_segment_in_pic_flag
5450            (0, 1),     // no_output
5451            (0b1, 1),   // pps_id ue -> 0
5452            (0b011, 3), // slice_type ue -> I
5453            // slice_temporal_mvp_enabled_flag: absent for an IDR.
5454            (1, 1),   // sao_luma
5455            (0, 1),   // sao_chroma
5456            (0b1, 1), // slice_qp_delta se -> 0
5457            (1, 1),   // slice_loop_filter_across_slices_enabled_flag
5458            (0b1, 1), // num_entry_point_offsets ue -> 0
5459            (1, 1),   // byte_alignment '1'
5460        ]);
5461        let rbsp = pack_bits(&bits);
5462        let sh = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect("slice header");
5463        let eps = sh.entry_point_offsets.expect("entry-point block present");
5464        assert_eq!(eps.num_entry_point_offsets, 0);
5465        assert_eq!(eps.offset_len_minus1, 0);
5466        assert!(eps.entry_point_offset_minus1.is_empty());
5467        assert!(eps.subset_length(0).is_none());
5468    }
5469
5470    /// §7.4.7.1: when `entropy_coding_sync_enabled_flag == 1`, the
5471    /// upper bound on `num_entry_point_offsets` is `PicHeightInCtbsY −
5472    /// 1`. Build an SPS with `PicHeightInCtbsY == 1` (16×16 with CTB
5473    /// size 16): the bound is 0, so a wire value of 1 must fail the
5474    /// range check.
5475    #[test]
5476    fn rejects_wpp_entry_point_offsets_above_pic_height_bound() {
5477        let sps = ctx_sps(1, false, true, true, 16, 16, 1, 0, 4);
5478        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
5479        pps.entropy_coding_sync_enabled_flag = true;
5480
5481        let bits = concat_bits(&[
5482            (1, 1),     // first_slice_segment_in_pic_flag
5483            (0, 1),     // no_output (IRAP); tmvp flag absent for an IDR
5484            (0b1, 1),   // pps_id ue -> 0
5485            (0b011, 3), // slice_type ue -> I
5486            (1, 1),     // sao_luma
5487            (0, 1),     // sao_chroma
5488            (0b1, 1),   // slice_qp_delta se -> 0
5489            (1, 1),     // lf_across
5490            (0b010, 3), // num_entry_point_offsets ue -> 1, breaches bound 0
5491        ]);
5492        let rbsp = pack_bits(&bits);
5493        let err = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect_err("must error");
5494        assert_eq!(
5495            err,
5496            SliceError::ValueOutOfRange {
5497                field: "num_entry_point_offsets",
5498                got: 1,
5499            }
5500        );
5501    }
5502
5503    /// §7.4.7.1: `offset_len_minus1` is bounded to `0..=31`. Build a
5504    /// WPP-enabled fixture with a wire value of 32 and verify the
5505    /// parser rejects.
5506    #[test]
5507    fn rejects_offset_len_minus1_above_31() {
5508        let sps = ctx_sps(1, false, true, true, 16, 48, 1, 0, 4);
5509        let mut pps = PicParameterSet::parse(TINY_PPS_RBSP).expect("PPS");
5510        pps.entropy_coding_sync_enabled_flag = true;
5511
5512        let bits = concat_bits(&[
5513            (1, 1),     // first_slice_segment_in_pic_flag
5514            (0, 1),     // no_output (IRAP); tmvp flag absent for an IDR
5515            (0b1, 1),   // pps_id ue -> 0
5516            (0b011, 3), // slice_type ue -> I
5517            (1, 1),     // sao_luma
5518            (0, 1),     // sao_chroma
5519            (0b1, 1),   // slice_qp_delta se -> 0
5520            (1, 1),     // lf_across
5521            (0b010, 3), // num_entry_point_offsets ue -> 1
5522            // offset_len_minus1 = 32, encoded ue: codeNum 32 has
5523            // M = floor(log2(33)) = 5 leading zeros, then '1', then
5524            // 5-bit suffix (33 - 32 = 1 → 00001). 11 bits total:
5525            // 00000 1 00001 → 0b000_0010_0001.
5526            (0b000_0010_0001, 11),
5527        ]);
5528        let rbsp = pack_bits(&bits);
5529        let err = SliceSegmentHeader::parse(&rbsp, IDR_N_LP, &sps, &pps).expect_err("must error");
5530        assert_eq!(
5531            err,
5532            SliceError::ValueOutOfRange {
5533                field: "offset_len_minus1",
5534                got: 32,
5535            }
5536        );
5537    }
5538
5539    // --- bit-packing test helpers ---
5540
5541    /// A `(value, width)` pair to be packed MSB-first.
5542    type BitField = (u32, u8);
5543
5544    /// Concatenate `(value, width)` fields into a single bit vector
5545    /// (each entry's `width` low bits of `value`, MSB-first).
5546    fn concat_bits(fields: &[BitField]) -> Vec<u8> {
5547        let mut bits = Vec::new();
5548        for &(value, width) in fields {
5549            for i in (0..width).rev() {
5550                bits.push(((value >> i) & 1) as u8);
5551            }
5552        }
5553        bits
5554    }
5555
5556    /// Pack an MSB-first bit vector into bytes, zero-padding the final
5557    /// byte (matching `byte_alignment()`'s zero pad).
5558    fn pack_bits(bits: &[u8]) -> Vec<u8> {
5559        let mut out = vec![0u8; bits.len().div_ceil(8)];
5560        for (i, &b) in bits.iter().enumerate() {
5561            if b != 0 {
5562                out[i / 8] |= 1 << (7 - (i % 8));
5563            }
5564        }
5565        out
5566    }
5567}