Skip to main content

rusty_h264_decoder/
lib.rs

1//! Pure-Rust H.264 (Constrained Baseline) decoder.
2//!
3//! Parses SPS/PPS/IDR-slice headers and reconstructs I_16x16 (DC-predicted)
4//! macroblocks: CAVLC residual decode, inverse transform (incl. luma/chroma DC
5//! Hadamard), intra DC prediction. The reconstruction path is shared with the
6//! encoder so the two agree bit-for-bit. Inter prediction and deblocking land in
7//! later generations behind this same API.
8
9mod cabac;
10mod cabac_tables;
11mod mb16;
12mod params;
13
14pub use params::{Pps, Sps};
15
16use mb16::{FrameDecoder, WeightTable};
17use rusty_h264_common::bit_reader::OutOfData;
18use rusty_h264_common::nal::{emulation_unprevent, split_annex_b};
19use rusty_h264_common::{BitReader, NalUnitType, YuvFrame};
20
21/// Decode errors.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum DecodeError {
24    /// Bitstream ended unexpectedly.
25    Truncated,
26    /// A required parameter set was missing before a slice.
27    MissingParameterSet,
28    /// A coding tool outside the implemented subset appeared in the stream.
29    Unsupported(&'static str),
30}
31
32impl From<OutOfData> for DecodeError {
33    fn from(_: OutOfData) -> Self {
34        DecodeError::Truncated
35    }
36}
37
38impl core::fmt::Display for DecodeError {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        match self {
41            DecodeError::Truncated => f.write_str("bitstream truncated"),
42            DecodeError::MissingParameterSet => f.write_str("slice before SPS/PPS"),
43            DecodeError::Unsupported(s) => write!(f, "unsupported coding tool: {s}"),
44        }
45    }
46}
47
48impl std::error::Error for DecodeError {}
49
50/// A reference picture: deblocked reconstruction at coded resolution.
51/// Stored now (4a); read by motion compensation in 4b.
52#[derive(Debug, Clone, Default)]
53#[allow(dead_code)]
54pub(crate) struct RefFrame {
55    pub y: Vec<u8>,
56    pub u: Vec<u8>,
57    pub v: Vec<u8>,
58    pub cw: usize,
59    pub ch: usize,
60    /// `frame_num` of the picture, for PicNum-based reference-list reordering.
61    pub frame_num: u32,
62    /// `PicOrderCnt` of the picture, for B-slice reference-list ordering.
63    pub poc: i32,
64    /// Per-4×4-block List-0 motion field (motion vector + reference index, `-1`
65    /// for intra), and the block-grid width. Read as the *co-located* picture's
66    /// motion for B-slice direct prediction (`colZeroFlag`, temporal direct).
67    pub mv: Vec<(i32, i32)>,
68    pub ref_idx: Vec<i32>,
69    /// Per-4×4-block POC of the List-0 picture each block referenced (`i32::MIN`
70    /// for intra). Used by temporal direct's `MapColToList0` (the co-located
71    /// reference index alone is meaningless in the current list).
72    pub ref_poc: Vec<i32>,
73    pub w4: usize,
74    /// Long-term reference state. Long-term refs sit after short-term ones in
75    /// `RefPicList0` (ordered by `long_term_idx` ascending) and survive the
76    /// sliding window until explicitly unmarked (spec §8.2.4).
77    pub long_term: bool,
78    pub long_term_idx: u32,
79}
80
81/// A memory-management control operation (`dec_ref_pic_marking`, spec §7.4.3.3).
82#[derive(Clone, Copy)]
83enum Mmco {
84    /// 1: mark a short-term reference (by PicNum) as unused.
85    Unref(u32),
86    /// 2: mark a long-term reference (by LongTermPicNum) as unused.
87    UnrefLong(u32),
88    /// 3: assign a short-term reference (by PicNum) a LongTermFrameIdx.
89    AssignLong(u32, u32),
90    /// 4: drop long-term references with idx ≥ max_long_term_frame_idx_plus1.
91    MaxLong(u32),
92    /// 5: empty the DPB (and reset the current picture's frame_num to 0).
93    Reset,
94    /// 6: mark the current picture long-term with this LongTermFrameIdx.
95    CurrentLong(u32),
96}
97
98/// A picture being assembled from one or more slices (spec allows a picture to
99/// be split into multiple slices). Finalized — deblocked, output, and entered
100/// into the DPB — once all its macroblocks are decoded.
101struct PendingPic {
102    fd: mb16::FrameDecoder,
103    frame_num: u32,
104    poc: i32,
105    next_mb: usize,
106    total_mb: usize,
107    slice_count: u16,
108    deblock: bool,
109    filter_offset_a: i32,
110    filter_offset_b: i32,
111    crop_r: usize,
112    crop_b: usize,
113    max_refs: usize,
114    log2_max_frame_num: u32,
115    /// `false` for a non-reference picture (nal_ref_idc == 0): output it but do
116    /// not enter it into the DPB.
117    is_reference: bool,
118    idr_long_term: bool,
119    mmco_ops: Vec<Mmco>,
120}
121
122/// A Constrained Baseline H.264 decoder. Holds the most recent parameter sets
123/// and the previous decoded picture (the inter reference) across calls.
124#[derive(Default)]
125pub struct Decoder {
126    /// Active parameter sets, keyed by id — a stream may carry several and switch
127    /// between them per slice (spec §7.3.2.1/.2).
128    sps: std::collections::HashMap<u32, Sps>,
129    pps: std::collections::HashMap<u32, Pps>,
130    /// Decoded-picture buffer (most-recent first); `ref_idx` indexes into this.
131    refs: Vec<RefFrame>,
132    /// The picture currently being assembled from its slices, if any.
133    cur: Option<PendingPic>,
134    /// Picture-order-count state (spec §8.2.1). Tracks the previous reference
135    /// picture's MSB/LSB (type 0) and frame-num offset (types 1/2) so display
136    /// order can be recovered — needed once B-pictures (out-of-order) land.
137    poc: PocState,
138    /// `PicOrderCnt` of the most recently returned picture (display-order key).
139    last_poc: i32,
140    /// `frame_num` of the previous short-term reference picture, for detecting
141    /// gaps in `frame_num` (spec §8.2.5.2).
142    prev_ref_frame_num: u32,
143}
144
145/// Running picture-order-count derivation state.
146#[derive(Default)]
147struct PocState {
148    prev_msb: i32,
149    prev_lsb: i32,
150    prev_frame_num: u32,
151    prev_frame_num_offset: i64,
152}
153
154impl Decoder {
155    /// Creates a decoder with no parameter sets yet.
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Decodes a complete Annex-B access unit, returning the reconstructed,
161    /// cropped frame if the access unit contained a coded picture.
162    pub fn decode(&mut self, annex_b: &[u8]) -> Result<Option<YuvFrame>, DecodeError> {
163        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Total);
164        let mut frame = None;
165        for nal in split_annex_b(annex_b) {
166            if nal.is_empty() {
167                continue;
168            }
169            let nal_type = NalUnitType::from_id(nal[0]);
170            let rbsp = emulation_unprevent(&nal[1..]);
171            match nal_type {
172                NalUnitType::Sps => {
173                    let s = Sps::parse(&rbsp)?;
174                    self.sps.insert(s.seq_parameter_set_id, s);
175                }
176                NalUnitType::Pps => {
177                    let p = Pps::parse(&rbsp)?;
178                    self.pps.insert(p.pic_parameter_set_id, p);
179                }
180                NalUnitType::IdrSlice | NalUnitType::NonIdrSlice => {
181                    let nal_ref_idc = (nal[0] >> 5) & 3;
182                    let is_idr = nal_type == NalUnitType::IdrSlice;
183                    if let Some(f) = self.decode_slice(&rbsp, is_idr, nal_ref_idc)? {
184                        frame = Some(f);
185                    }
186                }
187                _ => {} // SEI, AUD, etc. ignored
188            }
189        }
190        Ok(frame)
191    }
192
193    /// Decodes a complete Annex-B byte stream and returns every picture in
194    /// **display order** (`PicOrderCnt` within each GOP; an IDR ends a GOP).
195    ///
196    /// This is the convenient whole-stream entry point — it handles access-unit
197    /// splitting, multi-slice picture assembly, and B-picture reordering — versus
198    /// the lower-level per-access-unit [`Decoder::decode`], which returns pictures
199    /// in decode order.
200    pub fn decode_stream(&mut self, annex_b: &[u8]) -> Result<Vec<YuvFrame>, DecodeError> {
201        let mut out = Vec::new();
202        let mut gop: Vec<(i32, YuvFrame)> = Vec::new();
203        for au in split_access_units(annex_b) {
204            if au_is_idr(au) {
205                flush_gop(&mut gop, &mut out); // emit the prior GOP before the IDR
206            }
207            if let Some(frame) = self.decode(au)? {
208                gop.push((self.last_poc, frame));
209            }
210        }
211        flush_gop(&mut gop, &mut out);
212        Ok(out)
213    }
214
215    fn decode_slice(
216        &mut self,
217        rbsp: &[u8],
218        is_idr: bool,
219        nal_ref_idc: u8,
220    ) -> Result<Option<YuvFrame>, DecodeError> {
221        let mut r = BitReader::new(rbsp);
222        // --- slice_header ---
223        let first_mb_in_slice = r.read_ue()? as usize;
224        let slice_type = r.read_ue()?;
225        let is_p = matches!(slice_type, 0 | 5);
226        let is_b = matches!(slice_type, 1 | 6);
227        let is_i = matches!(slice_type, 2 | 7);
228        if !is_p && !is_b && !is_i {
229            return Err(DecodeError::Unsupported("SP/SI slices"));
230        }
231        // Resolve the parameter sets this slice references (by id).
232        let pic_parameter_set_id = r.read_ue()?;
233        let pps = self.pps.get(&pic_parameter_set_id).cloned().ok_or(DecodeError::MissingParameterSet)?;
234        let sps = self.sps.get(&pps.seq_parameter_set_id).cloned().ok_or(DecodeError::MissingParameterSet)?;
235        let sps = &sps;
236        let pps = &pps;
237        // CABAC (entropy_coding_mode_flag=1) has an entirely different slice-data parse
238        // (docs/cabac-decode-plan.md). I-slice CABAC is being brought up; the CABAC MB
239        // loop gates P/B until Phase 3. `cabac_init_idc` (P/B only) is read below.
240        let cabac = pps.entropy_coding_mode_flag;
241        let frame_num = r.read_bits(sps.log2_max_frame_num)?;
242        if is_idr {
243            let _idr_pic_id = r.read_ue()?;
244        }
245        // pic_order_cnt fields (spec §7.3.3). `field_pic_flag` is always 0
246        // (frame_mbs_only). Captured to derive PicOrderCnt for display ordering.
247        let mut poc_lsb = 0u32;
248        let mut delta_poc_bottom = 0i32;
249        if sps.pic_order_cnt_type == 0 {
250            poc_lsb = r.read_bits(sps.log2_max_pic_order_cnt_lsb)?;
251            if pps.bottom_field_pic_order_present {
252                delta_poc_bottom = r.read_se()?;
253            }
254        } else if sps.pic_order_cnt_type == 1 && !sps.delta_pic_order_always_zero {
255            let _delta_pic_order_cnt_0 = r.read_se()?;
256            if pps.bottom_field_pic_order_present {
257                let _delta_pic_order_cnt_1 = r.read_se()?;
258            }
259        }
260        // PicOrderCnt is determined by the first slice of the picture; later
261        // slices share it (and must not re-advance the POC state).
262        let pic_poc = if first_mb_in_slice == 0 {
263            self.compute_poc(sps, is_idr, nal_ref_idc, frame_num, poc_lsb, delta_poc_bottom)
264        } else {
265            self.cur.as_ref().map_or(0, |p| p.poc)
266        };
267        // redundant_pic_cnt: a non-zero value marks a *redundant* coded picture
268        // (an alternative representation of the primary picture). A primary
269        // decoder discards it (spec §7.4.3, §8.2.5 note). Must be read here or the
270        // rest of the slice header desyncs.
271        if pps.redundant_pic_cnt_present_flag {
272            let redundant_pic_cnt = r.read_ue()?;
273            if redundant_pic_cnt != 0 {
274                return Ok(None);
275            }
276        }
277        // B slices choose direct-mode derivation here (spec §7.3.3).
278        let direct_spatial = if is_b { r.read_bit()? } else { true };
279        let mut num_ref_idx_l0 = pps.num_ref_idx_l0_default as usize;
280        let mut num_ref_idx_l1 = pps.num_ref_idx_l1_default as usize;
281        let mut reorder_l0: Vec<(u32, u32)> = Vec::new();
282        let mut reorder_l1: Vec<(u32, u32)> = Vec::new();
283        if is_p || is_b {
284            // num_ref_idx_active_override_flag
285            if r.read_bit()? {
286                num_ref_idx_l0 = (r.read_ue()? + 1) as usize;
287                if is_b {
288                    num_ref_idx_l1 = (r.read_ue()? + 1) as usize;
289                }
290            }
291            // ref_pic_list_modification_flag_l0
292            if r.read_bit()? {
293                parse_ref_pic_list_modification(&mut r, &mut reorder_l0)?;
294            }
295            if is_b && r.read_bit()? {
296                // ref_pic_list_modification_flag_l1
297                parse_ref_pic_list_modification(&mut r, &mut reorder_l1)?;
298            }
299        }
300        // Explicit weighted prediction carries a pred_weight_table() here. P
301        // (weighted_pred) uses single-list weights; B explicit bipred (idc 1) is
302        // not yet wired into the bi-pred averaging, so refuse that. Implicit
303        // bipred (idc 2) carries no table.
304        let weights = if is_p && pps.weighted_pred {
305            Some(parse_pred_weight_table(&mut r, num_ref_idx_l0, 0, false)?)
306        } else if is_b && pps.weighted_bipred_idc == 1 {
307            return Err(DecodeError::Unsupported("explicit B weighted prediction"));
308        } else {
309            None
310        };
311        // dec_ref_pic_marking (spec §7.3.3.3) — present only for reference
312        // pictures (nal_ref_idc != 0). Reading it for a non-reference slice would
313        // desync the rest of the header.
314        let mut idr_long_term = false;
315        let mut mmco_ops: Vec<Mmco> = Vec::new();
316        if nal_ref_idc == 0 {
317            // non-reference picture: no marking syntax
318        } else if is_idr {
319            let _no_output_of_prior_pics = r.read_bit()?;
320            idr_long_term = r.read_bit()?; // long_term_reference_flag
321        } else if r.read_bit()? {
322            // adaptive_ref_pic_marking_mode_flag
323            loop {
324                let op = r.read_ue()?;
325                match op {
326                    0 => break,
327                    1 => mmco_ops.push(Mmco::Unref(r.read_ue()?)),
328                    2 => mmco_ops.push(Mmco::UnrefLong(r.read_ue()?)),
329                    3 => {
330                        let diff = r.read_ue()?;
331                        let idx = r.read_ue()?;
332                        mmco_ops.push(Mmco::AssignLong(diff, idx));
333                    }
334                    4 => mmco_ops.push(Mmco::MaxLong(r.read_ue()?)),
335                    5 => mmco_ops.push(Mmco::Reset),
336                    6 => mmco_ops.push(Mmco::CurrentLong(r.read_ue()?)),
337                    _ => return Err(DecodeError::Unsupported("invalid MMCO")),
338                }
339                if mmco_ops.len() > 128 {
340                    return Err(DecodeError::Truncated);
341                }
342            }
343        }
344        // cabac_init_idc (spec §7.3.3) — CABAC context-model preset, P/B slices only.
345        // Spec range [0,2]; a larger (corrupt) value would index the 4-model context-init
346        // table out of bounds, so reject it here.
347        let cabac_init_idc = if cabac && !is_i {
348            let v = r.read_ue()?;
349            if v > 2 {
350                return Err(DecodeError::Unsupported("invalid cabac_init_idc"));
351            }
352            v
353        } else {
354            0
355        };
356        let slice_qp_delta = r.read_se()?;
357        // When deblocking_filter_control_present_flag is 0 the slice carries no
358        // disable_deblocking_filter_idc and it is inferred 0 — i.e. the in-loop
359        // filter is ON by default (spec §7.4.3). (Our own encoder always signals
360        // the control explicitly, so this default was previously untested.)
361        let mut deblock = true;
362        let (mut filter_offset_a, mut filter_offset_b) = (0i32, 0i32);
363        if pps.deblocking_filter_control_present_flag {
364            let disable_deblocking_filter_idc = r.read_ue()?;
365            // idc 1 = filter off; idc 0 = on; idc 2 = on but not across slice
366            // boundaries (equivalent to on for single-slice pictures).
367            deblock = disable_deblocking_filter_idc != 1;
368            if disable_deblocking_filter_idc != 1 {
369                // FilterOffset = slice_*_offset_div2 × 2 (spec §7.4.3).
370                filter_offset_a = r.read_se()? * 2;
371                filter_offset_b = r.read_se()? * 2;
372            }
373        }
374        let slice_qp = (pps.pic_init_qp + slice_qp_delta).clamp(0, 51) as u8;
375
376        // Synthesize placeholder short-term references for any gap in frame_num
377        // (spec §8.2.5.2) so the DPB / PicNum mapping stays correct.
378        if first_mb_in_slice == 0 && !is_idr && sps.gaps_in_frame_num_allowed {
379            self.insert_frame_num_gaps(
380                frame_num,
381                1u32 << sps.log2_max_frame_num,
382                sps.max_num_ref_frames.max(1) as usize,
383                sps.pic_width_in_mbs * 16,
384                sps.pic_height_in_mbs * 16,
385            );
386        }
387
388        // Build the reference list(s) for this slice. P uses RefPicList0 only;
389        // B uses RefPicList0 and RefPicList1 (POC-ordered).
390        let max_fn = 1u32 << sps.log2_max_frame_num;
391        let (ref_list0, ref_list1) = if is_b {
392            build_ref_list_b(
393                &self.refs, pic_poc, frame_num, max_fn,
394                num_ref_idx_l0, num_ref_idx_l1, &reorder_l0, &reorder_l1,
395            )?
396        } else if is_p {
397            (build_ref_list_p(&self.refs, frame_num, max_fn, num_ref_idx_l0, &reorder_l0)?, Vec::new())
398        } else {
399            (Vec::new(), Vec::new())
400        };
401        // --- picture assembly ---
402        // first_mb_in_slice == 0 starts a new picture; otherwise this slice
403        // continues the one in flight. An IDR clears the DPB at its first slice.
404        if first_mb_in_slice == 0 {
405            if is_idr {
406                self.refs.clear();
407            }
408            let mut fd = FrameDecoder::new(
409                sps.pic_width_in_mbs,
410                sps.pic_height_in_mbs,
411                slice_qp,
412                pps.chroma_qp_index_offset,
413                ref_list0,
414                num_ref_idx_l0,
415                pps.constrained_intra_pred_flag,
416                pps.transform_8x8_mode_flag,
417                sps.profile_idc != 66, // b_possible: Baseline/Constrained Baseline (66) forbid B
418            );
419            if is_b {
420                fd.set_b_context(
421                    ref_list1,
422                    num_ref_idx_l1,
423                    direct_spatial,
424                    pic_poc,
425                    pps.weighted_bipred_idc,
426                    sps.direct_8x8_inference,
427                );
428            }
429            if sps.has_scaling || pps.pic_scaling_matrix_present {
430                let (s4, s8) = resolve_scaling(sps, pps);
431                fd.set_scaling(s4, s8);
432            }
433            if let Some(w) = weights {
434                fd.set_weights(w);
435            }
436            self.cur = Some(PendingPic {
437                fd,
438                frame_num,
439                poc: pic_poc,
440                next_mb: 0,
441                total_mb: sps.pic_width_in_mbs * sps.pic_height_in_mbs,
442                slice_count: 0,
443                deblock,
444                filter_offset_a,
445                filter_offset_b,
446                crop_r: sps.frame_crop_right as usize,
447                crop_b: sps.frame_crop_bottom as usize,
448                max_refs: sps.max_num_ref_frames.max(1) as usize,
449                log2_max_frame_num: sps.log2_max_frame_num,
450                is_reference: nal_ref_idc != 0,
451                idr_long_term,
452                mmco_ops,
453            });
454        } else {
455            // Continuation slice: reset the per-slice QP + reference list.
456            let Some(pic) = self.cur.as_mut() else {
457                return Err(DecodeError::Unsupported("slice continues a missing picture"));
458            };
459            pic.fd.begin_slice(slice_qp, ref_list0, num_ref_idx_l0);
460            if is_b {
461                pic.fd.set_b_context(
462                    ref_list1,
463                    num_ref_idx_l1,
464                    direct_spatial,
465                    pic.poc,
466                    pps.weighted_bipred_idc,
467                    sps.direct_8x8_inference,
468                );
469            }
470            if sps.has_scaling || pps.pic_scaling_matrix_present {
471                let (s4, s8) = resolve_scaling(sps, pps);
472                pic.fd.set_scaling(s4, s8);
473            }
474            if let Some(w) = weights {
475                pic.fd.set_weights(w);
476            }
477            // Latest slice's marking/deblock parameters win at finalization.
478            pic.deblock = deblock;
479            pic.filter_offset_a = filter_offset_a;
480            pic.filter_offset_b = filter_offset_b;
481            pic.idr_long_term |= idr_long_term;
482            pic.mmco_ops.extend(mmco_ops);
483        }
484
485        let pic = self.cur.as_mut().expect("pending picture set above");
486        let first = first_mb_in_slice.min(pic.total_mb);
487        let next = if cabac {
488            // cabac_alignment_one_bit → the slice data is byte-aligned from here.
489            r.align_to_byte().map_err(|_| DecodeError::Truncated)?;
490            let (data, start) = (r.data(), r.bit_pos() / 8);
491            pic.fd
492                .decode_slice_data_cabac(data, start, slice_qp, cabac_init_idc, is_i, is_p, first)
493        } else {
494            pic.fd.decode_slice_data(&mut r, is_p, first)
495        }
496        .map_err(|e| match e {
497            mb16::MbError::Truncated => DecodeError::Truncated,
498            mb16::MbError::Unsupported(s) => DecodeError::Unsupported(s),
499        })?;
500        pic.next_mb = next;
501        pic.slice_count += 1;
502
503        if pic.next_mb < pic.total_mb {
504            return Ok(None); // picture not yet complete
505        }
506
507        // --- finalize the completed picture ---
508        let pic = self.cur.take().expect("pending picture");
509        let PendingPic {
510            mut fd,
511            frame_num,
512            poc,
513            deblock,
514            filter_offset_a,
515            filter_offset_b,
516            crop_r,
517            crop_b,
518            max_refs,
519            log2_max_frame_num,
520            is_reference,
521            idr_long_term,
522            mmco_ops,
523            ..
524        } = pic;
525        self.last_poc = poc;
526        if deblock {
527            fd.deblock(filter_offset_a, filter_offset_b);
528        }
529        // The necessary DPB plane clone (rec_y/u/v → RefFrame) — measured as its own
530        // stage, OUTSIDE the Finalize scope so the two don't double-count.
531        let reference = if is_reference {
532            let _dg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DpbClone);
533            Some(fd.as_reference())
534        } else {
535            // A non-reference picture is output but never enters the DPB.
536            None
537        };
538        let _fg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Finalize);
539        if let Some(mut reference) = reference {
540            reference.frame_num = frame_num;
541            reference.poc = poc;
542            if idr_long_term {
543                reference.long_term = true;
544                reference.long_term_idx = 0;
545            }
546            // Track the reference frame_num for gap detection (0 after MMCO 5).
547            self.prev_ref_frame_num =
548                self.apply_ref_marking(reference, &mmco_ops, frame_num, log2_max_frame_num, max_refs);
549        }
550        Ok(Some(fd.into_frame(crop_r, crop_b)))
551    }
552
553    /// Inserts "non-existing" short-term reference frames for each `frame_num`
554    /// skipped since the previous reference picture (spec §8.2.5.2). Their samples
555    /// are unspecified (a conformant stream never references them); we use mid-grey
556    /// so any accidental reference is benign. They occupy DPB slots and advance the
557    /// sliding window, keeping PicNum/ref-list derivation correct.
558    fn insert_frame_num_gaps(&mut self, frame_num: u32, max_fn: u32, max_refs: usize, w: usize, h: usize) {
559        if max_fn == 0 {
560            return;
561        }
562        let start = (self.prev_ref_frame_num + 1) % max_fn;
563        let gap = (frame_num + max_fn - start) % max_fn;
564        if gap == 0 {
565            return;
566        }
567        // Each placeholder is inserted at the front then the DPB is truncated to
568        // `max_refs`, so for a gap larger than that only the most recent `max_refs`
569        // placeholders can survive. Materialise just those — a malformed stream can
570        // declare a gap of MaxFrameNum-1 (up to 65535), and allocating that many
571        // full frames would be a CPU/memory DoS.
572        let cap = max_refs.max(1);
573        let n = (gap as usize).min(cap);
574        let (cw, ch) = (w, h);
575        let mut expected = (frame_num + max_fn - n as u32) % max_fn;
576        for _ in 0..n {
577            self.refs.insert(
578                0,
579                RefFrame {
580                    y: vec![128; cw * ch],
581                    u: vec![128; (cw / 2) * (ch / 2)],
582                    v: vec![128; (cw / 2) * (ch / 2)],
583                    cw,
584                    ch,
585                    frame_num: expected,
586                    poc: 0,
587                    mv: Vec::new(),
588                    ref_idx: Vec::new(),
589                    ref_poc: Vec::new(),
590                    w4: 0,
591                    long_term: false,
592                    long_term_idx: 0,
593                },
594            );
595            self.refs.truncate(cap);
596            expected = (expected + 1) % max_fn;
597        }
598        self.prev_ref_frame_num = (frame_num + max_fn - 1) % max_fn;
599    }
600
601    /// The `PicOrderCnt` of the most recently returned picture. Pictures are
602    /// returned in decode order; sorting them by this value yields display order
603    /// (the only difference is reordered B-pictures).
604    pub fn last_poc(&self) -> i32 {
605        self.last_poc
606    }
607
608    /// Derives `PicOrderCnt` for the current picture (spec §8.2.1) and advances
609    /// the POC state. Types 0 and 2 are exact; type 1 is approximated by
610    /// frame-num order (no B-stream in scope uses it).
611    fn compute_poc(
612        &mut self,
613        sps: &Sps,
614        is_idr: bool,
615        nal_ref_idc: u8,
616        frame_num: u32,
617        poc_lsb: u32,
618        delta_bottom: i32,
619    ) -> i32 {
620        match sps.pic_order_cnt_type {
621            0 => {
622                let max_lsb = 1i32 << sps.log2_max_pic_order_cnt_lsb;
623                let (prev_msb, prev_lsb) =
624                    if is_idr { (0, 0) } else { (self.poc.prev_msb, self.poc.prev_lsb) };
625                let lsb = poc_lsb as i32;
626                let msb = if lsb < prev_lsb && prev_lsb - lsb >= max_lsb / 2 {
627                    prev_msb + max_lsb
628                } else if lsb > prev_lsb && lsb - prev_lsb > max_lsb / 2 {
629                    prev_msb - max_lsb
630                } else {
631                    prev_msb
632                };
633                let top = msb + lsb;
634                let poc = top.min(top + delta_bottom);
635                if nal_ref_idc != 0 {
636                    self.poc.prev_msb = msb;
637                    self.poc.prev_lsb = lsb;
638                }
639                poc
640            }
641            2 => {
642                let max_fn = 1i64 << sps.log2_max_frame_num;
643                let offset = if is_idr {
644                    0
645                } else if self.poc.prev_frame_num > frame_num {
646                    self.poc.prev_frame_num_offset + max_fn
647                } else {
648                    self.poc.prev_frame_num_offset
649                };
650                let poc = if is_idr {
651                    0
652                } else {
653                    2 * (offset + frame_num as i64) - i64::from(nal_ref_idc == 0)
654                };
655                self.poc.prev_frame_num_offset = offset;
656                self.poc.prev_frame_num = frame_num;
657                poc as i32
658            }
659            _ => {
660                self.poc.prev_frame_num = frame_num;
661                frame_num as i32 * 2
662            }
663        }
664    }
665
666    /// Inserts the just-decoded picture into the DPB and marks references
667    /// (spec §8.2.5). With no MMCO commands this is the sliding window (evict the
668    /// oldest short-term reference past capacity); with MMCO it is adaptive
669    /// marking, including long-term assignment.
670    ///
671    /// Takes `reference` BY VALUE and MOVES it into the DPB (the caller's local is
672    /// dropped right after) — the old `&mut` + `insert(0, reference.clone())` cloned
673    /// all three planes (~1.35 MB/frame) a second time, on top of `as_reference`'s
674    /// necessary clone. Returns the picture's final `frame_num` (0 after MMCO 5) for
675    /// the caller's gap-detection tracking, since `reference` is gone after the move.
676    fn apply_ref_marking(
677        &mut self,
678        mut reference: RefFrame,
679        ops: &[Mmco],
680        frame_num: u32,
681        log2_max_frame_num: u32,
682        max_refs: usize,
683    ) -> u32 {
684        let max = 1i64 << log2_max_frame_num;
685        let curr = frame_num as i64;
686        let pic_num = |rf: &RefFrame| -> i64 {
687            if (rf.frame_num as i64) > curr {
688                rf.frame_num as i64 - max
689            } else {
690                rf.frame_num as i64
691            }
692        };
693
694        if ops.is_empty() {
695            // Sliding window: insert the current (short-term) picture, then evict
696            // the oldest short-term reference while over capacity (long-term refs
697            // are retained).
698            let out_fn = reference.frame_num;
699            self.refs.insert(0, reference);
700            while self.refs.len() > max_refs {
701                match self.refs.iter().rposition(|r| !r.long_term) {
702                    Some(pos) => {
703                        self.refs.remove(pos);
704                    }
705                    None => break,
706                }
707            }
708            return out_fn;
709        }
710
711        // Adaptive marking (MMCO), applied in order.
712        for &op in ops {
713            match op {
714                Mmco::Unref(diff) => {
715                    let target = curr - (diff as i64 + 1);
716                    self.refs.retain(|r| r.long_term || pic_num(r) != target);
717                }
718                Mmco::UnrefLong(ltpn) => {
719                    self.refs.retain(|r| !(r.long_term && r.long_term_idx == ltpn));
720                }
721                Mmco::AssignLong(diff, idx) => {
722                    let target = curr - (diff as i64 + 1);
723                    self.refs.retain(|r| !(r.long_term && r.long_term_idx == idx));
724                    for r in self.refs.iter_mut() {
725                        if !r.long_term && pic_num(r) == target {
726                            r.long_term = true;
727                            r.long_term_idx = idx;
728                        }
729                    }
730                }
731                Mmco::MaxLong(max_plus1) => {
732                    self.refs.retain(|r| !(r.long_term && r.long_term_idx + 1 > max_plus1));
733                }
734                Mmco::Reset => {
735                    self.refs.clear();
736                    reference.frame_num = 0;
737                }
738                Mmco::CurrentLong(idx) => {
739                    self.refs.retain(|r| !(r.long_term && r.long_term_idx == idx));
740                    reference.long_term = true;
741                    reference.long_term_idx = idx;
742                }
743            }
744        }
745        let out_fn = reference.frame_num;
746        self.refs.insert(0, reference);
747        // Safety net so a malformed marking stream can't grow the DPB unbounded.
748        let cap = max_refs.max(16);
749        if self.refs.len() > cap {
750            self.refs.truncate(cap);
751        }
752        out_fn
753    }
754}
755
756/// Emits a GOP's buffered pictures in display order (sorted by `PicOrderCnt`).
757fn flush_gop(gop: &mut Vec<(i32, YuvFrame)>, out: &mut Vec<YuvFrame>) {
758    gop.sort_by_key(|(poc, _)| *poc);
759    out.extend(gop.drain(..).map(|(_, f)| f));
760}
761
762/// Whether an access unit contains an IDR coded-slice NAL.
763fn au_is_idr(au: &[u8]) -> bool {
764    split_annex_b(au)
765        .iter()
766        .any(|n| !n.is_empty() && NalUnitType::from_id(n[0]) == NalUnitType::IdrSlice)
767}
768
769/// Splits an Annex-B byte stream into access units, each ending after a VCL
770/// (coded-slice) NAL with any preceding parameter-set/SEI NALs attached. Start
771/// codes are preserved so each unit can be passed straight to [`Decoder::decode`].
772fn split_access_units(stream: &[u8]) -> Vec<&[u8]> {
773    // (offset of the start code, whether the NAL it begins is a VCL slice).
774    let mut codes: Vec<(usize, bool)> = Vec::new();
775    let mut i = 0;
776    while i + 3 <= stream.len() {
777        if stream[i] == 0 && stream[i + 1] == 0 && stream[i + 2] == 1 {
778            let nal_type = NalUnitType::from_id(stream.get(i + 3).copied().unwrap_or(0));
779            let is_vcl = matches!(nal_type, NalUnitType::IdrSlice | NalUnitType::NonIdrSlice);
780            // Include a leading zero (4-byte start code) in the unit boundary.
781            let sc = if i > 0 && stream[i - 1] == 0 { i - 1 } else { i };
782            codes.push((sc, is_vcl));
783            i += 3;
784        } else {
785            i += 1;
786        }
787    }
788    if codes.is_empty() {
789        return vec![stream];
790    }
791    let mut aus = Vec::new();
792    let mut start = codes[0].0;
793    for k in 0..codes.len() {
794        if codes[k].1 {
795            let end = codes.get(k + 1).map_or(stream.len(), |c| c.0);
796            aus.push(&stream[start..end]);
797            start = end;
798        }
799    }
800    aus
801}
802
803/// Parses a `pred_weight_table()` (spec §7.3.3.2) for the active reference lists
804/// (4:2:0 → chroma weights always present). List 1 is parsed only for B slices.
805fn parse_pred_weight_table(
806    r: &mut BitReader,
807    num_l0: usize,
808    num_l1: usize,
809    is_b: bool,
810) -> Result<WeightTable, DecodeError> {
811    let luma_log2_denom = r.read_ue()? as i32;
812    let chroma_log2_denom = r.read_ue()? as i32;
813    let mut wt = WeightTable {
814        luma_log2_denom,
815        chroma_log2_denom,
816        ..Default::default()
817    };
818    let lists: &[(usize, usize)] = if is_b {
819        &[(0, num_l0), (1, num_l1)]
820    } else {
821        &[(0, num_l0)]
822    };
823    for &(list, n) in lists {
824        let mut luma = Vec::with_capacity(n);
825        let mut chroma = Vec::with_capacity(n);
826        for _ in 0..n {
827            let (mut lw, mut lo) = (1 << luma_log2_denom, 0);
828            if r.read_bit()? {
829                lw = r.read_se()?;
830                lo = r.read_se()?;
831            }
832            luma.push((lw, lo));
833            let mut ch = [(1 << chroma_log2_denom, 0); 2];
834            if r.read_bit()? {
835                for slot in ch.iter_mut() {
836                    *slot = (r.read_se()?, r.read_se()?);
837                }
838            }
839            chroma.push(ch);
840        }
841        wt.luma[list] = luma;
842        wt.chroma[list] = chroma;
843    }
844    Ok(wt)
845}
846
847/// Resolves the effective scaling matrices for a slice from the SPS lists and
848/// any PPS override (fall-back rule B), returning them un-zig-zagged to raster
849/// order: six 4×4 lists [Y/Cb/Cr intra, Y/Cb/Cr inter] and two 8×8 luma lists
850/// [Y-intra, Y-inter].
851fn resolve_scaling(sps: &Sps, pps: &Pps) -> ([[i32; 16]; 6], [[i32; 64]; 2]) {
852    use crate::params::{
853        DEFAULT_4X4_INTER, DEFAULT_4X4_INTRA, DEFAULT_8X8_INTER, DEFAULT_8X8_INTRA,
854    };
855    const ZZ4: [usize; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15];
856    // 8×8 frame zig-zag scan → raster index (spec Table 8-12).
857    const ZZ8: [usize; 64] = [
858        0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27,
859        20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
860        58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
861    ];
862    // Effective zig-zag lists: a PPS override (rule B) takes precedence; an absent
863    // PPS list falls back to the SPS list (or the default / previous PPS list).
864    let mut z4 = [[16u8; 16]; 6];
865    for i in 0..6 {
866        z4[i] = if pps.pic_scaling_matrix_present {
867            if pps.scaling_present_4x4[i] {
868                pps.scaling_4x4[i]
869            } else {
870                match i {
871                    0 if sps.has_scaling => sps.scaling_4x4[0],
872                    0 => DEFAULT_4X4_INTRA,
873                    3 if sps.has_scaling => sps.scaling_4x4[3],
874                    3 => DEFAULT_4X4_INTER,
875                    _ => z4[i - 1],
876                }
877            }
878        } else {
879            sps.scaling_4x4[i]
880        };
881    }
882    let mut z8 = [[16u8; 64]; 2];
883    for (i, list) in z8.iter_mut().enumerate() {
884        *list = if pps.pic_scaling_matrix_present {
885            if pps.scaling_present_8x8[i] {
886                pps.scaling_8x8[i]
887            } else if sps.has_scaling {
888                sps.scaling_8x8[i]
889            } else if i == 0 {
890                DEFAULT_8X8_INTRA
891            } else {
892                DEFAULT_8X8_INTER
893            }
894        } else {
895            sps.scaling_8x8[i]
896        };
897    }
898    let mut out4 = [[16i32; 16]; 6];
899    for (li, list) in out4.iter_mut().enumerate() {
900        for k in 0..16 {
901            list[ZZ4[k]] = z4[li][k] as i32;
902        }
903    }
904    let mut out8 = [[16i32; 64]; 2];
905    for (li, list) in out8.iter_mut().enumerate() {
906        for k in 0..64 {
907            list[ZZ8[k]] = z8[li][k] as i32;
908        }
909    }
910    (out4, out8)
911}
912
913/// Parses a `ref_pic_list_modification` command list (spec §7.3.3.1) into
914/// `(modification_of_pic_nums_idc, value)` pairs, stopping at idc 3.
915fn parse_ref_pic_list_modification(
916    r: &mut BitReader,
917    out: &mut Vec<(u32, u32)>,
918) -> Result<(), DecodeError> {
919    loop {
920        let idc = r.read_ue()?;
921        if idc == 3 {
922            break;
923        }
924        if idc > 3 {
925            return Err(DecodeError::Unsupported("invalid ref_pic_list_modification"));
926        }
927        let val = r.read_ue()?; // abs_diff_pic_num_minus1 / long_term_pic_num
928        out.push((idc, val));
929        if out.len() > 64 {
930            return Err(DecodeError::Truncated); // runaway / corrupt
931        }
932    }
933    Ok(())
934}
935
936/// Builds the P-slice `RefPicList0`: short-term references ordered by descending
937/// `FrameNumWrap`, then long-term by ascending idx (spec §8.2.4.2.1), with any
938/// `ref_pic_list_modification` applied.
939fn build_ref_list_p(
940    dpb: &[RefFrame],
941    curr_frame_num: u32,
942    max_frame_num: u32,
943    num_active: usize,
944    mods: &[(u32, u32)],
945) -> Result<Vec<RefFrame>, DecodeError> {
946    let curr = curr_frame_num as i64;
947    let max = max_frame_num as i64;
948    let pic_num = |fnum: u32| -> i64 {
949        let f = fnum as i64;
950        if f > curr { f - max } else { f }
951    };
952    let mut init: Vec<RefFrame> = dpb.iter().filter(|r| !r.long_term).cloned().collect();
953    init.sort_by_key(|rf| core::cmp::Reverse(pic_num(rf.frame_num)));
954    let mut long: Vec<RefFrame> = dpb.iter().filter(|r| r.long_term).cloned().collect();
955    long.sort_by_key(|rf| rf.long_term_idx);
956    init.extend(long);
957    apply_list_modification(init, curr_frame_num, max_frame_num, num_active, mods)
958}
959
960/// Builds the B-slice `RefPicList0` and `RefPicList1` (spec §8.2.4.2.3), ordered
961/// by `PicOrderCnt` relative to the current picture: List0 leads with nearer
962/// past pictures, List1 with nearer future pictures. Long-term references follow.
963/// Per-list `ref_pic_list_modification` is then applied.
964#[allow(clippy::too_many_arguments)]
965fn build_ref_list_b(
966    dpb: &[RefFrame],
967    curr_poc: i32,
968    curr_frame_num: u32,
969    max_frame_num: u32,
970    num0: usize,
971    num1: usize,
972    mods0: &[(u32, u32)],
973    mods1: &[(u32, u32)],
974) -> Result<(Vec<RefFrame>, Vec<RefFrame>), DecodeError> {
975    let mut less: Vec<RefFrame> =
976        dpb.iter().filter(|r| !r.long_term && r.poc < curr_poc).cloned().collect();
977    let mut greater: Vec<RefFrame> =
978        dpb.iter().filter(|r| !r.long_term && r.poc > curr_poc).cloned().collect();
979    let mut long: Vec<RefFrame> = dpb.iter().filter(|r| r.long_term).cloned().collect();
980    less.sort_by_key(|r| core::cmp::Reverse(r.poc)); // nearest past first
981    greater.sort_by_key(|r| r.poc); // nearest future first
982    long.sort_by_key(|r| r.long_term_idx);
983
984    let mut init0 = less.clone();
985    init0.extend(greater.clone());
986    init0.extend(long.clone());
987    let mut init1 = greater;
988    init1.extend(less);
989    init1.extend(long);
990
991    // When List1 (truncated to its active length) equals List0 and has more than
992    // one entry, swap its first two entries (spec §8.2.4.2.3).
993    let eq_len = num0.min(num1).min(init0.len()).min(init1.len());
994    if num1 > 1
995        && init1.len() > 1
996        && (0..eq_len).all(|i| same_picture(&init0[i], &init1[i]))
997        && eq_len == num1.min(init1.len())
998        && eq_len == num0.min(init0.len())
999    {
1000        init1.swap(0, 1);
1001    }
1002
1003    let list0 = apply_list_modification(init0, curr_frame_num, max_frame_num, num0, mods0)?;
1004    let list1 = apply_list_modification(init1, curr_frame_num, max_frame_num, num1, mods1)?;
1005    Ok((list0, list1))
1006}
1007
1008/// Two DPB entries refer to the same picture (used for the List1 swap rule).
1009fn same_picture(a: &RefFrame, b: &RefFrame) -> bool {
1010    a.long_term == b.long_term
1011        && if a.long_term { a.long_term_idx == b.long_term_idx } else { a.poc == b.poc }
1012}
1013
1014/// Applies `ref_pic_list_modification` to an initialized reference list and
1015/// truncates it to `num_active` (spec §8.2.4.3). `init` is the full ordered list;
1016/// the result is `num_active` entries, possibly reordered. idc 0/1 reference
1017/// short-term pictures by PicNum, idc 2 long-term ones by LongTermFrameIdx.
1018fn apply_list_modification(
1019    init: Vec<RefFrame>,
1020    curr_frame_num: u32,
1021    max_frame_num: u32,
1022    num_active: usize,
1023    mods: &[(u32, u32)],
1024) -> Result<Vec<RefFrame>, DecodeError> {
1025    if mods.is_empty() {
1026        let mut init = init;
1027        init.truncate(num_active.max(1));
1028        return Ok(init);
1029    }
1030    let curr = curr_frame_num as i64;
1031    let max = max_frame_num as i64;
1032    let mut list = init.clone();
1033    let mut pic_num_pred = curr;
1034    let mut refidx = 0usize;
1035    for &(idc, val) in mods {
1036        let matches: Box<dyn Fn(&RefFrame) -> bool> = if idc == 2 {
1037            Box::new(move |r: &RefFrame| r.long_term && r.long_term_idx == val)
1038        } else {
1039            let abs_diff = (val as i64) + 1;
1040            let no_wrap = if idc == 0 {
1041                let x = pic_num_pred - abs_diff;
1042                if x < 0 { x + max } else { x }
1043            } else {
1044                let x = pic_num_pred + abs_diff;
1045                if x >= max { x - max } else { x }
1046            };
1047            pic_num_pred = no_wrap;
1048            let target = if no_wrap > curr { no_wrap - max } else { no_wrap };
1049            Box::new(move |r: &RefFrame| {
1050                let pn = if r.frame_num as i64 > curr {
1051                    r.frame_num as i64 - max
1052                } else {
1053                    r.frame_num as i64
1054                };
1055                !r.long_term && pn == target
1056            })
1057        };
1058        let found = init.iter().find(|r| matches(r)).cloned();
1059        let Some(found) = found else {
1060            return Err(DecodeError::Truncated); // references a picture not in the DPB
1061        };
1062        if refidx > list.len() {
1063            break;
1064        }
1065        list.insert(refidx, found);
1066        if let Some(dup) = list.iter().enumerate().skip(refidx + 1).find(|(_, r)| matches(r)).map(|(i, _)| i) {
1067            list.remove(dup);
1068        }
1069        refidx += 1;
1070        if refidx >= num_active {
1071            break;
1072        }
1073    }
1074    list.truncate(num_active.max(1));
1075    Ok(list)
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081
1082    fn ref_at(poc: i32, fnum: u32) -> RefFrame {
1083        RefFrame {
1084            y: vec![],
1085            u: vec![],
1086            v: vec![],
1087            cw: 0,
1088            ch: 0,
1089            frame_num: fnum,
1090            poc,
1091            mv: Vec::new(),
1092            ref_idx: Vec::new(),
1093            ref_poc: Vec::new(),
1094            w4: 0,
1095            long_term: false,
1096            long_term_idx: 0,
1097        }
1098    }
1099
1100    #[test]
1101    fn b_ref_lists_ordered_by_poc() {
1102        // Current POC 4; DPB has past (0,2) and future (6,8) references.
1103        let dpb = vec![ref_at(8, 4), ref_at(6, 3), ref_at(2, 1), ref_at(0, 0)];
1104        let (l0, l1) = build_ref_list_b(&dpb, 4, 5, 16, 4, 4, &[], &[]).unwrap();
1105        // List0: nearer past first (desc), then nearer future (asc).
1106        assert_eq!(l0.iter().map(|r| r.poc).collect::<Vec<_>>(), vec![2, 0, 6, 8]);
1107        // List1: nearer future first (asc), then nearer past (desc).
1108        assert_eq!(l1.iter().map(|r| r.poc).collect::<Vec<_>>(), vec![6, 8, 2, 0]);
1109    }
1110
1111    #[test]
1112    fn b_ref_list1_swap_when_equal() {
1113        // Only past references -> List0 and List1 initialize identically, so
1114        // List1's first two entries are swapped (spec §8.2.4.2.3).
1115        let dpb = vec![ref_at(4, 2), ref_at(2, 1), ref_at(0, 0)];
1116        let (l0, l1) = build_ref_list_b(&dpb, 6, 3, 16, 3, 3, &[], &[]).unwrap();
1117        assert_eq!(l0.iter().map(|r| r.poc).collect::<Vec<_>>(), vec![4, 2, 0]);
1118        assert_eq!(l1.iter().map(|r| r.poc).collect::<Vec<_>>(), vec![2, 4, 0]);
1119    }
1120
1121    #[test]
1122    fn frame_num_gaps_insert_placeholders() {
1123        let mut d = Decoder::new();
1124        d.prev_ref_frame_num = 2;
1125        // frame_num jumps 2 -> 5: placeholders for the skipped 3 and 4.
1126        d.insert_frame_num_gaps(5, 16, 8, 16, 16);
1127        let fns: Vec<u32> = d.refs.iter().map(|r| r.frame_num).collect();
1128        assert_eq!(fns, vec![4, 3], "most-recent placeholder at the front");
1129        assert_eq!(d.prev_ref_frame_num, 4);
1130        assert!(d.refs.iter().all(|r| r.y.iter().all(|&p| p == 128)), "grey fill");
1131    }
1132
1133    #[test]
1134    fn frame_num_gaps_wrap_and_noop() {
1135        // Wrap across MaxFrameNum: prev 14, frame_num 1 (max 16) -> fill 15, 0.
1136        let mut d = Decoder::new();
1137        d.prev_ref_frame_num = 14;
1138        d.insert_frame_num_gaps(1, 16, 8, 16, 16);
1139        assert_eq!(d.refs.iter().map(|r| r.frame_num).collect::<Vec<_>>(), vec![0, 15]);
1140        // No gap (consecutive) inserts nothing.
1141        let mut d = Decoder::new();
1142        d.prev_ref_frame_num = 3;
1143        d.insert_frame_num_gaps(4, 16, 8, 16, 16);
1144        assert!(d.refs.is_empty());
1145    }
1146
1147    #[test]
1148    fn missing_param_sets_errors() {
1149        let mut d = Decoder::new();
1150        // A lone (fake) IDR slice header: first_mb_in_slice=0, slice_type=7 (I),
1151        // pic_parameter_set_id=0 — then the PPS lookup fails (none stored).
1152        let nal = rusty_h264_common::NalUnit::new(3, NalUnitType::IdrSlice, vec![0x88, 0x80]);
1153        let err = d.decode(&nal.to_annex_b()).unwrap_err();
1154        assert_eq!(err, DecodeError::MissingParameterSet);
1155    }
1156}