Skip to main content

rusty_h264_decoder/
lib.rs

1//! Pure-Rust H.264 decoder — Constrained Baseline **+ B-slices + most of High
2//! profile**, CAVLC and CABAC.
3//!
4//! Validated **bit-exact against Cisco's `h264dec`** on 35 of 35 clean streams
5//! from openh264's conformance corpus; the CABAC paths were brought up
6//! symbol-by-symbol against an instrumented openh264 oracle and are gated
7//! **pixel-exact vs ffmpeg**. The reconstruction path is shared with the encoder
8//! (via `rusty_h264-common`), so the two halves agree bit-for-bit by
9//! construction.
10//!
11//! Decodes: full intra (`I_16x16`/`I_4x4`/`I_8x8`/`I_PCM`), inter
12//! (`P_Skip`/16×16/16×8/8×16/`P_8x8`) with quarter-pel motion compensation,
13//! B-slices (temporal + spatial direct, implicit + explicit weighted
14//! prediction), the 8×8 transform and 8×8 intra prediction, scaling matrices,
15//! in-loop deblocking, and a multi-reference DPB with POC reordering and MMCO.
16//! CABAC covers I, P and B slices incl. High-profile 8×8 residual (not yet: `I_PCM`).
17//!
18//! This crate is `#![forbid(unsafe_code)]` and is **fuzzed to never panic or
19//! hang** on malformed input — errors surface as [`DecodeError`].
20//!
21//! [`Decoder::decode_stream`] is the one-call entry point (frames in display
22//! order); [`Decoder::decode`] is the streaming form (one picture per access
23//! unit, in decode order — pair it with [`Decoder::last_poc`]).
24
25#![cfg_attr(not(feature = "std"), no_std)]
26// One thread without `std`: the frame-thread clients, the E2 worker seam and
27// the row-progress slots are compiled but never reached. Gating each of them
28// would scatter cfgs through the hot loop, so that arm allows the lint.
29#![cfg_attr(not(feature = "std"), allow(dead_code))]
30
31extern crate alloc;
32#[cfg(test)]
33extern crate std;
34
35// ---------------------------------------------------------------------------
36// `no_std` shims (see rusty_h264-common for the knob and once-cell shims this
37// crate reuses). Without `std` there is no environment, no stderr and no
38// second thread: a knob reads as unset, a print is a no-op, and the
39// cross-thread progress machinery below collapses to single-thread cells.
40// Defined before the modules so the macros are in textual scope.
41// ---------------------------------------------------------------------------
42
43#[cfg(not(feature = "std"))]
44macro_rules! eprintln {
45    ($($t:tt)*) => {{
46        let _ = ::core::format_args!($($t)*);
47    }};
48}
49#[cfg(not(feature = "std"))]
50#[allow(unused_macros)]
51macro_rules! println {
52    ($($t:tt)*) => {{
53        let _ = ::core::format_args!($($t)*);
54    }};
55}
56/// `thread_local!` without threads: each `NAME.with(|v| ..)` builds the value
57/// fresh (the encoder's polyfill, verbatim).
58#[cfg(not(feature = "std"))]
59macro_rules! thread_local {
60    () => {};
61    ($(#[$m:meta])* $vis:vis static $name:ident: $ty:ty = const { $init:expr }; $($rest:tt)*) => {
62        $(#[$m])* #[allow(non_camel_case_types)] $vis struct $name;
63        impl $name {
64            #[allow(dead_code)]
65            pub fn with<R>(&self, f: impl FnOnce(&$ty) -> R) -> R {
66                let v: $ty = $init;
67                f(&v)
68            }
69        }
70        thread_local!($($rest)*);
71    };
72    ($(#[$m:meta])* $vis:vis static $name:ident: $ty:ty = $init:expr; $($rest:tt)*) => {
73        $(#[$m])* #[allow(non_camel_case_types)] $vis struct $name;
74        impl $name {
75            #[allow(dead_code)]
76            pub fn with<R>(&self, f: impl FnOnce(&$ty) -> R) -> R {
77                let v: $ty = $init;
78                f(&v)
79            }
80        }
81        thread_local!($($rest)*);
82    };
83}
84
85/// The synchronisation vocabulary the frame-MT progress machinery uses, on
86/// both sides of the ladder. With `std` these are the real `std::sync` types.
87/// Without it there is exactly one thread, so a lock is a `RefCell` (a
88/// re-entrant borrow would be a bug either way and panics the same), a
89/// condition variable never has anyone to wait for, and the worker channels
90/// are placeholder types that are never constructed — the EDC worker and the
91/// frame pool live behind `std`, so nothing sends.
92pub(crate) mod sync {
93    #[cfg(not(feature = "std"))]
94    pub use core::cell::OnceCell as Once;
95    /// The write-once cell for a reference frame's frozen planes: the `std`
96    /// `OnceLock` (frame threads share reference frames), a plain `OnceCell`
97    /// without it (one thread, nothing to be `Sync` for). Both have the
98    /// `take(&mut self)` the recycling path uses; the common crate's shim is
99    /// for statics and has not.
100    #[cfg(feature = "std")]
101    pub use std::sync::OnceLock as Once;
102    #[cfg(feature = "std")]
103    pub use std::sync::{mpsc, Arc, Condvar, Mutex, RwLock, RwLockReadGuard};
104    #[cfg(feature = "std")]
105    pub fn yield_now() {
106        std::thread::yield_now();
107    }
108
109    #[cfg(not(feature = "std"))]
110    pub use alloc::sync::Arc;
111    #[cfg(not(feature = "std"))]
112    pub fn yield_now() {
113        core::hint::spin_loop();
114    }
115    #[cfg(not(feature = "std"))]
116    pub use single::*;
117
118    /// POISON-TOLERANT LOCK ACCESS — the reason this crate has no `.unwrap()`
119    /// on a lock anywhere.
120    ///
121    /// `RwLock::read()` fails for exactly one reason: some other thread panicked
122    /// while holding the lock. The stock idiom is `.unwrap()`, which turns that
123    /// into a SECOND panic, in a thread that did nothing wrong. For a decoder
124    /// eating attacker-controlled bytes that is the wrong trade twice over:
125    ///
126    /// * This crate is `#![forbid(unsafe_code)]`, so a poisoned guard still
127    ///   protects a well-formed Rust value. The worst a poisoned pixel plane can
128    ///   hold is a PARTIALLY WRITTEN frame -- never torn memory, never UB.
129    /// * A partially written frame is a degraded picture. A panic is a killed
130    ///   process. Turning the first into the second hands an attacker a
131    ///   denial-of-service out of a decode artefact.
132    ///
133    /// So every lock is taken with `into_inner()`, which recovers the guard and
134    /// carries on. The `no_std` shims cannot be poisoned at all (one thread,
135    /// `RefCell`), and mirror these names so call sites are identical.
136    #[cfg(feature = "std")]
137    mod poison_free {
138        use std::sync::{
139            Condvar, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard,
140        };
141
142        pub trait LockPf<T: ?Sized> {
143            fn read_pf(&self) -> RwLockReadGuard<'_, T>;
144            fn write_pf(&self) -> RwLockWriteGuard<'_, T>;
145        }
146        impl<T: ?Sized> LockPf<T> for RwLock<T> {
147            #[inline]
148            fn read_pf(&self) -> RwLockReadGuard<'_, T> {
149                self.read().unwrap_or_else(|e| e.into_inner())
150            }
151            #[inline]
152            fn write_pf(&self) -> RwLockWriteGuard<'_, T> {
153                self.write().unwrap_or_else(|e| e.into_inner())
154            }
155        }
156
157        pub trait MutexPf<T: ?Sized> {
158            fn lock_pf(&self) -> MutexGuard<'_, T>;
159        }
160        impl<T: ?Sized> MutexPf<T> for Mutex<T> {
161            #[inline]
162            fn lock_pf(&self) -> MutexGuard<'_, T> {
163                self.lock().unwrap_or_else(|e| e.into_inner())
164            }
165        }
166
167        pub trait CondvarPf {
168            fn wait_pf<'a, T>(&self, g: MutexGuard<'a, T>) -> MutexGuard<'a, T>;
169        }
170        impl CondvarPf for Condvar {
171            #[inline]
172            fn wait_pf<'a, T>(&self, g: MutexGuard<'a, T>) -> MutexGuard<'a, T> {
173                self.wait(g).unwrap_or_else(|e| e.into_inner())
174            }
175        }
176    }
177
178    #[cfg(not(feature = "std"))]
179    mod poison_free {
180        use super::single::{Condvar, Mutex, RwLock};
181        use core::cell::{Ref, RefMut};
182
183        pub trait LockPf<T> {
184            fn read_pf(&self) -> Ref<'_, T>;
185            fn write_pf(&self) -> RefMut<'_, T>;
186        }
187        impl<T> LockPf<T> for RwLock<T> {
188            #[inline]
189            fn read_pf(&self) -> Ref<'_, T> {
190                // The shim is a `RefCell` on one thread: `read` cannot fail.
191                self.read().unwrap_or_else(|()| unreachable!())
192            }
193            #[inline]
194            fn write_pf(&self) -> RefMut<'_, T> {
195                self.write().unwrap_or_else(|()| unreachable!())
196            }
197        }
198
199        pub trait MutexPf<T> {
200            fn lock_pf(&self) -> RefMut<'_, T>;
201        }
202        impl<T> MutexPf<T> for Mutex<T> {
203            #[inline]
204            fn lock_pf(&self) -> RefMut<'_, T> {
205                self.lock().unwrap_or_else(|()| unreachable!())
206            }
207        }
208
209        pub trait CondvarPf {
210            fn wait_pf<'a, T>(&self, g: RefMut<'a, T>) -> RefMut<'a, T>;
211        }
212        impl CondvarPf for Condvar {
213            #[inline]
214            fn wait_pf<'a, T>(&self, g: RefMut<'a, T>) -> RefMut<'a, T> {
215                self.wait(g).unwrap_or_else(|()| unreachable!())
216            }
217        }
218    }
219
220    pub use poison_free::{CondvarPf, LockPf, MutexPf};
221
222    #[cfg(not(feature = "std"))]
223    mod single {
224        use core::cell::{Ref, RefCell, RefMut};
225
226        #[derive(Debug, Default)]
227        pub struct RwLock<T>(RefCell<T>);
228        pub type RwLockReadGuard<'a, T> = Ref<'a, T>;
229        impl<T> RwLock<T> {
230            pub const fn new(v: T) -> Self {
231                RwLock(RefCell::new(v))
232            }
233            pub fn read(&self) -> Result<Ref<'_, T>, ()> {
234                Ok(self.0.borrow())
235            }
236            pub fn write(&self) -> Result<RefMut<'_, T>, ()> {
237                Ok(self.0.borrow_mut())
238            }
239        }
240
241        #[derive(Debug, Default)]
242        pub struct Mutex<T>(RefCell<T>);
243        impl<T> Mutex<T> {
244            pub const fn new(v: T) -> Self {
245                Mutex(RefCell::new(v))
246            }
247            pub fn lock(&self) -> Result<RefMut<'_, T>, ()> {
248                Ok(self.0.borrow_mut())
249            }
250        }
251
252        #[derive(Debug, Default)]
253        pub struct Condvar;
254        impl Condvar {
255            pub const fn new() -> Self {
256                Condvar
257            }
258            pub fn wait<'a, T>(&self, g: RefMut<'a, T>) -> Result<RefMut<'a, T>, ()> {
259                Ok(g)
260            }
261            pub fn notify_all(&self) {}
262            pub fn notify_one(&self) {}
263        }
264
265        /// Channel placeholders: never constructed without `std` (the only
266        /// constructors sit inside the `std`-gated worker tails), so their
267        /// methods are unreachable by construction.
268        pub mod mpsc {
269            use core::marker::PhantomData;
270            pub struct SyncSender<T>(PhantomData<T>);
271            pub struct Sender<T>(PhantomData<T>);
272            pub struct Receiver<T>(PhantomData<T>);
273            pub struct SendError<T>(pub T);
274            #[derive(Debug)]
275            pub struct RecvError;
276            impl<T> core::fmt::Debug for SendError<T> {
277                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
278                    f.write_str("SendError")
279                }
280            }
281            impl<T> SyncSender<T> {
282                pub fn send(&self, _t: T) -> Result<(), SendError<T>> {
283                    unreachable!("no worker thread without std")
284                }
285            }
286            impl<T> Sender<T> {
287                pub fn send(&self, _t: T) -> Result<(), SendError<T>> {
288                    unreachable!("no worker thread without std")
289                }
290            }
291            impl<T> Receiver<T> {
292                pub fn recv(&self) -> Result<T, RecvError> {
293                    unreachable!("no worker thread without std")
294                }
295            }
296        }
297    }
298}
299
300mod cabac;
301/// Profile-only re-export of the CABAC bin census for benchmarking harnesses.
302#[cfg(feature = "profile")]
303pub use cabac::bin_census;
304#[cfg(feature = "std")]
305mod frame_mt;
306mod mb16;
307mod params;
308
309pub use mb16::MvField;
310#[cfg(feature = "std")]
311pub use mb16::MV_DUMP;
312pub use params::{Pps, Sps};
313
314/// Frame-MT worker count (`RS_H264_FRAME_THREADS`); one thread without `std`.
315#[cfg(feature = "std")]
316pub(crate) fn frame_threads() -> usize {
317    frame_mt::frame_threads()
318}
319/// Frame-MT worker count; one thread without `std`.
320#[cfg(not(feature = "std"))]
321pub(crate) fn frame_threads() -> usize {
322    1
323}
324
325/// Frame-MT Phase B row publishing; never without `std` (one thread).
326#[cfg(feature = "std")]
327pub(crate) fn row_publish_on() -> bool {
328    frame_mt::row_publish_on()
329}
330/// Frame-MT Phase B row publishing; never without `std` (one thread).
331#[cfg(not(feature = "std"))]
332pub(crate) fn row_publish_on() -> bool {
333    false
334}
335/// Frame-MT Phase B row-progress waits; never without `std` (one thread).
336#[cfg(feature = "std")]
337pub(crate) fn row_progress_on() -> bool {
338    frame_mt::row_progress_on()
339}
340/// Frame-MT Phase B row-progress waits; never without `std` (one thread).
341#[cfg(not(feature = "std"))]
342pub(crate) fn row_progress_on() -> bool {
343    false
344}
345
346/// Print the E2 worker-seam counters (D7) if `RS_H264_EDC_STATS` is set.
347pub fn edc_stats_report() {
348    mb16::edcstat::report();
349    rusty_h264_common::deblock::filtstat::report();
350}
351
352/// Test-only re-export of the CABAC arithmetic *decoder* so the encoder crate can
353/// round-trip-validate its CABAC *encoder* against the exact reference engine.
354#[doc(hidden)]
355/// MEASUREMENT KNOB — `RFF_ABL_DEBLOCK=1` skips the loop filter so it can be
356/// priced by ablation on the UNINSTRUMENTED binary. Read once; inert when unset.
357fn abl_deblock() -> bool {
358    // ROUTED AT BUILD TIME, like the rest of the knob inventory: an ablation arm
359    // is only reachable under `--features knobs`, and leaving it runtime keeps the
360    // skip-the-filter branch live in the shipping decoder.
361    #[cfg(not(feature = "knobs"))]
362    {
363        false
364    }
365    #[cfg(feature = "knobs")]
366    {
367        static ON: rusty_h264_common::once::OnceLock<bool> =
368            rusty_h264_common::once::OnceLock::new();
369        *ON.get_or_init(|| rusty_h264_common::knob("RFF_ABL_DEBLOCK").map_or(false, |v| v != "0"))
370    }
371}
372
373pub mod cabac_test {
374    pub use crate::cabac::Cabac;
375    pub use crate::mb16::b_inter_shape;
376    pub use crate::mb16::parse_cbp_cabac;
377    pub use crate::mb16::parse_mb_qp_delta_cabac;
378    pub use crate::mb16::parse_mb_type_b;
379    pub use crate::mb16::parse_ref_idx_cabac;
380}
381
382#[allow(unused_imports)]
383use alloc::borrow::ToOwned;
384use crate::sync::{CondvarPf, LockPf, MutexPf};
385#[allow(unused_imports)]
386use alloc::boxed::Box;
387#[allow(unused_imports)]
388use alloc::format;
389#[allow(unused_imports)]
390use alloc::string::{String, ToString};
391#[allow(unused_imports)]
392use alloc::vec;
393#[allow(unused_imports)]
394use alloc::vec::Vec;
395use mb16::{FrameDecoder, GridPool, WeightTable};
396use rusty_h264_common::bit_reader::OutOfData;
397use rusty_h264_common::nal::{emulation_unprevent, split_annex_b};
398use rusty_h264_common::{BitReader, NalUnitType, YuvFrame};
399
400/// Decode errors.
401#[derive(Debug, Clone, PartialEq, Eq)]
402pub enum DecodeError {
403    /// Bitstream ended unexpectedly.
404    Truncated,
405    /// A required parameter set was missing before a slice.
406    MissingParameterSet,
407    /// A coding tool outside the implemented subset appeared in the stream.
408    Unsupported(&'static str),
409    /// A DECODER invariant did not hold -- a helper thread died, a pooled
410    /// buffer was missing, a picture was not where the state machine expected
411    /// it. The stream is not necessarily at fault.
412    ///
413    /// This variant exists so those conditions can be REPORTED rather than
414    /// panicked. A decoder eats attacker-controlled bytes, so an unwind is a
415    /// denial-of-service primitive; a `Result` the caller can handle is not.
416    /// Every site that once carried an `.expect()` on an internal invariant
417    /// returns this instead.
418    Internal(&'static str),
419}
420
421impl From<OutOfData> for DecodeError {
422    fn from(_: OutOfData) -> Self {
423        DecodeError::Truncated
424    }
425}
426
427impl core::fmt::Display for DecodeError {
428    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
429        match self {
430            DecodeError::Truncated => f.write_str("bitstream truncated"),
431            DecodeError::MissingParameterSet => f.write_str("slice before SPS/PPS"),
432            DecodeError::Unsupported(s) => write!(f, "unsupported coding tool: {s}"),
433            DecodeError::Internal(s) => write!(f, "decoder invariant violated: {s}"),
434        }
435    }
436}
437
438impl core::error::Error for DecodeError {}
439
440/// A reference picture: deblocked reconstruction at coded resolution.
441/// Stored now (4a); read by motion compensation in 4b.
442/// Shared handle to a reference picture. The DPB and every per-slice reference
443/// list hold `Arc`s: list construction used to DEEP-CLONE each entry's planes +
444/// motion grids per slice (H-32 found ~600 KB+/slice of pure memcpy on B
445/// streams); an `Arc` clone is a refcount bump reading the same bytes, so the
446/// change is byte-identical by construction. `Arc::make_mut` covers the one
447/// mutation site (MMCO long-term marking).
448pub(crate) type Ref = crate::sync::Arc<RefFrame>;
449
450#[derive(Debug, Default)]
451#[allow(dead_code)]
452pub(crate) struct RefFrame {
453    /// EDGE-PADDED planes (openh264 `ExpandPicture`): built ONCE per reference
454    /// frame so motion compensation reads them in place — the per-MC-call
455    /// clamped-tile extraction (~400 B copied per call, ~100 MB/clip on real
456    /// streams) dies with this. Luma pad [`LPAD`], chroma pad [`CPAD`]; strides
457    /// via [`RefFrame::lstride`]/[`RefFrame::cstride`].
458    pub py: Vec<u8>,
459    pub pu: Vec<u8>,
460    pub pv: Vec<u8>,
461    pub cw: usize,
462    pub ch: usize,
463    /// Frame-MT Phase B: luma rows whose reconstruction (+row deblock when
464    /// enabled) is visible to other threads' MC. Phase A publishes `ch` (fully
465    /// ready) when the picture commits. `0` means not yet usable.
466    pub ready_rows: core::sync::atomic::AtomicUsize,
467    /// Phase B concurrent planes. When set, producers publish filtered rows into
468    /// these locks and consumers wait on [`Self::ready_rows`] then read here;
469    /// [`Self::py`]/[`Self::pu`]/[`Self::pv`] stay unused until commit copies
470    /// them out (serial / Phase A leave this `None` — zero lock tax on 1T).
471    pub live: Option<crate::sync::Arc<LivePlanes>>,
472    /// After picture finalize: lock-free planes for steady-state DPB MC.
473    /// Preferred over [`Self::live`] / empty [`Self::py`] once set.
474    pub frozen: crate::sync::Once<FrozenPlanes>,
475    /// `frame_num` of the picture, for PicNum-based reference-list reordering.
476    pub frame_num: u32,
477    /// `PicOrderCnt` of the picture, for B-slice reference-list ordering.
478    pub poc: i32,
479    /// Per-4×4-block List-0 motion field (motion vector + reference index, `-1`
480    /// for intra), and the block-grid width. Read as the *co-located* picture's
481    /// motion for B-slice direct prediction (`colZeroFlag`, temporal direct).
482    /// Narrowed with the grid it mirrors (see `FrameDecoder::mv_y`).
483    pub mv: Vec<(i16, i16)>,
484    /// Narrowed with the grid it mirrors (a `ref_idx` is -1..31; see
485    /// `FrameDecoder::ref_idx_y`).
486    pub ref_idx: Vec<i8>,
487    /// Per-4×4-block **List-1** motion. Needed because the co-located motion
488    /// derivation (spec §8.4.1.2.1) falls back to List-1 when the co-located block
489    /// has no List-0 prediction (`predFlagL0Col == 0`). A co-located picture only
490    /// contains L1-only blocks when it is itself a B picture — which is precisely
491    /// what b-pyramid produces, so this stayed unexercised until B-references
492    /// appeared.
493    pub mv1: Vec<(i16, i16)>,
494    pub ref_idx1: Vec<i8>,
495    /// The List-0 picture POC each block referenced, as a 32-ENTRY LUT rather
496    /// than a per-block expansion of it.
497    ///
498    /// This was `ref_poc: Vec<i32>` -- one POC per 4x4 block, 230 KB at 720p,
499    /// materialised for EVERY reference picture and cloned again with the frame.
500    /// But its every element was `poc_lut[ref_idx[i]]`, and `ref_idx` is right
501    /// there in this same struct: a 230 KB expansion of a 128-byte table. The
502    /// only consumer (temporal direct's `MapColToList0`) reads a single entry at
503    /// a time, so it can do the lookup itself.
504    ///
505    /// Slot `i` is the POC of `RefPicList0[i]`, or `i32::MIN` past the list end
506    /// -- which is exactly what the old `.get()` returned for an out-of-range
507    /// index, so the intra/unavailable case is unchanged.
508    ///
509    /// It also retires a hazard the old code documented against itself: `mv` and
510    /// `ref_poc` were PARALLEL Vecs whose lengths nothing tied together, so a
511    /// guard on one said nothing about the other. A fixed-size array cannot
512    /// desynchronise from anything.
513    pub poc_lut: [i32; 32],
514    pub w4: usize,
515    /// Long-term reference state. Long-term refs sit after short-term ones in
516    /// `RefPicList0` (ordered by `long_term_idx` ascending) and survive the
517    /// sliding window until explicitly unmarked (spec §8.2.4).
518    pub long_term: bool,
519    pub long_term_idx: u32,
520}
521
522impl Clone for RefFrame {
523    fn clone(&self) -> Self {
524        use core::sync::atomic::Ordering::Relaxed;
525        let frozen = crate::sync::Once::new();
526        if let Some(p) = self.frozen.get() {
527            let _ = frozen.set(p.clone());
528        }
529        Self {
530            py: self.py.clone(),
531            pu: self.pu.clone(),
532            pv: self.pv.clone(),
533            cw: self.cw,
534            ch: self.ch,
535            ready_rows: core::sync::atomic::AtomicUsize::new(self.ready_rows.load(Relaxed)),
536            live: self.live.clone(),
537            frozen,
538            frame_num: self.frame_num,
539            poc: self.poc,
540            mv: self.mv.clone(),
541            ref_idx: self.ref_idx.clone(),
542            mv1: self.mv1.clone(),
543            ref_idx1: self.ref_idx1.clone(),
544            poc_lut: self.poc_lut,
545            w4: self.w4,
546            long_term: self.long_term,
547            long_term_idx: self.long_term_idx,
548        }
549    }
550}
551
552/// Luma / chroma pad of every [`RefFrame`] plane. Luma 16 serves MVs overshooting
553/// the picture by up to ~14 px in place (chroma: half that, matching); wilder MVs
554/// take `mc_*_padded`'s clamped-halo fallback — correct, just slower.
555pub(crate) const LPAD: usize = 16;
556pub(crate) const CPAD: usize = 8;
557
558thread_local! {
559    /// Per-thread MB-row MC watermark (`rows_needed_for_mb`). `usize::MAX` = unset.
560    static MC_ROW_NEED: core::cell::Cell<usize> = const { core::cell::Cell::new(usize::MAX) };
561}
562
563/// Lock-free padded planes after a Phase B progress slot is finalized.
564#[derive(Debug, Clone)]
565pub(crate) struct FrozenPlanes {
566    pub py: Vec<u8>,
567    pub pu: Vec<u8>,
568    pub pv: Vec<u8>,
569}
570
571/// Concurrent padded planes + metadata for frame-MT Phase B (row-progress).
572#[derive(Debug)]
573pub(crate) struct LivePlanes {
574    pub py: crate::sync::RwLock<Vec<u8>>,
575    pub pu: crate::sync::RwLock<Vec<u8>>,
576    pub pv: crate::sync::RwLock<Vec<u8>>,
577    /// Identity + coloc motion; written at submit (fn/poc) and finalize (mv).
578    pub meta: crate::sync::RwLock<LiveMeta>,
579    /// Park consumers until [`RefFrame::ready_rows`] advances (no spin tax).
580    pub wait: crate::sync::Mutex<()>,
581    pub cv: crate::sync::Condvar,
582}
583
584#[derive(Debug, Clone, Default)]
585pub(crate) struct LiveMeta {
586    pub frame_num: u32,
587    pub poc: i32,
588    pub long_term: bool,
589    pub long_term_idx: u32,
590    /// Narrowed with the grid it mirrors (see `FrameDecoder::mv_y`).
591    pub mv: Vec<(i16, i16)>,
592    /// Narrowed with the grid it mirrors (a `ref_idx` is -1..31; see
593    /// `FrameDecoder::ref_idx_y`).
594    pub ref_idx: Vec<i8>,
595    pub mv1: Vec<(i16, i16)>,
596    pub ref_idx1: Vec<i8>,
597    pub poc_lut: [i32; 32],
598    pub w4: usize,
599    /// True once finalize has published coloc motion (temporal direct may read).
600    pub motion_ready: bool,
601}
602
603/// Guard over a luma/chroma plane (borrowed or Phase B live lock).
604pub(crate) enum PlaneGuard<'a> {
605    Borrowed(&'a [u8]),
606    Locked(crate::sync::RwLockReadGuard<'a, Vec<u8>>),
607}
608
609impl core::ops::Deref for PlaneGuard<'_> {
610    type Target = [u8];
611    fn deref(&self) -> &[u8] {
612        match self {
613            Self::Borrowed(s) => s,
614            Self::Locked(g) => g.as_slice(),
615        }
616    }
617}
618
619impl RefFrame {
620    /// Conservative luma rows needed before MC of MB row `mb_y` (MV overshoot pad).
621    #[inline]
622    pub(crate) fn rows_needed_for_mb(mb_y: usize, ch: usize) -> usize {
623        ((mb_y + 1) * 16 + LPAD).min(ch.max(1))
624    }
625
626    /// Publish the current MB-row MC watermark for this thread (parse or EDC worker).
627    /// [`Self::luma_guard`] / chroma take `min(caller_need, hint)` so legacy
628    /// `guard(ch)` call sites still early-start correctly under Phase B.
629    #[inline]
630    pub(crate) fn set_mc_row_need(mb_y: usize, ch: usize) {
631        // 1T / Phase A: no in-flight watermarks. TLS write on every MC was a
632        // no-op consumer of the hint (guards still waited on ready_rows).
633        if !crate::row_progress_on() {
634            return;
635        }
636        MC_ROW_NEED.with(|c| c.set(Self::rows_needed_for_mb(mb_y, ch)));
637    }
638
639    #[inline]
640    fn effective_need(&self, need_rows: usize) -> usize {
641        let hint = MC_ROW_NEED.with(|c| c.get());
642        need_rows.min(hint).min(self.ch.max(1))
643    }
644
645    /// Luma plane for MC, waiting on Phase B row watermark when needed.
646    /// Prefers frozen / plain planes (no lock); only in-flight slots take `live`.
647    #[inline]
648    pub(crate) fn luma_guard(&self, need_rows: usize) -> PlaneGuard<'_> {
649        if self.live.is_none() {
650            // 1T / Phase A: planes are complete. Skip TLS + ready_rows.
651            if let Some(f) = self.frozen.get() {
652                return PlaneGuard::Borrowed(&f.py);
653            }
654            return PlaneGuard::Borrowed(&self.py);
655        }
656        let need = self.effective_need(need_rows);
657        self.wait_ready_rows(need);
658        if let Some(f) = self.frozen.get() {
659            return PlaneGuard::Borrowed(&f.py);
660        }
661        if !self.py.is_empty() {
662            return PlaneGuard::Borrowed(&self.py);
663        }
664        if let Some(live) = &self.live {
665            PlaneGuard::Locked(live.py.read_pf())
666        } else {
667            PlaneGuard::Borrowed(&self.py)
668        }
669    }
670
671    #[inline]
672    pub(crate) fn chroma_guard(&self, plane: usize, need_rows: usize) -> PlaneGuard<'_> {
673        if self.live.is_none() {
674            if let Some(f) = self.frozen.get() {
675                return if plane == 0 {
676                    PlaneGuard::Borrowed(&f.pu)
677                } else {
678                    PlaneGuard::Borrowed(&f.pv)
679                };
680            }
681            return if plane == 0 {
682                PlaneGuard::Borrowed(&self.pu)
683            } else {
684                PlaneGuard::Borrowed(&self.pv)
685            };
686        }
687        let need = self.effective_need(need_rows);
688        self.wait_ready_rows(need);
689        if let Some(f) = self.frozen.get() {
690            return if plane == 0 {
691                PlaneGuard::Borrowed(&f.pu)
692            } else {
693                PlaneGuard::Borrowed(&f.pv)
694            };
695        }
696        if !self.py.is_empty() {
697            return if plane == 0 {
698                PlaneGuard::Borrowed(&self.pu)
699            } else {
700                PlaneGuard::Borrowed(&self.pv)
701            };
702        }
703        if let Some(live) = &self.live {
704            if plane == 0 {
705                PlaneGuard::Locked(live.pu.read_pf())
706            } else {
707                PlaneGuard::Locked(live.pv.read_pf())
708            }
709        } else if plane == 0 {
710            PlaneGuard::Borrowed(&self.pu)
711        } else {
712            PlaneGuard::Borrowed(&self.pv)
713        }
714    }
715    pub(crate) fn new_progress_slot(cw: usize, ch: usize, b_possible: bool, mb_w: usize) -> Ref {
716        let (lpw, lph) = (cw + 2 * LPAD, ch + 2 * LPAD);
717        let (cpw, cph) = (cw / 2 + 2 * CPAD, ch / 2 + 2 * CPAD);
718        let w4 = if b_possible { mb_w * 4 } else { 0 };
719        let n4 = if b_possible {
720            mb_w * 4 * (ch / 16) * 4
721        } else {
722            0
723        };
724        // PRICED: this allocates the four motion grids TWICE -- once inside
725        // LiveMeta and once on the RefFrame itself -- zero-filled, before
726        // `as_reference_pooled` replaces them with the real ones. Count it
727        // before assuming it matters: the frame-MT progress slot may not fire
728        // on the single-threaded path at all.
729        crate::mb16::cpystat::note(
730            &crate::mb16::cpystat::PROGRESS_SLOT,
731            2 * (n4 * core::mem::size_of::<(i32, i32)>() * 2 + n4 * 2),
732        );
733        // Fat live planes only when strip-publishing; otherwise meta+CV only
734        // (MC parks until finalize freezes lock-free planes).
735        let (py, pu, pv) = if crate::row_publish_on() {
736            (vec![0; lpw * lph], vec![0; cpw * cph], vec![0; cpw * cph])
737        } else {
738            (Vec::new(), Vec::new(), Vec::new())
739        };
740        crate::sync::Arc::new(RefFrame {
741            py: Vec::new(),
742            pu: Vec::new(),
743            pv: Vec::new(),
744            cw,
745            ch,
746            ready_rows: core::sync::atomic::AtomicUsize::new(0),
747            live: Some(crate::sync::Arc::new(LivePlanes {
748                py: crate::sync::RwLock::new(py),
749                pu: crate::sync::RwLock::new(pu),
750                pv: crate::sync::RwLock::new(pv),
751                meta: crate::sync::RwLock::new(LiveMeta {
752                    mv: vec![(0, 0); n4],
753                    ref_idx: vec![-1; n4],
754                    mv1: vec![(0, 0); n4],
755                    ref_idx1: vec![-1; n4],
756                    poc_lut: [i32::MIN; 32],
757                    w4,
758                    ..LiveMeta::default()
759                }),
760                wait: crate::sync::Mutex::new(()),
761                cv: crate::sync::Condvar::new(),
762            })),
763            frozen: crate::sync::Once::new(),
764            frame_num: 0,
765            poc: 0,
766            mv: vec![(0, 0); n4],
767            ref_idx: vec![-1; n4],
768            mv1: vec![(0, 0); n4],
769            ref_idx1: vec![-1; n4],
770            poc_lut: [i32::MIN; 32],
771            w4,
772            long_term: false,
773            long_term_idx: 0,
774        })
775    }
776
777    /// Set identity while the progress Arc is still unique (submit thread).
778    pub(crate) fn init_progress_identity(slot: &mut Ref, frame_num: u32, poc: i32) {
779        if let Some(s) = crate::sync::Arc::get_mut(slot) {
780            s.frame_num = frame_num;
781            s.poc = poc;
782            if let Some(live) = &s.live {
783                let mut m = live.meta.write_pf();
784                m.frame_num = frame_num;
785                m.poc = poc;
786            }
787        }
788    }
789
790    #[inline]
791    pub(crate) fn fn_num(&self) -> u32 {
792        if let Some(live) = &self.live {
793            live.meta.read_pf().frame_num
794        } else {
795            self.frame_num
796        }
797    }
798
799    #[inline]
800    pub(crate) fn pic_poc(&self) -> i32 {
801        if let Some(live) = &self.live {
802            live.meta.read_pf().poc
803        } else {
804            self.poc
805        }
806    }
807
808    #[inline]
809    pub(crate) fn is_long_term(&self) -> bool {
810        if let Some(live) = &self.live {
811            live.meta.read_pf().long_term
812        } else {
813            self.long_term
814        }
815    }
816
817    #[inline]
818    pub(crate) fn lt_idx(&self) -> u32 {
819        if let Some(live) = &self.live {
820            live.meta.read_pf().long_term_idx
821        } else {
822            self.long_term_idx
823        }
824    }
825
826    pub(crate) fn set_long_term_marks(&self, long_term: bool, idx: u32) {
827        if let Some(live) = &self.live {
828            let mut m = live.meta.write_pf();
829            m.long_term = long_term;
830            m.long_term_idx = idx;
831        }
832    }
833
834    pub(crate) fn set_frame_num_live(&self, frame_num: u32) {
835        if let Some(live) = &self.live {
836            live.meta.write_pf().frame_num = frame_num;
837        }
838    }
839
840    /// Wait until coloc motion is published (picture fully finalized).
841    pub(crate) fn wait_motion_ready(&self) {
842        if self.live.is_none() {
843            return;
844        }
845        if let Some(live) = &self.live {
846            if live.meta.read_pf().motion_ready {
847                return;
848            }
849        }
850        if let Some(live) = &self.live {
851            let mut g = live.wait.lock_pf();
852            while !live.meta.read_pf().motion_ready {
853                g = live.cv.wait_pf(g);
854            }
855        }
856    }
857
858    /// Mark this reference fully ready (Phase A commit / serial path).
859    #[inline]
860    pub fn mark_fully_ready(&self) {
861        if let Some(live) = &self.live {
862            let _g = live.wait.lock_pf();
863            self.ready_rows
864                .store(self.ch, core::sync::atomic::Ordering::Release);
865            live.cv.notify_all();
866        } else {
867            self.ready_rows
868                .store(self.ch, core::sync::atomic::Ordering::Release);
869        }
870    }
871
872    /// Frame-MT Phase B: publish that luma rows `[0, rows)` are MC-safe.
873    #[inline]
874    pub fn publish_ready_rows(&self, rows: usize) {
875        let r = rows.min(self.ch);
876        if let Some(live) = &self.live {
877            let _g = live.wait.lock_pf();
878            let prev = self
879                .ready_rows
880                .fetch_max(r, core::sync::atomic::Ordering::Release);
881            if r > prev {
882                live.cv.notify_all();
883            }
884        } else {
885            let _ = self
886                .ready_rows
887                .fetch_max(r, core::sync::atomic::Ordering::Release);
888        }
889    }
890
891    /// Block until at least `rows` luma rows are ready (Phase B). Phase A refs
892    /// are published fully ready, so this returns immediately.
893    #[inline]
894    pub fn wait_ready_rows(&self, rows: usize) {
895        use core::sync::atomic::Ordering::Acquire;
896        let need = rows.min(self.ch);
897        if need == 0 || self.ready_rows.load(Acquire) >= need {
898            return;
899        }
900        if self.frozen.get().is_some() {
901            return;
902        }
903        if let Some(live) = &self.live {
904            let mut g = live.wait.lock_pf();
905            while self.ready_rows.load(Acquire) < need {
906                g = live.cv.wait_pf(g);
907            }
908        } else {
909            while self.ready_rows.load(Acquire) < need {
910                crate::sync::yield_now();
911            }
912        }
913    }
914
915    #[inline]
916    pub fn lstride(&self) -> usize {
917        self.cw + 2 * LPAD
918    }
919    #[inline]
920    pub fn cstride(&self) -> usize {
921        self.cw / 2 + 2 * CPAD
922    }
923    /// Rows in the PADDED luma plane. Four call sites recovered this as
924    /// `plane.len() / lstride()` -- an integer DIVIDE by a runtime value, per
925    /// P_Skip macroblock and per B-skip validity test, to recompute a constant
926    /// property of the frame. The plane is allocated `(cw + 2*LPAD) *
927    /// (ch + 2*LPAD)` and the stride is the first factor, so the row count is
928    /// the second one, exactly.
929    #[inline]
930    pub fn lrows(&self) -> usize {
931        self.ch + 2 * LPAD
932    }
933    /// Rows in either PADDED chroma plane (`(cw/2 + 2*CPAD) * (ch/2 + 2*CPAD)`).
934    #[inline]
935    pub fn crows(&self) -> usize {
936        self.ch / 2 + 2 * CPAD
937    }
938}
939
940/// A memory-management control operation (`dec_ref_pic_marking`, spec §7.4.3.3).
941#[derive(Clone, Copy)]
942enum Mmco {
943    /// 1: mark a short-term reference (by PicNum) as unused.
944    Unref(u32),
945    /// 2: mark a long-term reference (by LongTermPicNum) as unused.
946    UnrefLong(u32),
947    /// 3: assign a short-term reference (by PicNum) a LongTermFrameIdx.
948    AssignLong(u32, u32),
949    /// 4: drop long-term references with idx ≥ max_long_term_frame_idx_plus1.
950    MaxLong(u32),
951    /// 5: empty the DPB (and reset the current picture's frame_num to 0).
952    Reset,
953    /// 6: mark the current picture long-term with this LongTermFrameIdx.
954    CurrentLong(u32),
955}
956
957/// A picture being assembled from one or more slices (spec allows a picture to
958/// be split into multiple slices). Finalized — deblocked, output, and entered
959/// into the DPB — once all its macroblocks are decoded.
960/// GATE 1 content route (big-oppy-decoder §2): the 4-way cost tier the last
961/// completed picture classified into. ROUTER ONLY — nothing consumes it yet;
962/// consumers land per-route with their own gates.
963#[derive(Clone, Copy, PartialEq, Eq, Debug)]
964pub enum ContentRoute {
965    Light,
966    Mid,
967    DenseInter,
968    EntropyExtreme,
969}
970
971/// Per-tier route trees on the DEPLOYED counters (bits/MB, skip fraction,
972/// coded-MB fraction). Thresholds are calibrated on the deployed estimator
973/// itself (calibration table in docs/big-oppy-decoder-truthtable.xlsx) —
974/// never transplanted from an offline probe of a same-named signal.
975fn route_for(
976    cabac: bool,
977    t8x8: bool,
978    bits_per_mb: f64,
979    skip_frac: f64,
980    coded_frac: f64,
981) -> ContentRoute {
982    // Calibrated 2026-08-20 on the deployed counters over 68 streams
983    // (17 clips x 4 x264 tiers): LOCO-CV 17/17 cavlc, 15/17 main,
984    // 32/34 on the unified 8x8 signature (high+default) = 64/68.
985    let (root_coded, light_skip, extreme_bits) = if !cabac {
986        (0.6116, 0.6433, 406.4)
987    } else if t8x8 {
988        (0.4244, 0.6358, 331.7)
989    } else {
990        (0.3709, 0.6343, 315.2)
991    };
992    if coded_frac <= root_coded {
993        if skip_frac > light_skip {
994            ContentRoute::Light
995        } else {
996            ContentRoute::Mid
997        }
998    } else if bits_per_mb <= extreme_bits {
999        ContentRoute::DenseInter
1000    } else {
1001        ContentRoute::EntropyExtreme
1002    }
1003}
1004
1005struct PendingPic {
1006    fd: mb16::FrameDecoder,
1007    frame_num: u32,
1008    poc: i32,
1009    next_mb: usize,
1010    total_mb: usize,
1011    slice_count: u16,
1012    deblock: bool,
1013    filter_offset_a: i32,
1014    filter_offset_b: i32,
1015    crop_r: usize,
1016    crop_b: usize,
1017    max_refs: usize,
1018    log2_max_frame_num: u32,
1019    /// `false` for a non-reference picture (nal_ref_idc == 0): output it but do
1020    /// not enter it into the DPB.
1021    is_reference: bool,
1022    idr_long_term: bool,
1023    mmco_ops: Vec<Mmco>,
1024    /// GATE 1 router inputs accumulated per picture.
1025    route_bits: u64,
1026    route_cabac: bool,
1027    route_t8x8: bool,
1028}
1029
1030/// Measurement knob: disable grid pooling, restoring per-picture allocation.
1031fn no_pool() -> bool {
1032    // ROUTED AT BUILD TIME. A measurement knob is only reachable under
1033    // `--features knobs`; left runtime it costs an atomic load per picture AND
1034    // keeps the un-pooled per-picture allocation path live in the binary.
1035    #[cfg(not(feature = "knobs"))]
1036    {
1037        false
1038    }
1039    #[cfg(feature = "knobs")]
1040    {
1041        use core::sync::atomic::{AtomicU8, Ordering};
1042        static ON: AtomicU8 = AtomicU8::new(0);
1043        match ON.load(Ordering::Relaxed) {
1044            0 => {
1045                let v = rusty_h264_common::knob("RS_H264_NO_POOL").is_some_and(|v| v == "1");
1046                ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
1047                v
1048            }
1049            n => n == 1,
1050        }
1051    }
1052}
1053
1054/// A Constrained Baseline H.264 decoder. Holds the most recent parameter sets
1055/// and the previous decoded picture (the inter reference) across calls.
1056
1057#[derive(Default)]
1058pub struct Decoder {
1059    /// GATE 1 route of the most recently completed picture (router only).
1060    last_route: Option<ContentRoute>,
1061    /// EMA'd router signals (bits/MB, skip frac, coded frac). The thresholds
1062    /// were calibrated on per-STREAM means; a per-picture read sits below the
1063    /// root on P-pictures of boundary streams (shields) while I-pictures sit
1064    /// far above — the EMA (alpha 1/8) reproduces the calibrated estimator at
1065    /// steady state and adapts over ~8 pictures.
1066    route_ema: Option<(f64, f64, f64)>,
1067    /// Active parameter sets, keyed by id — a stream may carry several and switch
1068    /// between them per slice (spec §7.3.2.1/.2).
1069    // 11.11: Arc'd so the per-slice "clone to end the map borrow" is a
1070    // refcount bump, not a struct copy (scaling lists included).
1071    pub(crate) sps: alloc::collections::BTreeMap<u32, crate::sync::Arc<Sps>>,
1072    pub(crate) pps: alloc::collections::BTreeMap<u32, crate::sync::Arc<Pps>>,
1073    /// Decoded-picture buffer (most-recent first); `ref_idx` indexes into this.
1074    pub(crate) refs: Vec<Ref>,
1075    /// The picture currently being assembled from its slices, if any.
1076    cur: Option<PendingPic>,
1077    /// Picture-order-count state (spec §8.2.1). Tracks the previous reference
1078    /// picture's MSB/LSB (type 0) and frame-num offset (types 1/2) so display
1079    /// order can be recovered — needed once B-pictures (out-of-order) land.
1080    pub(crate) poc: PocState,
1081    /// `PicOrderCnt` of the most recently returned picture (display-order key).
1082    pub(crate) last_poc: i32,
1083    /// `frame_num` of the previous short-term reference picture, for detecting
1084    /// gaps in `frame_num` (spec §8.2.5.2).
1085    pub(crate) prev_ref_frame_num: u32,
1086    /// Per-picture grid allocations, handed from the finished picture to the next
1087    /// one instead of being freed and re-allocated. See `mb16::GridPool`.
1088    grid_pool: GridPool,
1089    /// 11.11: slice-header scratch (reorder lists + MMCO ops), recycled.
1090    sc_reorder0: Vec<(u32, u32)>,
1091    sc_reorder1: Vec<(u32, u32)>,
1092    sc_mmco: Vec<Mmco>,
1093    /// Recycled padded-plane buffers from evicted reference frames, drawn by
1094    /// `as_reference_pooled`. Bounded (see `reclaim_retired`).
1095    plane_pool: Vec<Vec<u8>>,
1096    /// The DPB motion field's recycle pools, fed by `reclaim_retired` exactly
1097    /// as `plane_pool` is. `as_reference_pooled` used `self.mv_y.clone()`, and
1098    /// a clone is an allocation PLUS a copy: at 720p the two motion grids are
1099    /// 230,400 B each, which the allocation census measured as 6,720 large
1100    /// allocations and 1.55 GB over a 1260-frame decode. The copy is real (the
1101    /// live grid is refilled next picture); the allocation is not.
1102    mv_pool: Vec<Vec<(i16, i16)>>,
1103    ref_pool: Vec<Vec<i8>>,
1104    /// Reference frames evicted from the DPB whose planes have not been
1105    /// reclaimed yet. Reclamation must wait until the evicting picture's
1106    /// `FrameDecoder` is consumed — while it lives it still holds `Arc` clones
1107    /// of its ref lists, so `Arc::try_unwrap` would fail at eviction time.
1108    retired: Vec<Ref>,
1109    /// Frame-MT: when set, finalize does not apply DPB marking; the new ref is
1110    /// stashed in [`Self::detached_ref`] for the scheduler to commit in order.
1111    pub(crate) detach_dpb: bool,
1112    /// Detached reference as `Arc` (Phase B progress slot or freshly wrapped).
1113    pub(crate) detached_ref: Option<Ref>,
1114    /// Phase B: pre-installed progress Arc filled during decode / finalize.
1115    pub(crate) progress_slot: Option<Ref>,
1116    detached_mmco: Vec<Mmco>,
1117    detached_frame_num: u32,
1118    detached_log2_max_frame_num: u32,
1119    detached_max_refs: usize,
1120    detached_idr_long_term: bool,
1121    /// Frame-MT Phase B: publish row watermarks while decoding.
1122    pub(crate) frame_mt_row_progress: bool,
1123}
1124
1125/// Running picture-order-count derivation state.
1126#[derive(Clone, Default)]
1127pub(crate) struct PocState {
1128    prev_msb: i32,
1129    prev_lsb: i32,
1130    prev_frame_num: u32,
1131    prev_frame_num_offset: i64,
1132}
1133
1134impl PocState {
1135    /// Derives `PicOrderCnt` for the current picture (spec §8.2.1) and advances
1136    /// this state. Types 0 and 2 are exact; type 1 is approximated by frame-num
1137    /// order (no B-stream in scope uses it).
1138    pub(crate) fn compute_poc(
1139        &mut self,
1140        sps: &Sps,
1141        is_idr: bool,
1142        nal_ref_idc: u8,
1143        frame_num: u32,
1144        poc_lsb: u32,
1145        delta_bottom: i32,
1146    ) -> i32 {
1147        match sps.pic_order_cnt_type {
1148            0 => {
1149                let max_lsb = 1i32 << sps.log2_max_pic_order_cnt_lsb;
1150                let (prev_msb, prev_lsb) = if is_idr {
1151                    (0, 0)
1152                } else {
1153                    (self.prev_msb, self.prev_lsb)
1154                };
1155                let lsb = poc_lsb as i32;
1156                let msb = if lsb < prev_lsb && prev_lsb - lsb >= max_lsb / 2 {
1157                    prev_msb + max_lsb
1158                } else if lsb > prev_lsb && lsb - prev_lsb > max_lsb / 2 {
1159                    prev_msb - max_lsb
1160                } else {
1161                    prev_msb
1162                };
1163                let top = msb + lsb;
1164                let poc = top.min(top + delta_bottom);
1165                if nal_ref_idc != 0 {
1166                    self.prev_msb = msb;
1167                    self.prev_lsb = lsb;
1168                }
1169                poc
1170            }
1171            2 => {
1172                let max_fn = 1i64 << sps.log2_max_frame_num;
1173                let offset = if is_idr {
1174                    0
1175                } else if self.prev_frame_num > frame_num {
1176                    self.prev_frame_num_offset + max_fn
1177                } else {
1178                    self.prev_frame_num_offset
1179                };
1180                let poc = if is_idr {
1181                    0
1182                } else {
1183                    2 * (offset + frame_num as i64) - i64::from(nal_ref_idc == 0)
1184                };
1185                self.prev_frame_num_offset = offset;
1186                self.prev_frame_num = frame_num;
1187                poc as i32
1188            }
1189            _ => {
1190                self.prev_frame_num = frame_num;
1191                frame_num as i32 * 2
1192            }
1193        }
1194    }
1195}
1196
1197impl Decoder {
1198    /// Creates a decoder with no parameter sets yet.
1199    pub fn new() -> Self {
1200        Self::default()
1201    }
1202
1203    /// Decodes a complete Annex-B access unit, returning the reconstructed,
1204    /// cropped frame if the access unit contained a coded picture.
1205    pub fn decode(&mut self, annex_b: &[u8]) -> Result<Option<YuvFrame>, DecodeError> {
1206        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Total);
1207        let mut frame = None;
1208        // The Annex-B scan and the RBSP unescape are each a FULL byte-wise pass over
1209        // the stream, and neither was timed — they landed in the unnamed residue that
1210        // the anatomy measured at 23-30% of decode outside the MB bodies.
1211        let nals = {
1212            let _s = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecNalSplit);
1213            split_annex_b(annex_b)
1214        };
1215        for nal in nals {
1216            if nal.is_empty() {
1217                continue;
1218            }
1219            let nal_type = NalUnitType::from_id(nal[0]);
1220            let rbsp = {
1221                let _s = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecRbsp);
1222                emulation_unprevent(&nal[1..])
1223            };
1224            match nal_type {
1225                NalUnitType::Sps => {
1226                    let s = Sps::parse(&rbsp)?;
1227                    self.sps
1228                        .insert(s.seq_parameter_set_id, crate::sync::Arc::new(s));
1229                }
1230                NalUnitType::Pps => {
1231                    let p = Pps::parse(&rbsp)?;
1232                    self.pps
1233                        .insert(p.pic_parameter_set_id, crate::sync::Arc::new(p));
1234                }
1235                NalUnitType::IdrSlice | NalUnitType::NonIdrSlice => {
1236                    let nal_ref_idc = (nal[0] >> 5) & 3;
1237                    let is_idr = nal_type == NalUnitType::IdrSlice;
1238                    if let Some(f) = self.decode_slice(&rbsp, is_idr, nal_ref_idc)? {
1239                        frame = Some(f);
1240                    }
1241                }
1242                _ => {} // SEI, AUD, etc. ignored
1243            }
1244        }
1245        Ok(frame)
1246    }
1247
1248    /// Decodes a complete Annex-B byte stream and returns every picture in
1249    /// **display order** (`PicOrderCnt` within each GOP; an IDR ends a GOP).
1250    ///
1251    /// This is the convenient whole-stream entry point — it handles access-unit
1252    /// splitting, multi-slice picture assembly, and B-picture reordering — versus
1253    /// the lower-level per-access-unit [`Decoder::decode`], which returns pictures
1254    /// in decode order.
1255    ///
1256    /// When `RS_H264_FRAME_THREADS` is set to N>1 (or the caller uses
1257    /// [`Decoder::decode_stream_threaded`]), pictures decode on a worker pool
1258    /// under a full-reference barrier (campaign #1 Phase A). Measure with
1259    /// `bench/pinmt.ps1` (WALL+CPU, multi-core mask) — not the 1T CPU race.
1260    pub fn decode_stream(&mut self, annex_b: &[u8]) -> Result<Vec<YuvFrame>, DecodeError> {
1261        #[cfg(feature = "std")]
1262        {
1263            let n = frame_mt::frame_threads();
1264            if n > 1 {
1265                return frame_mt::decode_stream_threaded(annex_b, n);
1266            }
1267        }
1268        self.decode_stream_serial(annex_b)
1269    }
1270
1271    /// Force frame-MT with an explicit worker count (0/1 = serial).
1272    #[cfg(feature = "std")]
1273    pub fn decode_stream_threaded(
1274        &mut self,
1275        annex_b: &[u8],
1276        threads: usize,
1277    ) -> Result<Vec<YuvFrame>, DecodeError> {
1278        if threads <= 1 {
1279            return self.decode_stream_serial(annex_b);
1280        }
1281        frame_mt::decode_stream_threaded(annex_b, threads)
1282    }
1283
1284    /// Frame-MT decode that invokes `sink` for each display-ordered frame
1285    /// (avoids retaining all YUV — use from `decode_bench` timed path).
1286    #[cfg(feature = "std")]
1287    pub fn decode_stream_threaded_sink(
1288        &mut self,
1289        annex_b: &[u8],
1290        threads: usize,
1291        sink: impl FnMut(YuvFrame),
1292    ) -> Result<usize, DecodeError> {
1293        if threads <= 1 {
1294            let frames = self.decode_stream_serial(annex_b)?;
1295            let n = frames.len();
1296            let mut sink = sink;
1297            for f in frames {
1298                sink(f);
1299            }
1300            return Ok(n);
1301        }
1302        frame_mt::decode_stream_threaded_sink(annex_b, threads, sink)
1303    }
1304
1305    fn decode_stream_serial(&mut self, annex_b: &[u8]) -> Result<Vec<YuvFrame>, DecodeError> {
1306        let mut out = Vec::new();
1307        let mut gop: Vec<(i32, YuvFrame)> = Vec::new();
1308        for au in split_access_units(annex_b) {
1309            if au_is_idr(au) {
1310                flush_gop(&mut gop, &mut out); // emit the prior GOP before the IDR
1311            }
1312            if let Some(frame) = self.decode(au)? {
1313                gop.push((self.last_poc, frame));
1314            }
1315        }
1316        flush_gop(&mut gop, &mut out);
1317        Ok(out)
1318    }
1319
1320    fn decode_slice(
1321        &mut self,
1322        rbsp: &[u8],
1323        is_idr: bool,
1324        nal_ref_idc: u8,
1325    ) -> Result<Option<YuvFrame>, DecodeError> {
1326        let mut r = BitReader::new(rbsp);
1327        // --- slice_header ---
1328        let first_mb_in_slice = r.read_ue()? as usize;
1329        let slice_type = r.read_ue()?;
1330        let is_p = matches!(slice_type, 0 | 5);
1331        let is_b = matches!(slice_type, 1 | 6);
1332        let is_i = matches!(slice_type, 2 | 7);
1333        if !is_p && !is_b && !is_i {
1334            return Err(DecodeError::Unsupported("SP/SI slices"));
1335        }
1336        // Resolve the parameter sets this slice references (by id).
1337        let pic_parameter_set_id = r.read_ue()?;
1338        let pps = self
1339            .pps
1340            .get(&pic_parameter_set_id)
1341            .cloned()
1342            .ok_or(DecodeError::MissingParameterSet)?;
1343        let sps = self
1344            .sps
1345            .get(&pps.seq_parameter_set_id)
1346            .cloned()
1347            .ok_or(DecodeError::MissingParameterSet)?;
1348        let sps = &sps;
1349        let pps = &pps;
1350        // CABAC (entropy_coding_mode_flag=1) has an entirely different slice-data parse
1351        // (docs/cabac-decode-plan.md). I-slice CABAC is being brought up; the CABAC MB
1352        // loop gates P/B until Phase 3. `cabac_init_idc` (P/B only) is read below.
1353        let cabac = pps.entropy_coding_mode_flag;
1354        let frame_num = r.read_bits(sps.log2_max_frame_num)?;
1355        // field_pic_flag is present whenever field coding is permitted; only frame
1356        // pictures are decoded (MBAFF is refused at the SPS).
1357        if !sps.frame_mbs_only && r.read_bit()? {
1358            return Err(DecodeError::Unsupported(
1359                "interlace / field coding (field picture)",
1360            ));
1361        }
1362        if is_idr {
1363            let _idr_pic_id = r.read_ue()?;
1364        }
1365        // pic_order_cnt fields (spec §7.3.3), for a frame picture. Captured to derive
1366        // PicOrderCnt for display ordering.
1367        let mut poc_lsb = 0u32;
1368        let mut delta_poc_bottom = 0i32;
1369        if sps.pic_order_cnt_type == 0 {
1370            poc_lsb = r.read_bits(sps.log2_max_pic_order_cnt_lsb)?;
1371            if pps.bottom_field_pic_order_present {
1372                delta_poc_bottom = r.read_se()?;
1373            }
1374        } else if sps.pic_order_cnt_type == 1 && !sps.delta_pic_order_always_zero {
1375            let _delta_pic_order_cnt_0 = r.read_se()?;
1376            if pps.bottom_field_pic_order_present {
1377                let _delta_pic_order_cnt_1 = r.read_se()?;
1378            }
1379        }
1380        // PicOrderCnt is determined by the first slice of the picture; later
1381        // slices share it (and must not re-advance the POC state).
1382        let pic_poc = if first_mb_in_slice == 0 {
1383            self.compute_poc(
1384                sps,
1385                is_idr,
1386                nal_ref_idc,
1387                frame_num,
1388                poc_lsb,
1389                delta_poc_bottom,
1390            )
1391        } else {
1392            self.cur.as_ref().map_or(0, |p| p.poc)
1393        };
1394        // redundant_pic_cnt: a non-zero value marks a *redundant* coded picture
1395        // (an alternative representation of the primary picture). A primary
1396        // decoder discards it (spec §7.4.3, §8.2.5 note). Must be read here or the
1397        // rest of the slice header desyncs.
1398        if pps.redundant_pic_cnt_present_flag {
1399            let redundant_pic_cnt = r.read_ue()?;
1400            if redundant_pic_cnt != 0 {
1401                return Ok(None);
1402            }
1403        }
1404        if crate::mb16::dump_mb_on() {
1405            eprintln!(
1406                "SLICE fn={frame_num} poc={pic_poc} nal_ref_idc={nal_ref_idc} is_p={is_p} is_b={is_b} first_mb={first_mb_in_slice}"
1407            );
1408        }
1409        // B slices choose direct-mode derivation here (spec §7.3.3).
1410        let direct_spatial = if is_b { r.read_bit()? } else { true };
1411        let mut num_ref_idx_l0 = pps.num_ref_idx_l0_default as usize;
1412        let mut num_ref_idx_l1 = pps.num_ref_idx_l1_default as usize;
1413        let mut reorder_l0: Vec<(u32, u32)> = core::mem::take(&mut self.sc_reorder0);
1414        reorder_l0.clear();
1415        let mut reorder_l1: Vec<(u32, u32)> = core::mem::take(&mut self.sc_reorder1);
1416        reorder_l1.clear();
1417        if is_p || is_b {
1418            // num_ref_idx_active_override_flag
1419            if r.read_bit()? {
1420                num_ref_idx_l0 = (r.read_ue()? + 1) as usize;
1421                if is_b {
1422                    num_ref_idx_l1 = (r.read_ue()? + 1) as usize;
1423                }
1424            }
1425            // ref_pic_list_modification_flag_l0
1426            if r.read_bit()? {
1427                parse_ref_pic_list_modification(&mut r, &mut reorder_l0)?;
1428            }
1429            if is_b && r.read_bit()? {
1430                // ref_pic_list_modification_flag_l1
1431                parse_ref_pic_list_modification(&mut r, &mut reorder_l1)?;
1432            }
1433        }
1434        // Explicit weighted prediction carries a pred_weight_table() here. P
1435        // (weighted_pred) uses single-list weights; B explicit bipred (idc 1) is
1436        // not yet wired into the bi-pred averaging, so refuse that. Implicit
1437        // bipred (idc 2) carries no table.
1438        let weights = if is_p && pps.weighted_pred {
1439            Some(parse_pred_weight_table(&mut r, num_ref_idx_l0, 0, false)?)
1440        } else if is_b && pps.weighted_bipred_idc == 1 {
1441            return Err(DecodeError::Unsupported("explicit B weighted prediction"));
1442        } else {
1443            None
1444        };
1445        // dec_ref_pic_marking (spec §7.3.3.3) — present only for reference
1446        // pictures (nal_ref_idc != 0). Reading it for a non-reference slice would
1447        // desync the rest of the header.
1448        let mut idr_long_term = false;
1449        let mut mmco_ops: Vec<Mmco> = core::mem::take(&mut self.sc_mmco);
1450        mmco_ops.clear();
1451        if nal_ref_idc == 0 {
1452            // non-reference picture: no marking syntax
1453        } else if is_idr {
1454            let _no_output_of_prior_pics = r.read_bit()?;
1455            idr_long_term = r.read_bit()?; // long_term_reference_flag
1456        } else if r.read_bit()? {
1457            // adaptive_ref_pic_marking_mode_flag
1458            loop {
1459                let op = r.read_ue()?;
1460                match op {
1461                    0 => break,
1462                    1 => mmco_ops.push(Mmco::Unref(r.read_ue()?)),
1463                    2 => mmco_ops.push(Mmco::UnrefLong(r.read_ue()?)),
1464                    3 => {
1465                        let diff = r.read_ue()?;
1466                        let idx = r.read_ue()?;
1467                        mmco_ops.push(Mmco::AssignLong(diff, idx));
1468                    }
1469                    4 => mmco_ops.push(Mmco::MaxLong(r.read_ue()?)),
1470                    5 => mmco_ops.push(Mmco::Reset),
1471                    6 => mmco_ops.push(Mmco::CurrentLong(r.read_ue()?)),
1472                    _ => return Err(DecodeError::Unsupported("invalid MMCO")),
1473                }
1474                if mmco_ops.len() > 128 {
1475                    return Err(DecodeError::Truncated);
1476                }
1477            }
1478        }
1479        // cabac_init_idc (spec §7.3.3) — CABAC context-model preset, P/B slices only.
1480        // Spec range [0,2]; a larger (corrupt) value would index the 4-model context-init
1481        // table out of bounds, so reject it here.
1482        let cabac_init_idc = if cabac && !is_i {
1483            let v = r.read_ue()?;
1484            if v > 2 {
1485                return Err(DecodeError::Unsupported("invalid cabac_init_idc"));
1486            }
1487            v
1488        } else {
1489            0
1490        };
1491        let slice_qp_delta = r.read_se()?;
1492        // When deblocking_filter_control_present_flag is 0 the slice carries no
1493        // disable_deblocking_filter_idc and it is inferred 0 — i.e. the in-loop
1494        // filter is ON by default (spec §7.4.3). (Our own encoder always signals
1495        // the control explicitly, so this default was previously untested.)
1496        let mut deblock = true;
1497        let mut deblock_idc2 = false;
1498        let (mut filter_offset_a, mut filter_offset_b) = (0i32, 0i32);
1499        if pps.deblocking_filter_control_present_flag {
1500            let disable_deblocking_filter_idc = r.read_ue()?;
1501            // idc 1 = filter off; idc 0 = on; idc 2 = on, but this slice's MB
1502            // edges against OTHER slices are not filtered (bS forced 0 at the
1503            // crossing edges in derive_bs_row).
1504            deblock = disable_deblocking_filter_idc != 1;
1505            deblock_idc2 = disable_deblocking_filter_idc == 2;
1506            if disable_deblocking_filter_idc != 1 {
1507                // FilterOffset = slice_*_offset_div2 × 2 (spec §7.4.3).
1508                filter_offset_a = r.read_se()? * 2;
1509                filter_offset_b = r.read_se()? * 2;
1510            }
1511        }
1512        let slice_qp = (pps.pic_init_qp + slice_qp_delta).clamp(0, 51) as u8;
1513
1514        // Synthesize placeholder short-term references for any gap in frame_num
1515        // (spec §8.2.5.2) so the DPB / PicNum mapping stays correct.
1516        if first_mb_in_slice == 0 && !is_idr && sps.gaps_in_frame_num_allowed {
1517            self.insert_frame_num_gaps(
1518                frame_num,
1519                1u32 << sps.log2_max_frame_num,
1520                sps.max_num_ref_frames.max(1) as usize,
1521                sps.pic_width_in_mbs * 16,
1522                sps.pic_height_in_mbs * 16,
1523            );
1524        }
1525
1526        // Build the reference list(s) for this slice. P uses RefPicList0 only;
1527        // B uses RefPicList0 and RefPicList1 (POC-ordered).
1528        let max_fn = 1u32 << sps.log2_max_frame_num;
1529        let (ref_list0, ref_list1) = if is_b {
1530            build_ref_list_b(
1531                &self.refs,
1532                pic_poc,
1533                frame_num,
1534                max_fn,
1535                num_ref_idx_l0,
1536                num_ref_idx_l1,
1537                &reorder_l0,
1538                &reorder_l1,
1539            )?
1540        } else if is_p {
1541            (
1542                build_ref_list_p(&self.refs, frame_num, max_fn, num_ref_idx_l0, &reorder_l0)?,
1543                Vec::new(),
1544            )
1545        } else {
1546            (Vec::new(), Vec::new())
1547        };
1548        // Return the reorder scratch: capacity survives to the next slice.
1549        self.sc_reorder0 = reorder_l0;
1550        self.sc_reorder1 = reorder_l1;
1551        // --- picture assembly ---
1552        // first_mb_in_slice == 0 starts a new picture; otherwise this slice
1553        // continues the one in flight. An IDR clears the DPB at its first slice.
1554        if first_mb_in_slice == 0 {
1555            if is_idr {
1556                // Park, don't drop: an IDR empties the whole DPB, so this is
1557                // the single largest return of recyclable planes in the stream
1558                // and it was the last removal path that freed them instead.
1559                self.retired.append(&mut self.refs);
1560            }
1561            // H-49: the CABAC macroblock loop never decodes `transform_size_8x8_flag`
1562            // — both reads of it sit on the CAVLC `BitReader`, and `decode_i8x8` only
1563            // accepts one. A PPS with `transform_8x8_mode_flag` set therefore desyncs
1564            // the arithmetic decoder within a few macroblocks, and the failure surfaces
1565            // as a bogus `CABAC I_PCM` far from its cause (the mb_type parse lands on
1566            // 25 out of garbage). Fail fast and accurately instead: a wrong error that
1567            // points at the wrong feature costs more than a missing feature does.
1568            // Removing this guard requires the CABAC 8×8 residual path — see H-49.
1569            // DecSetup was declared in the Stage enum but never actually scoped, so
1570            // the per-picture grid allocation had been invisible in every profile.
1571            let _g_setup = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecSetup);
1572            let mut fd = FrameDecoder::with_pool(
1573                sps.pic_width_in_mbs,
1574                sps.pic_height_in_mbs,
1575                slice_qp,
1576                pps.chroma_qp_index_offset,
1577                ref_list0,
1578                num_ref_idx_l0,
1579                pps.constrained_intra_pred_flag,
1580                pps.transform_8x8_mode_flag,
1581                sps.profile_idc != 66, // b_possible: Baseline/Constrained Baseline (66) forbid B
1582                // `RS_H264_NO_POOL=1` reproduces the pre-pool behaviour exactly (a
1583                // fresh allocation per picture) so the pool can be A/B'd paired on
1584                // one binary, with no rebuild between arms.
1585                if no_pool() {
1586                    GridPool::default()
1587                } else {
1588                    core::mem::take(&mut self.grid_pool)
1589                },
1590            );
1591            if is_b {
1592                fd.set_b_context(
1593                    ref_list1,
1594                    num_ref_idx_l1,
1595                    direct_spatial,
1596                    pic_poc,
1597                    pps.weighted_bipred_idc,
1598                    sps.direct_8x8_inference,
1599                );
1600            }
1601            if sps.has_scaling || pps.pic_scaling_matrix_present {
1602                let (s4, s8) = resolve_scaling(sps, pps);
1603                fd.set_scaling(s4, s8);
1604            }
1605            fd.set_transform_bypass(sps.transform_bypass);
1606            if let Some(w) = weights {
1607                fd.set_weights(w);
1608            }
1609            if let Some(slot) = self.progress_slot.clone() {
1610                fd.set_progress_slot(slot);
1611            }
1612            // A pending picture still here means the previous one never reached
1613            // total_mb and a new picture is now displacing it. That is a DECODER
1614            // desync, and dropping it silently is how the missing-B-slice-ref_idx
1615            // defect stayed hidden: the picture simply never entered the DPB, and
1616            // the failure surfaced hundreds of macroblocks later as "bitstream
1617            // truncated" from a reference-list modification asking for it. Refuse
1618            // to swallow it -- an incomplete picture must announce itself.
1619            if let Some(prev) = self.cur.take() {
1620                if prev.next_mb < prev.total_mb {
1621                    return Err(DecodeError::Truncated);
1622                }
1623            }
1624            self.cur = Some(PendingPic {
1625                fd,
1626                frame_num,
1627                poc: pic_poc,
1628                next_mb: 0,
1629                route_bits: 0,
1630                route_cabac: pps.entropy_coding_mode_flag,
1631                route_t8x8: pps.transform_8x8_mode_flag,
1632                total_mb: sps.pic_width_in_mbs * sps.pic_height_in_mbs,
1633                slice_count: 0,
1634                deblock,
1635                filter_offset_a,
1636                filter_offset_b,
1637                crop_r: sps.frame_crop_right as usize,
1638                // into_frame crops in units of 2 luma rows; CropUnitY is 4 when field
1639                // coding is permitted.
1640                crop_b: sps.frame_crop_bottom as usize * sps.crop_unit_y() / 2,
1641                max_refs: sps.max_num_ref_frames.max(1) as usize,
1642                log2_max_frame_num: sps.log2_max_frame_num,
1643                is_reference: nal_ref_idc != 0,
1644                idr_long_term,
1645                mmco_ops,
1646            });
1647        } else {
1648            // Continuation slice: reset the per-slice QP + reference list.
1649            let Some(pic) = self.cur.as_mut() else {
1650                return Err(DecodeError::Unsupported(
1651                    "slice continues a missing picture",
1652                ));
1653            };
1654            pic.fd.begin_slice(slice_qp, ref_list0, num_ref_idx_l0);
1655            if is_b {
1656                pic.fd.set_b_context(
1657                    ref_list1,
1658                    num_ref_idx_l1,
1659                    direct_spatial,
1660                    pic.poc,
1661                    pps.weighted_bipred_idc,
1662                    sps.direct_8x8_inference,
1663                );
1664            }
1665            if sps.has_scaling || pps.pic_scaling_matrix_present {
1666                let (s4, s8) = resolve_scaling(sps, pps);
1667                pic.fd.set_scaling(s4, s8);
1668            }
1669            pic.fd.set_transform_bypass(sps.transform_bypass);
1670            if let Some(w) = weights {
1671                pic.fd.set_weights(w);
1672            }
1673            // Latest slice's marking/deblock parameters win at finalization.
1674            pic.deblock = deblock;
1675            pic.filter_offset_a = filter_offset_a;
1676            pic.filter_offset_b = filter_offset_b;
1677            pic.idr_long_term |= idr_long_term;
1678            pic.mmco_ops.extend(mmco_ops);
1679        }
1680
1681        let pic = self
1682            .cur
1683            .as_mut()
1684            .ok_or(DecodeError::Internal("no pending picture at slice decode"))?;
1685        // Row-interleave (mb16::row_hook) needs the CURRENT slice's deblock
1686        // parameters during decode; `abl_deblock` resolved here so mb16 stays
1687        // knob-agnostic.
1688        pic.fd.set_deblock_params(
1689            deblock && !abl_deblock(),
1690            filter_offset_a,
1691            filter_offset_b,
1692            deblock_idc2,
1693        );
1694        let first = first_mb_in_slice.min(pic.total_mb);
1695        pic.route_bits += r.data().len() as u64;
1696        let next = if cabac {
1697            // cabac_alignment_one_bit → the slice data is byte-aligned from here.
1698            r.align_to_byte().map_err(|_| DecodeError::Truncated)?;
1699            let (data, start) = (r.data(), r.bit_pos() / 8);
1700            pic.fd
1701                .decode_slice_data_cabac(data, start, slice_qp, cabac_init_idc, is_i, is_p, first)
1702        } else {
1703            pic.fd.decode_slice_data(&mut r, is_p, first)
1704        }
1705        .map_err(|e| match e {
1706            mb16::MbError::Truncated => DecodeError::Truncated,
1707            mb16::MbError::Unsupported(s) => DecodeError::Unsupported(s),
1708        })?;
1709        pic.next_mb = next;
1710        pic.slice_count += 1;
1711        if crate::mb16::dump_mb_on() {
1712            eprintln!(
1713                "  slice decoded {}/{} MBs{}",
1714                next,
1715                pic.total_mb,
1716                if next < pic.total_mb {
1717                    "   <-- INCOMPLETE"
1718                } else {
1719                    ""
1720                }
1721            );
1722        }
1723
1724        if pic.next_mb < pic.total_mb {
1725            return Ok(None); // picture not yet complete
1726        }
1727
1728        // --- finalize the completed picture: evaluate the GATE 1 route ---
1729        let pic = self
1730            .cur
1731            .take()
1732            .ok_or(DecodeError::Internal("no pending picture at finalize"))?;
1733        // SURFACE ANY INTERNAL FAULT rather than returning the picture as if it
1734        // were right. A dead EDC worker means some rows were never deblocked, so
1735        // the pixels are wrong; the old code panicked at the send instead. This
1736        // is the one place that knows the picture is finished, so it is where
1737        // the fault becomes a typed error.
1738        if let Some(what) = pic.fd.fault() {
1739            return Err(DecodeError::Internal(what));
1740        }
1741        {
1742            let (skips, coded) = pic.fd.route_counters();
1743            let mbs = pic.total_mb.max(1) as f64;
1744            let bits_per_mb = pic.route_bits as f64 * 8.0 / mbs;
1745            let (skip_frac, coded_frac) = (skips as f64 / mbs, coded as f64 / mbs);
1746            let (eb, es, ec) = match self.route_ema {
1747                None => (bits_per_mb, skip_frac, coded_frac),
1748                Some((pb, ps, pc)) => (
1749                    pb + (bits_per_mb - pb) / 8.0,
1750                    ps + (skip_frac - ps) / 8.0,
1751                    pc + (coded_frac - pc) / 8.0,
1752                ),
1753            };
1754            self.route_ema = Some((eb, es, ec));
1755            let route = route_for(pic.route_cabac, pic.route_t8x8, eb, es, ec);
1756            self.last_route = Some(route);
1757            // Same treatment as RH264_DUMP_MB: a bare knob() here is env::var plus a
1758            // String allocation on EVERY picture, and it kept the eprintln! formatting
1759            // machinery compiled in for a dump nobody enables.
1760            if crate::mb16::route_dump_on() {
1761                eprintln!(
1762                    "ROUTE cabac={} t8x8={} bits_per_mb={bits_per_mb:.2} skip_frac={skip_frac:.4} coded_frac={coded_frac:.4} ema=({eb:.2},{es:.4},{ec:.4}) -> {route:?}",
1763                    pic.route_cabac, pic.route_t8x8
1764                );
1765            }
1766        }
1767        let PendingPic {
1768            mut fd,
1769            frame_num,
1770            poc,
1771            deblock,
1772            filter_offset_a,
1773            filter_offset_b,
1774            crop_r,
1775            crop_b,
1776            max_refs,
1777            log2_max_frame_num,
1778            is_reference,
1779            idr_long_term,
1780            mut mmco_ops,
1781            ..
1782        } = pic;
1783        self.last_poc = poc;
1784        // MEASUREMENT KNOB (`RFF_ABL_DEBLOCK=1`): skip the loop filter to price it
1785        // with ZERO instrument tax. The scope-based profiler charges an rdtsc pair
1786        // per scope, and at ~20M per-MB scopes that tax reached 1.3-1.4x of the
1787        // whole decode -- so a per-MB stage's share cannot be read off it. Ablation
1788        // on the UNINSTRUMENTED binary is the honest price. Output is wrong while
1789        // set; decode WORK is unchanged (the filter reads and writes samples but
1790        // decides nothing), so the timing stays comparable.
1791        if deblock && !abl_deblock() {
1792            fd.deblock(filter_offset_a, filter_offset_b);
1793        } else {
1794            fd.flush_deferred();
1795        }
1796        // The necessary DPB plane clone (rec_y/u/v → RefFrame) — measured as its own
1797        // stage, OUTSIDE the Finalize scope so the two don't double-count.
1798        let reference = if is_reference {
1799            let _dg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DpbClone);
1800            Some(fd.as_reference_pooled(
1801                &mut self.plane_pool,
1802                &mut self.mv_pool,
1803                &mut self.ref_pool,
1804            ))
1805        } else {
1806            None
1807        };
1808        let _fg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Finalize);
1809        if let Some(mut reference) = reference {
1810            reference.frame_num = frame_num;
1811            reference.poc = poc;
1812            if crate::mb16::dump_mb_on() {
1813                eprintln!("DPB-ADD fn={frame_num} poc={poc}");
1814            }
1815            if idr_long_term {
1816                reference.long_term = true;
1817                reference.long_term_idx = 0;
1818            }
1819            let reference = reference; // RefFrame from as_reference_pooled
1820            if self.detach_dpb {
1821                self.detached_mmco = core::mem::take(&mut mmco_ops);
1822                self.detached_frame_num = frame_num;
1823                self.detached_log2_max_frame_num = log2_max_frame_num;
1824                self.detached_max_refs = max_refs;
1825                self.detached_idr_long_term = idr_long_term;
1826                if let Some(slot) = self.progress_slot.take() {
1827                    // Phase B: fold finished planes into the pre-shared Arc.
1828                    Self::fill_progress_slot(&slot, reference);
1829                    slot.mark_fully_ready();
1830                    self.detached_ref = Some(slot);
1831                } else {
1832                    let arc = crate::sync::Arc::new(reference);
1833                    arc.mark_fully_ready();
1834                    self.detached_ref = Some(arc);
1835                }
1836            } else {
1837                reference.mark_fully_ready();
1838                self.prev_ref_frame_num = self.apply_ref_marking(
1839                    reference,
1840                    &mmco_ops,
1841                    frame_num,
1842                    log2_max_frame_num,
1843                    max_refs,
1844                );
1845            }
1846        }
1847        self.sc_mmco = mmco_ops; // scratch back (capacity survives)
1848        let (frame, pool) = fd.into_frame_recycle(crop_r, crop_b);
1849        self.grid_pool = pool;
1850        self.reclaim_retired();
1851        Ok(Some(frame))
1852    }
1853
1854    /// Frame-MT: commit an already-shared reference Arc (Phase B progress slot).
1855    /// Applies the detached picture's reference + MMCO onto `self.refs` and
1856    /// returns the updated `prev_ref_frame_num`.
1857    pub(crate) fn commit_detached_ref_arc(&mut self, reference: Ref) -> Result<u32, DecodeError> {
1858        reference.mark_fully_ready();
1859        let mmco = core::mem::take(&mut self.detached_mmco);
1860        let frame_num = self.detached_frame_num;
1861        let log2 = self.detached_log2_max_frame_num;
1862        let max_refs = self.detached_max_refs;
1863        self.prev_ref_frame_num =
1864            self.apply_ref_marking_arc(reference, &mmco, frame_num, log2, max_refs);
1865        Ok(self.prev_ref_frame_num)
1866    }
1867
1868    /// Fold a finished `as_reference_pooled` snapshot into a Phase B progress Arc:
1869    /// lock-free frozen planes + coloc meta (no steady-state RwLock on DPB MC).
1870    fn fill_progress_slot(slot: &Ref, mut finished: crate::RefFrame) {
1871        let planes = FrozenPlanes {
1872            py: core::mem::take(&mut finished.py),
1873            pu: core::mem::take(&mut finished.pu),
1874            pv: core::mem::take(&mut finished.pv),
1875        };
1876        if let Some(live) = &slot.live {
1877            let _w = live.wait.lock_pf();
1878            {
1879                let mut m = live.meta.write_pf();
1880                m.frame_num = finished.frame_num;
1881                m.poc = finished.poc;
1882                m.long_term = finished.long_term;
1883                m.long_term_idx = finished.long_term_idx;
1884                m.mv = core::mem::take(&mut finished.mv);
1885                m.ref_idx = core::mem::take(&mut finished.ref_idx);
1886                m.mv1 = core::mem::take(&mut finished.mv1);
1887                m.ref_idx1 = core::mem::take(&mut finished.ref_idx1);
1888                m.poc_lut = finished.poc_lut;
1889                m.w4 = finished.w4;
1890                m.motion_ready = true;
1891            }
1892            let _ = slot.frozen.set(planes);
1893            slot.ready_rows
1894                .store(slot.ch, core::sync::atomic::Ordering::Release);
1895            live.cv.notify_all();
1896        } else {
1897            let _ = slot.frozen.set(planes);
1898            slot.mark_fully_ready();
1899        }
1900    }
1901
1902    /// `Vec::retain` over the DPB, except a reference removed by adaptive
1903    /// (MMCO) marking is PARKED in `retired` instead of dropped, so
1904    /// `reclaim_retired` can recycle its padded planes.
1905    ///
1906    /// WHY THIS EXISTS. `plane_pool` was fed by exactly one site -- the
1907    /// sliding-window eviction -- while the MMCO arm used plain `retain`, which
1908    /// drops the `Arc` and frees the planes. On any stream that marks
1909    /// adaptively (x264 does, for multi-ref and B-pyramid) the pool was
1910    /// therefore never fed, and `as_reference_pooled` missed on every single
1911    /// lookup: a measured 10,080 of 10,080. Each miss is a padded plane that
1912    /// `pad_plane_into` resizes -- so the allocator zeroes it -- and then
1913    /// overwrites in full, which makes the zeroing pure waste.
1914    ///
1915    /// The two arms disagreeing was invisible to every gate: recycling cannot
1916    /// change decoded output, so byte-identity says nothing either way, and the
1917    /// pool degrades silently to "always allocate" rather than failing.
1918    fn retire_retain(&mut self, keep: impl Fn(&Ref) -> bool) {
1919        let mut i = 0;
1920        while i < self.refs.len() {
1921            if keep(&self.refs[i]) {
1922                i += 1;
1923            } else {
1924                let dropped = self.refs.remove(i);
1925                self.retired.push(dropped);
1926            }
1927        }
1928    }
1929
1930    /// Moves the padded planes of retired (DPB-evicted) reference frames into
1931    /// the recycle pool. Called after the current picture's `FrameDecoder` is
1932    /// consumed, at which point a retired frame's `Arc` is normally unique; a
1933    /// frame something still holds (it shouldn't) is simply dropped un-recycled.
1934    fn reclaim_retired(&mut self) {
1935        for arc in self.retired.drain(..) {
1936            if let Ok(mut rf) = crate::sync::Arc::try_unwrap(arc) {
1937                if let Some(f) = rf.frozen.take() {
1938                    if !f.py.is_empty() {
1939                        self.plane_pool.push(f.py);
1940                        self.plane_pool.push(f.pu);
1941                        self.plane_pool.push(f.pv);
1942                    }
1943                } else if !rf.py.is_empty() {
1944                    self.plane_pool.push(rf.py);
1945                    self.plane_pool.push(rf.pu);
1946                    self.plane_pool.push(rf.pv);
1947                }
1948                // The motion snapshot retires with the frame. Empty grids (a
1949                // Baseline stream, where `b_possible` gates them off) are not
1950                // worth pooling -- every take would only scan past them.
1951                for v in [core::mem::take(&mut rf.mv), core::mem::take(&mut rf.mv1)] {
1952                    if !v.is_empty() {
1953                        self.mv_pool.push(v);
1954                    }
1955                }
1956                for v in [
1957                    core::mem::take(&mut rf.ref_idx),
1958                    core::mem::take(&mut rf.ref_idx1),
1959                ] {
1960                    if !v.is_empty() {
1961                        self.ref_pool.push(v);
1962                    }
1963                }
1964            }
1965        }
1966        // Bound the pool: 6 pictures' worth of planes (3 each) covers any
1967        // realistic ref churn; beyond that we'd just be hoarding memory.
1968        self.mv_pool.truncate(12);
1969        self.ref_pool.truncate(12);
1970        self.plane_pool.truncate(18);
1971    }
1972
1973    /// Inserts "non-existing" short-term reference frames for each `frame_num`
1974    /// skipped since the previous reference picture (spec §8.2.5.2). Their samples
1975    /// are unspecified (a conformant stream never references them); we use mid-grey
1976    /// so any accidental reference is benign. They occupy DPB slots and advance the
1977    /// sliding window, keeping PicNum/ref-list derivation correct.
1978    fn insert_frame_num_gaps(
1979        &mut self,
1980        frame_num: u32,
1981        max_fn: u32,
1982        max_refs: usize,
1983        w: usize,
1984        h: usize,
1985    ) {
1986        if max_fn == 0 {
1987            return;
1988        }
1989        let start = (self.prev_ref_frame_num + 1) % max_fn;
1990        let gap = (frame_num + max_fn - start) % max_fn;
1991        if gap == 0 {
1992            return;
1993        }
1994        // Each placeholder is inserted at the front then the DPB is truncated to
1995        // `max_refs`, so for a gap larger than that only the most recent `max_refs`
1996        // placeholders can survive. Materialise just those — a malformed stream can
1997        // declare a gap of MaxFrameNum-1 (up to 65535), and allocating that many
1998        // full frames would be a CPU/memory DoS.
1999        let cap = max_refs.max(1);
2000        let n = (gap as usize).min(cap);
2001        let (cw, ch) = (w, h);
2002        let mut expected = (frame_num + max_fn - n as u32) % max_fn;
2003        for _ in 0..n {
2004            self.refs.insert(
2005                0,
2006                crate::sync::Arc::new(RefFrame {
2007                    // Uniform grey: the padded plane of a uniform plane is itself.
2008                    py: vec![128; (cw + 2 * LPAD) * (ch + 2 * LPAD)],
2009                    pu: vec![128; (cw / 2 + 2 * CPAD) * (ch / 2 + 2 * CPAD)],
2010                    pv: vec![128; (cw / 2 + 2 * CPAD) * (ch / 2 + 2 * CPAD)],
2011                    cw,
2012                    ch,
2013                    ready_rows: core::sync::atomic::AtomicUsize::new(ch),
2014                    live: None,
2015                    frozen: crate::sync::Once::new(),
2016                    frame_num: expected,
2017                    poc: 0,
2018                    mv: Vec::new(),
2019                    ref_idx: Vec::new(),
2020                    mv1: Vec::new(),
2021                    ref_idx1: Vec::new(),
2022                    poc_lut: [i32::MIN; 32],
2023                    w4: 0,
2024                    long_term: false,
2025                    long_term_idx: 0,
2026                }),
2027            );
2028            if self.refs.len() > cap {
2029                // Gap-frame insertion can push a REAL reference off the tail;
2030                // park it so its planes are recycled. The synthesised
2031                // "non-existing" frames carry empty planes and 
2032                // skips those, so this cannot pollute the pool.
2033                self.retired.extend(self.refs.drain(cap..));
2034            }
2035            expected = (expected + 1) % max_fn;
2036        }
2037        self.prev_ref_frame_num = (frame_num + max_fn - 1) % max_fn;
2038    }
2039
2040    /// The `PicOrderCnt` of the most recently returned picture. Pictures are
2041    /// returned in decode order; sorting them by this value yields display order
2042    /// (the only difference is reordered B-pictures).
2043    /// GATE 1 content route of the last completed picture — the 4-way cost
2044    /// tier from big-oppy-decoder §2. Trailing signal: consumers apply it to
2045    /// the NEXT picture. `None` until the first picture completes.
2046    pub fn content_route(&self) -> Option<ContentRoute> {
2047        self.last_route
2048    }
2049
2050    pub fn last_poc(&self) -> i32 {
2051        self.last_poc
2052    }
2053
2054    fn compute_poc(
2055        &mut self,
2056        sps: &Sps,
2057        is_idr: bool,
2058        nal_ref_idc: u8,
2059        frame_num: u32,
2060        poc_lsb: u32,
2061        delta_bottom: i32,
2062    ) -> i32 {
2063        self.poc
2064            .compute_poc(sps, is_idr, nal_ref_idc, frame_num, poc_lsb, delta_bottom)
2065    }
2066
2067    /// Inserts the just-decoded picture into the DPB and marks references
2068    /// (spec §8.2.5). With no MMCO commands this is the sliding window (evict the
2069    /// oldest short-term reference past capacity); with MMCO it is adaptive
2070    /// marking, including long-term assignment.
2071    ///
2072    /// Takes `reference` BY VALUE and MOVES it into the DPB (the caller's local is
2073    /// dropped right after) — the old `&mut` + `insert(0, reference.clone())` cloned
2074    /// all three planes (~1.35 MB/frame) a second time, on top of `as_reference`'s
2075    /// necessary clone. Returns the picture's final `frame_num` (0 after MMCO 5) for
2076    /// the caller's gap-detection tracking, since `reference` is gone after the move.
2077    fn apply_ref_marking(
2078        &mut self,
2079        mut reference: RefFrame,
2080        ops: &[Mmco],
2081        frame_num: u32,
2082        log2_max_frame_num: u32,
2083        max_refs: usize,
2084    ) -> u32 {
2085        let max = 1i64 << log2_max_frame_num;
2086        let curr = frame_num as i64;
2087        let pic_num = |rf: &RefFrame| -> i64 {
2088            if (rf.frame_num as i64) > curr {
2089                rf.frame_num as i64 - max
2090            } else {
2091                rf.frame_num as i64
2092            }
2093        };
2094
2095        if ops.is_empty() {
2096            // Sliding window: insert the current (short-term) picture, then evict
2097            // the oldest short-term reference while over capacity (long-term refs
2098            // are retained).
2099            let out_fn = reference.frame_num;
2100            self.refs.insert(0, crate::sync::Arc::new(reference));
2101            while self.refs.len() > max_refs {
2102                match self.refs.iter().rposition(|r| !r.long_term) {
2103                    Some(pos) => {
2104                        // Park the evicted frame; its planes are reclaimed on the
2105                        // next picture boundary (see `reclaim_retired`).
2106                        let evicted = self.refs.remove(pos);
2107                        self.retired.push(evicted);
2108                    }
2109                    None => break,
2110                }
2111            }
2112            return out_fn;
2113        }
2114
2115        // Adaptive marking (MMCO), applied in order.
2116        for &op in ops {
2117            match op {
2118                Mmco::Unref(diff) => {
2119                    let target = curr - (diff as i64 + 1);
2120                    self.retire_retain(|r| r.long_term || pic_num(r) != target);
2121                }
2122                Mmco::UnrefLong(ltpn) => {
2123                    self.retire_retain(|r| !(r.long_term && r.long_term_idx == ltpn));
2124                }
2125                Mmco::AssignLong(diff, idx) => {
2126                    let target = curr - (diff as i64 + 1);
2127                    self.retire_retain(|r| !(r.long_term && r.long_term_idx == idx));
2128                    for r in self.refs.iter_mut() {
2129                        if !r.long_term && pic_num(r) == target {
2130                            // Rare op; make_mut only copies if a slice still holds it.
2131                            let r = crate::sync::Arc::make_mut(r);
2132                            r.long_term = true;
2133                            r.long_term_idx = idx;
2134                        }
2135                    }
2136                }
2137                Mmco::MaxLong(max_plus1) => {
2138                    self.retire_retain(|r| !(r.long_term && r.long_term_idx + 1 > max_plus1));
2139                }
2140                Mmco::Reset => {
2141                    self.retired.append(&mut self.refs);
2142                    reference.frame_num = 0;
2143                }
2144                Mmco::CurrentLong(idx) => {
2145                    self.retire_retain(|r| !(r.long_term && r.long_term_idx == idx));
2146                    reference.long_term = true;
2147                    reference.long_term_idx = idx;
2148                }
2149            }
2150        }
2151        let out_fn = reference.frame_num;
2152        self.refs.insert(0, crate::sync::Arc::new(reference));
2153        // Safety net so a malformed marking stream can't grow the DPB unbounded.
2154        let cap = max_refs.max(16);
2155        if self.refs.len() > cap {
2156            self.retired.extend(self.refs.drain(cap..));
2157        }
2158        out_fn
2159    }
2160
2161    /// Like [`Self::apply_ref_marking`] but inserts an existing `Arc` (Phase B
2162    /// progress slot / detached worker output).
2163    fn apply_ref_marking_arc(
2164        &mut self,
2165        mut reference: Ref,
2166        ops: &[Mmco],
2167        frame_num: u32,
2168        log2_max_frame_num: u32,
2169        max_refs: usize,
2170    ) -> u32 {
2171        let max = 1i64 << log2_max_frame_num;
2172        let curr = frame_num as i64;
2173        let pic_num = |rf: &RefFrame| -> i64 {
2174            let f = rf.fn_num() as i64;
2175            if f > curr {
2176                f - max
2177            } else {
2178                f
2179            }
2180        };
2181
2182        if ops.is_empty() {
2183            let out_fn = reference.fn_num();
2184            self.refs.insert(0, reference);
2185            while self.refs.len() > max_refs {
2186                match self.refs.iter().rposition(|r| !r.is_long_term()) {
2187                    Some(pos) => {
2188                        let evicted = self.refs.remove(pos);
2189                        self.retired.push(evicted);
2190                    }
2191                    None => break,
2192                }
2193            }
2194            return out_fn;
2195        }
2196
2197        for &op in ops {
2198            match op {
2199                Mmco::Unref(diff) => {
2200                    let target = curr - (diff as i64 + 1);
2201                    self.refs
2202                        .retain(|r| r.is_long_term() || pic_num(r) != target);
2203                }
2204                Mmco::UnrefLong(ltpn) => {
2205                    self.retire_retain(|r| !(r.is_long_term() && r.lt_idx() == ltpn));
2206                }
2207                Mmco::AssignLong(diff, idx) => {
2208                    let target = curr - (diff as i64 + 1);
2209                    self.retire_retain(|r| !(r.is_long_term() && r.lt_idx() == idx));
2210                    for r in self.refs.iter_mut() {
2211                        if !r.is_long_term() && pic_num(r) == target {
2212                            if r.live.is_some() {
2213                                r.set_long_term_marks(true, idx);
2214                            } else {
2215                                let r = crate::sync::Arc::make_mut(r);
2216                                r.long_term = true;
2217                                r.long_term_idx = idx;
2218                            }
2219                        }
2220                    }
2221                }
2222                Mmco::MaxLong(max_plus1) => {
2223                    self.retire_retain(|r| !(r.is_long_term() && r.lt_idx() + 1 > max_plus1));
2224                }
2225                Mmco::Reset => {
2226                    self.retired.append(&mut self.refs);
2227                    reference.set_frame_num_live(0);
2228                    if let Some(r) = crate::sync::Arc::get_mut(&mut reference) {
2229                        r.frame_num = 0;
2230                    }
2231                }
2232                Mmco::CurrentLong(idx) => {
2233                    self.retire_retain(|r| !(r.is_long_term() && r.lt_idx() == idx));
2234                    reference.set_long_term_marks(true, idx);
2235                    if let Some(r) = crate::sync::Arc::get_mut(&mut reference) {
2236                        r.long_term = true;
2237                        r.long_term_idx = idx;
2238                    }
2239                }
2240            }
2241        }
2242        let out_fn = reference.fn_num();
2243        self.refs.insert(0, reference);
2244        let cap = max_refs.max(16);
2245        if self.refs.len() > cap {
2246            self.retired.extend(self.refs.drain(cap..));
2247        }
2248        out_fn
2249    }
2250}
2251
2252/// Emits a GOP's buffered pictures in display order (sorted by `PicOrderCnt`).
2253pub(crate) fn flush_gop(gop: &mut Vec<(i32, YuvFrame)>, out: &mut Vec<YuvFrame>) {
2254    gop.sort_by_key(|(poc, _)| *poc);
2255    out.extend(gop.drain(..).map(|(_, f)| f));
2256}
2257
2258/// Whether an access unit contains an IDR coded-slice NAL.
2259///
2260/// Public for harnesses that reimplement `decode_stream`'s display-order emit
2261/// with an early stop (e.g. correctness probes that only need the first N pictures).
2262pub fn au_is_idr(au: &[u8]) -> bool {
2263    split_annex_b(au)
2264        .iter()
2265        .any(|n| !n.is_empty() && NalUnitType::from_id(n[0]) == NalUnitType::IdrSlice)
2266}
2267
2268/// Splits an Annex-B byte stream into access units, each ending after a VCL
2269/// (coded-slice) NAL with any preceding parameter-set/SEI NALs attached. Start
2270/// codes are preserved so each unit can be passed straight to [`Decoder::decode`].
2271///
2272/// Public because [`Decoder::decode`] takes ONE access unit: a caller that wants
2273/// decode-order pictures, or wants to drop each picture as it arrives instead of
2274/// accumulating the stream like [`Decoder::decode_stream`], needs this to feed it.
2275pub fn split_access_units(stream: &[u8]) -> Vec<&[u8]> {
2276    // (offset of the start code, whether the NAL it begins is a VCL slice).
2277    let mut codes: Vec<(usize, bool)> = Vec::new();
2278    let mut i = 0;
2279    // `get(i..i + 3)` rather than `i + 3 <= len` plus three whole-buffer
2280    // indexes: the loop condition bounds `i` but LLVM re-checks each of the
2281    // three reads anyway, and this runs once per BYTE of the bitstream.
2282    while let Some(w) = stream.get(i..i + 3) {
2283        if w[0] == 0 && w[1] == 0 && w[2] == 1 {
2284            let nal_type = NalUnitType::from_id(stream.get(i + 3).copied().unwrap_or(0));
2285            let is_vcl = matches!(nal_type, NalUnitType::IdrSlice | NalUnitType::NonIdrSlice);
2286            // Include a leading zero (4-byte start code) in the unit boundary.
2287            let sc = if i > 0 && stream.get(i - 1) == Some(&0) {
2288                i - 1
2289            } else {
2290                i
2291            };
2292            codes.push((sc, is_vcl));
2293            i += 3;
2294        } else {
2295            i += 1;
2296        }
2297    }
2298    if codes.is_empty() {
2299        return vec![stream];
2300    }
2301    let mut aus = Vec::new();
2302    let mut start = codes[0].0;
2303    for k in 0..codes.len() {
2304        if codes[k].1 {
2305            let end = codes.get(k + 1).map_or(stream.len(), |c| c.0);
2306            aus.push(&stream[start..end]);
2307            start = end;
2308        }
2309    }
2310    aus
2311}
2312
2313/// Parses a `pred_weight_table()` (spec §7.3.3.2) for the active reference lists
2314/// (4:2:0 → chroma weights always present). List 1 is parsed only for B slices.
2315fn parse_pred_weight_table(
2316    r: &mut BitReader,
2317    num_l0: usize,
2318    num_l1: usize,
2319    is_b: bool,
2320) -> Result<WeightTable, DecodeError> {
2321    let luma_log2_denom = r.read_ue()? as i32;
2322    let chroma_log2_denom = r.read_ue()? as i32;
2323    // Spec §7.4.3.2 constrains both weight denoms to [0, 7]; a malformed stream can
2324    // carry any ue(v). Reject before `1 << denom` (which overflows for denom ≥ 31)
2325    // so a corrupt bitstream is rejected gracefully, never panics.
2326    if !(0..=7).contains(&luma_log2_denom) || !(0..=7).contains(&chroma_log2_denom) {
2327        return Err(DecodeError::Unsupported("invalid weight denom"));
2328    }
2329    let mut wt = WeightTable {
2330        luma_log2_denom,
2331        chroma_log2_denom,
2332        ..Default::default()
2333    };
2334    let lists: &[(usize, usize)] = if is_b {
2335        &[(0, num_l0), (1, num_l1)]
2336    } else {
2337        &[(0, num_l0)]
2338    };
2339    for &(list, n) in lists {
2340        let mut luma = Vec::with_capacity(n);
2341        let mut chroma = Vec::with_capacity(n);
2342        for _ in 0..n {
2343            let (mut lw, mut lo) = (1 << luma_log2_denom, 0);
2344            if r.read_bit()? {
2345                lw = r.read_se()?;
2346                lo = r.read_se()?;
2347            }
2348            luma.push((lw, lo));
2349            let mut ch = [(1 << chroma_log2_denom, 0); 2];
2350            if r.read_bit()? {
2351                for slot in ch.iter_mut() {
2352                    *slot = (r.read_se()?, r.read_se()?);
2353                }
2354            }
2355            chroma.push(ch);
2356        }
2357        wt.luma[list & 1] = luma;
2358        wt.chroma[list & 1] = chroma;
2359    }
2360    Ok(wt)
2361}
2362
2363/// Resolves the effective scaling matrices for a slice from the SPS lists and
2364/// any PPS override (fall-back rule B), returning them un-zig-zagged to raster
2365/// order: six 4×4 lists [Y/Cb/Cr intra, Y/Cb/Cr inter] and two 8×8 luma lists
2366/// [Y-intra, Y-inter].
2367fn resolve_scaling(sps: &Sps, pps: &Pps) -> ([[i32; 16]; 6], [[i32; 64]; 2]) {
2368    use crate::params::{
2369        DEFAULT_4X4_INTER, DEFAULT_4X4_INTRA, DEFAULT_8X8_INTER, DEFAULT_8X8_INTRA,
2370    };
2371    const ZZ4: [usize; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15];
2372    // 8×8 frame zig-zag scan → raster index (spec Table 8-12).
2373    const ZZ8: [usize; 64] = [
2374        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,
2375        20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
2376        58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
2377    ];
2378    // Effective zig-zag lists: a PPS override (rule B) takes precedence; an absent
2379    // PPS list falls back to the SPS list (or the default / previous PPS list).
2380    let mut z4 = [[16u8; 16]; 6];
2381    for i in 0..6 {
2382        z4[i] = if pps.pic_scaling_matrix_present {
2383            if pps.scaling_present_4x4[i] {
2384                pps.scaling_4x4[i]
2385            } else {
2386                match i {
2387                    0 if sps.has_scaling => sps.scaling_4x4[0],
2388                    0 => DEFAULT_4X4_INTRA,
2389                    3 if sps.has_scaling => sps.scaling_4x4[3],
2390                    3 => DEFAULT_4X4_INTER,
2391                    _ => z4[i - 1],
2392                }
2393            }
2394        } else {
2395            sps.scaling_4x4[i]
2396        };
2397    }
2398    let mut z8 = [[16u8; 64]; 2];
2399    for (i, list) in z8.iter_mut().enumerate() {
2400        *list = if pps.pic_scaling_matrix_present {
2401            if pps.scaling_present_8x8[i] {
2402                pps.scaling_8x8[i]
2403            } else if sps.has_scaling {
2404                sps.scaling_8x8[i]
2405            } else if i == 0 {
2406                DEFAULT_8X8_INTRA
2407            } else {
2408                DEFAULT_8X8_INTER
2409            }
2410        } else {
2411            sps.scaling_8x8[i]
2412        };
2413    }
2414    let mut out4 = [[16i32; 16]; 6];
2415    for (li, list) in out4.iter_mut().enumerate() {
2416        for k in 0..16 {
2417            list[ZZ4[k]] = z4[li][k] as i32;
2418        }
2419    }
2420    let mut out8 = [[16i32; 64]; 2];
2421    for (li, list) in out8.iter_mut().enumerate() {
2422        for k in 0..64 {
2423            list[ZZ8[k]] = z8[li][k] as i32;
2424        }
2425    }
2426    (out4, out8)
2427}
2428
2429/// Parses a `ref_pic_list_modification` command list (spec §7.3.3.1) into
2430/// `(modification_of_pic_nums_idc, value)` pairs, stopping at idc 3.
2431fn parse_ref_pic_list_modification(
2432    r: &mut BitReader,
2433    out: &mut Vec<(u32, u32)>,
2434) -> Result<(), DecodeError> {
2435    loop {
2436        let idc = r.read_ue()?;
2437        if idc == 3 {
2438            break;
2439        }
2440        if idc > 3 {
2441            return Err(DecodeError::Unsupported(
2442                "invalid ref_pic_list_modification",
2443            ));
2444        }
2445        let val = r.read_ue()?; // abs_diff_pic_num_minus1 / long_term_pic_num
2446        out.push((idc, val));
2447        if out.len() > 64 {
2448            return Err(DecodeError::Truncated); // runaway / corrupt
2449        }
2450    }
2451    Ok(())
2452}
2453
2454/// Builds the P-slice `RefPicList0`: short-term references ordered by descending
2455/// `FrameNumWrap`, then long-term by ascending idx (spec §8.2.4.2.1), with any
2456/// `ref_pic_list_modification` applied.
2457fn build_ref_list_p(
2458    dpb: &[Ref],
2459    curr_frame_num: u32,
2460    max_frame_num: u32,
2461    num_active: usize,
2462    mods: &[(u32, u32)],
2463) -> Result<Vec<Ref>, DecodeError> {
2464    let curr = curr_frame_num as i64;
2465    let max = max_frame_num as i64;
2466    let pic_num = |fnum: u32| -> i64 {
2467        let f = fnum as i64;
2468        if f > curr {
2469            f - max
2470        } else {
2471            f
2472        }
2473    };
2474    let mut init: Vec<Ref> = dpb.iter().filter(|r| !r.is_long_term()).cloned().collect();
2475    init.sort_by_key(|rf| core::cmp::Reverse(pic_num(rf.fn_num())));
2476    let mut long: Vec<Ref> = dpb.iter().filter(|r| r.is_long_term()).cloned().collect();
2477    long.sort_by_key(|rf| rf.lt_idx());
2478    init.extend(long);
2479    apply_list_modification(init, curr_frame_num, max_frame_num, num_active, mods)
2480}
2481
2482/// Builds the B-slice `RefPicList0` and `RefPicList1` (spec §8.2.4.2.3), ordered
2483/// by `PicOrderCnt` relative to the current picture: List0 leads with nearer
2484/// past pictures, List1 with nearer future pictures. Long-term references follow.
2485/// Per-list `ref_pic_list_modification` is then applied.
2486fn build_ref_list_b(
2487    dpb: &[Ref],
2488    curr_poc: i32,
2489    curr_frame_num: u32,
2490    max_frame_num: u32,
2491    num0: usize,
2492    num1: usize,
2493    mods0: &[(u32, u32)],
2494    mods1: &[(u32, u32)],
2495) -> Result<(Vec<Ref>, Vec<Ref>), DecodeError> {
2496    let mut less: Vec<Ref> = dpb
2497        .iter()
2498        .filter(|r| !r.is_long_term() && r.pic_poc() < curr_poc)
2499        .cloned()
2500        .collect();
2501    let mut greater: Vec<Ref> = dpb
2502        .iter()
2503        .filter(|r| !r.is_long_term() && r.pic_poc() > curr_poc)
2504        .cloned()
2505        .collect();
2506    let mut long: Vec<Ref> = dpb.iter().filter(|r| r.is_long_term()).cloned().collect();
2507    less.sort_by_key(|r| core::cmp::Reverse(r.pic_poc())); // nearest past first
2508    greater.sort_by_key(|r| r.pic_poc()); // nearest future first
2509    long.sort_by_key(|r| r.lt_idx());
2510
2511    let mut init0 = less.clone();
2512    init0.extend(greater.clone());
2513    init0.extend(long.clone());
2514    let mut init1 = greater;
2515    init1.extend(less);
2516    init1.extend(long);
2517
2518    // When List1 (truncated to its active length) equals List0 and has more than
2519    // one entry, swap its first two entries (spec §8.2.4.2.3).
2520    let eq_len = num0.min(num1).min(init0.len()).min(init1.len());
2521    if num1 > 1
2522        && init1.len() > 1
2523        && (0..eq_len).all(|i| same_picture(&init0[i], &init1[i]))
2524        && eq_len == num1.min(init1.len())
2525        && eq_len == num0.min(init0.len())
2526    {
2527        // Slice pattern: `swap(0, 1)` checks both indexes even under the
2528        // `len() > 1` guard above; destructuring proves them.
2529        if let [a, b, ..] = &mut init1[..] {
2530            core::mem::swap(a, b);
2531        }
2532    }
2533
2534    let list0 = apply_list_modification(init0, curr_frame_num, max_frame_num, num0, mods0)?;
2535    let list1 = apply_list_modification(init1, curr_frame_num, max_frame_num, num1, mods1)?;
2536    Ok((list0, list1))
2537}
2538
2539/// Two DPB entries refer to the same picture (used for the List1 swap rule).
2540fn same_picture(a: &RefFrame, b: &RefFrame) -> bool {
2541    a.is_long_term() == b.is_long_term()
2542        && if a.is_long_term() {
2543            a.lt_idx() == b.lt_idx()
2544        } else {
2545            a.pic_poc() == b.pic_poc()
2546        }
2547}
2548
2549/// Applies `ref_pic_list_modification` to an initialized reference list and
2550/// truncates it to `num_active` (spec §8.2.4.3). `init` is the full ordered list;
2551/// the result is `num_active` entries, possibly reordered. idc 0/1 reference
2552/// short-term pictures by PicNum, idc 2 long-term ones by LongTermFrameIdx.
2553fn apply_list_modification(
2554    init: Vec<Ref>,
2555    curr_frame_num: u32,
2556    max_frame_num: u32,
2557    num_active: usize,
2558    mods: &[(u32, u32)],
2559) -> Result<Vec<Ref>, DecodeError> {
2560    if mods.is_empty() {
2561        let mut init = init;
2562        init.truncate(num_active.max(1));
2563        return Ok(init);
2564    }
2565    let curr = curr_frame_num as i64;
2566    let max = max_frame_num as i64;
2567    let mut list = init.clone();
2568    let mut pic_num_pred = curr;
2569    let mut refidx = 0usize;
2570    for &(idc, val) in mods {
2571        let matches: Box<dyn Fn(&RefFrame) -> bool> = if idc == 2 {
2572            Box::new(move |r: &RefFrame| r.is_long_term() && r.lt_idx() == val)
2573        } else {
2574            let abs_diff = (val as i64) + 1;
2575            let no_wrap = if idc == 0 {
2576                let x = pic_num_pred - abs_diff;
2577                if x < 0 {
2578                    x + max
2579                } else {
2580                    x
2581                }
2582            } else {
2583                let x = pic_num_pred + abs_diff;
2584                if x >= max {
2585                    x - max
2586                } else {
2587                    x
2588                }
2589            };
2590            pic_num_pred = no_wrap;
2591            let target = if no_wrap > curr {
2592                no_wrap - max
2593            } else {
2594                no_wrap
2595            };
2596            Box::new(move |r: &RefFrame| {
2597                let f = r.fn_num() as i64;
2598                let pn = if f > curr { f - max } else { f };
2599                !r.is_long_term() && pn == target
2600            })
2601        };
2602        let found = init.iter().find(|r| matches(r)).cloned();
2603        let Some(found) = found else {
2604            if crate::mb16::dump_mb_on() {
2605                let cand: Vec<String> = init
2606                    .iter()
2607                    .map(|r| {
2608                        let f = r.fn_num() as i64;
2609                        let pn = if f > curr { f - max } else { f };
2610                        format!(
2611                            "(fn={} poc={} lt={} picnum={})",
2612                            r.fn_num(),
2613                            r.pic_poc(),
2614                            r.is_long_term(),
2615                            pn
2616                        )
2617                    })
2618                    .collect();
2619                eprintln!(
2620                    "MODFAIL idc={idc} val={val}  curr_frame_num={curr} max={max}  init={}",
2621                    cand.join(" ")
2622                );
2623            }
2624            return Err(DecodeError::Truncated); // references a picture not in the DPB
2625        };
2626        if refidx > list.len() {
2627            break;
2628        }
2629        list.insert(refidx, found);
2630        if let Some(dup) = list
2631            .iter()
2632            .enumerate()
2633            .skip(refidx + 1)
2634            .find(|(_, r)| matches(r))
2635            .map(|(i, _)| i)
2636        {
2637            list.remove(dup);
2638        }
2639        refidx += 1;
2640        if refidx >= num_active {
2641            break;
2642        }
2643    }
2644    list.truncate(num_active.max(1));
2645    Ok(list)
2646}
2647
2648#[cfg(test)]
2649mod tests {
2650    use super::*;
2651
2652    fn ref_at(poc: i32, fnum: u32) -> Ref {
2653        crate::sync::Arc::new(RefFrame {
2654            py: vec![],
2655            pu: vec![],
2656            pv: vec![],
2657            cw: 0,
2658            ch: 0,
2659            ready_rows: core::sync::atomic::AtomicUsize::new(0),
2660            live: None,
2661            frozen: crate::sync::Once::new(),
2662            frame_num: fnum,
2663            poc,
2664            mv: Vec::new(),
2665            ref_idx: Vec::new(),
2666            mv1: Vec::new(),
2667            ref_idx1: Vec::new(),
2668            poc_lut: [i32::MIN; 32],
2669            w4: 0,
2670            long_term: false,
2671            long_term_idx: 0,
2672        })
2673    }
2674
2675    #[test]
2676    fn b_ref_lists_ordered_by_poc() {
2677        // Current POC 4; DPB has past (0,2) and future (6,8) references.
2678        let dpb = vec![ref_at(8, 4), ref_at(6, 3), ref_at(2, 1), ref_at(0, 0)];
2679        let (l0, l1) = build_ref_list_b(&dpb, 4, 5, 16, 4, 4, &[], &[]).unwrap();
2680        // List0: nearer past first (desc), then nearer future (asc).
2681        assert_eq!(
2682            l0.iter().map(|r| r.poc).collect::<Vec<_>>(),
2683            vec![2, 0, 6, 8]
2684        );
2685        // List1: nearer future first (asc), then nearer past (desc).
2686        assert_eq!(
2687            l1.iter().map(|r| r.poc).collect::<Vec<_>>(),
2688            vec![6, 8, 2, 0]
2689        );
2690    }
2691
2692    #[test]
2693    fn b_ref_list1_swap_when_equal() {
2694        // Only past references -> List0 and List1 initialize identically, so
2695        // List1's first two entries are swapped (spec §8.2.4.2.3).
2696        let dpb = vec![ref_at(4, 2), ref_at(2, 1), ref_at(0, 0)];
2697        let (l0, l1) = build_ref_list_b(&dpb, 6, 3, 16, 3, 3, &[], &[]).unwrap();
2698        assert_eq!(l0.iter().map(|r| r.poc).collect::<Vec<_>>(), vec![4, 2, 0]);
2699        assert_eq!(l1.iter().map(|r| r.poc).collect::<Vec<_>>(), vec![2, 4, 0]);
2700    }
2701
2702    #[test]
2703    fn frame_num_gaps_insert_placeholders() {
2704        let mut d = Decoder::new();
2705        d.prev_ref_frame_num = 2;
2706        // frame_num jumps 2 -> 5: placeholders for the skipped 3 and 4.
2707        d.insert_frame_num_gaps(5, 16, 8, 16, 16);
2708        let fns: Vec<u32> = d.refs.iter().map(|r| r.frame_num).collect();
2709        assert_eq!(fns, vec![4, 3], "most-recent placeholder at the front");
2710        assert_eq!(d.prev_ref_frame_num, 4);
2711        assert!(
2712            d.refs.iter().all(|r| r.py.iter().all(|&p| p == 128)),
2713            "grey fill"
2714        );
2715    }
2716
2717    #[test]
2718    fn frame_num_gaps_wrap_and_noop() {
2719        // Wrap across MaxFrameNum: prev 14, frame_num 1 (max 16) -> fill 15, 0.
2720        let mut d = Decoder::new();
2721        d.prev_ref_frame_num = 14;
2722        d.insert_frame_num_gaps(1, 16, 8, 16, 16);
2723        assert_eq!(
2724            d.refs.iter().map(|r| r.frame_num).collect::<Vec<_>>(),
2725            vec![0, 15]
2726        );
2727        // No gap (consecutive) inserts nothing.
2728        let mut d = Decoder::new();
2729        d.prev_ref_frame_num = 3;
2730        d.insert_frame_num_gaps(4, 16, 8, 16, 16);
2731        assert!(d.refs.is_empty());
2732    }
2733
2734    #[test]
2735    fn missing_param_sets_errors() {
2736        let mut d = Decoder::new();
2737        // A lone (fake) IDR slice header: first_mb_in_slice=0, slice_type=7 (I),
2738        // pic_parameter_set_id=0 — then the PPS lookup fails (none stored).
2739        let nal = rusty_h264_common::NalUnit::new(3, NalUnitType::IdrSlice, vec![0x88, 0x80]);
2740        let err = d.decode(&nal.to_annex_b()).unwrap_err();
2741        assert_eq!(err, DecodeError::MissingParameterSet);
2742    }
2743}