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 <_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, <);
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, <);
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, <)
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, <)
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, <);
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, <)
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, <).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, <)
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, <)
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, <).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, <).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, <).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}