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