Skip to main content

rusty_h264_decoder/
mb16.rs

1//! I_16x16 macroblock decoding — the mirror of the encoder's `mb16`.
2//!
3//! Parses each macroblock's residuals and reconstructs it with the exact same
4//! prediction + inverse-transform helpers the encoder uses, so decoder output
5//! matches encoder reconstruction bit-for-bit.
6#![allow(clippy::needless_range_loop)]
7
8use rusty_h264_common::bit_reader::OutOfData;
9use rusty_h264_common::cavlc::{
10    decode_residual_block, read_cbp_inter, read_cbp_intra, un_scan_4x4_ac_into, un_scan_4x4_dcac,
11};
12use rusty_h264_common::inter::{
13    inter_partitions, mc_chroma_padded, mc_luma_padded, predict_mv, predict_partition_mv,
14    MvNeighbor,
15};
16use rusty_h264_common::predict::{
17    add_residual_8x8, chroma8x8_pred, chroma_qp, intra4x4_pred, intra8x8_pred, luma16x16_pred,
18    reconstruct_4x4, reconstruct_4x4_dc, reconstruct_4x4_dc_into, reconstruct_4x4_into, I16Mode,
19    CHROMA_4X4_SCAN_XY, LUMA_4X4_SCAN_XY,
20};
21use rusty_h264_common::transform::{
22    dequant_scatter_4x4, dequantize, dequantize_weighted, inverse_quant_8x8,
23    inverse_quant_chroma_dc,
24    inverse_quant_chroma_dc_weighted, inverse_quant_luma_dc, inverse_quant_luma_dc_weighted,
25};
26use rusty_h264_common::{BitReader, YuvFrame};
27
28/// One frame's motion field, in 4x4-block raster (`mb_w*4` wide).
29///
30/// Captured from any conformant stream this decoder parses — including x264's —
31/// so a harness can compare motion fields between encoders without depending on
32/// external MV-export tooling.
33pub struct MvField {
34    pub mb_w: usize,
35    pub mb_h: usize,
36    pub mv: Vec<(i32, i32)>,
37    pub ref_idx: Vec<i32>,
38    pub inter: Vec<bool>,
39}
40
41/// Frames captured in decode order when `RFF_MV_DUMP=1`. Diagnostic only.
42pub static MV_DUMP: std::sync::Mutex<Vec<MvField>> = std::sync::Mutex::new(Vec::new());
43
44pub fn mv_dump_on() -> bool {
45    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
46    *ON.get_or_init(|| std::env::var("RFF_MV_DUMP").map_or(false, |v| v != "0"))
47}
48
49/// Reconstructed coded-size planes plus CAVLC `nnz` context grids.
50pub struct FrameDecoder {
51    mb_w: usize,
52    mb_h: usize,
53    /// Slice QP (`SliceQPy`) — the deblock filter's frame-level QP.
54    qp: u8,
55    /// Running luma QP (`QPy`), carried across macroblocks and stepped by each
56    /// `mb_qp_delta` (spec §7.4.5). Equals `qp` on constant-QP streams.
57    cur_qp: u8,
58    /// `chroma_qp_index_offset` from the active PPS (§8.5.8).
59    chroma_qp_offset: i32,
60    cw: usize,
61    ch: usize,
62    ccw: usize,
63    cch: usize,
64    rec_y: Vec<u8>,
65    rec_u: Vec<u8>,
66    rec_v: Vec<u8>,
67    /// Per-macroblock luma QP (`QPy`), for per-edge deblock strength.
68    mb_qp: Vec<u8>,
69    /// First macroblock address of the slice currently being decoded. Neighbors
70    /// with a lower address belong to an earlier slice and are "not available"
71    /// for prediction (spec §8.3/§8.4). Slices are contiguous raster ranges (we
72    /// reject FMO/slice-groups), so address ≥ this ⇔ same slice.
73    slice_first_mb: usize,
74    nnz_y: Vec<u8>,
75    nnz_c: [Vec<u8>; 2],
76    modes_y: Vec<u8>,
77    coded_y: Vec<bool>,
78    /// Per-4×4-block List-0 motion (mv + ref index, `-1` = no L0). For P slices
79    /// this is the only motion; B slices add the List-1 grids below.
80    mv_y: Vec<(i32, i32)>,
81    inter_y: Vec<bool>,
82    ref_idx_y: Vec<i32>,
83    /// Per-4×4-block List-1 motion for B slices (`ref_idx1 = -1` = no L1).
84    mv1: Vec<(i32, i32)>,
85    ref_idx1: Vec<i32>,
86    /// `RefPicList1` and B-slice flags (unused outside B slices).
87    refs1: Vec<crate::Ref>,
88    num_ref_active1: usize,
89    is_b: bool,
90    /// True if the stream's profile permits B-slices (`profile_idc != 66`). When
91    /// false (Baseline / Constrained Baseline), `as_reference` skips the per-block
92    /// motion (mv/ref_idx/ref_poc) that only B temporal/spatial direct ever reads.
93    b_possible: bool,
94    direct_spatial: bool,
95    nnz_l_cache: [u8; 25],
96    nnz_c_cache: [[u8; 9]; 2],
97    /// Decoded-picture buffer (most-recent first); empty in I-slices. `ref_idx`
98    /// indexes into this list.
99    refs: Vec<crate::Ref>,
100    /// `num_ref_idx_l0_active` for the current slice — drives whether `ref_idx`
101    /// is coded (active > 1) and its te(v)/ue(v) form, independently of how many
102    /// reference pictures actually exist (spec §7.4.5.1, §9.1).
103    num_ref_active: usize,
104    /// `constrained_intra_pred_flag`: when set, intra prediction may only use
105    /// samples from intra-coded neighbors (inter neighbors are "not available").
106    constrained_intra: bool,
107    /// High-profile 4×4 scaling matrices in **raster** order, indexed by
108    /// `[Y-intra, Cb-intra, Cr-intra, Y-inter, Cb-inter, Cr-inter]`. `None` = flat.
109    scaling: Option<[[i32; 16]; 6]>,
110    /// High-profile 8×8 luma scaling matrices in raster order `[Y-intra, Y-inter]`
111    /// (4:2:0 has only these two). `None` = flat.
112    scaling8: Option<[[i32; 64]; 2]>,
113    /// `transform_8x8_mode_flag` from the PPS: enables `transform_size_8x8_flag`.
114    transform_8x8_mode: bool,
115    /// Per-macroblock `transform_size_8x8_flag` (for deblocking: internal 4×4
116    /// luma edges of 8×8-transform MBs are not filtered).
117    mb_t8x8: Vec<bool>,
118    // ---- Row-interleaved deblocking state (docs/row-interleave-plan.md) ----
119    /// Per-MB boundary strengths, filled row-by-row as decode completes rows.
120    bs_frame: Vec<rusty_h264_common::deblock::MbBs>,
121    /// Rows whose bS is derived (watermark).
122    bs_rows: usize,
123    /// Rows already deblock-FILTERED (watermark; R3).
124    flt_rows: usize,
125    /// Two-row rolling window of packed records (prev = row r-1, cur = row r).
126    pk_prev: Vec<rusty_h264_common::deblock::MbPack>,
127    pk_cur: Vec<rusty_h264_common::deblock::MbPack>,
128    /// Transform-block coded mask (nnz with the 8x8 OR applied), filled per row.
129    nnz_dbr: Vec<u8>,
130    /// Unfiltered bottom rows of the last-filtered MB row (intra reads these).
131    bak_y: Vec<u8>,
132    bak_u: Vec<u8>,
133    bak_v: Vec<u8>,
134    /// Entropy-decouple: deferred pixel jobs + the per-slice activation flag
135    /// (CABAC slices only — the CAVLC loop has no flush hooks).
136    edc_jobs: Vec<EdcJob>,
137    edc_active: bool,
138    /// Current slice's deblock parameters (set per slice by the caller).
139    db_ena: bool,
140    db_oa: i32,
141    db_ob: i32,
142    /// Per-macroblock deblock derivation CLASS (`MB_KIND_*`), so the loop filter
143    /// can skip the 24-block neighbourhood gather on macroblocks whose strengths
144    /// are determined by syntax alone. Starts UNSET; anything left UNSET simply
145    /// takes the blind path, so a missed producer site costs speed, not
146    /// correctness. Only classes that are uniform BY SYNTAX are written — notably
147    /// NOT `B_Skip`/`B_Direct`, whose direct-derived motion varies per 4×4.
148    mb_kind: Vec<u8>,
149    /// Explicit weighted-prediction tables, when active for this slice.
150    weights: Option<WeightTable>,
151    /// Current picture's `PicOrderCnt` (for temporal direct + implicit weighting).
152    cur_poc: i32,
153    /// `weighted_bipred_idc` (0 = none/average, 1 = explicit, 2 = implicit).
154    weighted_bipred_idc: u8,
155    /// `direct_8x8_inference_flag` (B direct co-located sub-block selection).
156    direct_8x8_inference: bool,
157}
158
159/// Explicit weighted-prediction tables (spec §7.4.3.2 / §8.4.2.3.2). Per
160/// reference list, per ref index: a luma `(weight, offset)` and two chroma
161/// `(weight, offset)` (Cb, Cr). `log2` denominators are shared.
162#[derive(Clone, Default)]
163pub struct WeightTable {
164    pub luma_log2_denom: i32,
165    pub chroma_log2_denom: i32,
166    /// `[list][ref_idx] = (weight, offset)`.
167    pub luma: [Vec<(i32, i32)>; 2],
168    /// `[list][ref_idx][cb=0/cr=1] = (weight, offset)`.
169    pub chroma: [Vec<[(i32, i32); 2]>; 2],
170}
171
172impl WeightTable {
173    /// Applies a single-list (uni-prediction) luma weight (spec §8.4.2.3.2).
174    fn apply_luma(&self, sample: u8, list: usize, refi: usize) -> u8 {
175        let (w, o) = self.luma[list][refi];
176        let lwd = self.luma_log2_denom;
177        let v = if lwd >= 1 {
178            ((sample as i32 * w + (1 << (lwd - 1))) >> lwd) + o
179        } else {
180            sample as i32 * w + o
181        };
182        v.clamp(0, 255) as u8
183    }
184
185    /// Applies a single-list (uni-prediction) chroma weight for component `cc`.
186    fn apply_chroma(&self, sample: u8, list: usize, refi: usize, cc: usize) -> u8 {
187        let (w, o) = self.chroma[list][refi][cc];
188        let cwd = self.chroma_log2_denom;
189        let v = if cwd >= 1 {
190            ((sample as i32 * w + (1 << (cwd - 1))) >> cwd) + o
191        } else {
192            sample as i32 * w + o
193        };
194        v.clamp(0, 255) as u8
195    }
196}
197
198/// Why a macroblock could not be decoded.
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum MbError {
201    Truncated,
202    Unsupported(&'static str),
203}
204
205impl From<OutOfData> for MbError {
206    fn from(_: OutOfData) -> Self {
207        MbError::Truncated
208    }
209}
210
211/// Recycled per-picture scratch grids.
212///
213/// `FrameDecoder::new` used to allocate ~1.65 MB of frame-wide grids for EVERY
214/// coded picture and drop them when the picture finished. The sampled profiler
215/// prices that (stage `dec-setup`) at 6.7% of decode — larger than dequant,
216/// reconstruct and intra prediction combined, and none of it is codec work.
217///
218/// Two costs are being paid, and the allocation is the bigger one. A ~460 KB
219/// `Vec` goes straight to the OS, so every page is a fresh zero page and the
220/// decoder takes a soft page fault on FIRST TOUCH of each 4 KB — a cost charged
221/// to whatever per-macroblock stage happens to touch it first, not to the
222/// allocation. Handing the same buffers back keeps the pages mapped and warm.
223///
224/// The initialising fill is NOT skipped: these grids are read as neighbour
225/// context (`modes_y` must read 2/DC, `ref_idx_y` must read -1) before every
226/// block that writes them, so a stale value from the previous picture is a
227/// correctness bug, not a performance trade. `clear()` + `resize()` keeps the
228/// fill and drops only the allocation.
229///
230/// The reconstruction planes are deliberately NOT pooled: `into_frame` MOVES
231/// them out as the caller's output frame, so there is nothing to hand back.
232#[derive(Default)]
233pub struct GridPool {
234    mb_qp: Vec<u8>,
235    bs_frame: Vec<rusty_h264_common::deblock::MbBs>,
236    pk_prev: Vec<rusty_h264_common::deblock::MbPack>,
237    pk_cur: Vec<rusty_h264_common::deblock::MbPack>,
238    nnz_dbr: Vec<u8>,
239    bak_y: Vec<u8>,
240    bak_u: Vec<u8>,
241    bak_v: Vec<u8>,
242    nnz_y: Vec<u8>,
243    nnz_c0: Vec<u8>,
244    nnz_c1: Vec<u8>,
245    modes_y: Vec<u8>,
246    coded_y: Vec<bool>,
247    mv_y: Vec<(i32, i32)>,
248    inter_y: Vec<bool>,
249    ref_idx_y: Vec<i32>,
250    mv1: Vec<(i32, i32)>,
251    ref_idx1: Vec<i32>,
252    mb_t8x8: Vec<bool>,
253    mb_kind: Vec<u8>,
254}
255
256/// Reuse `v`'s allocation for `n` copies of `val`. Identical OBSERVABLE result to
257/// `vec![val; n]`; differs only in that it reuses the existing allocation when the
258/// capacity already suffices.
259#[inline]
260fn refill<T: Clone>(mut v: Vec<T>, n: usize, val: T) -> Vec<T> {
261    v.clear();
262    v.resize(n, val);
263    v
264}
265
266impl FrameDecoder {
267    pub fn new(
268        mb_w: usize,
269        mb_h: usize,
270        qp: u8,
271        chroma_qp_offset: i32,
272        refs: Vec<crate::Ref>,
273        num_ref_active: usize,
274        constrained_intra: bool,
275        transform_8x8_mode: bool,
276        b_possible: bool,
277    ) -> Self {
278        Self::with_pool(
279            mb_w,
280            mb_h,
281            qp,
282            chroma_qp_offset,
283            refs,
284            num_ref_active,
285            constrained_intra,
286            transform_8x8_mode,
287            b_possible,
288            GridPool::default(),
289        )
290    }
291
292    /// As `new`, but reusing a previous picture's grid allocations. See `GridPool`.
293    #[allow(clippy::too_many_arguments)]
294    pub fn with_pool(
295        mb_w: usize,
296        mb_h: usize,
297        qp: u8,
298        chroma_qp_offset: i32,
299        refs: Vec<crate::Ref>,
300        num_ref_active: usize,
301        constrained_intra: bool,
302        transform_8x8_mode: bool,
303        b_possible: bool,
304        pool: GridPool,
305    ) -> Self {
306        let (cw, ch) = (mb_w * 16, mb_h * 16);
307        let (ccw, cch) = (cw / 2, ch / 2);
308        Self {
309            mb_w,
310            mb_h,
311            qp,
312            cur_qp: qp,
313            chroma_qp_offset,
314            cw,
315            ch,
316            ccw,
317            cch,
318            rec_y: vec![0; cw * ch],
319            rec_u: vec![0; ccw * cch],
320            rec_v: vec![0; ccw * cch],
321            mb_qp: refill(pool.mb_qp, mb_w * mb_h, qp),
322            slice_first_mb: 0,
323            nnz_y: refill(pool.nnz_y, (mb_w * 4) * (mb_h * 4), 0),
324            nnz_c: [
325                refill(pool.nnz_c0, (mb_w * 2) * (mb_h * 2), 0),
326                refill(pool.nnz_c1, (mb_w * 2) * (mb_h * 2), 0),
327            ],
328            modes_y: refill(pool.modes_y, (mb_w * 4) * (mb_h * 4), 2),
329            coded_y: refill(pool.coded_y, (mb_w * 4) * (mb_h * 4), false),
330            mv_y: refill(pool.mv_y, (mb_w * 4) * (mb_h * 4), (0, 0)),
331            inter_y: refill(pool.inter_y, (mb_w * 4) * (mb_h * 4), false),
332            ref_idx_y: refill(pool.ref_idx_y, (mb_w * 4) * (mb_h * 4), -1),
333            mv1: refill(pool.mv1, (mb_w * 4) * (mb_h * 4), (0, 0)),
334            ref_idx1: refill(pool.ref_idx1, (mb_w * 4) * (mb_h * 4), -1),
335            refs1: Vec::new(),
336            num_ref_active1: 0,
337            is_b: false,
338            b_possible,
339            direct_spatial: true,
340            nnz_l_cache: [0x80; 25],
341            nnz_c_cache: [[0x80; 9]; 2],
342            refs,
343            num_ref_active,
344            constrained_intra,
345            scaling: None,
346            scaling8: None,
347            transform_8x8_mode,
348            mb_t8x8: refill(pool.mb_t8x8, mb_w * mb_h, false),
349            bs_frame: refill(pool.bs_frame, mb_w * mb_h, Default::default()),
350            bs_rows: 0,
351            flt_rows: 0,
352            pk_prev: {
353                let mut v = pool.pk_prev;
354                v.clear();
355                v
356            },
357            pk_cur: {
358                let mut v = pool.pk_cur;
359                v.clear();
360                v
361            },
362            nnz_dbr: refill(pool.nnz_dbr, (mb_w * 4) * (mb_h * 4), 0),
363            bak_y: refill(pool.bak_y, cw, 0),
364            bak_u: refill(pool.bak_u, ccw, 0),
365            bak_v: refill(pool.bak_v, ccw, 0),
366            edc_jobs: Vec::new(),
367            edc_active: false,
368            db_ena: false,
369            db_oa: 0,
370            db_ob: 0,
371            mb_kind: refill(
372                pool.mb_kind,
373                mb_w * mb_h,
374                rusty_h264_common::deblock::MB_KIND_UNSET,
375            ),
376            weights: None,
377            cur_poc: 0,
378            weighted_bipred_idc: 0,
379            direct_8x8_inference: false,
380        }
381    }
382
383    /// Sets the explicit weighted-prediction tables for this slice.
384    pub fn set_weights(&mut self, weights: WeightTable) {
385        self.weights = Some(weights);
386    }
387
388    /// Applies explicit uni-prediction weighting to a motion-compensated partition
389    /// (luma `pred_y` region + the two chroma planes), if weighting is active.
390    /// `list` is the reference list and `refi` the partition's reference index.
391    fn weight_partition(
392        &self,
393        pred_y: &mut [u8; 256],
394        c_pred: &mut [[u8; 64]; 2],
395        list: usize,
396        refi: usize,
397        rx: usize,
398        ry: usize,
399        rw: usize,
400        rh: usize,
401    ) {
402        let Some(wt) = &self.weights else { return };
403        for dy in 0..rh {
404            for dx in 0..rw {
405                let i = (ry + dy) * 16 + (rx + dx);
406                pred_y[i] = wt.apply_luma(pred_y[i], list, refi);
407            }
408        }
409        let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
410        for cc in 0..2 {
411            for dy in 0..crh {
412                for dx in 0..crw {
413                    let i = (cry + dy) * 8 + (crx + dx);
414                    c_pred[cc][i] = wt.apply_chroma(c_pred[cc][i], list, refi, cc);
415                }
416            }
417        }
418    }
419
420    /// Sets the High-profile scaling matrices (raster order: six 4×4 lists, two
421    /// 8×8 luma lists). The caller un-zig-zags the SPS lists. Flat is the default.
422    pub fn set_scaling(&mut self, scaling: [[i32; 16]; 6], scaling8: [[i32; 64]; 2]) {
423        self.scaling = Some(scaling);
424        self.scaling8 = Some(scaling8);
425    }
426
427    /// Dequantizes a 4×4 AC block with scaling list `list` (flat if none active).
428    fn dequant(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
429        match &self.scaling {
430            Some(s) => dequantize_weighted(levels, qp, &s[list]),
431            None => dequantize(levels, qp),
432        }
433    }
434
435    /// Single-coefficient twin of `dequant` for position 0 (DC-only fast path).
436    fn dequant_dc4(&self, level: i32, qp: u8, list: usize) -> i32 {
437        rusty_h264_common::transform::dequantize_dc4(
438            level,
439            qp,
440            self.scaling.as_ref().map(|s| s[list][0]),
441        )
442    }
443
444    /// Inverse-quantizes the I_16x16 luma DC with scaling list `list`'s DC weight.
445    fn dequant_luma_dc(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
446        match &self.scaling {
447            Some(s) => inverse_quant_luma_dc_weighted(levels, qp, s[list][0]),
448            None => inverse_quant_luma_dc(levels, qp),
449        }
450    }
451
452    /// Inverse-quantizes a chroma DC block with scaling list `list`'s DC weight.
453    fn dequant_chroma_dc(&self, levels: &[i32; 4], qp: u8, list: usize) -> [i32; 4] {
454        match &self.scaling {
455            Some(s) => inverse_quant_chroma_dc_weighted(levels, qp, s[list][0]),
456            None => inverse_quant_chroma_dc(levels, qp),
457        }
458    }
459
460    /// Sets the B-slice context for the slice about to be decoded: `RefPicList1`,
461    /// its active count, and the direct-mode flag.
462    #[allow(clippy::too_many_arguments)]
463    pub fn set_b_context(
464        &mut self,
465        refs1: Vec<crate::Ref>,
466        num_ref_active1: usize,
467        direct_spatial: bool,
468        cur_poc: i32,
469        weighted_bipred_idc: u8,
470        direct_8x8_inference: bool,
471    ) {
472        self.is_b = true;
473        self.refs1 = refs1;
474        self.num_ref_active1 = num_ref_active1;
475        self.direct_spatial = direct_spatial;
476        self.cur_poc = cur_poc;
477        self.weighted_bipred_idc = weighted_bipred_idc;
478        self.direct_8x8_inference = direct_8x8_inference;
479    }
480
481    /// Steps the running luma QP by a `mb_qp_delta` (spec §7.4.5, 8-bit depth):
482    /// `QPy = (QPy_prev + delta + 52) % 52`.
483    fn step_qp(&mut self, delta: i32) {
484        self.cur_qp = (self.cur_qp as i32 + delta + 52).rem_euclid(52) as u8;
485    }
486
487    /// Maps a luma QP to its chroma QP, applying `chroma_qp_index_offset`
488    /// (spec §8.5.8): `QPc = qpc_table(Clip3(0, 51, QPy + offset))`.
489    fn chroma_qp_for(&self, qp_y: u8) -> u8 {
490        let qpi = (qp_y as i32 + self.chroma_qp_offset).clamp(0, 51) as u8;
491        chroma_qp(qpi)
492    }
493
494    /// Resets per-slice state before decoding a continuation slice of the same
495    /// picture: the running QP (each slice carries its own `slice_qp`) and the
496    /// reference list (each slice may reorder it).
497    pub fn begin_slice(&mut self, slice_qp: u8, refs: Vec<crate::Ref>, num_ref_active: usize) {
498        self.cur_qp = slice_qp;
499        self.qp = slice_qp;
500        self.refs = refs;
501        self.num_ref_active = num_ref_active;
502        self.weights = None; // re-set per slice if a pred_weight_table is present
503    }
504
505    /// Whether the neighbor macroblock at `(nbx, nby)` is in the slice currently
506    /// being decoded (address ≥ the slice's first MB). For single-slice pictures
507    /// `slice_first_mb == 0`, so this is always true and prediction is unchanged.
508    #[inline]
509    fn nbr_in_slice(&self, nbx: usize, nby: usize) -> bool {
510        nby * self.mb_w + nbx >= self.slice_first_mb
511    }
512
513    /// Whether the neighbor 4×4 block at `(nbx, nby)` may contribute to intra
514    /// prediction. With `constrained_intra_pred`, an inter-coded neighbor is
515    /// treated as unavailable (spec §8.3.1.2.{1,2}); otherwise always usable.
516    #[inline]
517    fn intra_nbr_ok(&self, nbx: usize, nby: usize) -> bool {
518        !self.constrained_intra || !self.inter_y[nby * (self.mb_w * 4) + nbx]
519    }
520
521    fn mv_neighbors(&self, mb_x: usize, mb_y: usize) -> [MvNeighbor; 3] {
522        let w4 = self.mb_w * 4;
523        let get = |avail: bool, bx: isize, by: isize| {
524            if avail {
525                let idx = by as usize * w4 + bx as usize;
526                MvNeighbor {
527                    available: true,
528                    mv: self.mv_y[idx],
529                    ref_idx: self.ref_idx_y[idx],
530                }
531            } else {
532                MvNeighbor::NONE
533            }
534        };
535        let (bx, by) = (mb_x as isize * 4, mb_y as isize * 4);
536        let a = get(mb_x > 0 && self.nbr_in_slice(mb_x - 1, mb_y), bx - 1, by);
537        let b = get(mb_y > 0 && self.nbr_in_slice(mb_x, mb_y - 1), bx, by - 1);
538        let c = if mb_y > 0 && mb_x + 1 < self.mb_w && self.nbr_in_slice(mb_x + 1, mb_y - 1) {
539            get(true, bx + 4, by - 1)
540        } else {
541            get(mb_x > 0 && mb_y > 0 && self.nbr_in_slice(mb_x - 1, mb_y - 1), bx - 1, by - 1)
542        };
543        [a, b, c]
544    }
545
546    fn mv_neighbors_block(&self, pbx: isize, pby: isize, pwb: isize) -> [MvNeighbor; 3] {
547        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Neighbors);
548        let (w4, h4) = ((self.mb_w * 4) as isize, (self.mb_h * 4) as isize);
549        let get = |bx: isize, by: isize| -> MvNeighbor {
550            // Available iff inside the frame, decoded, and in the current slice.
551            if bx < 0
552                || by < 0
553                || bx >= w4
554                || by >= h4
555                || !self.coded_y[(by * w4 + bx) as usize]
556                || !self.nbr_in_slice(bx as usize / 4, by as usize / 4)
557            {
558                MvNeighbor::NONE
559            } else {
560                let idx = (by * w4 + bx) as usize;
561                MvNeighbor { available: true, mv: self.mv_y[idx], ref_idx: self.ref_idx_y[idx] }
562            }
563        };
564        let a = get(pbx - 1, pby);
565        let b = get(pbx, pby - 1);
566        let mut c = get(pbx + pwb, pby - 1);
567        if !c.available {
568            c = get(pbx - 1, pby - 1);
569        }
570        [a, b, c]
571    }
572
573    fn skip_mv(&self, mb_x: usize, mb_y: usize) -> (i32, i32) {
574        let [a, b, c] = self.mv_neighbors(mb_x, mb_y);
575        if !a.available
576            || !b.available
577            || (a.ref_idx == 0 && a.mv == (0, 0))
578            || (b.ref_idx == 0 && b.mv == (0, 0))
579        {
580            (0, 0)
581        } else {
582            predict_mv(a, b, c, 0)
583        }
584    }
585
586    fn set_mb_mv(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32), inter: bool, refi: i32) {
587        let w4 = self.mb_w * 4;
588        for dy in 0..4 {
589            for dx in 0..4 {
590                let idx = (mb_y * 4 + dy) * w4 + (mb_x * 4 + dx);
591                self.mv_y[idx] = mv;
592                self.inter_y[idx] = inter;
593                self.ref_idx_y[idx] = if inter { refi } else { -1 };
594            }
595        }
596    }
597
598    /// Commit one inter partition's motion into the 4×4 grid (ref 0, 1-ref P).
599    /// `(rx,ry,rw,rh)` are MB-relative luma pixels; committing before the next
600    /// partition's prediction is what lets a later partition predict from it.
601    fn commit_inter_grid(&mut self, mb_x: usize, mb_y: usize, rx: usize, ry: usize, rw: usize, rh: usize, mv: (i32, i32), refi: i8) {
602        let w4 = self.mb_w * 4;
603        for by in ry / 4..ry / 4 + rh / 4 {
604            for bx in rx / 4..rx / 4 + rw / 4 {
605                let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
606                self.mv_y[idx] = mv;
607                self.inter_y[idx] = true;
608                self.ref_idx_y[idx] = refi as i32;
609                self.coded_y[idx] = true;
610            }
611        }
612    }
613
614    /// Per-slice deblock parameters, needed DURING decode by the row-interleave
615    /// path. `ena` is already resolved against `RFF_ABL_DEBLOCK` by the caller.
616    pub fn set_deblock_params(&mut self, ena: bool, oa: i32, ob: i32) {
617        // Latch: the FIRST disabling slice turns row filtering off for the rest
618        // of the picture (see `row_hook`); rows already filtered stay counted
619        // in `flt_rows` and the picture-end tail handles the remainder.
620        self.db_ena = ena && (self.flt_rows == 0 || self.db_ena);
621        self.db_oa = oa;
622        self.db_ob = ob;
623    }
624
625    /// Derives bS for macroblock row `r` from the just-decoded (hot) grids into
626    /// `bs_frame`, maintaining the two-row rolling record window (R2 of
627    /// docs/row-interleave-plan.md).
628    fn derive_bs_row(&mut self, r: usize) {
629        // Same stage label the in-filter derivation used, so profiles keep
630        // pricing bS derivation wherever it lives.
631        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DebDerive);
632        use rusty_h264_common::deblock::{derive_mb_records, pack_mb, BlockInfo, MbBs};
633        let (mb_w, w4) = (self.mb_w, self.mb_w * 4);
634        // Transform-block coded mask for this row: raw nnz, then the 8x8 OR for
635        // t8 macroblocks (spec §8.7: the 8x8 transform's coded status is per 8x8).
636        for br in r * 4..r * 4 + 4 {
637            let a = br * w4;
638            self.nnz_dbr[a..a + w4].copy_from_slice(&self.nnz_y[a..a + w4]);
639        }
640        for mb_x in 0..mb_w {
641            if !self.mb_t8x8[r * mb_w + mb_x] {
642                continue;
643            }
644            for b8 in 0..4usize {
645                let (bx, by) = (mb_x * 4 + (b8 % 2) * 2, r * 4 + (b8 / 2) * 2);
646                let any = (0..2).any(|sy| (0..2).any(|sx| self.nnz_y[(by + sy) * w4 + bx + sx] > 0));
647                for sy in 0..2 {
648                    for sx in 0..2 {
649                        self.nnz_dbr[(by + sy) * w4 + bx + sx] = any as u8;
650                    }
651                }
652            }
653        }
654        let poc0: Vec<i32> = self.refs.iter().map(|f| f.poc).collect();
655        let poc1: Vec<i32> = self.refs1.iter().map(|f| f.poc).collect();
656        let info = BlockInfo {
657            inter: &self.inter_y,
658            nnz: &self.nnz_dbr,
659            mv: &self.mv_y,
660            ref_id: &self.ref_idx_y,
661            mv1: &self.mv1,
662            ref_id1: if poc1.is_empty() { &[] } else { &self.ref_idx1 },
663            w4,
664            t8x8: &self.mb_t8x8,
665            bs: &[],
666            poc0: &poc0,
667            poc1: &poc1,
668            kind: &[],
669        };
670        let has1 = !info.ref_id1.is_empty();
671        std::mem::swap(&mut self.pk_prev, &mut self.pk_cur);
672        self.pk_cur.clear();
673        for mb_x in 0..mb_w {
674            self.pk_cur.push(pack_mb(&info, has1, mb_x, r));
675            let cur = &self.pk_cur[mb_x];
676            let left = if mb_x > 0 { Some(&self.pk_cur[mb_x - 1]) } else { None };
677            let top = if r > 0 { Some(&self.pk_prev[mb_x]) } else { None };
678            let mb_t8 = self.mb_t8x8[r * mb_w + mb_x];
679            let (mut bv, mut bh) = ([[0i32; 4]; 4], [[0i32; 4]; 4]);
680            derive_mb_records(cur, left, top, mb_t8, &mut bv, &mut bh);
681            let mut m = MbBs::default();
682            for e in 0..4 {
683                for sg in 0..4 {
684                    m.v[e][sg] = bv[e][sg] as u8;
685                    m.h[e][sg] = bh[e][sg] as u8;
686                }
687            }
688            self.bs_frame[r * mb_w + mb_x] = m;
689        }
690    }
691
692    /// Decode-loop hook: called at each MB-loop head with the NEXT address to be
693    /// decoded; derives AND FILTERS (R3) every fully-decoded row. Filtering a
694    /// row here preserves the spec's raster per-MB filter order exactly (every
695    /// MB the row's edges touch is already decoded; bottom-adjacent edges
696    /// belong to the NEXT row's MBs, which filter later).
697    #[inline]
698    fn row_hook(&mut self, addr: usize) {
699        if !rowdb_on() {
700            // Even without row deblocking, completed rows' deferred pixel jobs
701            // must not pile up past a row boundary indefinitely; flush here so
702            // the queue stays row-sized.
703            self.edc_flush();
704            return;
705        }
706        let done = addr / self.mb_w;
707        if self.bs_rows < done {
708            self.edc_flush();
709        }
710        while self.bs_rows < done {
711            let r = self.bs_rows;
712            self.derive_bs_row(r);
713            self.bs_rows += 1;
714            // Row filtering requires deblock enabled on EVERY slice so far
715            // (`db_ena` latches false once any slice disables it): a mixed
716            // picture falls back to the picture-end tail so "latest slice
717            // wins" semantics are preserved.
718            if self.db_ena {
719                self.save_bak(r);
720                self.filter_row(r);
721                self.flt_rows = r + 1;
722            }
723        }
724    }
725
726    /// Saves the UNFILTERED bottom pixel rows of MB row `r` before filtering
727    /// modifies them: the next row's intra prediction must read pre-deblock
728    /// samples (spec §8.3), and filtering touches the bottom three rows while
729    /// intra reads exactly the bottom ONE (+ the corner) — so one backup row
730    /// per plane suffices, overwritten per row.
731    fn save_bak(&mut self, r: usize) {
732        let y0 = (r * 16 + 15) * self.cw;
733        self.bak_y.copy_from_slice(&self.rec_y[y0..y0 + self.cw]);
734        let c0 = (r * 8 + 7) * self.ccw;
735        self.bak_u.copy_from_slice(&self.rec_u[c0..c0 + self.ccw]);
736        self.bak_v.copy_from_slice(&self.rec_v[c0..c0 + self.ccw]);
737    }
738
739    /// Filters one MB row against the stored strengths, using the CURRENT
740    /// slice's alpha/beta offsets (single-offset streams — the whole corpus —
741    /// are bit-identical to the picture-end call; the plan's risk register
742    /// documents the multi-offset divergence).
743    fn filter_row(&mut self, r: usize) {
744        let info = rusty_h264_common::deblock::BlockInfo {
745            inter: &self.inter_y,
746            nnz: &self.nnz_dbr,
747            mv: &self.mv_y,
748            ref_id: &self.ref_idx_y,
749            mv1: &self.mv1,
750            ref_id1: &self.ref_idx1,
751            w4: self.mb_w * 4,
752            t8x8: &self.mb_t8x8,
753            bs: &self.bs_frame,
754            poc0: &[],
755            poc1: &[],
756            kind: &self.mb_kind,
757        };
758        rusty_h264_common::deblock::filter_frame_rows(
759            &mut self.rec_y,
760            &mut self.rec_u,
761            &mut self.rec_v,
762            self.mb_w,
763            self.mb_h,
764            r..r + 1,
765            &self.mb_qp,
766            self.chroma_qp_offset,
767            self.db_oa,
768            self.db_ob,
769            &info,
770        );
771    }
772
773    /// Top-neighbour LUMA pixel for intra prediction: reads the unfiltered
774    /// backup row when the row above has already been deblock-filtered by the
775    /// row-interleave (flt_rows gates it; 0 when the interleave is off, so
776    /// this compiles to the plain read on the fallback path).
777    #[inline]
778    fn top_y_px(&self, py: usize, x: usize) -> u8 {
779        if py % 16 == 0 && self.flt_rows * 16 >= py {
780            self.bak_y[x]
781        } else {
782            self.rec_y[(py - 1) * self.cw + x]
783        }
784    }
785
786    /// Slice form of [`Self::top_y_px`] for the contiguous 16-wide I16 gather.
787    #[inline]
788    fn top_y_row(&self, py: usize, x: usize, n: usize) -> &[u8] {
789        if py % 16 == 0 && self.flt_rows * 16 >= py {
790            &self.bak_y[x..x + n]
791        } else {
792            &self.rec_y[(py - 1) * self.cw + x..][..n]
793        }
794    }
795
796    /// Top-neighbour CHROMA pixel (plane `c`: 0 = U, 1 = V).
797    #[inline]
798    fn top_c_px(&self, c: usize, cy: usize, x: usize) -> u8 {
799        if cy % 8 == 0 && self.flt_rows * 8 >= cy {
800            if c == 0 { self.bak_u[x] } else { self.bak_v[x] }
801        } else {
802            let rec = if c == 0 { &self.rec_u } else { &self.rec_v };
803            rec[(cy - 1) * self.ccw + x]
804        }
805    }
806
807    /// Slice form of [`Self::top_c_px`] for the 8-wide chroma gather.
808    #[inline]
809    fn top_c_row(&self, c: usize, cy: usize, x: usize, n: usize) -> &[u8] {
810        if cy % 8 == 0 && self.flt_rows * 8 >= cy {
811            if c == 0 { &self.bak_u[x..x + n] } else { &self.bak_v[x..x + n] }
812        } else {
813            let rec = if c == 0 { &self.rec_u } else { &self.rec_v };
814            &rec[(cy - 1) * self.ccw + x..][..n]
815        }
816    }
817
818    /// Snapshots the (deblocked) reconstruction as a reference picture.
819    pub fn as_reference(&self) -> crate::RefFrame {
820        self.as_reference_pooled(&mut Vec::new())
821    }
822
823    /// `as_reference` drawing its padded-plane allocations from `pool` (recycled
824    /// planes of evicted DPB frames — see `Decoder::reclaim_retired`). ~1.9 MB of
825    /// fresh allocation per reference picture otherwise (`dpb-clone` stage, 3-4%
826    /// of decode, mostly first-touch page faults).
827    pub fn as_reference_pooled(&self, pool: &mut Vec<Vec<u8>>) -> crate::RefFrame {
828        // MV CAPTURE (`RFF_MV_DUMP=1`) — lets a harness read the motion field any
829        // conformant H.264 stream carries, including x264's, using this decoder as
830        // the parser. Diagnostic only; inert unless the env var is set.
831        if mv_dump_on() {
832            MV_DUMP.lock().unwrap().push(MvField {
833                mb_w: self.mb_w,
834                mb_h: self.mb_h,
835                mv: self.mv_y.clone(),
836                ref_idx: self.ref_idx_y.clone(),
837                inter: self.inter_y.clone(),
838            });
839        }
840
841        // The per-block motion (mv/ref_idx/ref_poc) is read ONLY by B temporal/spatial
842        // direct (`col.mv/ref_idx/ref_poc`, guarded on `w4 != 0` + `idx < len`). On
843        // Baseline/Constrained-Baseline streams (no B) it's pure waste — skip the two
844        // grid clones + the per-block ref_poc resolve/alloc. `w4 = 0` makes the B
845        // readers no-op even on malformed input.
846        let (mv, ref_idx, mv1, ref_idx1, ref_poc, w4) = if self.b_possible {
847            (
848                self.mv_y.clone(),
849                self.ref_idx_y.clone(),
850                self.mv1.clone(),
851                self.ref_idx1.clone(),
852                // Resolve each block's List-0 ref index to the referenced picture's
853                // POC, so temporal direct can map it into the current list.
854                self.ref_idx_y
855                    .iter()
856                    .map(|&r| {
857                        if r >= 0 {
858                            self.refs.get(r as usize).map_or(i32::MIN, |f| f.poc)
859                        } else {
860                            i32::MIN
861                        }
862                    })
863                    .collect(),
864                self.mb_w * 4,
865            )
866        } else {
867            (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), 0)
868        };
869        // Pop an exact-size recycled buffer per plane; a miss falls back to a
870        // fresh allocation inside `pad_plane_into`.
871        let mut take = |len: usize| -> Vec<u8> {
872            match pool.iter().position(|v| v.len() == len) {
873                Some(i) => pool.swap_remove(i),
874                None => Vec::new(),
875            }
876        };
877        let (lpw, lph) = (self.cw + 2 * crate::LPAD, self.ch + 2 * crate::LPAD);
878        let (cpw, cph) = (self.ccw + 2 * crate::CPAD, self.ch / 2 + 2 * crate::CPAD);
879        crate::RefFrame {
880            // Pad once here (ExpandPicture) instead of extracting a clamped tile
881            // on every MC call — same copy class as the old plane clone.
882            py: rusty_h264_common::inter::pad_plane_into(take(lpw * lph), &self.rec_y, self.cw, self.ch, crate::LPAD),
883            pu: rusty_h264_common::inter::pad_plane_into(take(cpw * cph), &self.rec_u, self.ccw, self.ch / 2, crate::CPAD),
884            pv: rusty_h264_common::inter::pad_plane_into(take(cpw * cph), &self.rec_v, self.ccw, self.ch / 2, crate::CPAD),
885            cw: self.cw,
886            ch: self.ch,
887            frame_num: 0, // set by the caller (decode_slice knows frame_num)
888            poc: 0,       // set by the caller
889            mv,
890            ref_idx,
891            mv1,
892            ref_idx1,
893            ref_poc,
894            w4,
895            long_term: false,
896            long_term_idx: 0,
897        }
898    }
899
900    fn nnz_cache_load(&mut self, mb_x: usize, mb_y: usize) {
901        let w4 = self.mb_w * 4;
902        let top_unavail = mb_y == 0 || !self.nbr_in_slice(mb_x, mb_y - 1);
903        let left_unavail = mb_x == 0 || !self.nbr_in_slice(mb_x - 1, mb_y);
904        for lbx in 0..4 {
905            self.nnz_l_cache[1 + lbx] =
906                if top_unavail { 0x80 } else { self.nnz_y[(mb_y * 4 - 1) * w4 + (mb_x * 4 + lbx)] };
907        }
908        for lby in 0..4 {
909            self.nnz_l_cache[(lby + 1) * 5] =
910                if left_unavail { 0x80 } else { self.nnz_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 - 1)] };
911        }
912    }
913    #[inline]
914    fn nc_pred(&self, lbx: usize, lby: usize) -> i32 {
915        let left = self.nnz_l_cache[(lby + 1) * 5 + lbx] as i32;
916        let top = self.nnz_l_cache[lby * 5 + (lbx + 1)] as i32;
917        let r = left + top;
918        if r < 0x80 { (r + 1) >> 1 } else { r & 0x7f }
919    }
920    #[inline]
921    fn nnz_cache_set(&mut self, lbx: usize, lby: usize, total: u8) {
922        self.nnz_l_cache[(lby + 1) * 5 + (lbx + 1)] = total;
923    }
924    fn chroma_cache_load(&mut self, mb_x: usize, mb_y: usize) {
925        let w2 = self.mb_w * 2;
926        let top_unavail = mb_y == 0 || !self.nbr_in_slice(mb_x, mb_y - 1);
927        let left_unavail = mb_x == 0 || !self.nbr_in_slice(mb_x - 1, mb_y);
928        for c in 0..2 {
929            for bx in 0..2 {
930                self.nnz_c_cache[c][1 + bx] =
931                    if top_unavail { 0x80 } else { self.nnz_c[c][(mb_y * 2 - 1) * w2 + (mb_x * 2 + bx)] };
932            }
933            for by in 0..2 {
934                self.nnz_c_cache[c][(by + 1) * 3] =
935                    if left_unavail { 0x80 } else { self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 - 1)] };
936            }
937        }
938    }
939    #[inline]
940    fn chroma_nc_pred(&self, c: usize, bx: usize, by: usize) -> i32 {
941        let left = self.nnz_c_cache[c][(by + 1) * 3 + bx] as i32;
942        let top = self.nnz_c_cache[c][by * 3 + (bx + 1)] as i32;
943        let r = left + top;
944        if r < 0x80 { (r + 1) >> 1 } else { r & 0x7f }
945    }
946    #[inline]
947    fn chroma_nnz_cache_set(&mut self, c: usize, bx: usize, by: usize, total: u8) {
948        self.nnz_c_cache[c][(by + 1) * 3 + (bx + 1)] = total;
949    }
950
951    /// Decodes one slice's macroblocks (raster order) starting at `first_mb`,
952    /// until `more_rbsp_data()` is exhausted or the picture is full. Returns the
953    /// next macroblock address (= total when the picture is complete). In a
954    /// P-slice each macroblock is preceded by `mb_skip_run`.
955    /// CABAC slice-data decode (docs/cabac-decode-plan.md), brought up brick by brick
956    /// against the instrumented openh264 oracle. Phase 1: verify engine init; the
957    /// syntax layer (Phase 2+) is WIP.
958    #[allow(clippy::too_many_arguments)]
959    pub fn decode_slice_data_cabac(
960        &mut self,
961        rbsp: &[u8],
962        start_byte: usize,
963        slice_qp: u8,
964        cabac_init_idc: u32,
965        is_i: bool,
966        is_p: bool,
967        first_mb: usize,
968    ) -> Result<usize, MbError> {
969        self.edc_active = edc_on();
970        let mut cab = crate::cabac::Cabac::new(rbsp, start_byte, slice_qp as i32, cabac_init_idc, is_i);
971        let (range, _offset) = cab.dbg_state();
972        let trace = std::env::var_os("RH_CABAC_TRACE").is_some();
973        debug_assert_eq!(range, 510, "CABAC init range must be 510");
974
975        const I16_CBP: [u32; 6] = [0, 16, 32, 15, 31, 47];
976        let mbw = self.mb_w;
977        let total = self.mb_w * self.mb_h;
978        // Per-MB neighbour state (single-slice assumption: avail == in-bounds).
979        let mut cat = vec![255u8; total]; // 0=I4x4, 2=I16, 255=unavailable
980        let mut mb_cbp = vec![0u8; total];
981        let mut cmode = vec![-1i32; total]; // chroma pred mode
982        let mut mb_nzc = vec![[0u8; 24]; total]; // 16 luma raster + 8 chroma
983        let mut cbf_dc = vec![0u16; total];
984        let mut mb_skip = vec![false; total];
985        let mut mb_ref = vec![[-1i8; 16]; total]; // per-4×4-block List-0 ref (-1 = intra)
986        let mut mb_mvd = vec![[[0i16; 2]; 16]; total]; // per-block mvd (for mvd ctxInc)
987        let mut mb_ref1 = vec![[-1i8; 16]; total]; // B: per-block List-1 ref (-1 = not in list)
988        let mut mb_mvd1 = vec![[[0i16; 2]; 16]; total]; // B: per-block List-1 mvd (ctxInc)
989        let mut mb_direct = vec![false; total]; // B: MB is (skip/)direct — for mb_type ctxInc
990        let mut last_delta_qp = 0i32;
991        let mut addr = first_mb;
992
993        loop {
994            // BOUND the entropy-coded loop. `decode_terminate` is the only exit, and a
995            // mutated stream can simply never produce it — the arithmetic decoder
996            // zero-fills past the end of the buffer and keeps yielding symbols. Without
997            // this the loop walks `addr` past the picture and indexes out of bounds.
998            // (Surfaced by the fuzzer the moment CABAC became the default; the CAVLC
999            // slice loop already had its own bound.)
1000            if addr >= total {
1001                return Err(MbError::Truncated);
1002            }
1003            self.row_hook(addr);
1004            let (mbx, mby) = (addr % mbw, addr / mbw);
1005            let left = (mbx > 0).then(|| addr - 1);
1006            let top = (mby > 0).then(|| addr - mbw);
1007
1008            // Brick 3.1/3.2: P-slice mb_skip_flag, then mb_type (P mb_type is neighbour-
1009            // independent; intra sub-types map to the I dispatch below).
1010            let mb_type;
1011            if is_p {
1012                let sctx = 11
1013                    + left.map_or(0, |a| (!mb_skip[a]) as usize)
1014                    + top.map_or(0, |a| (!mb_skip[a]) as usize);
1015                if parse_mb_skip_cabac(&mut cab, sctx) {
1016                    mb_skip[addr] = true;
1017                    cat[addr] = 100; // inter (not I16/PCM) for neighbour context
1018                    last_delta_qp = 0; // skip codes no mb_qp_delta → delta ctxInc resets
1019                    // P_Skip recon reuses the entropy-free CAVLC primitive verbatim: it
1020                    // takes no bit-reader (skip has no coded syntax past the flag), just
1021                    // predicts the skip MV, motion-compensates, and commits the grid.
1022                    self.decode_p_skip(mbx, mby)?;
1023                    self.mb_qp[addr] = self.cur_qp; // skip inherits QPy
1024                    let eos = cab.decode_terminate();
1025                    addr += 1;
1026                    if eos || addr >= total {
1027                        break;
1028                    }
1029                    continue;
1030                }
1031                let mbt = parse_mb_type_p_cabac(&mut cab);
1032                if mbt == 30 {
1033                    return Err(MbError::Unsupported("CABAC I_PCM (WIP)"));
1034                }
1035                if mbt <= 3 {
1036                    let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbP);
1037                    // noSubMbPartSizeLessThan8x8Flag (spec 7.3.5): P_8x8 permits the
1038                    // 8x8 transform only when every sub-partition is itself 8x8.
1039                    let mut allow8 = true;
1040                    // Inter MB (Bricks 3.3/3.4/3.5). 1-ref stream → ref_idx not coded (ref=0).
1041                    // Build the 30-entry mvd/ref neighbour cache (openh264 WelsFillCacheInterCabac).
1042                    let mut mvdc = [[0i16; 2]; 30];
1043                    let mut refc = [-1i8; 30];
1044                    if let Some(l) = left {
1045                        for (ci, bi) in [(6usize, 3usize), (12, 7), (18, 11), (24, 15)] {
1046                            refc[ci] = mb_ref[l][bi];
1047                            mvdc[ci] = mb_mvd[l][bi];
1048                        }
1049                    }
1050                    if let Some(t) = top {
1051                        for (ci, bi) in [(1usize, 12usize), (2, 13), (3, 14), (4, 15)] {
1052                            refc[ci] = mb_ref[t][bi];
1053                            mvdc[ci] = mb_mvd[t][bi];
1054                        }
1055                    }
1056                    if mbx > 0 && mby > 0 {
1057                        let a = addr - mbw - 1;
1058                        (refc[0], mvdc[0]) = (mb_ref[a][15], mb_mvd[a][15]);
1059                    }
1060                    if mby > 0 && mbx + 1 < mbw {
1061                        let a = addr - mbw + 1;
1062                        (refc[5], mvdc[5]) = (mb_ref[a][12], mb_mvd[a][12]);
1063                    }
1064                    let mut mmvd = [[0i16; 2]; 16];
1065                    let mut mref = [0i8; 16];
1066                    // mb_pred (spec 7.3.5.1): all ref_idx_l0 FIRST (only when >1 active
1067                    // ref), then all mvd + ref-aware predict + commit. `refidx!` parses one
1068                    // partition's ref_idx (ctxIdxOffset 54, ctx from neighbour refc) and
1069                    // seeds refc so a later partition's ref/mvd context sees it — mirror
1070                    // of the encoder's two-phase emit_mb_cabac_p_inter.
1071                    macro_rules! refidx {
1072                        ($pi:expr, $zb:expr) => {{
1073                            if self.num_ref_active > 1 {
1074                                let s = CACHE30[$pi];
1075                                let c0 = (refc[s - 1] > 0) as usize + 2 * (refc[s - 6] > 0) as usize;
1076                                let r = parse_ref_idx_cabac(&mut cab, c0);
1077                                for &zb in $zb.iter() {
1078                                    refc[CACHE30[zb]] = r;
1079                                }
1080                                r
1081                            } else {
1082                                0i8
1083                            }
1084                        }};
1085                    }
1086                    macro_rules! part {
1087                        ($pi:expr, $zb:expr, $pred:expr, $rx:expr, $ry:expr, $rw:expr, $rh:expr, $refi:expr) => {{
1088                            let (mvx, mvy) = parse_mvd_partition(&mut cab, $pi, $zb, &mut mvdc, &mut refc, &mut mmvd, &mut mref, $refi);
1089                            let [na, nb, nc] = self.mv_neighbors_block(
1090                                (mbx * 4 + $rx / 4) as isize,
1091                                (mby * 4 + $ry / 4) as isize,
1092                                ($rw / 4) as isize,
1093                            );
1094                            let pmv = $pred(na, nb, nc);
1095                            self.commit_inter_grid(mbx, mby, $rx, $ry, $rw, $rh, (pmv.0 + mvx, pmv.1 + mvy), $refi);
1096                        }};
1097                    }
1098                    match mbt {
1099                        0 => {
1100                            let r0 = refidx!(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
1101                            part!(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], |a, b, c| predict_partition_mv(0, 0, a, b, c, r0 as i32), 0, 0, 16, 16, r0);
1102                        }
1103                        1 => {
1104                            let r0 = refidx!(0, &[0, 1, 2, 3, 4, 5, 6, 7]);
1105                            let r1 = refidx!(8, &[8, 9, 10, 11, 12, 13, 14, 15]);
1106                            part!(0, &[0, 1, 2, 3, 4, 5, 6, 7], |a, b, c| predict_partition_mv(1, 0, a, b, c, r0 as i32), 0, 0, 16, 8, r0);
1107                            part!(8, &[8, 9, 10, 11, 12, 13, 14, 15], |a, b, c| predict_partition_mv(1, 1, a, b, c, r1 as i32), 0, 8, 16, 8, r1);
1108                        }
1109                        2 => {
1110                            let r0 = refidx!(0, &[0, 1, 2, 3, 8, 9, 10, 11]);
1111                            let r1 = refidx!(4, &[4, 5, 6, 7, 12, 13, 14, 15]);
1112                            part!(0, &[0, 1, 2, 3, 8, 9, 10, 11], |a, b, c| predict_partition_mv(2, 0, a, b, c, r0 as i32), 0, 0, 8, 16, r0);
1113                            part!(4, &[4, 5, 6, 7, 12, 13, 14, 15], |a, b, c| predict_partition_mv(2, 1, a, b, c, r1 as i32), 8, 0, 8, 16, r1);
1114                        }
1115                        _ => {
1116                            // P_8x8: 4 sub_mb_types, then 4 ref_idx (one per 8×8), then mvd.
1117                            let mut subt = [0u32; 4];
1118                            for st in &mut subt {
1119                                *st = parse_sub_mb_type_p_cabac(&mut cab);
1120                            }
1121                            allow8 = subt.iter().all(|&t| t == 0);
1122                            let mut pr = [0i8; 4];
1123                            for (i, r) in pr.iter_mut().enumerate() {
1124                                let b = i * 4;
1125                                *r = refidx!(b, &[b, b + 1, b + 2, b + 3]);
1126                            }
1127                            for i in 0..4usize {
1128                                let b = i * 4;
1129                                let (ox, oy) = ((i % 2) * 8, (i / 2) * 8); // 8×8 pixel origin in MB
1130                                let ri = pr[i];
1131                                match subt[i] {
1132                                    0 => part!(b, &[b, b + 1, b + 2, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 8, 8, ri),
1133                                    1 => {
1134                                        part!(b, &[b, b + 1], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 8, 4, ri);
1135                                        part!(b + 2, &[b + 2, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy + 4, 8, 4, ri);
1136                                    }
1137                                    2 => {
1138                                        part!(b, &[b, b + 2], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 4, 8, ri);
1139                                        part!(b + 1, &[b + 1, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox + 4, oy, 4, 8, ri);
1140                                    }
1141                                    _ => {
1142                                        for j in 0..4usize {
1143                                            let (sx, sy) = ((j % 2) * 4, (j / 2) * 4);
1144                                            part!(b + j, &[b + j], |a, b, c| predict_mv(a, b, c, ri as i32), ox + sx, oy + sy, 4, 4, ri);
1145                                        }
1146                                    }
1147                                }
1148                            }
1149                        }
1150                    }
1151                    mb_ref[addr] = mref;
1152                    mb_mvd[addr] = mmvd;
1153                    cat[addr] = 100;
1154
1155                    // Inter cbp + residual (is_intra = false → cbf default nA=nB=0).
1156                    let cbp = parse_cbp_cabac(&mut cab, top.map(|a| mb_cbp[a]), left.map(|a| mb_cbp[a]));
1157                    mb_cbp[addr] = cbp as u8;
1158                    // H-49: an INTER macroblock carries transform_size_8x8_flag AFTER cbp
1159                    // (spec 7.3.5), present only when CodedBlockPatternLuma > 0 and
1160                    // noSubMbPartSizeLessThan8x8Flag. Same context as the intra read.
1161                    let t8 = self.transform_8x8_mode && (cbp & 15) != 0 && allow8 && {
1162                        let a = left.map_or(0, |x| self.mb_t8x8[x] as usize);
1163                        let b = top.map_or(0, |x| self.mb_t8x8[x] as usize);
1164                        cab.decode_decision(399 + a + b) != 0
1165                    };
1166                    self.mb_t8x8[addr] = t8;
1167                    let mut luma8 = [[0i32; 64]; 4]; // per 8x8 block, 8x8 scan order (t8)
1168                    let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
1169                    let mut nzc = [0xffu8; 48];
1170                    if let Some(t) = top {
1171                        let tnz = mb_nzc[t];
1172                        nzc[1..5].copy_from_slice(&tnz[12..16]);
1173                        (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1174                        (nzc[6], nzc[7], nzc[30], nzc[31]) = (tnz[20], tnz[21], tnz[22], tnz[23]);
1175                    }
1176                    if let Some(l) = left {
1177                        let lnz = mb_nzc[l];
1178                        (nzc[8], nzc[16], nzc[24], nzc[32]) = (lnz[3], lnz[7], lnz[11], lnz[15]);
1179                        (nzc[13], nzc[21], nzc[37], nzc[45]) = (lnz[17], lnz[21], lnz[19], lnz[23]);
1180                    }
1181                    let mut cbfdc = 0u16;
1182                    let mut nnzs = [0u8; 24]; // parsed totalCoeff per block (see add_inter_residual)
1183                    let mut luma_scan = [[0i32; 16]; 16]; // per z-order 4×4 block (scan order)
1184                    let mut cdc = [[0i32; 4]; 2]; // chroma DC per plane (scan order)
1185                    let mut cac = [[[0i32; 16]; 4]; 2]; // chroma AC per plane, per 4×4 block
1186                    // A cbp==0 MB codes no mb_qp_delta → the next MB's delta ctxInc sees 0.
1187                    if cbp == 0 {
1188                        last_delta_qp = 0;
1189                    }
1190                    if cbp != 0 {
1191                        let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1192                        let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1193                        self.step_qp(qpd);
1194                        for id8 in 0..4usize {
1195                            if cbp_luma & (1 << id8) != 0 {
1196                                if t8 {
1197                                    nnzs[id8 * 4] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, false, ndc, &mut luma8[id8]) as u8;
1198                                } else {
1199                                    for id4 in 0..4usize {
1200                                        let iz = id8 * 4 + id4;
1201                                        nnzs[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, false, ndc, &mut luma_scan[iz]) as u8;
1202                                    }
1203                                }
1204                            } else {
1205                                for k in 0..4 {
1206                                    nzc[NZC_CACHE[id8 * 4 + k]] = 0;
1207                                }
1208                            }
1209                        }
1210                        if cbp_chroma >= 1 {
1211                            for i in 0..2usize {
1212                                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, false, ndc, &mut cdc[i]);
1213                            }
1214                        }
1215                        if cbp_chroma == 2 {
1216                            for i in 0..2usize {
1217                                for id4 in 0..4usize {
1218                                    nnzs[16 + i * 4 + id4] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, false, ndc, &mut cac[i][id4]) as u8;
1219                                }
1220                            }
1221                        }
1222                    }
1223                    self.mb_qp[addr] = self.cur_qp;
1224                    cbf_dc[addr] = cbfdc;
1225                    let _sc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecStateCache);
1226                    let mut mn = [0u8; 24];
1227                    for k in 0..4 {
1228                        mn[k] = nzc[9 + k];
1229                        mn[4 + k] = nzc[17 + k];
1230                        mn[8 + k] = nzc[25 + k];
1231                        mn[12 + k] = nzc[33 + k];
1232                    }
1233                    (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
1234                    (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
1235                    // A block whose residual was skipped (cbp bit clear / no chroma AC)
1236                    // has 0 coeffs, not "unavailable" — export 0 so an intra neighbour's
1237                    // CBF ctxInc reads 0 (not the 0xff sentinel → is_intra default).
1238                    for v in mn.iter_mut() {
1239                        if *v == 0xff {
1240                            *v = 0;
1241                        }
1242                    }
1243                    mb_nzc[addr] = mn;
1244                    drop(_sc);
1245
1246                    if self.refs.is_empty() {
1247                        return Err(MbError::Unsupported("inter without reference"));
1248                    }
1249                    let job = PInterJob {
1250                        mbx,
1251                        mby,
1252                        t8,
1253                        qp: self.cur_qp,
1254                        cbp_chroma,
1255                        luma_scan,
1256                        luma8,
1257                        cdc,
1258                        cac,
1259                        nnzs,
1260                    };
1261                    if self.edc_active {
1262                        self.edc_jobs.push(EdcJob::Inter(Box::new(job)));
1263                    } else {
1264                        self.recon_p_inter(&job);
1265                    }
1266
1267                    let eos = cab.decode_terminate();
1268                    addr += 1;
1269                    if eos || addr >= total {
1270                        break;
1271                    }
1272                    continue;
1273                }
1274                mb_type = mbt - 5; // 5→0 (I_4x4), 6..29→1..24 (I_16x16)
1275            } else if self.is_b {
1276                self.edc_flush(); // B path stays inline in E1
1277                let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbB);
1278                // noSubMbPartSizeLessThan8x8Flag for B: direct MBs qualify only under
1279                // direct_8x8_inference_flag; B_8x8 needs every sub-partition 8x8.
1280                let mut allow8 = true;
1281                // B-slice: mb_skip_flag (ctx 24 + neighbour-not-skip), then B mb_type.
1282                let sctx = 24
1283                    + left.map_or(0, |a| (!mb_skip[a]) as usize)
1284                    + top.map_or(0, |a| (!mb_skip[a]) as usize);
1285                if parse_mb_skip_cabac(&mut cab, sctx) {
1286                    mb_skip[addr] = true;
1287                    cat[addr] = 100;
1288                    mb_direct[addr] = true;
1289                    last_delta_qp = 0; // skip codes no mb_qp_delta → delta ctxInc resets
1290                    // B_Skip recon reuses the entropy-free CAVLC primitive (spatial/temporal
1291                    // direct with no residual), which also commits the motion grid.
1292                    self.decode_b_skip(mbx, mby)?;
1293                    self.mb_qp[addr] = self.cur_qp;
1294                    // Skip/direct blocks contribute mvd 0 to a later MB's mvd ctxInc; the
1295                    // ref stays in-list so |mvd|=0 is summed (same result either way).
1296                    mb_ref[addr] = [0i8; 16];
1297                    mb_ref1[addr] = [0i8; 16];
1298                    let eos = cab.decode_terminate();
1299                    addr += 1;
1300                    if eos || addr >= total {
1301                        break;
1302                    }
1303                    continue;
1304                }
1305                let bci = left.map_or(0, |a| (!mb_direct[a]) as usize)
1306                    + top.map_or(0, |a| (!mb_direct[a]) as usize);
1307                let bmt = parse_mb_type_b_cabac(&mut cab, bci);
1308                if bmt < 23 {
1309                    // ---- B inter: parse motion (mvd L0/L1; ref not coded on this 1-ref
1310                    // stream) + residual. Recon (b_mc/direct) deferred to B.3. ----
1311                    let mut mvdc0 = [[0i16; 2]; 30];
1312                    let mut refc0 = [-1i8; 30];
1313                    let mut mvdc1 = [[0i16; 2]; 30];
1314                    let mut refc1 = [-1i8; 30];
1315                    // WelsFillCacheInterCabac, per list (L0 = mb_ref/mb_mvd, L1 = mb_ref1/mb_mvd1).
1316                    macro_rules! fill {
1317                        ($mrf:expr, $mmv:expr, $rc:expr, $mc:expr) => {{
1318                            if let Some(l) = left {
1319                                for (ci, bi) in [(6usize, 3usize), (12, 7), (18, 11), (24, 15)] {
1320                                    $rc[ci] = $mrf[l][bi];
1321                                    $mc[ci] = $mmv[l][bi];
1322                                }
1323                            }
1324                            if let Some(t) = top {
1325                                for (ci, bi) in [(1usize, 12usize), (2, 13), (3, 14), (4, 15)] {
1326                                    $rc[ci] = $mrf[t][bi];
1327                                    $mc[ci] = $mmv[t][bi];
1328                                }
1329                            }
1330                            if mbx > 0 && mby > 0 {
1331                                let a = addr - mbw - 1;
1332                                ($rc[0], $mc[0]) = ($mrf[a][15], $mmv[a][15]);
1333                            }
1334                            if mby > 0 && mbx + 1 < mbw {
1335                                let a = addr - mbw + 1;
1336                                ($rc[5], $mc[5]) = ($mrf[a][12], $mmv[a][12]);
1337                            }
1338                        }};
1339                    }
1340                    fill!(mb_ref, mb_mvd, refc0, mvdc0);
1341                    fill!(mb_ref1, mb_mvd1, refc1, mvdc1);
1342                    let mut mmvd0 = [[0i16; 2]; 16];
1343                    let mut mref0 = [-1i8; 16];
1344                    let mut mmvd1 = [[0i16; 2]; 16];
1345                    let mut mref1 = [-1i8; 16];
1346                    if self.refs.is_empty() || self.refs1.is_empty() {
1347                        return Err(MbError::Unsupported("B without references"));
1348                    }
1349                    // Recon (mirrors CAVLC decode_b_mb / decode_b_8x8): predict each list's
1350                    // MV off the committed grid + the CABAC-parsed mvd, commit, MC (bi-pred
1351                    // blend), then add the residual. Prediction reads mmvd0/mmvd1 (the mvd
1352                    // per raster block, splatted during the parse above).
1353                    let mut pred_y = [0u8; 256];
1354                    let mut c_pred = [[0u8; 64]; 2];
1355
1356                    if bmt == 0 {
1357                        // B_Direct_16x16: no coded motion. A direct block contributes mvd 0
1358                        // to a later MB's mvd ctxInc with its ref in-list (|0| summed).
1359                        mb_direct[addr] = true;
1360                        allow8 = self.direct_8x8_inference;
1361                        (mref0, mref1) = ([0i8; 16], [0i8; 16]);
1362                        self.decode_b_direct(mbx, mby, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
1363                    } else if bmt == 22 {
1364                        // B_8x8: 4 sub_mb_types, (ref not coded on 1-ref), then mvd
1365                        // list-major → sub-MB → sub-partition (openh264 order).
1366                        let mut subt = [0u32; 4];
1367                        for s in &mut subt {
1368                            *s = parse_sub_mb_type_b_cabac(&mut cab);
1369                        }
1370                        allow8 = subt.iter().all(|&t| if t == 0 { self.direct_8x8_inference } else { (1..=3).contains(&t) });
1371                        // A direct sub-partition contributes mvd 0 / ref in-list to the
1372                        // ctxInc — both the per-MB export and the within-MB 30-cache that a
1373                        // later (non-direct) sub in this MB reads.
1374                        for i in 0..4usize {
1375                            if subt[i] == 0 {
1376                                let b = i * 4;
1377                                for &zb in &[b, b + 1, b + 2, b + 3] {
1378                                    (mref0[G_SCAN4[zb]], mref1[G_SCAN4[zb]]) = (0, 0);
1379                                    (refc0[CACHE30[zb]], refc1[CACHE30[zb]]) = (0, 0);
1380                                }
1381                            }
1382                        }
1383                        // ref_idx_l0 for all four 8x8s, then ref_idx_l1, then the mvds
1384                        // (spec 7.3.5.2 sub_mb_pred). ONE ref per 8x8 -- never per
1385                        // sub-partition -- and B_Direct_8x8 codes none.
1386                        let mut sref = [[0i8; 2]; 4]; // [sub-MB][list]
1387                        for list in 0..2usize {
1388                            let active = if list == 0 { self.num_ref_active } else { self.num_ref_active1 };
1389                            if active <= 1 {
1390                                continue;
1391                            }
1392                            let rc = if list == 0 { &mut refc0 } else { &mut refc1 };
1393                            for i in 0..4usize {
1394                                let st = subt[i];
1395                                if st == 0 || !b_sub_uses(st, list) {
1396                                    continue;
1397                                }
1398                                let b = i * 4;
1399                                let s = CACHE30[b];
1400                                let c0 = (rc[s - 1] > 0) as usize + 2 * (rc[s - 6] > 0) as usize;
1401                                let r = parse_ref_idx_cabac(&mut cab, c0);
1402                                for &zb in &[b, b + 1, b + 2, b + 3] {
1403                                    rc[CACHE30[zb]] = r;
1404                                }
1405                                sref[i][list] = r;
1406                            }
1407                        }
1408                        for list in 0..2usize {
1409                            let (mmv, mrf, mc, rc) = if list == 0 {
1410                                (&mut mmvd0, &mut mref0, &mut mvdc0, &mut refc0)
1411                            } else {
1412                                (&mut mmvd1, &mut mref1, &mut mvdc1, &mut refc1)
1413                            };
1414                            for i in 0..4usize {
1415                                let st = subt[i];
1416                                if st == 0 || !b_sub_uses(st, list) {
1417                                    continue;
1418                                }
1419                                let b = i * 4;
1420                                for &(sx, sy, sw, sh) in b_sub_parts(st) {
1421                                    let mut zb = [0usize; 4];
1422                                    let mut n = 0;
1423                                    for ly in sy / 4..sy / 4 + sh / 4 {
1424                                        for lx in sx / 4..sx / 4 + sw / 4 {
1425                                            zb[n] = b + ly * 2 + lx;
1426                                            n += 1;
1427                                        }
1428                                    }
1429                                    parse_mvd_partition(&mut cab, zb[0], &zb[..n], mc, rc, mmv, mrf, sref[i][list]);
1430                                }
1431                            }
1432                        }
1433                        // Recon each 8×8: direct sub → decode_b_direct; else per sub-part
1434                        // predict (median) + commit + MC.
1435                        for (p, &st) in subt.iter().enumerate() {
1436                            let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
1437                            if st == 0 {
1438                                self.decode_b_direct(mbx, mby, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred);
1439                                continue;
1440                            }
1441                            for &(sx, sy, sw, sh) in b_sub_parts(st) {
1442                                let (px, py) = (b8x + sx, b8y + sy);
1443                                let mut mv = [(0i32, 0i32); 2];
1444                                for list in 0..2usize {
1445                                    if b_sub_uses(st, list) {
1446                                        let d = if list == 0 { mmvd0 } else { mmvd1 }[(py / 4) * 4 + px / 4];
1447                                        let n = self.mv_neighbors_list((mbx * 4 + px / 4) as isize, (mby * 4 + py / 4) as isize, (sw / 4) as isize, list);
1448                                        let pmv = predict_mv(n[0], n[1], n[2], sref[p][list] as i32);
1449                                        mv[list] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
1450                                    }
1451                                }
1452                                let refi0 = if b_sub_uses(st, 0) { sref[p][0] as i32 } else { -1 };
1453                                let refi1 = if b_sub_uses(st, 1) { sref[p][1] as i32 } else { -1 };
1454                                self.b_set_motion(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1]);
1455                                self.b_mc(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
1456                            }
1457                        }
1458                    } else {
1459                        let (layout, mvmode, preds) = b_inter_layout(bmt);
1460                        let parts: &[(usize, &[usize])] = match mvmode {
1461                            0 => &[(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])],
1462                            1 => &[(0, &[0, 1, 2, 3, 4, 5, 6, 7]), (8, &[8, 9, 10, 11, 12, 13, 14, 15])],
1463                            _ => &[(0, &[0, 1, 2, 3, 8, 9, 10, 11]), (4, &[4, 5, 6, 7, 12, 13, 14, 15])],
1464                        };
1465                        // ref_idx_l0 for EVERY partition, then ref_idx_l1, then the mvds
1466                        // (spec 7.3.5.1 macroblock_prediction). This was missing entirely
1467                        // -- the B path assumed a single reference -- so any B slice with
1468                        // more than one active reference in either list desynced the
1469                        // arithmetic decoder at the first partition that codes a ref_idx,
1470                        // and the slice ended early at a phantom end_of_slice_flag.
1471                        let mut pref = [[0i8; 2]; 2]; // [partition][list]
1472                        for list in 0..2usize {
1473                            let active = if list == 0 { self.num_ref_active } else { self.num_ref_active1 };
1474                            if active <= 1 {
1475                                continue;
1476                            }
1477                            let rc = if list == 0 { &mut refc0 } else { &mut refc1 };
1478                            for (p, &(pidx, zb)) in parts.iter().enumerate() {
1479                                if !preds[p].uses(list) {
1480                                    continue;
1481                                }
1482                                let s = CACHE30[pidx];
1483                                let c0 = (rc[s - 1] > 0) as usize + 2 * (rc[s - 6] > 0) as usize;
1484                                let r = parse_ref_idx_cabac(&mut cab, c0);
1485                                // Seed the cache so a later partition's ref/mvd ctxInc sees it.
1486                                for &zbi in zb.iter() {
1487                                    rc[CACHE30[zbi]] = r;
1488                                }
1489                                pref[p][list] = r;
1490                            }
1491                        }
1492                        // mvd parse order: list-major, partition-minor (openh264
1493                        // ParseInterBMotionInfoCabac); the ctxInc reads the same-list cache.
1494                        for list in 0..2usize {
1495                            let (mmv, mrf, mc, rc) = if list == 0 {
1496                                (&mut mmvd0, &mut mref0, &mut mvdc0, &mut refc0)
1497                            } else {
1498                                (&mut mmvd1, &mut mref1, &mut mvdc1, &mut refc1)
1499                            };
1500                            for (p, &(pidx, zb)) in parts.iter().enumerate() {
1501                                if preds[p].uses(list) {
1502                                    parse_mvd_partition(&mut cab, pidx, zb, mc, rc, mmv, mrf, pref[p][list]);
1503                                }
1504                            }
1505                        }
1506                        // Per-partition recon: predict each list's MV, commit, MC.
1507                        for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
1508                            let mut mv = [(0i32, 0i32); 2];
1509                            for list in 0..2usize {
1510                                if preds[p].uses(list) {
1511                                    let d = if list == 0 { mmvd0 } else { mmvd1 }[(ry / 4) * 4 + rx / 4];
1512                                    let n = self.mv_neighbors_list((mbx * 4 + rx / 4) as isize, (mby * 4 + ry / 4) as isize, (rw / 4) as isize, list);
1513                                    let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], pref[p][list] as i32);
1514                                    mv[list] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
1515                                }
1516                            }
1517                            let refi0 = if preds[p].uses(0) { pref[p][0] as i32 } else { -1 };
1518                            let refi1 = if preds[p].uses(1) { pref[p][1] as i32 } else { -1 };
1519                            self.b_set_motion(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1]);
1520                            // Proper spec bi-prediction (average of L0+L1). NOTE: the CAVLC
1521                            // decode_b_mb replicates an openh264 bug here for a Bi 16×8/8×16
1522                            // partition; our pixel gate is ffmpeg (spec-correct), so we do NOT.
1523                            self.b_mc(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
1524                        }
1525                    }
1526                    mb_ref[addr] = mref0;
1527                    mb_mvd[addr] = mmvd0;
1528                    mb_ref1[addr] = mref1;
1529                    mb_mvd1[addr] = mmvd1;
1530                    cat[addr] = 100;
1531
1532                    // Inter cbp + residual (identical to the P path).
1533                    let cbp = parse_cbp_cabac(&mut cab, top.map(|a| mb_cbp[a]), left.map(|a| mb_cbp[a]));
1534                    mb_cbp[addr] = cbp as u8;
1535                    // H-49: an INTER macroblock carries transform_size_8x8_flag AFTER cbp
1536                    // (spec 7.3.5), present only when CodedBlockPatternLuma > 0 and
1537                    // noSubMbPartSizeLessThan8x8Flag. Same context as the intra read.
1538                    let t8 = self.transform_8x8_mode && (cbp & 15) != 0 && allow8 && {
1539                        let a = left.map_or(0, |x| self.mb_t8x8[x] as usize);
1540                        let b = top.map_or(0, |x| self.mb_t8x8[x] as usize);
1541                        cab.decode_decision(399 + a + b) != 0
1542                    };
1543                    self.mb_t8x8[addr] = t8;
1544                    let mut luma8 = [[0i32; 64]; 4]; // per 8x8 block, 8x8 scan order (t8)
1545                    let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
1546                    let mut nzc = [0xffu8; 48];
1547                    if let Some(t) = top {
1548                        let tnz = mb_nzc[t];
1549                        nzc[1..5].copy_from_slice(&tnz[12..16]);
1550                        (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1551                        (nzc[6], nzc[7], nzc[30], nzc[31]) = (tnz[20], tnz[21], tnz[22], tnz[23]);
1552                    }
1553                    if let Some(l) = left {
1554                        let lnz = mb_nzc[l];
1555                        (nzc[8], nzc[16], nzc[24], nzc[32]) = (lnz[3], lnz[7], lnz[11], lnz[15]);
1556                        (nzc[13], nzc[21], nzc[37], nzc[45]) = (lnz[17], lnz[21], lnz[19], lnz[23]);
1557                    }
1558                    let mut cbfdc = 0u16;
1559                    let mut nnzs = [0u8; 24]; // parsed totalCoeff per block
1560                    let mut luma_scan = [[0i32; 16]; 16];
1561                    let mut cdc = [[0i32; 4]; 2];
1562                    let mut cac = [[[0i32; 16]; 4]; 2];
1563                    if cbp == 0 {
1564                        last_delta_qp = 0;
1565                    }
1566                    if cbp != 0 {
1567                        let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1568                        let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1569                        self.step_qp(qpd);
1570                        for id8 in 0..4usize {
1571                            if cbp_luma & (1 << id8) != 0 {
1572                                if t8 {
1573                                    nnzs[id8 * 4] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, false, ndc, &mut luma8[id8]) as u8;
1574                                } else {
1575                                    for id4 in 0..4usize {
1576                                        let iz = id8 * 4 + id4;
1577                                        nnzs[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, false, ndc, &mut luma_scan[iz]) as u8;
1578                                    }
1579                                }
1580                            } else {
1581                                for k in 0..4 {
1582                                    nzc[NZC_CACHE[id8 * 4 + k]] = 0;
1583                                }
1584                            }
1585                        }
1586                        if cbp_chroma >= 1 {
1587                            for i in 0..2usize {
1588                                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, false, ndc, &mut cdc[i]);
1589                            }
1590                        }
1591                        if cbp_chroma == 2 {
1592                            for i in 0..2usize {
1593                                for id4 in 0..4usize {
1594                                    nnzs[16 + i * 4 + id4] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, false, ndc, &mut cac[i][id4]) as u8;
1595                                }
1596                            }
1597                        }
1598                    }
1599                    self.mb_qp[addr] = self.cur_qp;
1600                    cbf_dc[addr] = cbfdc;
1601                    let _sc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecStateCache);
1602                    let mut mn = [0u8; 24];
1603                    for k in 0..4 {
1604                        mn[k] = nzc[9 + k];
1605                        mn[4 + k] = nzc[17 + k];
1606                        mn[8 + k] = nzc[25 + k];
1607                        mn[12 + k] = nzc[33 + k];
1608                    }
1609                    (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
1610                    (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
1611                    // A block whose residual was skipped (cbp bit clear / no chroma AC)
1612                    // has 0 coeffs, not "unavailable" — export 0 so an intra neighbour's
1613                    // CBF ctxInc reads 0 (not the 0xff sentinel → is_intra default).
1614                    for v in mn.iter_mut() {
1615                        if *v == 0xff {
1616                            *v = 0;
1617                        }
1618                    }
1619                    mb_nzc[addr] = mn;
1620                    drop(_sc);
1621                    self.add_inter_residual(mbx, mby, &pred_y, &c_pred, &luma_scan, if t8 { Some(&luma8) } else { None }, &cdc, &cac, cbp_chroma, &nnzs);
1622
1623                    let eos = cab.decode_terminate();
1624                    addr += 1;
1625                    if eos || addr >= total {
1626                        break;
1627                    }
1628                    continue;
1629                }
1630                mb_type = bmt - 23; // 23→0 (I_4x4), 24..=47→1..24 (I_16x16), 48→25 (PCM)
1631                if mb_type == 25 {
1632                    return Err(MbError::Unsupported("CABAC I_PCM (WIP)"));
1633                }
1634            } else {
1635                let li = left.map_or(0, |a| (cat[a] >= 2) as usize);
1636                let ti = top.map_or(0, |a| (cat[a] >= 2) as usize);
1637                mb_type = parse_mb_type_i_cabac(&mut cab, li + ti);
1638                if mb_type == 25 {
1639                    return Err(MbError::Unsupported("CABAC I_PCM (WIP)"));
1640                }
1641            }
1642            // H-48: the CABAC intra path is INLINED in this loop, not routed through
1643            // `decode_intra_mb` (which only the CAVLC readers call) — wiring the scope
1644            // there reported ZERO calls against 480,510 intra-pred calls. All three
1645            // intra entries (I-slice, P-slice mb_type>3, B-slice bmt>=23) converge
1646            // here, so this is the one point that sees every intra MB.
1647            self.edc_flush(); // intra reconstruction reads neighbour PIXELS
1648            let _gi = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbI);
1649            // chroma-pred-mode ctxInc from neighbour chroma modes (1..=3).
1650            let cci = left.map_or(0, |a| (1..=3).contains(&cmode[a]) as usize)
1651                + top.map_or(0, |a| (1..=3).contains(&cmode[a]) as usize);
1652
1653            if mb_type != 0 {
1654                // ---- I_16x16 (mb_type 1..=24): pred mode & cbp DERIVED from mb_type;
1655                // luma DC always coded. Syntax order: intra_chroma_pred_mode, mb_qp_delta,
1656                // luma DC (Hadamard), luma AC (if cbp_luma), chroma DC/AC. Mirrors the CAVLC
1657                // decode_i16, driven by the CABAC residual. ----
1658                let mt = mb_type - 1;
1659                let pred_mode = I16Mode::from_id(mt % 4);
1660                let cbp_chroma = (mt % 12) / 4;
1661                let cbp_luma_15 = mt / 12 == 1;
1662                let chroma_mode = parse_intra_chroma_pred_mode_cabac(&mut cab, cci) as u8;
1663                cmode[addr] = chroma_mode as i32;
1664                cat[addr] = 2;
1665                mb_cbp[addr] = ((cbp_chroma as u8) << 4) | if cbp_luma_15 { 15 } else { 0 };
1666                let w4 = self.mb_w * 4;
1667
1668                let mut nzc = [0xffu8; 48];
1669                if let Some(t) = top {
1670                    let tn = mb_nzc[t];
1671                    nzc[1..5].copy_from_slice(&tn[12..16]);
1672                    (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1673                    (nzc[6], nzc[7]) = (tn[20], tn[21]);
1674                    (nzc[30], nzc[31]) = (tn[22], tn[23]);
1675                }
1676                if let Some(l) = left {
1677                    let ln = mb_nzc[l];
1678                    (nzc[8], nzc[16], nzc[24], nzc[32]) = (ln[3], ln[7], ln[11], ln[15]);
1679                    (nzc[13], nzc[21], nzc[37], nzc[45]) = (ln[17], ln[21], ln[19], ln[23]);
1680                }
1681
1682                let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1683                let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1684                self.step_qp(qpd);
1685                let qp = self.cur_qp;
1686                let mut cbfdc = 0u16;
1687
1688                // Luma DC (iz=0, category I16_LUMA_DC, 16 coeffs) → Hadamard dequant.
1689                let mut dc_scan = [0i32; 16];
1690                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 0, RP_I16_DC, true, ndc, &mut dc_scan);
1691                let recon_dc = self.dequant_luma_dc(&un_scan_4x4_dcac(&dc_scan), qp, 0);
1692
1693                // Luma AC (iz 0..15, category I16_LUMA_AC, 15 coeffs) when cbp_luma set.
1694                let mut q_blocks = [[0i32; 16]; 16];
1695                for (iz, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
1696                    let total = if cbp_luma_15 {
1697                        let mut ac = [0i32; 16];
1698                        let t = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_I16_AC, true, ndc, &mut ac);
1699                        un_scan_4x4_ac_into(&ac, &mut q_blocks[lby * 4 + lbx]);
1700                        t as u8
1701                    } else {
1702                        nzc[NZC_CACHE[iz]] = 0;
1703                        0
1704                    };
1705                    self.nnz_y[(mby * 4 + lby) * w4 + (mbx * 4 + lbx)] = total;
1706                }
1707
1708                let mut cdc = [[0i32; 4]; 2];
1709                let mut cac = [[[0i32; 16]; 4]; 2];
1710                if cbp_chroma >= 1 {
1711                    for i in 0..2usize {
1712                        parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, true, ndc, &mut cdc[i]);
1713                    }
1714                }
1715                if cbp_chroma == 2 {
1716                    for i in 0..2usize {
1717                        for id4 in 0..4usize {
1718                            parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, true, ndc, &mut cac[i][id4]);
1719                        }
1720                    }
1721                }
1722
1723                // Luma recon: 16×16 intra prediction, then per-4×4 (dequant AC + injected DC).
1724                let top_ok = mby > 0 && self.nbr_in_slice(mbx, mby - 1) && self.intra_nbr_ok(mbx * 4, mby * 4 - 1);
1725                let left_ok = mbx > 0 && self.nbr_in_slice(mbx - 1, mby) && self.intra_nbr_ok(mbx * 4 - 1, mby * 4);
1726                let (lx, ly) = (mbx * 16, mby * 16);
1727                let mut t16 = [0u8; 16];
1728                let mut l16 = [0u8; 16];
1729                if top_ok {
1730                    t16.copy_from_slice(self.top_y_row(ly, lx, 16));
1731                }
1732                if left_ok {
1733                    for i in 0..16 {
1734                        l16[i] = self.rec_y[(ly + i) * self.cw + lx - 1];
1735                    }
1736                }
1737                let corner = if top_ok && left_ok { self.top_y_px(ly, lx - 1) } else { 0 };
1738                let pred_l = luma16x16_pred(pred_mode, top_ok, left_ok, &t16, &l16, corner);
1739                for by in 0..4 {
1740                    for bx in 0..4 {
1741                        let mut deq = self.dequant(&q_blocks[by * 4 + bx], qp, 0);
1742                        deq[0] = recon_dc[by * 4 + bx];
1743                        let predb: [i32; 16] = std::array::from_fn(|i| pred_l[(by * 4 + i / 4) * 16 + (bx * 4 + i % 4)] as i32);
1744                        let s = reconstruct_4x4(&deq, &predb);
1745                        store(&mut self.rec_y, self.cw, lx + bx * 4, ly + by * 4, &s);
1746                        // I_16x16 blocks predict as DC for neighbour mode-prediction, and
1747                        // must be marked coded so a later I_4x4 MB's top-right availability
1748                        // (gather_i4 reads coded_y) sees this block as present.
1749                        self.modes_y[(mby * 4 + by) * w4 + (mbx * 4 + bx)] = 2;
1750                        self.coded_y[(mby * 4 + by) * w4 + (mbx * 4 + bx)] = true;
1751                    }
1752                }
1753                self.recon_chroma_cabac(mbx, mby, chroma_mode, &cdc, &cac, cbp_chroma, top_ok, left_ok);
1754
1755                self.mb_qp[addr] = self.cur_qp;
1756                cbf_dc[addr] = cbfdc;
1757                let mut mn = [0u8; 24];
1758                for k in 0..4 {
1759                    mn[k] = nzc[9 + k];
1760                    mn[4 + k] = nzc[17 + k];
1761                    mn[8 + k] = nzc[25 + k];
1762                    mn[12 + k] = nzc[33 + k];
1763                }
1764                (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
1765                (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
1766                for v in mn.iter_mut() {
1767                    if *v == 0xff {
1768                        *v = 0;
1769                    }
1770                }
1771                mb_nzc[addr] = mn;
1772
1773                let eos = cab.decode_terminate();
1774                addr += 1;
1775                if eos || addr >= total {
1776                    break;
1777                }
1778                continue;
1779            }
1780            cat[addr] = 0;
1781            let w4 = self.mb_w * 4;
1782            // H-49: transform_size_8x8_flag. For I_NxN it precedes the intra pred
1783            // modes (spec §7.3.5); ctxIdx = 399 + condTermFlagA + condTermFlagB,
1784            // each 1 when that neighbour MB carries the flag. Omitting this read is
1785            // what desynced every High-profile stream.
1786            let t8 = self.transform_8x8_mode && {
1787                let a = left.map_or(0, |x| self.mb_t8x8[x] as usize);
1788                let b = top.map_or(0, |x| self.mb_t8x8[x] as usize);
1789                cab.decode_decision(399 + a + b) != 0
1790            };
1791            self.mb_t8x8[addr] = t8;
1792            // Brick 2.4 + recon: derive & store each intra mode (prev-flag → the
1793            // neighbour-predicted mode, else rem), exactly as the CAVLC path.
1794            let mut modes = [2u8; 16]; // raster [lby*4+lbx]
1795            let mut modes8 = [2u8; 4]; // one per 8×8 when t8
1796            if t8 {
1797                // One mode per 8×8, broadcast to its four 4×4 cells so neighbour
1798                // mode prediction keeps working unchanged.
1799                for b8 in 0..4usize {
1800                    let (b8x, b8y) = (b8 % 2, b8 / 2);
1801                    let (bx, by) = (mbx * 4 + b8x * 2, mby * 4 + b8y * 2);
1802                    let predicted = self.predict_i4_mode(bx, by);
1803                    let rr = parse_intra4x4_pred_mode_cabac(&mut cab);
1804                    let actual = if rr < 0 {
1805                        predicted
1806                    } else {
1807                        let rem = rr as u8;
1808                        if rem < predicted { rem } else { rem + 1 }
1809                    };
1810                    modes8[b8] = actual;
1811                    for dy in 0..2 {
1812                        for dx in 0..2 {
1813                            self.modes_y[(by + dy) * w4 + (bx + dx)] = actual;
1814                            modes[(b8y * 2 + dy) * 4 + (b8x * 2 + dx)] = actual;
1815                        }
1816                    }
1817                }
1818            } else {
1819                for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
1820                    let (bx, by) = (mbx * 4 + lbx, mby * 4 + lby);
1821                    let predicted = self.predict_i4_mode(bx, by);
1822                    let rr = parse_intra4x4_pred_mode_cabac(&mut cab);
1823                    let actual = if rr < 0 {
1824                        predicted
1825                    } else {
1826                        let rem = rr as u8;
1827                        if rem < predicted { rem } else { rem + 1 }
1828                    };
1829                    self.modes_y[by * w4 + bx] = actual;
1830                    modes[lby * 4 + lbx] = actual;
1831                }
1832            }
1833            let chroma_mode = parse_intra_chroma_pred_mode_cabac(&mut cab, cci) as u8;
1834            cmode[addr] = chroma_mode as i32;
1835            let cbp = parse_cbp_cabac(&mut cab, top.map(|a| mb_cbp[a]), left.map(|a| mb_cbp[a]));
1836            mb_cbp[addr] = cbp as u8;
1837            let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
1838
1839            // Build the padded nzc cache from neighbours (openh264 WelsFillCacheNonZeroCount).
1840            let mut nzc = [0xffu8; 48];
1841            if let Some(t) = top {
1842                let tn = mb_nzc[t];
1843                nzc[1..5].copy_from_slice(&tn[12..16]);
1844                (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1845                (nzc[6], nzc[7]) = (tn[20], tn[21]);
1846                (nzc[30], nzc[31]) = (tn[22], tn[23]);
1847            }
1848            if let Some(l) = left {
1849                let ln = mb_nzc[l];
1850                (nzc[8], nzc[16], nzc[24], nzc[32]) = (ln[3], ln[7], ln[11], ln[15]);
1851                (nzc[13], nzc[21], nzc[37], nzc[45]) = (ln[17], ln[21], ln[19], ln[23]);
1852            }
1853
1854            // Bricks 2.6 + 2.7: mb_qp_delta + residual (I_4x4 luma 4×4 + chroma DC/AC),
1855            // storing scan-order coefficients for recon.
1856            let mut cbfdc = 0u16;
1857            let mut luma_scan = [[0i32; 16]; 16]; // per z-order 4×4 block
1858            let mut luma8 = [[0i32; 64]; 4]; // per 8×8 block, 8×8 scan order (t8)
1859            let mut cdc = [[0i32; 4]; 2]; // chroma DC per plane
1860            let mut cac = [[[0i32; 16]; 4]; 2]; // chroma AC per plane, per 4×4 block
1861            if cbp == 0 {
1862                last_delta_qp = 0;
1863            }
1864            if cbp != 0 {
1865                let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1866                let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1867                self.step_qp(qpd);
1868                for id8 in 0..4usize {
1869                    if cbp_luma & (1 << id8) != 0 {
1870                        if t8 {
1871                            // ctxBlockCat 5: ONE 64-coefficient block per 8×8, and no
1872                            // coded_block_flag — presence comes from cbp_luma alone.
1873                            let n = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, true, ndc, &mut luma8[id8]);
1874                            let (b8x, b8y) = (id8 % 2, id8 / 2);
1875                            for sy in 0..2 {
1876                                for sx in 0..2 {
1877                                    self.nnz_y[(mby * 4 + b8y * 2 + sy) * w4 + (mbx * 4 + b8x * 2 + sx)] = n as u8;
1878                                }
1879                            }
1880                        } else {
1881                            for id4 in 0..4usize {
1882                                let iz = id8 * 4 + id4;
1883                                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, true, ndc, &mut luma_scan[iz]);
1884                            }
1885                        }
1886                    } else {
1887                        for k in 0..4 {
1888                            nzc[NZC_CACHE[id8 * 4 + k]] = 0;
1889                        }
1890                        if t8 {
1891                            let (b8x, b8y) = (id8 % 2, id8 / 2);
1892                            for sy in 0..2 {
1893                                for sx in 0..2 {
1894                                    self.nnz_y[(mby * 4 + b8y * 2 + sy) * w4 + (mbx * 4 + b8x * 2 + sx)] = 0;
1895                                }
1896                            }
1897                        }
1898                    }
1899                }
1900                if cbp_chroma >= 1 {
1901                    for i in 0..2usize {
1902                        parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, true, ndc, &mut cdc[i]);
1903                    }
1904                }
1905                if cbp_chroma == 2 {
1906                    for i in 0..2usize {
1907                        for id4 in 0..4usize {
1908                            parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, true, ndc, &mut cac[i][id4]);
1909                        }
1910                    }
1911                }
1912            }
1913            self.mb_qp[addr] = self.cur_qp;
1914            cbf_dc[addr] = cbfdc;
1915            // Extract the MB's nzc (raster luma + chroma) for future neighbours.
1916            let mut mn = [0u8; 24];
1917            for k in 0..4 {
1918                mn[k] = nzc[9 + k];
1919                mn[4 + k] = nzc[17 + k];
1920                mn[8 + k] = nzc[25 + k];
1921                mn[12 + k] = nzc[33 + k];
1922            }
1923            (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
1924            (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
1925            for v in mn.iter_mut() {
1926                if *v == 0xff {
1927                    *v = 0;
1928                }
1929            }
1930            mb_nzc[addr] = mn;
1931
1932            // ---- Brick 4.3a: recon (I_4x4 luma + chroma) via the CAVLC-proven primitives.
1933            let qp = self.cur_qp;
1934            let top_ok = mby > 0 && self.nbr_in_slice(mbx, mby - 1) && self.intra_nbr_ok(mbx * 4, mby * 4 - 1);
1935            let left_ok = mbx > 0 && self.nbr_in_slice(mbx - 1, mby) && self.intra_nbr_ok(mbx * 4 - 1, mby * 4);
1936            if t8 {
1937                // I_8x8 recon, reusing the CAVLC-proven primitives verbatim
1938                // (un_scan_8x8 / inv_quant8 / gather_i8 / intra8x8_pred /
1939                // add_residual_8x8). Only the ENTROPY half differed.
1940                for b8 in 0..4usize {
1941                    let (b8x, b8y) = (b8 % 2, b8 / 2);
1942                    let (bx, by) = (mbx * 4 + b8x * 2, mby * 4 + b8y * 2);
1943                    let (px, py) = (bx * 4, by * 4);
1944                    let res8 = if cbp_luma & (1 << b8) != 0 {
1945                        let raster = un_scan_8x8(&luma8[b8]);
1946                        self.inv_quant8(&raster, qp, 0)
1947                    } else {
1948                        [0i32; 64]
1949                    };
1950                    let avail_top = b8y > 0 || top_ok;
1951                    let avail_left = b8x > 0 || left_ok;
1952                    let (t, l, corner, avail_corner) =
1953                        self.gather_i8(px, py, avail_top, avail_left, bx, by);
1954                    let pred =
1955                        intra8x8_pred(modes8[b8], avail_top, avail_left, avail_corner, &t, &l, corner);
1956                    let mut predb = [0i32; 64];
1957                    for i in 0..64 {
1958                        predb[i] = pred[i] as i32;
1959                    }
1960                    let recon = add_residual_8x8(&res8, &predb);
1961                    for dy in 0..8 {
1962                        for dx in 0..8 {
1963                            self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
1964                        }
1965                    }
1966                    for sy in 0..2 {
1967                        for sx in 0..2 {
1968                            self.coded_y[(by + sy) * w4 + (bx + sx)] = true;
1969                        }
1970                    }
1971                }
1972            }
1973            for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
1974                if t8 {
1975                    break;
1976                }
1977                let (bx, by) = (mbx * 4 + lbx, mby * 4 + lby);
1978                let (px, py) = (bx * 4, by * 4);
1979                let at = lby > 0 || top_ok;
1980                let al = lbx > 0 || left_ok;
1981                let qb = un_scan_4x4_dcac(&luma_scan[blk]);
1982                self.nnz_y[by * w4 + bx] = luma_scan[blk].iter().filter(|&&v| v != 0).count() as u8;
1983                let (t, l, corner) = self.gather_i4(px, py, at, al, bx, by);
1984                let pred = intra4x4_pred(modes[lby * 4 + lbx], at, al, &t, &l, corner);
1985                let predb = std::array::from_fn(|i| pred[i] as i32);
1986                let s = reconstruct_4x4(&self.dequant(&qb, qp, 0), &predb);
1987                store(&mut self.rec_y, self.cw, px, py, &s);
1988                self.coded_y[by * w4 + bx] = true;
1989            }
1990            self.recon_chroma_cabac(mbx, mby, chroma_mode, &cdc, &cac, cbp_chroma, top_ok, left_ok);
1991
1992            // Brick 2.1: end_of_slice_flag.
1993            let eos = cab.decode_terminate();
1994            addr += 1;
1995            if eos || addr >= total {
1996                break;
1997            }
1998        }
1999        if trace {
2000            eprintln!("# CABAC decoded {} MBs (of {total})", addr - first_mb);
2001        }
2002        self.edc_flush(); // slice end: no job crosses a slice boundary
2003        Ok(addr)
2004    }
2005
2006    /// CABAC chroma recon (mirrors `decode_chroma`'s reconstruction, driven by the
2007    /// CABAC-parsed DC/AC coefficients). `cdc[c]` = 2×2 DC (scan order); `cac[c][blk]`
2008    /// = 15 AC per 4×4 block (scan order).
2009    #[allow(clippy::too_many_arguments)]
2010    /// Add a CABAC-parsed inter residual to an already-built motion-comp prediction
2011    /// (`pred_y`/`c_pred`), writing the reconstruction. Shared by the P and B inter
2012    /// paths — same `reconstruct_4x4` as intra, MC output as the prediction, inter
2013    /// scaling lists (luma 3 / chroma 4+c). `luma_scan[z]`/`cdc`/`cac` are the
2014    /// scan-order coefficients; uncoded blocks are zero so recon == prediction.
2015    #[allow(clippy::too_many_arguments)]
2016    fn add_inter_residual(
2017        &mut self,
2018        mb_x: usize,
2019        mb_y: usize,
2020        pred_y: &[u8; 256],
2021        c_pred: &[[u8; 64]; 2],
2022        luma_scan: &[[i32; 16]; 16],
2023        // `Some` when the macroblock carries transform_size_8x8_flag: four 8x8
2024        // blocks in 8x8 scan order, replacing the sixteen 4x4 luma blocks.
2025        luma8: Option<&[[i32; 64]; 4]>,
2026        cdc: &[[i32; 4]; 2],
2027        cac: &[[[i32; 16]; 4]; 2],
2028        cbp_chroma: u32,
2029        // Parsed totalCoeff per block, indexed exactly as the parse's `iz`:
2030        // [0..16] luma 4x4 z-order (for t8, the 8x8 count sits at `id8*4`),
2031        // [16..24] chroma AC as `16 + c*4 + id4`. The parser already counted
2032        // every significant coefficient; re-deriving the counts here scanned
2033        // 16-64 array elements per block (~400 loads/MB) for information the
2034        // caller was holding — the diagnosis's stage-boundary re-derivation tax.
2035        nnzs: &[u8; 24],
2036    ) {
2037        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecResidAdd);
2038        let qp = self.cur_qp;
2039        let qpc = self.chroma_qp_for(qp);
2040        let (w4r, w2r) = (self.mb_w * 4, self.mb_w * 2);
2041        if let Some(l8) = luma8 {
2042            // INTER 8x8 luma: same primitives the I_8x8 and CAVLC paths use.
2043            for b8 in 0..4usize {
2044                let (b8x, b8y) = (b8 % 2, b8 / 2);
2045                let nnz = nnzs[b8 * 4];
2046                for sy in 0..2 {
2047                    for sx in 0..2 {
2048                        self.nnz_y[(mb_y * 4 + b8y * 2 + sy) * w4r + (mb_x * 4 + b8x * 2 + sx)] = nnz;
2049                    }
2050                }
2051                let res8 = if nnz == 0 {
2052                    [0i32; 64]
2053                } else {
2054                    let raster = un_scan_8x8(&l8[b8]);
2055                    // list 1 = INTER 8x8 luma scaling list (0 is the intra one).
2056                    self.inv_quant8(&raster, qp, 1)
2057                };
2058                // The 4x4 inter path marks coded_y per block; the 8x8 branch must too,
2059                // or a later intra macroblock's neighbour availability is wrong.
2060                for sy in 0..2 {
2061                    for sx in 0..2 {
2062                        self.coded_y[(mb_y * 4 + b8y * 2 + sy) * w4r + (mb_x * 4 + b8x * 2 + sx)] = true;
2063                    }
2064                }
2065                let predb: [i32; 64] =
2066                    std::array::from_fn(|i| pred_y[(b8y * 8 + i / 8) * 16 + (b8x * 8 + i % 8)] as i32);
2067                let recon = add_residual_8x8(&res8, &predb);
2068                let (px, py) = (mb_x * 16 + b8x * 8, mb_y * 16 + b8y * 8);
2069                for dy in 0..8 {
2070                    for dx in 0..8 {
2071                        self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
2072                    }
2073                }
2074            }
2075        }
2076        for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
2077            if luma8.is_some() {
2078                break;
2079            }
2080            let nnz = nnzs[blk];
2081            self.nnz_y[(mb_y * 4 + lby) * w4r + (mb_x * 4 + lbx)] = nnz;
2082            let cw = self.cw;
2083            let p_off = (lby * 4) * 16 + lbx * 4;
2084            let r_off = (mb_y * 4 + lby) * 4 * cw + (mb_x * 4 + lbx) * 4;
2085            if nnz == 0 {
2086                // Zero residual → recon == prediction EXACTLY (the integer IDCT is
2087                // linear so zeros map to zeros, and pred is already 0..=255) — copy
2088                // the pred rows straight into the plane. On real (sparse-cbp)
2089                // streams this is MOST of the 4×4 blocks.
2090                for r in 0..4 {
2091                    self.rec_y[r_off + r * cw..r_off + r * cw + 4]
2092                        .copy_from_slice(&pred_y[p_off + r * 16..p_off + r * 16 + 4]);
2093                }
2094                continue;
2095            }
2096            // DC-ONLY: the sole significant coefficient is scan position 0 (the
2097            // zig-zag starts at DC, and un_scan keeps it at raster 0), so the
2098            // whole dequant + IDCT collapses to one multiply and a flat add.
2099            if nnz == 1 && luma_scan[blk][0] != 0 {
2100                let f = self.dequant_dc4(luma_scan[blk][0], qp, 3);
2101                reconstruct_4x4_dc_into((f + 32) >> 6, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
2102            } else {
2103                // Fused un-scan + dequant over ONLY the significant coefficients,
2104                // then IDCT + add + clip straight into the plane — no `qb`, no
2105                // `deq`-from-dense, no `predb` gather, no `s`, no `store` call.
2106                //
2107                // HYBRID: the scatter walks scan positions with a data-dependent
2108                // branch per slot, which beats the branchless dense 16-multiply
2109                // loop only while the block is SPARSE. The DC/zero fast paths
2110                // already removed the sparsest blocks, so the population here
2111                // skews denser — above ~6 coefficients the dense loop wins.
2112                let deq = if nnz <= 6 {
2113                    dequant_scatter_4x4(&luma_scan[blk], nnz, 0, qp, self.scaling.as_ref().map(|sc| &sc[3]))
2114                } else {
2115                    self.dequant(&un_scan_4x4_dcac(&luma_scan[blk]), qp, 3)
2116                };
2117                reconstruct_4x4_into(&deq, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
2118            }
2119        }
2120        let mut c_dc = [[0i32; 4]; 2];
2121        if cbp_chroma != 0 {
2122            for c in 0..2 {
2123                c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 4 + c);
2124            }
2125        }
2126        let ccw = self.ccw;
2127        for c in 0..2 {
2128            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2129                let mut ac_nz = false;
2130                if cbp_chroma == 2 {
2131                    let n = nnzs[16 + c * 4 + by * 2 + bx];
2132                    self.nnz_c[c][(mb_y * 2 + by) * w2r + (mb_x * 2 + bx)] = n;
2133                    ac_nz = n != 0;
2134                }
2135                let dc = c_dc[c][by * 2 + bx];
2136                let p_off = (by * 4) * 8 + bx * 4;
2137                let r_off = (mb_y * 2 + by) * 4 * ccw + (mb_x * 2 + bx) * 4;
2138                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2139                if dc == 0 && !ac_nz {
2140                    // Zero residual (no AC, zero DC) → recon == prediction exactly.
2141                    for r in 0..4 {
2142                        plane[r_off + r * ccw..r_off + r * ccw + 4]
2143                            .copy_from_slice(&c_pred[c][p_off + r * 8..p_off + r * 8 + 4]);
2144                    }
2145                    continue;
2146                }
2147                // DC-ONLY (no coded AC — covers every cbp_chroma==1 block and the
2148                // AC-empty blocks of cbp_chroma==2): the chroma DC arrives ALREADY
2149                // dequantized, so the residual is `(dc + 32) >> 6` flat.
2150                if !ac_nz {
2151                    reconstruct_4x4_dc_into((dc + 32) >> 6, &c_pred[c], p_off, 8, plane, r_off, ccw);
2152                    continue;
2153                }
2154                // AC-only scan: index i is overall scan position i+1 (ac_shift=1).
2155                // Same sparse/dense hybrid as luma.
2156                let n = nnzs[16 + c * 4 + by * 2 + bx];
2157                let mut deq = if n <= 6 {
2158                    dequant_scatter_4x4(&cac[c][by * 2 + bx], n, 1, qpc, self.scaling.as_ref().map(|sc| &sc[4 + c]))
2159                } else {
2160                    let mut ac = [0i32; 16];
2161                    un_scan_4x4_ac_into(&cac[c][by * 2 + bx], &mut ac);
2162                    // Free-fn dequant: `self.dequant` borrows all of `self`, which
2163                    // conflicts with the live `plane` (&mut self.rec_u/v) borrow.
2164                    match &self.scaling {
2165                        Some(sc) => dequantize_weighted(&ac, qpc, &sc[4 + c]),
2166                        None => dequantize(&ac, qpc),
2167                    }
2168                };
2169                deq[0] = dc;
2170                reconstruct_4x4_into(&deq, &c_pred[c], p_off, 8, plane, r_off, ccw);
2171            }
2172        }
2173    }
2174
2175    fn recon_chroma_cabac(
2176        &mut self,
2177        mb_x: usize,
2178        mb_y: usize,
2179        chroma_mode: u8,
2180        cdc: &[[i32; 4]; 2],
2181        cac: &[[[i32; 16]; 4]; 2],
2182        cbp_chroma: u32,
2183        avail_top: bool,
2184        avail_left: bool,
2185    ) {
2186        let qpc = self.chroma_qp_for(self.cur_qp);
2187        let (cx, cy) = (mb_x * 8, mb_y * 8);
2188        let mut c_dc = [[0i32; 4]; 2];
2189        if cbp_chroma != 0 {
2190            for c in 0..2 {
2191                c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 1 + c);
2192            }
2193        }
2194        let w2 = self.mb_w * 2;
2195        for c in 0..2 {
2196            let mut ctop = [0u8; 8];
2197            let mut cleft = [0u8; 8];
2198            let mut ccorner = 0u8;
2199            {
2200                let rec_c = if c == 0 { &self.rec_u } else { &self.rec_v };
2201                if avail_top {
2202                    ctop.copy_from_slice(self.top_c_row(c, cy, cx, 8));
2203                }
2204                if avail_left {
2205                    for i in 0..8 {
2206                        cleft[i] = rec_c[(cy + i) * self.ccw + cx - 1];
2207                    }
2208                }
2209                if avail_top && avail_left {
2210                    ccorner = self.top_c_px(c, cy, cx - 1);
2211                }
2212            }
2213            let pred8 = chroma8x8_pred(chroma_mode, avail_top, avail_left, &ctop, &cleft, ccorner);
2214            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2215                let mut ac = [0i32; 16];
2216                if cbp_chroma == 2 {
2217                    un_scan_4x4_ac_into(&cac[c][by * 2 + bx], &mut ac);
2218                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] =
2219                        cac[c][by * 2 + bx].iter().filter(|&&v| v != 0).count() as u8;
2220                }
2221                let mut deq = self.dequant(&ac, qpc, 1 + c);
2222                deq[0] = c_dc[c][by * 2 + bx];
2223                let predb: [i32; 16] =
2224                    std::array::from_fn(|i| pred8[(by * 4 + i / 4) * 8 + (bx * 4 + i % 4)] as i32);
2225                let s = reconstruct_4x4(&deq, &predb);
2226                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2227                store(plane, self.ccw, cx + bx * 4, cy + by * 4, &s);
2228            }
2229        }
2230    }
2231
2232    pub fn decode_slice_data(
2233        &mut self,
2234        r: &mut BitReader,
2235        is_p: bool,
2236        first_mb: usize,
2237    ) -> Result<usize, MbError> {
2238        let total = self.mb_w * self.mb_h;
2239        self.slice_first_mb = first_mb;
2240        self.edc_active = false; // CAVLC loop has no flush hooks
2241        let mut addr = first_mb;
2242        while addr < total {
2243            self.row_hook(addr);
2244            if is_p || self.is_b {
2245                let skip_run = {
2246                    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
2247                    r.read_ue()?
2248                } as usize;
2249                for _ in 0..skip_run {
2250                    if addr >= total {
2251                        break;
2252                    }
2253                    if self.is_b {
2254                        self.decode_b_skip(addr % self.mb_w, addr / self.mb_w)?;
2255                    } else {
2256                        self.decode_p_skip(addr % self.mb_w, addr / self.mb_w)?;
2257                    }
2258                    self.mb_qp[addr] = self.cur_qp; // skip inherits QPy
2259                    addr += 1;
2260                }
2261                if addr >= total {
2262                    break;
2263                }
2264                // A trailing skip run with no following macroblock ends the slice.
2265                if skip_run > 0 && !r.more_rbsp_data() {
2266                    break;
2267                }
2268            }
2269            if self.is_b {
2270                self.decode_b_mb(r, addr % self.mb_w, addr / self.mb_w)?;
2271            } else {
2272                self.decode_mb(r, addr % self.mb_w, addr / self.mb_w, is_p)?;
2273            }
2274            self.mb_qp[addr] = self.cur_qp;
2275            addr += 1;
2276            // CAVLC slice end: no more data after this macroblock.
2277            if !r.more_rbsp_data() {
2278                break;
2279            }
2280        }
2281        self.edc_flush(); // slice end: no job crosses a slice boundary
2282        Ok(addr)
2283    }
2284
2285    fn decode_mb(
2286        &mut self,
2287        r: &mut BitReader,
2288        mb_x: usize,
2289        mb_y: usize,
2290        is_p: bool,
2291    ) -> Result<(), MbError> {
2292        let mut mb_type = {
2293            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
2294            r.read_ue()?
2295        };
2296        if is_p {
2297            // In P-slices, mb_type 0/1/2 are inter (16×16, 16×8, 8×16),
2298            // 3 = P_8x8, 4 = P_8x8ref0 (ref_idx forced 0), 5+ intra.
2299            if mb_type <= 2 {
2300                return self.decode_inter(r, mb_x, mb_y, mb_type as u8);
2301            }
2302            if mb_type == 3 || mb_type == 4 {
2303                return self.decode_p8x8(r, mb_x, mb_y, mb_type == 4);
2304            }
2305            mb_type -= 5;
2306        }
2307        self.decode_intra_mb(r, mb_x, mb_y, mb_type)
2308    }
2309
2310    /// Decodes an intra macroblock given its intra `mb_type` (0 = I_4x4,
2311    /// 1..=24 = I_16x16, 25 = I_PCM) — shared by I-, P- and B-slice paths.
2312    fn decode_intra_mb(
2313        &mut self,
2314        r: &mut BitReader,
2315        mb_x: usize,
2316        mb_y: usize,
2317        mb_type: u32,
2318    ) -> Result<(), MbError> {
2319        // H-48: this scope was DECLARED and never wired, which is precisely why the
2320        // stage table left 19.8% unaccounted — 66,120 of 475,200 macroblocks on the
2321        // reference stream are I-type and had no scope at all.
2322        let _gi = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbI);
2323        if mb_type == 0 {
2324            // I_NxN: transform_size_8x8_flag (when enabled) selects I_8x8 vs I_4x4.
2325            if self.transform_8x8_mode && r.read_bit()? {
2326                self.decode_i8x8(r, mb_x, mb_y)?;
2327            } else {
2328                self.decode_i4x4(r, mb_x, mb_y)?;
2329            }
2330        } else if (1..=24).contains(&mb_type) {
2331            self.decode_i16(r, mb_x, mb_y, mb_type - 1)?;
2332        } else if mb_type == 25 {
2333            self.decode_ipcm(r, mb_x, mb_y)?;
2334        } else {
2335            return Err(MbError::Unsupported("only I_4x4 / I_16x16 / I_PCM macroblocks"));
2336        }
2337        // Mark all luma blocks coded for the next macroblock's top-right.
2338        let w4 = self.mb_w * 4;
2339        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2340            self.coded_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = true;
2341        }
2342        Ok(())
2343    }
2344
2345    /// Reconstructs an inter macroblock (`mode` 0 = P_L0_16x16, 1 = P_16x8,
2346    /// 2 = P_8x16): parse the per-partition motion vectors and residual,
2347    /// motion-compensate each partition, and add the residual.
2348    fn decode_inter(
2349        &mut self,
2350        r: &mut BitReader,
2351        mb_x: usize,
2352        mb_y: usize,
2353        mode: u8,
2354    ) -> Result<(), MbError> {
2355        if self.refs.is_empty() {
2356            return Err(MbError::Unsupported("inter without reference"));
2357        }
2358        // DEBLOCK CLASS: mode 0 is P_L0_16x16 — ONE partition, so all 16 blocks
2359        // share a reference and motion vector and no internal edge can reach
2360        // strength 1. Internal strengths then follow from coefficients alone, i.e.
2361        // 16 nnz bytes instead of a 24-block gather across 5-7 grids. Modes 1/2
2362        // (P_16x8 / P_8x16) have two partitions with independent motion and stay
2363        // UNSET (blind path).
2364        if mode == 0 {
2365            self.mb_kind[mb_y * self.mb_w + mb_x] =
2366                rusty_h264_common::deblock::MB_KIND_INTER_UNIFORM;
2367        }
2368        // QP (qp/qpc) is bound after mb_qp_delta is read below.
2369        let w4 = self.mb_w * 4;
2370        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
2371        let num_refs = self.refs.len();
2372        let layout = inter_partitions(mode);
2373
2374        // mb_pred order (spec 7.3.5.1): all ref_idx_l0 first (only when more than
2375        // one reference is active), then all mvd_l0.
2376        let nparts = layout.len();
2377        let mut ref_idxs = [0i32; 4];
2378        if self.num_ref_active > 1 {
2379            for ri in ref_idxs[..nparts].iter_mut() {
2380                *ri = read_ref_idx(r, self.num_ref_active)?;
2381                if *ri as usize >= num_refs {
2382                    return Err(MbError::Truncated); // references a non-existent picture
2383                }
2384            }
2385        }
2386
2387        // Phase 1: per partition, ref-aware MV prediction + mvd, committing the
2388        // motion grid so a later partition predicts from an earlier one.
2389        let mut part_mv = [(0i32, (0i32, 0i32)); 4];
2390        {
2391            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::MvGrid);
2392            for (part, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
2393                let refi = ref_idxs[part];
2394                let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
2395                let [a, b, c] = self.mv_neighbors_block(pbx, pby, (rw / 4) as isize);
2396                let pmv = predict_partition_mv(mode, part, a, b, c, refi);
2397                let mvd_x = r.read_se()?;
2398                let mvd_y = r.read_se()?;
2399                let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
2400                part_mv[part] = (refi, mv);
2401                for by in ry / 4..ry / 4 + rh / 4 {
2402                    for bx in rx / 4..rx / 4 + rw / 4 {
2403                        let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
2404                        self.mv_y[idx] = mv;
2405                        self.inter_y[idx] = true;
2406                        self.ref_idx_y[idx] = refi;
2407                        self.coded_y[idx] = true;
2408                    }
2409                }
2410            }
2411        }
2412
2413        // Phase 2: motion-compensate each partition from its reference.
2414        let mut pred_y = [0u8; 256];
2415        let mut c_pred = [[0u8; 64]; 2];
2416        for (part, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
2417            let (refi, mv) = part_mv[part];
2418            let reference = &self.refs[refi as usize];
2419            let mut tmp = [0u8; 256];
2420            mc_luma_padded(&reference.py, reference.lstride(), crate::LPAD, self.cw, ch, mb_x * 16 + rx, mb_y * 16 + ry, rw, rh, mv.0, mv.1, &mut tmp);
2421            {
2422                let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
2423                restride(&mut pred_y, 16, rx, ry, &tmp, rw, rh);
2424            }
2425            let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
2426            for cc in 0..2 {
2427                let rc = if cc == 0 { &reference.pu } else { &reference.pv };
2428                let mut tc = [0u8; 64];
2429                mc_chroma_padded(rc, reference.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8 + crx, mb_y * 8 + cry, crw, crh, mv.0, mv.1, &mut tc);
2430                {
2431                    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
2432                    restride(&mut c_pred[cc], 8, crx, cry, &tc, crw, crh);
2433                }
2434            }
2435            self.weight_partition(&mut pred_y, &mut c_pred, 0, refi as usize, rx, ry, rw, rh);
2436        }
2437
2438        // 16×16/16×8/8×16 partitions are all ≥ 8×8, so the 8×8 transform is allowed.
2439        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true)
2440    }
2441
2442    /// Shared inter tail: parse `coded_block_pattern` + `mb_qp_delta`, decode the
2443    /// luma/chroma residual, and add it to the already-built motion-compensated
2444    /// prediction. Used by both the 16×16/16×8/8×16 path and `P_8x8`.
2445    fn inter_finish(
2446        &mut self,
2447        r: &mut BitReader,
2448        mb_x: usize,
2449        mb_y: usize,
2450        pred_y: &[u8; 256],
2451        c_pred: &[[u8; 64]; 2],
2452        allow_8x8: bool,
2453    ) -> Result<(), MbError> {
2454        let w4 = self.mb_w * 4;
2455        let cbp = {
2456            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
2457            read_cbp_inter(r)?
2458        };
2459        let cbp_luma = cbp & 15;
2460        let cbp_chroma = cbp >> 4;
2461        // transform_size_8x8_flag follows cbp (before mb_qp_delta) when luma has
2462        // coefficients, the 8×8 transform is enabled, and every partition ≥ 8×8.
2463        let t8x8 = cbp_luma > 0 && self.transform_8x8_mode && allow_8x8 && r.read_bit()?;
2464        if t8x8 {
2465            self.mb_t8x8[mb_y * self.mb_w + mb_x] = true;
2466        }
2467        if cbp != 0 {
2468            self.step_qp(r.read_se()?);
2469        }
2470        let (qp, qpc) = (self.cur_qp, self.chroma_qp_for(self.cur_qp));
2471
2472        // ---- luma residual ----
2473        self.nnz_cache_load(mb_x, mb_y);
2474        let mut q_blocks = [[0i32; 16]; 16];
2475        let mut luma8 = [[0i32; 64]; 4]; // 8×8-transform residuals (when t8x8)
2476        if t8x8 {
2477            for b8 in 0..4 {
2478                let (b8x, b8y) = (b8 % 2, b8 / 2);
2479                let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
2480                if cbp_luma & (1 << b8) != 0 {
2481                    let mut scan8 = [0i32; 64];
2482                    for sub in 0..4 {
2483                        let (sx, sy) = (sub % 2, sub / 2);
2484                        let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
2485                        let nc = self.nc_pred(cx, cy);
2486                        let blk = decode_residual_block(r, 16, nc)?;
2487                        let total = blk.iter().filter(|&&v| v != 0).count() as u8;
2488                        self.nnz_cache_set(cx, cy, total);
2489                        self.nnz_y[(by + sy) * w4 + (bx + sx)] = total;
2490                        for k in 0..16 {
2491                            scan8[4 * k + sub] = blk[k];
2492                        }
2493                    }
2494                    luma8[b8] = self.inv_quant8(&un_scan_8x8(&scan8), qp, 1);
2495                } else {
2496                    for sub in 0..4 {
2497                        let (sx, sy) = (sub % 2, sub / 2);
2498                        self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
2499                        self.nnz_y[(by + sy) * w4 + (bx + sx)] = 0;
2500                    }
2501                }
2502            }
2503        } else {
2504            for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
2505                let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
2506                let total = if cbp_luma & (1 << (blk / 4)) != 0 {
2507                    let nc = self.nc_pred(lbx, lby);
2508                    let scan16 = decode_residual_block(r, 16, nc)?;
2509                    q_blocks[lby * 4 + lbx] = un_scan_4x4_dcac(&scan16);
2510                    scan16.iter().filter(|&&v| v != 0).count() as u8
2511                } else {
2512                    0
2513                };
2514                self.nnz_cache_set(lbx, lby, total);
2515                self.nnz_y[by * w4 + bx] = total;
2516            }
2517        }
2518
2519        // ---- chroma residual ----
2520        let mut c_recon_dc = [[0i32; 4]; 2];
2521        if cbp_chroma != 0 {
2522            for (c, slot) in c_recon_dc.iter_mut().enumerate() {
2523                let dc = decode_residual_block(r, 4, -1)?;
2524                *slot = self.dequant_chroma_dc(&[dc[0], dc[1], dc[2], dc[3]], qpc, 4 + c);
2525            }
2526        }
2527        let mut c_q = [[[0i32; 16]; 4]; 2];
2528        if cbp_chroma == 2 {
2529            self.chroma_cache_load(mb_x, mb_y);
2530            let w2 = self.mb_w * 2;
2531            for c in 0..2 {
2532                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2533                    let nc = self.chroma_nc_pred(c, bx, by);
2534                    let ac = decode_residual_block(r, 15, nc)?;
2535                    let total = ac.iter().filter(|&&v| v != 0).count() as u8;
2536                    self.chroma_nnz_cache_set(c, bx, by, total);
2537                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = total;
2538                    un_scan_4x4_ac_into(&ac, &mut c_q[c][by * 2 + bx]);
2539                }
2540            }
2541        }
2542
2543        // ---- reconstruction (prediction already built per partition) ----
2544        if t8x8 {
2545            for b8 in 0..4 {
2546                let (b8x, b8y) = (b8 % 2, b8 / 2);
2547                let (px, py) = (b8x * 8, b8y * 8);
2548                for dy in 0..8 {
2549                    for dx in 0..8 {
2550                        let p = pred_y[(py + dy) * 16 + (px + dx)] as i32;
2551                        let v = (p + luma8[b8][dy * 8 + dx]).clamp(0, 255) as u8;
2552                        self.rec_y[(mb_y * 16 + py + dy) * self.cw + (mb_x * 16 + px + dx)] = v;
2553                    }
2554                }
2555            }
2556        } else {
2557            // Inverse 4×4 transform + add prediction, per 8×8 region (four blocks).
2558            // An UNCODED region (its `cbp_luma` bit clear) has zero residual, so the
2559            // reconstruction *is* the prediction — copy it row-wise and skip the
2560            // transform entirely (openh264's residual-skip; bit-identical). The asm
2561            // path (`WelsIDctFourT4Rec`) does butterfly + `(x+32)>>6` + add-pred +
2562            // clip for four coded blocks at once.
2563            for b8 in 0..4 {
2564                let (b8x, b8y) = (b8 % 2, b8 / 2);
2565                let pred_off = (b8y * 8) * 16 + b8x * 8;
2566                let rec_off = (mb_y * 16 + b8y * 8) * self.cw + (mb_x * 16 + b8x * 8);
2567                if cbp_luma & (1 << b8) == 0 {
2568                    for r in 0..8 {
2569                        let (s, d) = (pred_off + r * 16, rec_off + r * self.cw);
2570                        self.rec_y[d..d + 8].copy_from_slice(&pred_y[s..s + 8]);
2571                    }
2572                    continue;
2573                }
2574                #[cfg(accel)]
2575                {
2576                    let mut dct = [0i16; 64];
2577                    for (i, (sx, sy)) in [(0, 0), (1, 0), (0, 1), (1, 1)].into_iter().enumerate() {
2578                        let (lbx, lby) = (2 * b8x + sx, 2 * b8y + sy);
2579                        let deq = self.dequant(&q_blocks[lby * 4 + lbx], qp, 3);
2580                        for k in 0..16 {
2581                            dct[i * 16 + k] = deq[k] as i16;
2582                        }
2583                    }
2584                    rusty_h264_accel::idct_four_t4_rec(
2585                        &mut self.rec_y[rec_off..],
2586                        self.cw,
2587                        &pred_y[pred_off..],
2588                        16,
2589                        &dct,
2590                    );
2591                }
2592                #[cfg(not(accel))]
2593                for (sx, sy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
2594                    let (lbx, lby) = (2 * b8x + sx, 2 * b8y + sy);
2595                    let mut predb = [0i32; 16];
2596                    for dy in 0..4 {
2597                        for dx in 0..4 {
2598                            predb[dy * 4 + dx] = pred_y[(lby * 4 + dy) * 16 + (lbx * 4 + dx)] as i32;
2599                        }
2600                    }
2601                    let deq = self.dequant(&q_blocks[lby * 4 + lbx], qp, 3);
2602                    let s = reconstruct_4x4(&deq, &predb);
2603                    store(&mut self.rec_y, self.cw, mb_x * 16 + lbx * 4, mb_y * 16 + lby * 4, &s);
2604                }
2605            }
2606        }
2607        // Chroma: an uncoded MB (cbp_chroma == 0) has zero chroma residual → the
2608        // prediction is the reconstruction. Copy row-wise and skip the transform.
2609        if cbp_chroma == 0 {
2610            for c in 0..2 {
2611                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2612                for dy in 0..8 {
2613                    let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
2614                    plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
2615                }
2616            }
2617        } else {
2618            for c in 0..2 {
2619                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2620                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2621                    let mut predb = [0i32; 16];
2622                    for dy in 0..4 {
2623                        for dx in 0..4 {
2624                            predb[dy * 4 + dx] = c_pred[c][(by * 4 + dy) * 8 + (bx * 4 + dx)] as i32;
2625                        }
2626                    }
2627                    let mut deq = match &self.scaling {
2628                        Some(s) => dequantize_weighted(&c_q[c][by * 2 + bx], qpc, &s[4 + c]),
2629                        None => dequantize(&c_q[c][by * 2 + bx], qpc),
2630                    };
2631                    deq[0] = c_recon_dc[c][by * 2 + bx];
2632                    let s = reconstruct_4x4(&deq, &predb);
2633                    store(plane, self.ccw, mb_x * 8 + bx * 4, mb_y * 8 + by * 4, &s);
2634                }
2635            }
2636        }
2637
2638        // MV grid + coded flags were set per partition; mark modes as DC.
2639        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2640            self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
2641        }
2642        Ok(())
2643    }
2644
2645    // ---------------------------------------------------------------------
2646    // B-slice macroblock decoding
2647    // ---------------------------------------------------------------------
2648
2649    /// Per-list (`list` 0 or 1) MV-prediction neighbors for the block region at
2650    /// `(pbx, pby)` of width `pwb` blocks — the L0/L1 analogue of
2651    /// `mv_neighbors_block`.
2652    fn mv_neighbors_list(&self, pbx: isize, pby: isize, pwb: isize, list: usize) -> [MvNeighbor; 3] {
2653        let (w4, h4) = ((self.mb_w * 4) as isize, (self.mb_h * 4) as isize);
2654        let (mvg, refg) = if list == 0 {
2655            (&self.mv_y, &self.ref_idx_y)
2656        } else {
2657            (&self.mv1, &self.ref_idx1)
2658        };
2659        let get = |bx: isize, by: isize| -> MvNeighbor {
2660            if bx < 0
2661                || by < 0
2662                || bx >= w4
2663                || by >= h4
2664                || !self.coded_y[(by * w4 + bx) as usize]
2665                || !self.nbr_in_slice(bx as usize / 4, by as usize / 4)
2666            {
2667                MvNeighbor::NONE
2668            } else {
2669                let idx = (by * w4 + bx) as usize;
2670                MvNeighbor { available: true, mv: mvg[idx], ref_idx: refg[idx] }
2671            }
2672        };
2673        let a = get(pbx - 1, pby);
2674        let b = get(pbx, pby - 1);
2675        let mut c = get(pbx + pwb, pby - 1);
2676        if !c.available {
2677            c = get(pbx - 1, pby - 1);
2678        }
2679        [a, b, c]
2680    }
2681
2682    /// `colZeroFlag` for the 4×4 block at absolute block coords `(bx, by)`: true
2683    /// when `RefPicList1[0]` is a short-term picture whose co-located block uses
2684    /// reference 0 with a near-zero motion vector (spec §8.4.1.2.2).
2685    /// Co-located 4x4 block coords for the current block's `(bx4, by4)` within the
2686    /// macroblock, per spec 8.4.1.2.1. Under `direct_8x8_inference_flag` every 4x4
2687    /// in an 8x8 takes that 8x8's OUTER CORNER (`luma4x4BlkIdx = 5 * mbPartIdx`,
2688    /// i.e. (0,0) (3,0) (0,3) (3,3)); otherwise motion is genuinely per-4x4.
2689    ///
2690    /// 8.4.1.2.1 is SHARED by both direct modes, so spatial and temporal must map
2691    /// identically. They did not: temporal mapped the corner and spatial read the
2692    /// block's own coords, which is invisible while every 4x4 in the co-located 8x8
2693    /// carries the same motion -- true of every stream until sub-8x8 P partitions
2694    /// (x264 `--partitions p4x4`) make them differ. Hence one function.
2695    #[inline]
2696    fn col_block(&self, bx4: usize, by4: usize) -> (usize, usize) {
2697        if self.direct_8x8_inference {
2698            ((bx4 / 2) * 3, (by4 / 2) * 3)
2699        } else {
2700            (bx4, by4)
2701        }
2702    }
2703
2704    fn col_zero(&self, bx: usize, by: usize) -> bool {
2705        let Some(col) = self.refs1.first() else { return false };
2706        if col.long_term || col.w4 == 0 {
2707            return false;
2708        }
2709        let idx = by * col.w4 + bx;
2710        if idx >= col.ref_idx.len() {
2711            return false;
2712        }
2713        // Spec 8.4.1.2.1: the co-located motion is List-0's when the co-located
2714        // block HAS a List-0 prediction, and List-1's otherwise (predFlagL0Col == 0).
2715        // Reading List-0 unconditionally treats an L1-only block as intra
2716        // (ref_idx -1), which silently suppresses colZeroFlag. An L1-only
2717        // co-located block can only exist when the co-located picture is itself a
2718        // B picture, i.e. only under b-pyramid -- which is why this survived every
2719        // non-pyramid B stream.
2720        let (cref, cmv) = if col.ref_idx[idx] >= 0 {
2721            (col.ref_idx[idx], col.mv[idx])
2722        } else if idx < col.ref_idx1.len() && col.ref_idx1[idx] >= 0 {
2723            (col.ref_idx1[idx], col.mv1[idx])
2724        } else {
2725            return false;
2726        };
2727        cref == 0 && cmv.0.abs() <= 1 && cmv.1.abs() <= 1
2728    }
2729
2730    /// Implicit bi-prediction weights `(w0, w1)` from POC distances (spec
2731    /// §8.4.2.3.2), or `None` for the plain average (idc≠2, uni-pred, or the
2732    /// equidistant / out-of-range fall-back to 32:32 which equals the average).
2733    fn implicit_weights(&self, refi0: i32, refi1: i32) -> Option<(i32, i32)> {
2734        if self.weighted_bipred_idc != 2 || refi0 < 0 || refi1 < 0 {
2735            return None;
2736        }
2737        let r0 = &self.refs[refi0 as usize];
2738        let r1 = &self.refs1[refi1 as usize];
2739        let td = (r1.poc - r0.poc).clamp(-128, 127);
2740        let tb = (self.cur_poc - r0.poc).clamp(-128, 127);
2741        if td == 0 || r0.long_term || r1.long_term {
2742            return None; // 32:32 → identical to the average
2743        }
2744        let tx = (16384 + td.abs() / 2) / td;
2745        let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
2746        let w1 = dsf >> 2;
2747        if !(-64..=128).contains(&w1) {
2748            return None; // out of range → 32:32 average
2749        }
2750        Some((64 - w1, w1))
2751    }
2752
2753    /// Motion-compensates a region with the given per-list refs/MVs. Bi-prediction
2754    /// is the simple `(a+b+1)>>1` average, or POC-weighted when implicit weighting
2755    /// (idc 2) is active. Writes into `pred_y`/`c_pred`.
2756    #[allow(clippy::too_many_arguments)]
2757    fn b_mc(
2758        &self,
2759        mb_x: usize,
2760        mb_y: usize,
2761        px: usize,
2762        py: usize,
2763        rw: usize,
2764        rh: usize,
2765        refi0: i32,
2766        mv0: (i32, i32),
2767        refi1: i32,
2768        mv1: (i32, i32),
2769        pred_y: &mut [u8; 256],
2770        c_pred: &mut [[u8; 64]; 2],
2771    ) {
2772        let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBMc);
2773        // Malformed-stream armor, mirroring the P path: now that B slices actually
2774        // PARSE ref_idx (they used to be hardcoded to 0), a mutated stream can hand
2775        // us an index past the end of either list. Clamp rather than panic — the
2776        // crate is `forbid(unsafe_code)` and fuzz-gated to never panic, and a
2777        // wrong picture on garbage input carries no conformance duty.
2778        let refi0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
2779        let refi1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
2780        if (refi0 >= 0 && self.refs.is_empty()) || (refi1 >= 0 && self.refs1.is_empty()) {
2781            return;
2782        }
2783        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
2784        let weights = {
2785            let _gw = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBWeights);
2786            self.implicit_weights(refi0, refi1)
2787        };
2788        // Bi-prediction blend: the weights decision is LOOP-INVARIANT, so every
2789        // blend site below matches on `weights` ONCE and runs a branch-free
2790        // pixel loop — the unweighted `(p+q+1)>>1` average then autovectorizes
2791        // (the per-pixel closure this replaces hid the invariant behind a
2792        // capture, and its chroma form was a &dyn call PER PIXEL).
2793        // FULL-WIDTH regions (px == 0, rw == 16 — every 16×16/16×8 partition and
2794        // most direct regions) occupy contiguous rows of `pred_y`, so MC writes
2795        // the destination DIRECTLY: uni-pred needs no staging at all, bi-pred
2796        // stages only the second list and blends in place. The staging arrays
2797        // (512 B zeroed per call before this) now exist only on the branches
2798        // that read them. Same fusion as the P path's mc_rect (WHYS Part 8).
2799        let full = px == 0 && rw == 16;
2800        let _gl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBLuma);
2801        // One scratch borrow for the whole region — both bi-pred passes included.
2802        // The closure yields whether the arm already ran the chroma half (the
2803        // bi-pred full-width arm does, to keep its staging alive) — a plain
2804        // `return` inside would exit the CLOSURE only and chroma would run twice.
2805        let chroma_done = rusty_h264_common::inter::with_mc_scratch(|scr| match (refi0 >= 0, refi1 >= 0, full) {
2806            (true, false, true) => {
2807                let rf = &self.refs[refi0 as usize];
2808                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
2809                false
2810            }
2811            (false, true, true) => {
2812                let rf = &self.refs1[refi1 as usize];
2813                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
2814                false
2815            }
2816            (true, true, true) => {
2817                let rf = &self.refs[refi0 as usize];
2818                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
2819                let mut b = [0u8; 256];
2820                let rf = &self.refs1[refi1 as usize];
2821                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut b[..rw * rh]);
2822                drop(_gl);
2823                let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
2824                // SLICE-then-zip: proving the bounds ONCE lets rustc emit the whole
2825                // 256-byte average as 8 straight-line vpavgb ops (verified in
2826                // isolation, x86-64-v3); the indexed form kept a per-iteration
2827                // bounds check and a loop. A hand AVX2 kernel is refuted — the
2828                // compiler already emits the ideal instruction.
2829                let dst = &mut pred_y[py * 16..py * 16 + rw * rh];
2830                match weights {
2831                    None => {
2832                        for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
2833                            *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
2834                        }
2835                    }
2836                    Some((w0, w1)) => {
2837                        for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
2838                            *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
2839                        }
2840                    }
2841                }
2842                let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
2843                self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
2844                true
2845            }
2846            _ => {
2847                // Narrow region — rows are strided in `pred_y`; stage and copy.
2848                let (mut a, mut b) = ([0u8; 256], [0u8; 256]);
2849                if refi0 >= 0 {
2850                    let rf = &self.refs[refi0 as usize];
2851                    rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16 + px, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut a[..rw * rh]);
2852                }
2853                if refi1 >= 0 {
2854                    let rf = &self.refs1[refi1 as usize];
2855                    rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16 + px, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut b[..rw * rh]);
2856                }
2857                drop(_gl);
2858                let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
2859                match (refi0 >= 0, refi1 >= 0) {
2860                    (true, true) => {
2861                        for dy in 0..rh {
2862                            let (ar, br) = (&a[dy * rw..dy * rw + rw], &b[dy * rw..dy * rw + rw]);
2863                            let base = (py + dy) * 16 + px;
2864                            let dst = &mut pred_y[base..base + rw];
2865                            match weights {
2866                                None => {
2867                                    for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
2868                                        *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
2869                                    }
2870                                }
2871                                Some((w0, w1)) => {
2872                                    for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
2873                                        *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
2874                                    }
2875                                }
2876                            }
2877                        }
2878                    }
2879                    (true, false) => {
2880                        for dy in 0..rh {
2881                            let d = (py + dy) * 16 + px;
2882                            pred_y[d..d + rw].copy_from_slice(&a[dy * rw..dy * rw + rw]);
2883                        }
2884                    }
2885                    _ => {
2886                        for dy in 0..rh {
2887                            let d = (py + dy) * 16 + px;
2888                            pred_y[d..d + rw].copy_from_slice(&b[dy * rw..dy * rw + rw]);
2889                        }
2890                    }
2891                }
2892                false
2893            }
2894        });
2895        if chroma_done {
2896            return;
2897        }
2898        let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
2899        self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
2900    }
2901
2902    /// Chroma half of `b_mc`, with the same full-width direct-write fusion
2903    /// (crw == 8 rows are contiguous in the 8-wide `c_pred` planes).
2904    #[allow(clippy::too_many_arguments)]
2905    fn b_mc_chroma(
2906        &self,
2907        mb_x: usize,
2908        mb_y: usize,
2909        px: usize,
2910        py: usize,
2911        rw: usize,
2912        rh: usize,
2913        refi0: i32,
2914        mv0: (i32, i32),
2915        refi1: i32,
2916        mv1: (i32, i32),
2917        c_pred: &mut [[u8; 64]; 2],
2918        weights: Option<(i32, i32)>,
2919        cch: usize,
2920    ) {
2921        let (crx, cry, crw, crh) = (px / 2, py / 2, rw / 2, rh / 2);
2922        let full = crx == 0 && crw == 8;
2923        for c in 0..2 {
2924            match (refi0 >= 0, refi1 >= 0, full) {
2925                (true, false, true) => {
2926                    let rf = &self.refs[refi0 as usize];
2927                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
2928                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
2929                }
2930                (false, true, true) => {
2931                    let rf = &self.refs1[refi1 as usize];
2932                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
2933                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
2934                }
2935                (true, true, true) => {
2936                    let rf = &self.refs[refi0 as usize];
2937                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
2938                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
2939                    let mut cb = [0u8; 64];
2940                    let rf = &self.refs1[refi1 as usize];
2941                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
2942                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut cb[..crw * crh]);
2943                    let dst = &mut c_pred[c][cry * 8..cry * 8 + crw * crh];
2944                    match weights {
2945                        None => {
2946                            for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
2947                                *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
2948                            }
2949                        }
2950                        Some((w0, w1)) => {
2951                            for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
2952                                *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
2953                            }
2954                        }
2955                    }
2956                }
2957                _ => {
2958                    let (mut ca, mut cb) = ([0u8; 64], [0u8; 64]);
2959                    if refi0 >= 0 {
2960                        let rf = &self.refs[refi0 as usize];
2961                        let pl = if c == 0 { &rf.pu } else { &rf.pv };
2962                        mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8 + crx, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut ca[..crw * crh]);
2963                    }
2964                    if refi1 >= 0 {
2965                        let rf = &self.refs1[refi1 as usize];
2966                        let pl = if c == 0 { &rf.pu } else { &rf.pv };
2967                        mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8 + crx, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut cb[..crw * crh]);
2968                    }
2969                    match (refi0 >= 0, refi1 >= 0) {
2970                        (true, true) => {
2971                            for dy in 0..crh {
2972                                let (pr, qr) = (&ca[dy * crw..dy * crw + crw], &cb[dy * crw..dy * crw + crw]);
2973                                let base = (cry + dy) * 8 + crx;
2974                                let dst = &mut c_pred[c][base..base + crw];
2975                                match weights {
2976                                    None => {
2977                                        for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
2978                                            *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
2979                                        }
2980                                    }
2981                                    Some((w0, w1)) => {
2982                                        for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
2983                                            *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
2984                                        }
2985                                    }
2986                                }
2987                            }
2988                        }
2989                        (true, false) => {
2990                            for dy in 0..crh {
2991                                let d = (cry + dy) * 8 + crx;
2992                                c_pred[c][d..d + crw].copy_from_slice(&ca[dy * crw..dy * crw + crw]);
2993                            }
2994                        }
2995                        _ => {
2996                            for dy in 0..crh {
2997                                let d = (cry + dy) * 8 + crx;
2998                                c_pred[c][d..d + crw].copy_from_slice(&cb[dy * crw..dy * crw + crw]);
2999                            }
3000                        }
3001                    }
3002                }
3003            }
3004        }
3005    }
3006
3007    /// Commits a region's per-list motion to the 4×4 grids (and marks coded).
3008    #[allow(clippy::too_many_arguments)]
3009    fn b_set_motion(&mut self, mb_x: usize, mb_y: usize, px: usize, py: usize, rw: usize, rh: usize, refi0: i32, mv0: (i32, i32), refi1: i32, mv1: (i32, i32)) {
3010        let _gs = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBSet);
3011        let w4 = self.mb_w * 4;
3012        for by in py / 4..(py + rh) / 4 {
3013            for bx in px / 4..(px + rw) / 4 {
3014                let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
3015                self.ref_idx_y[idx] = refi0;
3016                self.mv_y[idx] = if refi0 >= 0 { mv0 } else { (0, 0) };
3017                self.ref_idx1[idx] = refi1;
3018                self.mv1[idx] = if refi1 >= 0 { mv1 } else { (0, 0) };
3019                self.inter_y[idx] = true;
3020                self.coded_y[idx] = true;
3021                self.modes_y[idx] = 2;
3022            }
3023        }
3024    }
3025
3026    /// Spatial direct prediction for a region (whole MB or an 8×8): derives the
3027    /// per-list reference indices and base MVs, then motion-compensates each 4×4
3028    /// sub-block (applying `colZeroFlag`) and commits the motion (spec §8.4.1.2.2).
3029    #[allow(clippy::too_many_arguments)]
3030    /// Splits a `w`×`h` block region (4×4-block units) into the fewest rectangles
3031    /// whose contents are `uniform`, preferring partition-shaped cuts (whole →
3032    /// horizontal halves → vertical halves → quadrants). Emits at most w·h rects
3033    /// (the all-different worst case degenerates to per-block, i.e. the old loop).
3034    fn coalesce_region(
3035        x: usize,
3036        y: usize,
3037        w: usize,
3038        h: usize,
3039        uniform: &dyn Fn(usize, usize, usize, usize) -> bool,
3040        emit: &mut dyn FnMut(usize, usize, usize, usize),
3041    ) {
3042        if uniform(x, y, w, h) {
3043            emit(x, y, w, h);
3044            return;
3045        }
3046        if h > 1 && uniform(x, y, w, h / 2) && uniform(x, y + h / 2, w, h / 2) {
3047            emit(x, y, w, h / 2);
3048            emit(x, y + h / 2, w, h / 2);
3049            return;
3050        }
3051        if w > 1 && uniform(x, y, w / 2, h) && uniform(x + w / 2, y, w / 2, h) {
3052            emit(x, y, w / 2, h);
3053            emit(x + w / 2, y, w / 2, h);
3054            return;
3055        }
3056        match (w > 1, h > 1) {
3057            (true, true) => {
3058                for q in 0..4usize {
3059                    Self::coalesce_region(x + (q % 2) * (w / 2), y + (q / 2) * (h / 2), w / 2, h / 2, uniform, emit);
3060                }
3061            }
3062            (true, false) => {
3063                Self::coalesce_region(x, y, w / 2, h, uniform, emit);
3064                Self::coalesce_region(x + w / 2, y, w / 2, h, uniform, emit);
3065            }
3066            (false, true) => {
3067                Self::coalesce_region(x, y, w, h / 2, uniform, emit);
3068                Self::coalesce_region(x, y + h / 2, w, h / 2, uniform, emit);
3069            }
3070            (false, false) => emit(x, y, 1, 1),
3071        }
3072    }
3073
3074    fn decode_b_direct(&mut self, mb_x: usize, mb_y: usize, px: usize, py: usize, rw: usize, rh: usize, pred_y: &mut [u8; 256], c_pred: &mut [[u8; 64]; 2]) {
3075        let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDirect);
3076        if !self.direct_spatial {
3077            return self.decode_b_direct_temporal(mb_x, mb_y, px, py, rw, rh, pred_y, c_pred);
3078        }
3079        // H-48: DERIVATION-ONLY scope, dropped before the MC loop below. DecBDirect
3080        // wraps this function whole and therefore INCLUDES the `b_mc` calls it makes,
3081        // so its 1460 ns/call was never "MV derivation is slow" — that read was wrong.
3082        // This guard is what separates the two.
3083        let gd = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDeriv);
3084        // MB-level neighbors drive the direct reference indices and base MVs.
3085        let (nbx, nby) = ((mb_x * 4) as isize, (mb_y * 4) as isize);
3086        let n0 = self.mv_neighbors_list(nbx, nby, 4, 0);
3087        let n1 = self.mv_neighbors_list(nbx, nby, 4, 1);
3088        let min_pos = |a: i32, b: i32| if a < 0 { b } else if b < 0 { a } else { a.min(b) };
3089        let rid = |n: &[MvNeighbor; 3]| min_pos(min_pos(n[0].ref_idx, n[1].ref_idx), n[2].ref_idx);
3090        let (mut refi0, mut refi1) = (rid(&n0), rid(&n1));
3091        let direct_zero = refi0 < 0 && refi1 < 0;
3092        if direct_zero {
3093            refi0 = 0;
3094            refi1 = 0;
3095        }
3096        let mv0 = if refi0 >= 0 && !direct_zero { predict_mv(n0[0], n0[1], n0[2], refi0) } else { (0, 0) };
3097        let mv1 = if refi1 >= 0 && !direct_zero { predict_mv(n1[0], n1[1], n1[2], refi1) } else { (0, 0) };
3098        // Per 4×4 sub-block: colZeroFlag zeroes the ref-0 motion vector. cz is the
3099        // ONLY per-block variable (two possible (m0,m1) values for the region), and
3100        // the MC filters + bi-blend are per-output-pixel — so sub-blocks with equal
3101        // cz coalesce into one wider `b_mc`, BIT-IDENTICAL. A 16×16 direct MB paid
3102        // 16 bi-pred b_mc calls (~96 MC kernel entries) before this; typically 1 now.
3103        let (bx0, by0, bw, bh) = (px / 4, py / 4, rw / 4, rh / 4);
3104        let mut czg = [[false; 4]; 4]; // region-local, [dy][dx]
3105        for dy in 0..bh {
3106            for dx in 0..bw {
3107                let (colx, coly) = self.col_block(bx0 + dx, by0 + dy);
3108                czg[dy][dx] = !direct_zero && self.col_zero(mb_x * 4 + colx, mb_y * 4 + coly);
3109            }
3110        }
3111        let uniform = |x: usize, y: usize, w: usize, h: usize| -> bool {
3112            let t = czg[y][x];
3113            (y..y + h).all(|dy| (x..x + w).all(|dx| czg[dy][dx] == t))
3114        };
3115        let mut rects: [(usize, usize, usize, usize); 16] = [(0, 0, 0, 0); 16];
3116        let mut n = 0usize;
3117        Self::coalesce_region(0, 0, bw, bh, &uniform, &mut |x, y, w, h| {
3118            rects[n] = (x, y, w, h);
3119            n += 1;
3120        });
3121        drop(gd); // derivation ends; everything below is MC + motion-grid commit
3122        for &(x, y, w, h) in &rects[..n] {
3123            let cz = czg[y][x];
3124            let m0 = if refi0 == 0 && cz { (0, 0) } else { mv0 };
3125            let m1 = if refi1 == 0 && cz { (0, 0) } else { mv1 };
3126            let (lx, ly, lw, lh) = ((bx0 + x) * 4, (by0 + y) * 4, w * 4, h * 4);
3127            self.b_mc(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1, pred_y, c_pred);
3128            self.b_set_motion(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1);
3129        }
3130    }
3131
3132    /// Temporal direct prediction for a region (spec §8.4.1.2.3): for each 4×4
3133    /// (or per-8×8 corner under `direct_8x8_inference`), take the co-located
3134    /// List-0 motion from `RefPicList1[0]`, map its reference into the current
3135    /// List-0 by POC, and scale the motion vector by the POC distances.
3136    #[allow(clippy::too_many_arguments)]
3137    fn decode_b_direct_temporal(&mut self, mb_x: usize, mb_y: usize, px: usize, py: usize, rw: usize, rh: usize, pred_y: &mut [u8; 256], c_pred: &mut [[u8; 64]; 2]) {
3138        let poc1 = self.refs1.first().map_or(0, |f| f.poc);
3139        let infer = self.direct_8x8_inference;
3140        // Under direct_8x8_inference every 4×4 in an 8×8 takes the same MB-corner
3141        // co-located motion, so motion-compensate the whole 8×8 in one call — this
3142        // hits the width-8 MC asm and pays the per-call tile/blend setup 4× less.
3143        // Without inference, motion is genuinely per-4×4. Bit-identical either way
3144        // (MC of an 8×8 with one MV == four 4×4 MCs with that same MV).
3145        let step = if infer { 8 } else { 4 };
3146        let mut sy = py;
3147        while sy < py + rh {
3148            let mut sx = px;
3149            while sx < px + rw {
3150                // Co-located 4×4 (the 8×8's MB-corner under inference) — shared with
3151                // the spatial path's colZeroFlag, which must map identically.
3152                let (colx, coly) = self.col_block(sx / 4, sy / 4);
3153                let (mvcol, refpoc) = {
3154                    let col = &self.refs1[0];
3155                    let idx = (mb_y * 4 + coly) * col.w4 + (mb_x * 4 + colx);
3156                    if col.w4 != 0 && idx < col.mv.len() && col.ref_poc[idx] != i32::MIN {
3157                        (col.mv[idx], col.ref_poc[idx])
3158                    } else {
3159                        ((0, 0), i32::MIN) // intra co-located → zero motion, refIdxL0 = 0
3160                    }
3161                };
3162                // MapColToList0: the current-list index of the co-located reference.
3163                let (refi0, mvc) = if refpoc == i32::MIN {
3164                    (0, (0, 0))
3165                } else {
3166                    let r = self.refs.iter().position(|f| f.poc == refpoc).unwrap_or(0) as i32;
3167                    (r, mvcol)
3168                };
3169                let poc0 = self.refs[refi0 as usize].poc;
3170                let td = (poc1 - poc0).clamp(-128, 127);
3171                let tb = (self.cur_poc - poc0).clamp(-128, 127);
3172                let (mv0, mv1) = if td == 0 || self.refs[refi0 as usize].long_term {
3173                    (mvc, (0, 0))
3174                } else {
3175                    let tx = (16384 + td.abs() / 2) / td;
3176                    let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
3177                    let m0 = ((dsf * mvc.0 + 128) >> 8, (dsf * mvc.1 + 128) >> 8);
3178                    (m0, (m0.0 - mvc.0, m0.1 - mvc.1))
3179                };
3180                self.b_mc(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1, pred_y, c_pred);
3181                self.b_set_motion(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1);
3182                sx += step;
3183            }
3184            sy += step;
3185        }
3186    }
3187
3188    /// Reads `ref_idx_lX` for a B partition (te(v)/ue(v) by the list's active
3189    /// count), bounds-checked against the available reference count.
3190    fn read_b_ref(&self, r: &mut BitReader, list: usize) -> Result<i32, MbError> {
3191        let (active, avail) = if list == 0 {
3192            (self.num_ref_active, self.refs.len())
3193        } else {
3194            (self.num_ref_active1, self.refs1.len())
3195        };
3196        let v = if active > 1 { read_ref_idx(r, active)? } else { 0 };
3197        if v as usize >= avail {
3198            return Err(MbError::Truncated);
3199        }
3200        Ok(v)
3201    }
3202
3203    /// Reconstructs a `B_Skip` macroblock: spatial-direct prediction, no residual.
3204    fn decode_b_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3205        if self.refs.is_empty() || self.refs1.is_empty() {
3206            return Err(MbError::Unsupported("B without references"));
3207        }
3208        let mut pred_y = [0u8; 256];
3209        let mut c_pred = [[0u8; 64]; 2];
3210        self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
3211        // Zero residual: the prediction is the reconstruction — copy it row-wise.
3212        for dy in 0..16 {
3213            let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
3214            self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
3215        }
3216        for c in 0..2 {
3217            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3218            for dy in 0..8 {
3219                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
3220                plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
3221            }
3222        }
3223        // nnz stays 0 (no residual) — clear the grids for neighbor context.
3224        let w4 = self.mb_w * 4;
3225        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3226            self.nnz_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 0;
3227        }
3228        Ok(())
3229    }
3230
3231    /// Reconstructs a B macroblock (spec Table 7-14): direct, L0/L1/Bi partitions,
3232    /// `B_8x8`, or intra.
3233    fn decode_b_mb(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3234        let mb_type = r.read_ue()?;
3235        if mb_type >= 23 {
3236            return self.decode_intra_mb(r, mb_x, mb_y, mb_type - 23);
3237        }
3238        if self.refs.is_empty() || self.refs1.is_empty() {
3239            return Err(MbError::Unsupported("B without references"));
3240        }
3241        let mut pred_y = [0u8; 256];
3242        let mut c_pred = [[0u8; 64]; 2];
3243
3244        if mb_type == 0 {
3245            // B_Direct_16x16 — 8×8 transform allowed only with direct_8x8_inference.
3246            self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
3247            return self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, self.direct_8x8_inference);
3248        }
3249        if mb_type == 22 {
3250            return self.decode_b_8x8(r, mb_x, mb_y);
3251        }
3252
3253        // 16x16 / 16x8 / 8x16 partitions with per-partition L0/L1/Bi.
3254        let (layout, mvmode, preds) = b_inter_layout(mb_type);
3255        // mb_pred order: ref_idx_l0 (all L0 parts), ref_idx_l1, mvd_l0, mvd_l1.
3256        let mut refi = [[-1i32; 2]; 2]; // [part][list]
3257        for (p, &(_, _, _, _)) in layout.iter().enumerate() {
3258            if preds[p].uses(0) {
3259                refi[p][0] = self.read_b_ref(r, 0)?;
3260            }
3261        }
3262        for (p, _) in layout.iter().enumerate() {
3263            if preds[p].uses(1) {
3264                refi[p][1] = self.read_b_ref(r, 1)?;
3265            }
3266        }
3267        let mut mvd = [[(0i32, 0i32); 2]; 2];
3268        for (p, _) in layout.iter().enumerate() {
3269            if preds[p].uses(0) {
3270                mvd[p][0] = (r.read_se()?, r.read_se()?);
3271            }
3272        }
3273        for (p, _) in layout.iter().enumerate() {
3274            if preds[p].uses(1) {
3275                mvd[p][1] = (r.read_se()?, r.read_se()?);
3276            }
3277        }
3278        // Per partition: predict + commit each list's MV, then motion-compensate.
3279        for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
3280            let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
3281            let pwb = (rw / 4) as isize;
3282            let mut mv = [(0i32, 0i32); 2];
3283            for list in 0..2 {
3284                if refi[p][list] >= 0 {
3285                    let n = self.mv_neighbors_list(pbx, pby, pwb, list);
3286                    let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], refi[p][list]);
3287                    mv[list] = (pmv.0 + mvd[p][list].0, pmv.1 + mvd[p][list].1);
3288                }
3289            }
3290            self.b_set_motion(mb_x, mb_y, rx, ry, rw, rh, refi[p][0], mv[0], refi[p][1], mv[1]);
3291            // Spec-correct bi-prediction (average of L0 and L1), matching the CABAC
3292            // path. This used to replicate an openh264 bug for a Bi 16x8/8x16
3293            // partition -- openh264 mis-handles the destination buffer there, so
3294            // partition 0 came out List-1-only and partition 1 List-0-only. That was
3295            // deliberate when openh264's h264dec WAS the conformance oracle, but the
3296            // gate is ffmpeg now and the CABAC path already went spec-correct; the
3297            // CAVLC path was simply left behind. Measured: mb_type 12..21 (every B
3298            // 16x8/8x16 with at least one Bi partition) were 100% wrong vs ffmpeg,
3299            // while 1..11 (no Bi partition) were only collaterally damaged.
3300            self.b_mc(mb_x, mb_y, rx, ry, rw, rh, refi[p][0], mv[0], refi[p][1], mv[1], &mut pred_y, &mut c_pred);
3301        }
3302        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true)
3303    }
3304
3305    /// Reconstructs a `B_8x8` macroblock: four 8×8 sub-macroblock partitions, each
3306    /// direct or L0/L1/Bi with its own sub-partitioning (spec Table 7-18).
3307    fn decode_b_8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3308        let mut sub = [0u32; 4];
3309        for s in sub.iter_mut() {
3310            let v = r.read_ue()?;
3311            if v > 12 {
3312                return Err(MbError::Unsupported("invalid B sub_mb_type"));
3313            }
3314            *s = v;
3315        }
3316        let mut pred_y = [0u8; 256];
3317        let mut c_pred = [[0u8; 64]; 2];
3318        // ref_idx for all 8×8 partitions (L0 batch, then L1 batch), for the
3319        // non-direct sub-partitions.
3320        let mut refi = [[-1i32; 2]; 4];
3321        for (p, &st) in sub.iter().enumerate() {
3322            if st != 0 && b_sub_uses(st, 0) {
3323                refi[p][0] = self.read_b_ref(r, 0)?;
3324            }
3325        }
3326        for (p, &st) in sub.iter().enumerate() {
3327            if st != 0 && b_sub_uses(st, 1) {
3328                refi[p][1] = self.read_b_ref(r, 1)?;
3329            }
3330        }
3331        // mvd: all mvd_l0 (partition-major, sub-partition order), then all mvd_l1.
3332        let mut mvd0: Vec<(i32, i32)> = Vec::new();
3333        let mut mvd1: Vec<(i32, i32)> = Vec::new();
3334        for &st in &sub {
3335            if st != 0 && b_sub_uses(st, 0) {
3336                for _ in b_sub_parts(st) {
3337                    mvd0.push((r.read_se()?, r.read_se()?));
3338                }
3339            }
3340        }
3341        for &st in &sub {
3342            if st != 0 && b_sub_uses(st, 1) {
3343                for _ in b_sub_parts(st) {
3344                    mvd1.push((r.read_se()?, r.read_se()?));
3345                }
3346            }
3347        }
3348        // Decode each 8×8 partition.
3349        let (mut i0, mut i1) = (0usize, 0usize);
3350        for (p, &st) in sub.iter().enumerate() {
3351            let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
3352            if st == 0 {
3353                self.decode_b_direct(mb_x, mb_y, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred);
3354                continue;
3355            }
3356            for &(sx, sy, sw, sh) in b_sub_parts(st) {
3357                let (px, py) = (b8x + sx, b8y + sy);
3358                let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
3359                let pwb = (sw / 4) as isize;
3360                let mut mv = [(0i32, 0i32); 2];
3361                if b_sub_uses(st, 0) {
3362                    let n = self.mv_neighbors_list(pbx, pby, pwb, 0);
3363                    let pmv = predict_mv(n[0], n[1], n[2], refi[p][0]);
3364                    let d = mvd0[i0];
3365                    i0 += 1;
3366                    mv[0] = (pmv.0 + d.0, pmv.1 + d.1);
3367                }
3368                if b_sub_uses(st, 1) {
3369                    let n = self.mv_neighbors_list(pbx, pby, pwb, 1);
3370                    let pmv = predict_mv(n[0], n[1], n[2], refi[p][1]);
3371                    let d = mvd1[i1];
3372                    i1 += 1;
3373                    mv[1] = (pmv.0 + d.0, pmv.1 + d.1);
3374                }
3375                self.b_set_motion(mb_x, mb_y, px, py, sw, sh, refi[p][0], mv[0], refi[p][1], mv[1]);
3376                self.b_mc(mb_x, mb_y, px, py, sw, sh, refi[p][0], mv[0], refi[p][1], mv[1], &mut pred_y, &mut c_pred);
3377            }
3378        }
3379        // noSubMbPartSizeLessThan8x8: each sub-partition must be ≥ 8×8 (direct
3380        // counts only with the 8×8 inference flag).
3381        let allow_8x8 = sub
3382            .iter()
3383            .all(|&st| if st == 0 { self.direct_8x8_inference } else { st <= 3 });
3384        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8)
3385    }
3386
3387    /// Reconstructs a `P_8x8` macroblock: four 8×8 sub-macroblock partitions,
3388    /// each independently split (8×8 / 8×4 / 4×8 / 4×4) with its own motion
3389    /// vector(s). `ref0` is `P_8x8ref0` (every `ref_idx` forced to 0, not coded).
3390    fn decode_p8x8(
3391        &mut self,
3392        r: &mut BitReader,
3393        mb_x: usize,
3394        mb_y: usize,
3395        ref0: bool,
3396    ) -> Result<(), MbError> {
3397        if self.refs.is_empty() {
3398            return Err(MbError::Unsupported("inter without reference"));
3399        }
3400        let w4 = self.mb_w * 4;
3401        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
3402        let num_refs = self.refs.len();
3403
3404        // mb_pred order (spec §7.3.5.2): all sub_mb_type, then all ref_idx_l0,
3405        // then all mvd_l0 (partition-major, sub-partition order within each).
3406        let mut sub_types = [0u32; 4];
3407        for st in sub_types.iter_mut() {
3408            let v = r.read_ue()?;
3409            if v > 3 {
3410                return Err(MbError::Unsupported("B-slice / invalid sub_mb_type"));
3411            }
3412            *st = v;
3413        }
3414        let mut ref_idxs = [0i32; 4];
3415        if self.num_ref_active > 1 && !ref0 {
3416            for ri in ref_idxs.iter_mut() {
3417                *ri = read_ref_idx(r, self.num_ref_active)?;
3418                if *ri as usize >= num_refs {
3419                    return Err(MbError::Truncated); // references a non-existent picture
3420                }
3421            }
3422        }
3423
3424        // Per sub-partition (in decoding order): median MV prediction from the
3425        // committed neighbor grid, mvd, commit, then motion-compensate. Committing
3426        // before the next prediction is what lets sub-partitions chain correctly.
3427        let mut pred_y = [0u8; 256];
3428        let mut c_pred = [[0u8; 64]; 2];
3429        for part in 0..4usize {
3430            let refi = ref_idxs[part];
3431            let (b8x, b8y) = ((part % 2) * 8, (part / 2) * 8);
3432            for &(srx, sry, srw, srh) in sub_mb_partitions(sub_types[part]) {
3433                let (px, py) = (b8x + srx, b8y + sry);
3434                let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
3435                let [a, b, c] = self.mv_neighbors_block(pbx, pby, (srw / 4) as isize);
3436                let pmv = predict_mv(a, b, c, refi);
3437                let mvd_x = r.read_se()?;
3438                let mvd_y = r.read_se()?;
3439                let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
3440                for by in py / 4..py / 4 + srh / 4 {
3441                    for bx in px / 4..px / 4 + srw / 4 {
3442                        let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
3443                        self.mv_y[idx] = mv;
3444                        self.inter_y[idx] = true;
3445                        self.ref_idx_y[idx] = refi;
3446                        self.coded_y[idx] = true;
3447                    }
3448                }
3449                let reference = &self.refs[refi as usize];
3450                let mut tmp = [0u8; 256];
3451                mc_luma_padded(&reference.py, reference.lstride(), crate::LPAD, self.cw, ch, mb_x * 16 + px, mb_y * 16 + py, srw, srh, mv.0, mv.1, &mut tmp);
3452                restride(&mut pred_y, 16, px, py, &tmp, srw, srh);
3453                let (crx, cry, crw, crh) = (px / 2, py / 2, srw / 2, srh / 2);
3454                for cc in 0..2 {
3455                    let rc = if cc == 0 { &reference.pu } else { &reference.pv };
3456                    let mut tc = [0u8; 64];
3457                    mc_chroma_padded(rc, reference.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8 + crx, mb_y * 8 + cry, crw, crh, mv.0, mv.1, &mut tc);
3458                    restride(&mut c_pred[cc], 8, crx, cry, &tc, crw, crh);
3459                }
3460                self.weight_partition(
3461                    &mut pred_y, &mut c_pred, 0, refi as usize, px, py, srw, srh,
3462                );
3463            }
3464        }
3465
3466        // P_8x8 allows the 8×8 transform only when every sub-partition is 8×8.
3467        let allow_8x8 = sub_types.iter().all(|&t| t == 0);
3468        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8)
3469    }
3470
3471    /// Reconstructs a `P_Skip` macroblock: motion-compensate from the reference
3472    /// at the skip MV, with no residual.
3473    /// Flush the entropy-decouple job queue: replay every deferred pixel job
3474    /// in parse order. Called before any intra macroblock (its reconstruction
3475    /// reads neighbour PIXELS), before row filtering, at B-branch entry, at
3476    /// slice end, and at `deblock()` as a backstop.
3477    fn edc_flush(&mut self) {
3478        if self.edc_jobs.is_empty() {
3479            return;
3480        }
3481        let jobs = std::mem::take(&mut self.edc_jobs);
3482        for j in &jobs {
3483            match j {
3484                EdcJob::Skip { mbx, mby, mv } => self.recon_p_skip(*mbx, *mby, *mv),
3485                EdcJob::Inter(job) => self.recon_p_inter(job),
3486            }
3487        }
3488        // Hand the (now empty) Vec back so its allocation is reused.
3489        self.edc_jobs = jobs;
3490        self.edc_jobs.clear();
3491    }
3492
3493    /// Reconstructs one CABAC P inter macroblock from its parse job — the
3494    /// pixel half of the entropy-decouple seam (docs/entropy-decouple-plan.md
3495    /// E1). Reads NOTHING from parse state except the frame grids this MB's
3496    /// parse already committed (its own block MVs/refs, re-gathered below —
3497    /// stable after commit) and the immutable DPB; called either inline
3498    /// (seam off / flush disabled) or in-order at a flush point. Byte-
3499    /// identical to the former inline block by construction: replay order
3500    /// equals inline order at every pixel-observable point (intra reads, row
3501    /// filtering) because flushes precede both.
3502    fn recon_p_inter(&mut self, j: &PInterJob) {
3503        let mbw = self.mb_w;
3504        // `add_inter_residual` (and anything under it) reads `self.cur_qp`,
3505        // which at FLUSH time belongs to a later macroblock — replay must
3506        // restore this MB's qp. The x264 corpus (near-constant QP) could not
3507        // see this; the encoder's delta-QP roundtrip stream caught it.
3508        let saved_qp = self.cur_qp;
3509        self.cur_qp = j.qp;
3510                    // ---- Recon: motion-comp (per 4×4 luma / co-located 2×2 chroma using the
3511                    // committed grid MV — the 6-tap/bilinear filter is per-output-pixel, so
3512                    // per-block MC is bit-identical to per-partition MC) + residual add via the
3513                    // SAME reconstruct_4x4 as intra, with the MC output as the prediction.
3514                    let qp = j.qp;
3515                    let qpc = self.chroma_qp_for(qp);
3516                    let (w4r, w2r) = (mbw * 4, mbw * 2);
3517                    let mut pred_y = [0u8; 256];
3518                    let mut c_pred = [[0u8; 64]; 2];
3519                    {
3520                        // MC-CALL COALESCING (side-by-side descent, dec target #2): the old
3521                        // loop paid 16 mc_luma(4×4) + 32 mc_chroma(2×2) per MB regardless of
3522                        // partitioning — 48 calls even for a single-MV 16×16 MB, and the
3523                        // per-call glue around 2.4M calls was ~40% of decoding real-world
3524                        // (x264) streams. The 6-tap/bilinear filters are per-output-pixel,
3525                        // so merging blocks with equal (mv, ref) into one wider MC call is
3526                        // BIT-IDENTICAL; the rect ladder mirrors the partition shapes.
3527                        let _ms = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMcStage);
3528                        let (rh16, cch) = (self.mb_h * 16, self.mb_h * 8);
3529                        let mut gmv = [(0i32, 0i32); 16];
3530                        let mut gref = [0usize; 16];
3531                        let _gg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::MvGrid);
3532                        for by in 0..4usize {
3533                            for bx in 0..4usize {
3534                                let bidx = (j.mby * 4 + by) * w4r + (j.mbx * 4 + bx);
3535                                gmv[by * 4 + bx] = self.mv_y[bidx];
3536                                // Per-block reference (multi-ref P): ref_idx_l0 committed to the
3537                                // grid. Clamp — a corrupt stream can over-range it (never panic).
3538                                gref[by * 4 + bx] =
3539                                    (self.ref_idx_y[bidx].max(0) as usize).min(self.refs.len() - 1);
3540                            }
3541                        }
3542                        drop(_gg);
3543                        // All blocks of the rect (in 4×4-block units) match its top-left?
3544                        let rect_eq = |x4: usize, y4: usize, w4: usize, h4: usize| -> bool {
3545                            let t = y4 * 4 + x4;
3546                            (0..h4).all(|dy| {
3547                                (0..w4).all(|dx| {
3548                                    let b = (y4 + dy) * 4 + (x4 + dx);
3549                                    gmv[b] == gmv[t] && gref[b] == gref[t]
3550                                })
3551                            })
3552                        };
3553                        let refs = &self.refs;
3554                        let (cw, ccw) = (self.cw, self.ccw);
3555                        let mut mc_rect = |x4: usize,
3556                                           y4: usize,
3557                                           w4: usize,
3558                                           h4: usize,
3559                                           pred_y: &mut [u8; 256],
3560                                           c_pred: &mut [[u8; 64]; 2]| {
3561                            let b = y4 * 4 + x4;
3562                            let (mv, reference) = (gmv[b], &refs[gref[b]]);
3563                            let (w, h) = (w4 * 4, h4 * 4);
3564                            // A FULL-WIDTH rect (w == 16, so x4 == 0) occupies contiguous
3565                            // whole rows of `pred_y` — the MC output layout and the
3566                            // destination layout coincide, so MC writes the prediction
3567                            // buffer DIRECTLY. The staging copy exists only for narrow
3568                            // rects, whose rows really are strided in `pred_y`. This is
3569                            // the diagnosis's "stage-boundary materialization" tax paid
3570                            // by the dominant 16×16/16×8 shapes: 256 B of `t` zeroing
3571                            // plus a 256 B copy per rect, for nothing.
3572                            if w == 16 {
3573                                rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &reference.py, reference.lstride(), crate::LPAD, cw, rh16, j.mbx * 16, j.mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut pred_y[y4 * 64..y4 * 64 + w * h]));
3574                            } else {
3575                                let mut t = [0u8; 256];
3576                                rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &reference.py, reference.lstride(), crate::LPAD, cw, rh16, j.mbx * 16 + x4 * 4, j.mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut t[..w * h]));
3577                                let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
3578                                for dy in 0..h {
3579                                    pred_y[(y4 * 4 + dy) * 16 + x4 * 4..][..w]
3580                                        .copy_from_slice(&t[dy * w..dy * w + w]);
3581                                }
3582                            }
3583                            let (cw4, ch4) = (w4 * 2, h4 * 2);
3584                            for cc in 0..2 {
3585                                let rc = if cc == 0 { &reference.pu } else { &reference.pv };
3586                                // Same full-width coincidence for chroma: cw4 == 8 rows
3587                                // are contiguous in the 8-wide `c_pred` plane.
3588                                if cw4 == 8 {
3589                                    mc_chroma_padded(rc, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut c_pred[cc][y4 * 16..y4 * 16 + cw4 * ch4]);
3590                                    continue;
3591                                }
3592                                let mut tc = [0u8; 64];
3593                                mc_chroma_padded(rc, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8 + x4 * 2, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut tc[..cw4 * ch4]);
3594                                let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
3595                                for dy in 0..ch4 {
3596                                    c_pred[cc][(y4 * 2 + dy) * 8 + x4 * 2..][..cw4]
3597                                        .copy_from_slice(&tc[dy * cw4..dy * cw4 + cw4]);
3598                                }
3599                            }
3600                        };
3601                        if rect_eq(0, 0, 4, 4) {
3602                            mc_rect(0, 0, 4, 4, &mut pred_y, &mut c_pred);
3603                        } else if rect_eq(0, 0, 4, 2) && rect_eq(0, 2, 4, 2) {
3604                            mc_rect(0, 0, 4, 2, &mut pred_y, &mut c_pred);
3605                            mc_rect(0, 2, 4, 2, &mut pred_y, &mut c_pred);
3606                        } else if rect_eq(0, 0, 2, 4) && rect_eq(2, 0, 2, 4) {
3607                            mc_rect(0, 0, 2, 4, &mut pred_y, &mut c_pred);
3608                            mc_rect(2, 0, 2, 4, &mut pred_y, &mut c_pred);
3609                        } else {
3610                            for q in 0..4usize {
3611                                let (qx, qy) = ((q % 2) * 2, (q / 2) * 2);
3612                                if rect_eq(qx, qy, 2, 2) {
3613                                    mc_rect(qx, qy, 2, 2, &mut pred_y, &mut c_pred);
3614                                } else if rect_eq(qx, qy, 2, 1) && rect_eq(qx, qy + 1, 2, 1) {
3615                                    mc_rect(qx, qy, 2, 1, &mut pred_y, &mut c_pred);
3616                                    mc_rect(qx, qy + 1, 2, 1, &mut pred_y, &mut c_pred);
3617                                } else if rect_eq(qx, qy, 1, 2) && rect_eq(qx + 1, qy, 1, 2) {
3618                                    mc_rect(qx, qy, 1, 2, &mut pred_y, &mut c_pred);
3619                                    mc_rect(qx + 1, qy, 1, 2, &mut pred_y, &mut c_pred);
3620                                } else {
3621                                    for j in 0..4usize {
3622                                        mc_rect(qx + (j % 2), qy + (j / 2), 1, 1, &mut pred_y, &mut c_pred);
3623                                    }
3624                                }
3625                            }
3626                        }
3627                        // EXPLICIT WEIGHTED PREDICTION (spec 8.4.2.3). The CAVLC inter
3628                        // path weights each partition after MC; the MC-call-coalescing
3629                        // rewrite of this CABAC path lost it, and nothing caught that
3630                        // because the effect is invisible unless a stream actually
3631                        // carries non-default weights. x264's `weightp` DUPLICATES a
3632                        // reference and distinguishes the copy ONLY by its weights, so
3633                        // every macroblock picking the weighted index decoded unweighted
3634                        // -- a silent, accumulating luma drift.
3635                        //
3636                        // Applied per 4x4 block rather than per partition: the weight
3637                        // depends solely on the block's reference index, so the two are
3638                        // equivalent, and `gref` already holds it for every block
3639                        // regardless of which rect ladder rung ran.
3640                        if self.weights.is_some() {
3641                            for by in 0..4usize {
3642                                for bx in 0..4usize {
3643                                    let refi = gref[by * 4 + bx];
3644                                    self.weight_partition(
3645                                        &mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4,
3646                                    );
3647                                }
3648                            }
3649                        }
3650                    }
3651                    // Residual add — the SAME helper the B path uses (this inline
3652                    // copy was a duplicate; deduped when the zero-block fast path
3653                    // landed so both paths share it).
3654                    self.add_inter_residual(j.mbx, j.mby, &pred_y, &c_pred, &j.luma_scan, if j.t8 { Some(&j.luma8) } else { None }, &j.cdc, &j.cac, j.cbp_chroma, &j.nnzs);
3655        self.cur_qp = saved_qp;
3656    }
3657
3658    fn decode_p_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3659        // DEBLOCK CLASS: a P_Skip macroblock carries no coefficients and one
3660        // (ref, mv) for all 16 blocks, so every internal boundary strength is 0 by
3661        // §8.7.2.1 and the loop filter needs 9 block loads instead of 24. This is
3662        // the single highest-value classification: the MB-kind census measures Skip
3663        // at 36.4% (CAVLC) / 65.0% (main) / 57.8% (high) of real x264 corpora.
3664        // Written HERE because both the CAVLC and the CABAC slice loops funnel
3665        // through this one function.
3666        //
3667        // Deliberately NOT done for `B_Skip` — its motion is direct-derived and can
3668        // differ per 4×4 sub-block, so its internal edges can legally reach
3669        // strength 1. B_Skip stays UNSET and takes the blind path.
3670        self.mb_kind[mb_y * self.mb_w + mb_x] = rusty_h264_common::deblock::MB_KIND_SKIP;
3671        // P_Skip always references index 0 (the most recent picture). Borrow it —
3672        // a full-frame `.cloned()` here was ~86% of total decode time (one ~3 MB
3673        // plane copy per skip MB, thousands per frame).
3674        if self.refs.is_empty() {
3675            return Err(MbError::Unsupported("P_Skip without reference"));
3676        }
3677        let mv = self.skip_mv(mb_x, mb_y);
3678        // Grid commits are PARSE state (later macroblocks' MV prediction and
3679        // availability read them) — they run now; the pixel half reads only
3680        // the DPB + `mv`, so it defers cleanly (E1 seam).
3681        {
3682            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
3683            self.set_mb_mv(mb_x, mb_y, mv, true, 0);
3684            let w4 = self.mb_w * 4;
3685            for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3686                self.coded_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = true;
3687                self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
3688            }
3689        }
3690        if self.edc_active {
3691            self.edc_jobs.push(EdcJob::Skip { mbx: mb_x, mby: mb_y, mv });
3692            return Ok(());
3693        }
3694        self.recon_p_skip(mb_x, mb_y, mv);
3695        Ok(())
3696    }
3697
3698    /// Pixel half of P_Skip (see the E1 seam note on `recon_p_inter`).
3699    fn recon_p_skip(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32)) {
3700        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
3701
3702        let mut pred = [0u8; 256];
3703        let rf0 = &self.refs[0];
3704        mc_luma_padded(&rf0.py, rf0.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16, 16, 16, mv.0, mv.1, &mut pred);
3705        if let Some(wt) = &self.weights {
3706            for p in pred.iter_mut() {
3707                *p = wt.apply_luma(*p, 0, 0);
3708            }
3709        }
3710        {
3711            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
3712            for dy in 0..16 {
3713                let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
3714                self.rec_y[d..d + 16].copy_from_slice(&pred[dy * 16..dy * 16 + 16]);
3715            }
3716        }
3717        for c in 0..2 {
3718            let mut pc = [0u8; 64];
3719            let rf0 = &self.refs[0];
3720            let rc = if c == 0 { &rf0.pu } else { &rf0.pv };
3721            mc_chroma_padded(rc, rf0.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8, 8, 8, mv.0, mv.1, &mut pc);
3722            if let Some(wt) = &self.weights {
3723                for p in pc.iter_mut() {
3724                    *p = wt.apply_chroma(*p, 0, 0, c);
3725                }
3726            }
3727            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3728            for dy in 0..8 {
3729                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
3730                plane[d..d + 8].copy_from_slice(&pc[dy * 8..dy * 8 + 8]);
3731            }
3732        }
3733    }
3734
3735    /// Predicted `Intra_4x4` mode for the block at absolute coords `(bx, by)`.
3736    /// If either the left or top neighbor is outside the frame or in another
3737    /// slice, the prediction is DC (mode 2) (spec §8.3.1.1).
3738    fn predict_i4_mode(&self, bx: usize, by: usize) -> u8 {
3739        if bx == 0 || by == 0 {
3740            return 2;
3741        }
3742        // Left neighbor block (bx-1,by); top neighbor block (bx,by-1). A neighbor
3743        // in another slice — or, under constrained_intra, an inter neighbor — is
3744        // unavailable, forcing the predicted mode to DC.
3745        if !self.nbr_in_slice((bx - 1) / 4, by / 4)
3746            || !self.nbr_in_slice(bx / 4, (by - 1) / 4)
3747            || !self.intra_nbr_ok(bx - 1, by)
3748            || !self.intra_nbr_ok(bx, by - 1)
3749        {
3750            return 2;
3751        }
3752        let w4 = self.mb_w * 4;
3753        self.modes_y[by * w4 + (bx - 1)].min(self.modes_y[(by - 1) * w4 + bx])
3754    }
3755
3756    /// Gathers 4×4 luma intra neighbors at pixel `(px, py)` from `rec_y`.
3757    fn gather_i4(
3758        &self,
3759        px: usize,
3760        py: usize,
3761        avail_top: bool,
3762        avail_left: bool,
3763        bx: usize,
3764        by: usize,
3765    ) -> ([u8; 8], [u8; 4], u8) {
3766        let (cw, w4) = (self.cw, self.mb_w * 4);
3767        let mut top = [0u8; 8];
3768        let mut left = [0u8; 4];
3769        let mut corner = 0;
3770        if avail_top {
3771            for i in 0..4 {
3772                top[i] = self.top_y_px(py, px + i);
3773            }
3774            let tr_avail = bx + 1 < w4
3775                && self.coded_y[(by - 1) * w4 + (bx + 1)]
3776                && self.nbr_in_slice((bx + 1) / 4, (by - 1) / 4)
3777                && self.intra_nbr_ok(bx + 1, by - 1);
3778            for i in 0..4 {
3779                top[4 + i] = if tr_avail {
3780                    self.top_y_px(py, px + 4 + i)
3781                } else {
3782                    top[3]
3783                };
3784            }
3785        }
3786        if avail_left {
3787            for i in 0..4 {
3788                left[i] = self.rec_y[(py + i) * cw + px - 1];
3789            }
3790        }
3791        // The above-left corner has its own availability (block D); under
3792        // constrained_intra it is gone if that block is inter.
3793        if avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1) {
3794            corner = self.top_y_px(py, px - 1);
3795        }
3796        (top, left, corner)
3797    }
3798
3799    /// Reconstructs an `I_PCM` macroblock: byte-aligned raw 8-bit samples, no
3800    /// prediction/transform/quant (spec §7.3.5, §8.3.5).
3801    fn decode_ipcm(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3802        r.align_to_byte()?;
3803        let (lx, ly) = (mb_x * 16, mb_y * 16);
3804        for dy in 0..16 {
3805            for dx in 0..16 {
3806                self.rec_y[(ly + dy) * self.cw + (lx + dx)] = r.read_bits(8)? as u8;
3807            }
3808        }
3809        let (cx, cy) = (mb_x * 8, mb_y * 8);
3810        for plane in [&mut self.rec_u, &mut self.rec_v] {
3811            for dy in 0..8 {
3812                for dx in 0..8 {
3813                    plane[(cy + dy) * self.ccw + (cx + dx)] = r.read_bits(8)? as u8;
3814                }
3815            }
3816        }
3817        // Neighbor context: an I_PCM block contributes TotalCoeff = 16, counts as
3818        // intra with DC mode for prediction, and has no motion (§9.2.1, §8.3.1.2.2).
3819        let (w4, w2) = (self.mb_w * 4, self.mb_w * 2);
3820        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3821            let idx = (mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx);
3822            self.nnz_y[idx] = 16;
3823            self.modes_y[idx] = 2;
3824            self.inter_y[idx] = false;
3825            self.ref_idx_y[idx] = -1;
3826            self.mv_y[idx] = (0, 0);
3827        }
3828        for c in 0..2 {
3829            for by in 0..2 {
3830                for bx in 0..2 {
3831                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = 16;
3832                }
3833            }
3834        }
3835        Ok(())
3836    }
3837
3838    fn decode_i4x4(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3839        let w4 = self.mb_w * 4;
3840
3841        // intra4x4 mode signalling
3842        let mut modes = [2u8; 16]; // raster [lby*4+lbx]
3843        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3844            let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
3845            let predicted = self.predict_i4_mode(bx, by);
3846            let actual = if r.read_bit()? {
3847                predicted
3848            } else {
3849                let rem = r.read_bits(3)? as u8;
3850                if rem < predicted {
3851                    rem
3852                } else {
3853                    rem + 1
3854                }
3855            };
3856            self.modes_y[by * w4 + bx] = actual;
3857            modes[lby * 4 + lbx] = actual;
3858        }
3859
3860        let chroma_mode = r.read_ue()? as u8;
3861        let cbp = read_cbp_intra(r)?;
3862        let cbp_luma = cbp & 15;
3863        let cbp_chroma = cbp >> 4;
3864        if cbp != 0 {
3865            self.step_qp(r.read_se()?);
3866        }
3867        let qp = self.cur_qp;
3868
3869        // luma residuals + serial reconstruction. Cross-MB neighbors are only
3870        // available when the adjacent macroblock is in this slice (and, under
3871        // constrained_intra_pred, is itself intra-coded).
3872        let top_mb_avail = mb_y > 0
3873            && self.nbr_in_slice(mb_x, mb_y - 1)
3874            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
3875        let left_mb_avail = mb_x > 0
3876            && self.nbr_in_slice(mb_x - 1, mb_y)
3877            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
3878        self.nnz_cache_load(mb_x, mb_y);
3879        for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
3880            let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
3881            let (px, py) = (bx * 4, by * 4);
3882            let avail_top = lby > 0 || top_mb_avail;
3883            let avail_left = lbx > 0 || left_mb_avail;
3884            let mut qb = [0i32; 16];
3885            let total = if cbp_luma & (1 << (blk / 4)) != 0 {
3886                let nc = self.nc_pred(lbx, lby);
3887                let scan16 = decode_residual_block(r, 16, nc)?;
3888                qb = un_scan_4x4_dcac(&scan16);
3889                scan16.iter().filter(|&&v| v != 0).count() as u8
3890            } else {
3891                0
3892            };
3893            self.nnz_cache_set(lbx, lby, total);
3894            self.nnz_y[by * w4 + bx] = total;
3895            let (top, left, corner) = self.gather_i4(px, py, avail_top, avail_left, bx, by);
3896            let pred = intra4x4_pred(modes[lby * 4 + lbx], avail_top, avail_left, &top, &left, corner);
3897            let mut predb = [0i32; 16];
3898            for i in 0..16 {
3899                predb[i] = pred[i] as i32;
3900            }
3901            let s = reconstruct_4x4(&self.dequant(&qb, qp, 0), &predb);
3902            store(&mut self.rec_y, self.cw, px, py, &s);
3903            self.coded_y[by * w4 + bx] = true;
3904        }
3905
3906        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
3907    }
3908
3909    /// Decodes an `I_8x8` macroblock (High profile): four 8×8 luma blocks, each
3910    /// with its own intra mode, 8×8 transform residual (CAVLC = four interleaved
3911    /// 4×4 blocks), and 8×8 intra prediction.
3912    fn decode_i8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3913        let w4 = self.mb_w * 4;
3914        self.mb_t8x8[mb_y * self.mb_w + mb_x] = true;
3915
3916        // intra8x8 mode signalling — one mode per 8×8 block (raster 0..3),
3917        // stored into all four of its 4×4 cells so neighbors can read it.
3918        let mut modes8 = [2u8; 4];
3919        for (b8, mode) in modes8.iter_mut().enumerate() {
3920            let (b8x, b8y) = (b8 % 2, b8 / 2);
3921            let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
3922            let predicted = self.predict_i4_mode(bx, by);
3923            let actual = if r.read_bit()? {
3924                predicted
3925            } else {
3926                let rem = r.read_bits(3)? as u8;
3927                if rem < predicted { rem } else { rem + 1 }
3928            };
3929            *mode = actual;
3930            for sy in 0..2 {
3931                for sx in 0..2 {
3932                    self.modes_y[(by + sy) * w4 + (bx + sx)] = actual;
3933                }
3934            }
3935        }
3936
3937        let chroma_mode = r.read_ue()? as u8;
3938        let cbp = read_cbp_intra(r)?;
3939        let cbp_luma = cbp & 15;
3940        let cbp_chroma = cbp >> 4;
3941        if cbp != 0 {
3942            self.step_qp(r.read_se()?);
3943        }
3944        let qp = self.cur_qp;
3945
3946        let top_mb_avail = mb_y > 0
3947            && self.nbr_in_slice(mb_x, mb_y - 1)
3948            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
3949        let left_mb_avail = mb_x > 0
3950            && self.nbr_in_slice(mb_x - 1, mb_y)
3951            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
3952        self.nnz_cache_load(mb_x, mb_y);
3953
3954        for b8 in 0..4 {
3955            let (b8x, b8y) = (b8 % 2, b8 / 2);
3956            let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
3957            let (px, py) = (bx * 4, by * 4);
3958
3959            // residual: 8×8 CAVLC = four 4×4 sub-blocks, coeff k of sub-block s
3960            // mapping to 8×8 scan position 4·k + s (spec §7.3.5.3.2).
3961            let mut res8 = [0i32; 64];
3962            if cbp_luma & (1 << b8) != 0 {
3963                let mut scan8 = [0i32; 64];
3964                for sub in 0..4 {
3965                    let (sx, sy) = (sub % 2, sub / 2);
3966                    let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
3967                    let nc = self.nc_pred(cx, cy);
3968                    let blk = decode_residual_block(r, 16, nc)?;
3969                    let total = blk.iter().filter(|&&v| v != 0).count() as u8;
3970                    self.nnz_cache_set(cx, cy, total);
3971                    self.nnz_y[(by + sy) * w4 + (bx + sx)] = total;
3972                    for k in 0..16 {
3973                        scan8[4 * k + sub] = blk[k];
3974                    }
3975                }
3976                let raster = un_scan_8x8(&scan8);
3977                res8 = self.inv_quant8(&raster, qp, 0);
3978            } else {
3979                for sub in 0..4 {
3980                    let (sx, sy) = (sub % 2, sub / 2);
3981                    self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
3982                    self.nnz_y[(by + sy) * w4 + (bx + sx)] = 0;
3983                }
3984            }
3985
3986            let avail_top = b8y > 0 || top_mb_avail;
3987            let avail_left = b8x > 0 || left_mb_avail;
3988            let (top, left, corner, avail_corner) =
3989                self.gather_i8(px, py, avail_top, avail_left, bx, by);
3990            let pred = intra8x8_pred(
3991                modes8[b8], avail_top, avail_left, avail_corner, &top, &left, corner,
3992            );
3993            let mut predb = [0i32; 64];
3994            for i in 0..64 {
3995                predb[i] = pred[i] as i32;
3996            }
3997            let recon = add_residual_8x8(&res8, &predb);
3998            for dy in 0..8 {
3999                for dx in 0..8 {
4000                    self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
4001                }
4002            }
4003            for sy in 0..2 {
4004                for sx in 0..2 {
4005                    self.coded_y[(by + sy) * w4 + (bx + sx)] = true;
4006                }
4007            }
4008        }
4009
4010        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
4011    }
4012
4013    /// Dequantizes + inverse-transforms an 8×8 luma block, applying the scaling
4014    /// matrix `list` (0 = intra, 1 = inter) or flat weights.
4015    fn inv_quant8(&self, raster: &[i32; 64], qp: u8, list: usize) -> [i32; 64] {
4016        match &self.scaling8 {
4017            Some(s) => inverse_quant_8x8(raster, qp, &s[list]),
4018            None => inverse_quant_8x8(raster, qp, &[16i32; 64]),
4019        }
4020    }
4021
4022    /// Gathers the 8×8 luma intra reference samples at pixel `(px, py)`: the 16
4023    /// top samples (8..15 substituted from the last when no top-right), 8 left
4024    /// samples, the above-left corner, and whether the corner is available.
4025    #[allow(clippy::too_many_arguments)]
4026    fn gather_i8(
4027        &self,
4028        px: usize,
4029        py: usize,
4030        avail_top: bool,
4031        avail_left: bool,
4032        bx: usize,
4033        by: usize,
4034    ) -> ([u8; 16], [u8; 8], u8, bool) {
4035        let (cw, w4) = (self.cw, self.mb_w * 4);
4036        let mut top = [0u8; 16];
4037        let mut left = [0u8; 8];
4038        let mut corner = 0;
4039        if avail_top {
4040            for i in 0..8 {
4041                top[i] = self.top_y_px(py, px + i);
4042            }
4043            let tr_avail = bx + 2 < w4
4044                && self.coded_y[(by - 1) * w4 + (bx + 2)]
4045                && self.nbr_in_slice((bx + 2) / 4, (by - 1) / 4)
4046                && self.intra_nbr_ok(bx + 2, by - 1);
4047            for i in 0..8 {
4048                top[8 + i] = if tr_avail {
4049                    self.top_y_px(py, px + 8 + i)
4050                } else {
4051                    top[7]
4052                };
4053            }
4054        }
4055        if avail_left {
4056            for i in 0..8 {
4057                left[i] = self.rec_y[(py + i) * cw + px - 1];
4058            }
4059        }
4060        let avail_corner = avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1);
4061        if avail_corner {
4062            corner = self.top_y_px(py, px - 1);
4063        }
4064        (top, left, corner, avail_corner)
4065    }
4066
4067    fn decode_i16(
4068        &mut self,
4069        r: &mut BitReader,
4070        mb_x: usize,
4071        mb_y: usize,
4072        mt: u32,
4073    ) -> Result<(), MbError> {
4074        let pred_mode = I16Mode::from_id(mt % 4);
4075        let cbp_chroma = (mt % 12) / 4;
4076        let cbp_luma_15 = mt / 12 == 1;
4077        let chroma_mode = r.read_ue()? as u8;
4078        self.step_qp(r.read_se()?);
4079        let qp = self.cur_qp;
4080        let w4 = self.mb_w * 4;
4081
4082        // luma DC
4083        self.nnz_cache_load(mb_x, mb_y);
4084        let nc_dc = self.nc_pred(0, 0);
4085        let dc_scan = decode_residual_block(r, 16, nc_dc)?;
4086        let dc_levels = un_scan_4x4_dcac(&dc_scan);
4087        let recon_dc = self.dequant_luma_dc(&dc_levels, qp, 0);
4088
4089        // luma AC (nnz set for all 16 blocks: 0 when DC-only, matching the encoder)
4090        let mut q_blocks = [[0i32; 16]; 16];
4091        for &(bx, by) in &LUMA_4X4_SCAN_XY {
4092            let total = if cbp_luma_15 {
4093                let nc = self.nc_pred(bx, by);
4094                let ac = decode_residual_block(r, 15, nc)?;
4095                un_scan_4x4_ac_into(&ac, &mut q_blocks[by * 4 + bx]);
4096                ac.iter().filter(|&&v| v != 0).count() as u8
4097            } else {
4098                0
4099            };
4100            self.nnz_cache_set(bx, by, total);
4101            self.nnz_y[(mb_y * 4 + by) * w4 + (mb_x * 4 + bx)] = total;
4102        }
4103
4104        // prediction + reconstruction
4105        let avail_top = mb_y > 0
4106            && self.nbr_in_slice(mb_x, mb_y - 1)
4107            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
4108        let avail_left = mb_x > 0
4109            && self.nbr_in_slice(mb_x - 1, mb_y)
4110            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
4111        let (lx, ly) = (mb_x * 16, mb_y * 16);
4112        let mut top = [0u8; 16];
4113        let mut left = [0u8; 16];
4114        if avail_top {
4115            for i in 0..16 {
4116                top[i] = self.top_y_px(ly, lx + i);
4117            }
4118        }
4119        if avail_left {
4120            for i in 0..16 {
4121                left[i] = self.rec_y[(ly + i) * self.cw + lx - 1];
4122            }
4123        }
4124        let corner = if avail_top && avail_left {
4125            self.top_y_px(ly, lx - 1)
4126        } else {
4127            0
4128        };
4129        let pred_l = luma16x16_pred(pred_mode, avail_top, avail_left, &top, &left, corner);
4130        for by in 0..4 {
4131            for bx in 0..4 {
4132                let mut deq = self.dequant(&q_blocks[by * 4 + bx], qp, 0);
4133                deq[0] = recon_dc[by * 4 + bx];
4134                let mut predb = [0i32; 16];
4135                for dy in 0..4 {
4136                    for dx in 0..4 {
4137                        predb[dy * 4 + dx] = pred_l[(by * 4 + dy) * 16 + (bx * 4 + dx)] as i32;
4138                    }
4139                }
4140                let s = reconstruct_4x4(&deq, &predb);
4141                store(&mut self.rec_y, self.cw, lx + bx * 4, ly + by * 4, &s);
4142            }
4143        }
4144        // I_16x16 blocks are treated as DC for neighbor mode prediction.
4145        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
4146            self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
4147        }
4148
4149        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
4150    }
4151
4152    /// Reads and reconstructs the chroma residual (shared by both luma types).
4153    fn decode_chroma(
4154        &mut self,
4155        r: &mut BitReader,
4156        mb_x: usize,
4157        mb_y: usize,
4158        cbp_chroma: u32,
4159        chroma_mode: u8,
4160    ) -> Result<(), MbError> {
4161        let qpc = self.chroma_qp_for(self.cur_qp);
4162        let (cx, cy) = (mb_x * 8, mb_y * 8);
4163        let avail_top = mb_y > 0
4164            && self.nbr_in_slice(mb_x, mb_y - 1)
4165            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
4166        let avail_left = mb_x > 0
4167            && self.nbr_in_slice(mb_x - 1, mb_y)
4168            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
4169
4170        let mut c_recon_dc = [[0i32; 4]; 2];
4171        if cbp_chroma != 0 {
4172            for (c, slot) in c_recon_dc.iter_mut().enumerate() {
4173                let dc = decode_residual_block(r, 4, -1)?;
4174                *slot = self.dequant_chroma_dc(&[dc[0], dc[1], dc[2], dc[3]], qpc, 1 + c);
4175            }
4176        }
4177        let mut c_q_blocks = [[[0i32; 16]; 4]; 2];
4178        if cbp_chroma == 2 {
4179            self.chroma_cache_load(mb_x, mb_y);
4180            let w2 = self.mb_w * 2;
4181            for c in 0..2 {
4182                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
4183                    let nc = self.chroma_nc_pred(c, bx, by);
4184                    let ac = decode_residual_block(r, 15, nc)?;
4185                    let total = ac.iter().filter(|&&v| v != 0).count() as u8;
4186                    self.chroma_nnz_cache_set(c, bx, by, total);
4187                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = total;
4188                    un_scan_4x4_ac_into(&ac, &mut c_q_blocks[c][by * 2 + bx]);
4189                }
4190            }
4191        }
4192        for c in 0..2 {
4193            let mut ctop = [0u8; 8];
4194            let mut cleft = [0u8; 8];
4195            let mut ccorner = 0u8;
4196            {
4197                let rec_c = if c == 0 { &self.rec_u } else { &self.rec_v };
4198                if avail_top {
4199                    for i in 0..8 {
4200                        ctop[i] = self.top_c_px(c, cy, cx + i);
4201                    }
4202                }
4203                if avail_left {
4204                    for i in 0..8 {
4205                        cleft[i] = rec_c[(cy + i) * self.ccw + cx - 1];
4206                    }
4207                }
4208                if avail_top && avail_left {
4209                    ccorner = self.top_c_px(c, cy, cx - 1);
4210                }
4211            }
4212            let pred8 = chroma8x8_pred(chroma_mode, avail_top, avail_left, &ctop, &cleft, ccorner);
4213            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
4214                let mut predb = [0i32; 16];
4215                for dy in 0..4 {
4216                    for dx in 0..4 {
4217                        predb[dy * 4 + dx] = pred8[(by * 4 + dy) * 8 + (bx * 4 + dx)] as i32;
4218                    }
4219                }
4220                let mut deq = self.dequant(&c_q_blocks[c][by * 2 + bx], qpc, 1 + c);
4221                deq[0] = c_recon_dc[c][by * 2 + bx];
4222                let s = reconstruct_4x4(&deq, &predb);
4223                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
4224                store(plane, self.ccw, cx + bx * 4, cy + by * 4, &s);
4225            }
4226        }
4227        Ok(())
4228    }
4229
4230    /// Applies the in-loop deblocking filter to the reconstructed frame, with
4231    /// the slice's `FilterOffsetA`/`FilterOffsetB` (each = the coded `*_div2`
4232    /// value × 2).
4233    /// Per-frame per-MB dump for conformance bisection, keyed on `RH264_DUMP_MB`.
4234    /// Prints one char per macroblock: `i` = intra, otherwise the List-0 reference
4235    /// index of the MB's top-left 4x4 block. Directly comparable with ffmpeg's
4236    /// `-debug mb_type` map, which is the only per-MB ground truth we can get out
4237    /// of the reference decoder.
4238    fn dump_mb_map(&self) {
4239        if std::env::var_os("RH264_DUMP_MB").is_none() {
4240            return;
4241        }
4242        let w4 = self.mb_w * 4;
4243        let mut hist = [0usize; 4];
4244        eprintln!("--- frame poc {} ---", self.cur_poc);
4245        for mb_y in 0..self.mb_h {
4246            let mut row = String::new();
4247            for mb_x in 0..self.mb_w {
4248                let b = (mb_y * 4) * w4 + mb_x * 4;
4249                let r = self.ref_idx_y[b];
4250                if r < 0 {
4251                    row.push('i');
4252                } else {
4253                    if (r as usize) < 4 {
4254                        hist[r as usize] += 1;
4255                    }
4256                    row.push((b'0' + (r as u8).min(9)) as char);
4257                }
4258            }
4259            eprintln!("{row}");
4260        }
4261        eprintln!(
4262            "ref histogram: {hist:?}   num_ref_active={} refs.len()={}   OUT-OF-RANGE={}",
4263            self.num_ref_active,
4264            self.refs.len(),
4265            hist.iter().skip(self.refs.len()).sum::<usize>()
4266        );
4267        let list: Vec<String> = self
4268            .refs
4269            .iter()
4270            .enumerate()
4271            .map(|(i, f)| {
4272                // A synthesized frame_num-gap frame is uniform grey with w4 == 0;
4273                // flag it, because it silently displaces real pictures in the list.
4274                let synth = if f.w4 == 0 { " SYNTH-GREY" } else { "" };
4275                format!("[{i}] poc={} fn={}{synth}", f.poc, f.frame_num)
4276            })
4277            .collect();
4278        eprintln!("  RefPicList0: {}", list.join("  "));
4279    }
4280
4281    pub fn deblock(&mut self, offset_a: i32, offset_b: i32) {
4282        self.edc_flush(); // backstop: no pixel job may survive to filtering
4283        self.dump_mb_map();
4284        // ROW MODE: finish any rows not derived during decode (mid-row slice
4285        // ends, error paths) FIRST, while `self` is still mutably borrowable.
4286        if rowdb_on() {
4287            while self.bs_rows < self.mb_h {
4288                let r = self.bs_rows;
4289                self.derive_bs_row(r);
4290                self.bs_rows += 1;
4291            }
4292        }
4293        // Deblock boundary strength uses the *transform block's* coded status. For
4294        // an 8×8-transform macroblock the unit is the whole 8×8, so every 4×4 cell
4295        // shares the 8×8's coefficient presence (OR of its four sub-block counts)
4296        // — distinct from the per-sub-block `nnz_y` used for the CAVLC nC context.
4297        // Only differs from `nnz_y` when some MB uses the 8×8 transform (High
4298        // profile). On Baseline (no 8×8) it's identical — skip the clone + rewrite.
4299        let nnz_db_storage;
4300        let nnz_db: &[u8] = if self.mb_t8x8.iter().any(|&t| t) {
4301            let mut n = self.nnz_y.clone();
4302            let w4 = self.mb_w * 4;
4303            for mb_y in 0..self.mb_h {
4304                for mb_x in 0..self.mb_w {
4305                    if !self.mb_t8x8[mb_y * self.mb_w + mb_x] {
4306                        continue;
4307                    }
4308                    for b8 in 0..4 {
4309                        let (bx, by) = (mb_x * 4 + (b8 % 2) * 2, mb_y * 4 + (b8 / 2) * 2);
4310                        let any = (0..2).any(|sy| (0..2).any(|sx| self.nnz_y[(by + sy) * w4 + (bx + sx)] > 0));
4311                        for sy in 0..2 {
4312                            for sx in 0..2 {
4313                                n[(by + sy) * w4 + (bx + sx)] = u8::from(any);
4314                            }
4315                        }
4316                    }
4317                }
4318            }
4319            nnz_db_storage = n;
4320            &nnz_db_storage
4321        } else {
4322            &self.nnz_y
4323        };
4324        // Map per-block reference indices to a stable picture identity (POC) so
4325        // the boundary-strength comparison recognises the same picture across lists.
4326        // ≤16-entry ref→POC maps; the frame-wide pre-mapped Vec shims this
4327        // replaces cost 230-460 KB + 57,600 mapped elements PER FRAME (WHYS
4328        // Part 15 item 2) — `pack_frame` now maps per block via `poc0`/`poc1`.
4329        let poc0: Vec<i32> = self.refs.iter().map(|f| f.poc).collect();
4330        let poc1: Vec<i32> = self.refs1.iter().map(|f| f.poc).collect();
4331        let mut info = rusty_h264_common::deblock::BlockInfo {
4332            inter: &self.inter_y,
4333            nnz: nnz_db,
4334            mv: &self.mv_y,
4335            ref_id: &self.ref_idx_y,
4336            mv1: &self.mv1,
4337            ref_id1: if poc1.is_empty() { &[] } else { &self.ref_idx1 },
4338            w4: self.mb_w * 4,
4339            t8x8: &self.mb_t8x8,
4340            bs: &[],
4341            poc0: &poc0,
4342            poc1: &poc1,
4343            kind: &self.mb_kind,
4344        };
4345        // ROW MODE (R2): rows were derived during decode; the remainder was
4346        // finished above (before `info` borrowed the grids). Fallback: the
4347        // Part 16/17 picture-end precompute; `RS_H264_BS_PRE=0` further falls
4348        // back to the pack-then-derive-in-loop pipeline.
4349        let bs_store;
4350        if rowdb_on() {
4351            bs_store = std::mem::take(&mut self.bs_frame);
4352            info.bs = &bs_store;
4353        } else if !std::env::var_os("RS_H264_BS_PRE").is_some_and(|v| v == "0") {
4354            let mut buf = Vec::new();
4355            rusty_h264_common::deblock::precompute_bs_frame(&info, self.mb_w, self.mb_h, &mut buf);
4356            bs_store = buf;
4357            info.bs = &bs_store;
4358        } else {
4359            bs_store = Vec::new();
4360        }
4361        let first_row = if rowdb_on() { self.flt_rows } else { 0 };
4362        rusty_h264_common::deblock::filter_frame_rows(
4363            &mut self.rec_y,
4364            &mut self.rec_u,
4365            &mut self.rec_v,
4366            self.mb_w,
4367            self.mb_h,
4368            first_row..self.mb_h,
4369            &self.mb_qp,
4370            self.chroma_qp_offset,
4371            offset_a,
4372            offset_b,
4373            &info,
4374        );
4375        drop(info);
4376        if rowdb_on() {
4377            self.bs_frame = bs_store;
4378        }
4379    }
4380
4381    /// Crops the reconstructed coded-size planes to the display window.
4382    /// `into_frame`, additionally handing the per-picture grids back for reuse by
4383    /// the next picture. See `GridPool` for why this is worth doing.
4384    pub fn into_frame_recycle(mut self, crop_r: usize, crop_b: usize) -> (YuvFrame, GridPool) {
4385        let [c0, c1] = std::mem::take(&mut self.nnz_c);
4386        let pool = GridPool {
4387            mb_qp: std::mem::take(&mut self.mb_qp),
4388            bs_frame: std::mem::take(&mut self.bs_frame),
4389            pk_prev: std::mem::take(&mut self.pk_prev),
4390            pk_cur: std::mem::take(&mut self.pk_cur),
4391            nnz_dbr: std::mem::take(&mut self.nnz_dbr),
4392            bak_y: std::mem::take(&mut self.bak_y),
4393            bak_u: std::mem::take(&mut self.bak_u),
4394            bak_v: std::mem::take(&mut self.bak_v),
4395            nnz_y: std::mem::take(&mut self.nnz_y),
4396            nnz_c0: c0,
4397            nnz_c1: c1,
4398            modes_y: std::mem::take(&mut self.modes_y),
4399            coded_y: std::mem::take(&mut self.coded_y),
4400            mv_y: std::mem::take(&mut self.mv_y),
4401            inter_y: std::mem::take(&mut self.inter_y),
4402            ref_idx_y: std::mem::take(&mut self.ref_idx_y),
4403            mv1: std::mem::take(&mut self.mv1),
4404            ref_idx1: std::mem::take(&mut self.ref_idx1),
4405            mb_t8x8: std::mem::take(&mut self.mb_t8x8),
4406            mb_kind: std::mem::take(&mut self.mb_kind),
4407        };
4408        (self.into_frame(crop_r, crop_b), pool)
4409    }
4410
4411    pub fn into_frame(self, crop_r: usize, crop_b: usize) -> YuvFrame {
4412        // No cropping (the common case): the reconstruction planes ARE the output —
4413        // move them out instead of allocating + copying three full planes per frame.
4414        if crop_r == 0 && crop_b == 0 {
4415            return YuvFrame {
4416                width: self.cw,
4417                height: self.ch,
4418                y: self.rec_y,
4419                u: self.rec_u,
4420                v: self.rec_v,
4421            };
4422        }
4423        let dw = self.cw - 2 * crop_r;
4424        let dh = self.ch - 2 * crop_b;
4425        let mut y = vec![0u8; dw * dh];
4426        for row in 0..dh {
4427            y[row * dw..row * dw + dw].copy_from_slice(&self.rec_y[row * self.cw..row * self.cw + dw]);
4428        }
4429        let (cdw, cdh) = (dw / 2, dh / 2);
4430        let mut u = vec![0u8; cdw * cdh];
4431        let mut v = vec![0u8; cdw * cdh];
4432        for row in 0..cdh {
4433            u[row * cdw..row * cdw + cdw]
4434                .copy_from_slice(&self.rec_u[row * self.ccw..row * self.ccw + cdw]);
4435            v[row * cdw..row * cdw + cdw]
4436                .copy_from_slice(&self.rec_v[row * self.ccw..row * self.ccw + cdw]);
4437        }
4438        let _ = self.cch;
4439        YuvFrame {
4440            width: dw,
4441            height: dh,
4442            y,
4443            u,
4444            v,
4445        }
4446    }
4447}
4448
4449/// Reads `ref_idx_l0` as `te(v)` with range `num_ref_active - 1`: a single flag
4450/// when exactly two references are active (cMax == 1), else `ue(v)`.
4451// ---- CABAC binarization engine helpers (openh264 cabac_decoder.cpp) ----
4452
4453/// Unary bin (`DecodeUnaryBinCabac`): bin0 at `ctx`; if 1, count bins at `ctx+off`
4454/// (including the terminating 0) until a 0.
4455fn cabac_unary(cab: &mut crate::cabac::Cabac, ctx: usize, off: usize) -> u32 {
4456    if cab.decode_decision(ctx) == 0 {
4457        return 0;
4458    }
4459    let mut sym = 0;
4460    loop {
4461        let bin = cab.decode_decision(ctx + off);
4462        sym += 1;
4463        // Cap the unary run: no valid H.264 element coded through this helper
4464        // (mb_qp_delta) exceeds a few dozen bins, but on malformed / buffer-exhausted
4465        // input the arithmetic engine keeps yielding 1s (it zero-fills past the end),
4466        // which would loop forever. 512 is far beyond any legal value.
4467        if bin == 0 || sym >= 512 {
4468            break;
4469        }
4470    }
4471    sym
4472}
4473
4474/// k-th order Exp-Golomb in bypass (`DecodeExpBypassCabac`).
4475fn cabac_exp_bypass(cab: &mut crate::cabac::Cabac, mut count: i32) -> u32 {
4476    let mut sym = 0u32;
4477    loop {
4478        let c = cab.decode_bypass();
4479        if c == 1 {
4480            sym += 1 << count;
4481            count += 1;
4482        }
4483        if c == 0 || count == 16 {
4484            break;
4485        }
4486    }
4487    let mut sym2 = 0u32;
4488    while count > 0 {
4489        count -= 1;
4490        if cab.decode_bypass() != 0 {
4491            sym2 |= 1 << count;
4492        }
4493    }
4494    sym + sym2
4495}
4496
4497/// UEG0 coeff-level suffix (`DecodeUEGLevelCabac`): TU prefix at `ctx` (≤13) then an
4498/// EG0 bypass suffix.
4499fn cabac_ueg_level(cab: &mut crate::cabac::Cabac, ctx: usize) -> u32 {
4500    if cab.decode_decision(ctx) == 0 {
4501        return 0;
4502    }
4503    let mut code = 0u32;
4504    let mut count = 1;
4505    let mut tmp;
4506    loop {
4507        tmp = cab.decode_decision(ctx);
4508        code += 1;
4509        count += 1;
4510        if tmp == 0 || count == 13 {
4511            break;
4512        }
4513    }
4514    if tmp != 0 {
4515        code += cabac_exp_bypass(cab, 0) + 1;
4516    }
4517    code
4518}
4519
4520/// `mb_qp_delta` CABAC (`ParseDeltaQpCabac`): ctxIdxOffset 60, ctxInc = (prev delta ≠ 0).
4521fn parse_mb_qp_delta_cabac(cab: &mut crate::cabac::Cabac, last_delta_qp: &mut i32) -> i32 {
4522    const O: usize = 60;
4523    let ctx_inc = (*last_delta_qp != 0) as usize;
4524    let mut qp_delta = 0;
4525    if cab.decode_decision(O + ctx_inc) != 0 {
4526        let code = cabac_unary(cab, O + 2, 1) + 1;
4527        qp_delta = ((code + 1) >> 1) as i32;
4528        if code & 1 == 0 {
4529            qp_delta = -qp_delta;
4530        }
4531    }
4532    *last_delta_qp = qp_delta;
4533    qp_delta
4534}
4535
4536/// z-order block → padded (8-stride) nzc-cache index (openh264 g_kCacheNzcScanIdx):
4537/// 16 luma, 4 Cb, 4 Cr. Top neighbour = cache[idx-8], left = cache[idx-1].
4538const NZC_CACHE: [usize; 24] = [
4539    9, 10, 17, 18, 11, 12, 19, 20, 25, 26, 33, 34, 27, 28, 35, 36, // luma
4540    14, 15, 22, 23, // Cb
4541    38, 39, 46, 47, // Cr
4542];
4543
4544// g_kBlockCat2CtxOffset* + maxPos/maxC2, indexed by CABAC res-property (1..10; 0 unused).
4545const RES_MAXPOS: [i32; 11] = [0, 15, 14, 15, 3, 14, 63, 3, 3, 14, 14];
4546const RES_MAXC2: [i32; 11] = [0, 4, 4, 4, 3, 4, 4, 3, 3, 4, 4];
4547const RES_CBF: [usize; 11] = [0, 0, 4, 8, 12, 16, 0, 12, 12, 16, 16];
4548const RES_MAP: [usize; 11] = [0, 0, 15, 29, 44, 47, 0, 44, 44, 47, 47];
4549// Index 6 (luma 8×8) = 199 so that 227+199 = 426 and 232+199 = 431 — the spec's
4550// coeff_abs_level_minus1 base for ctxBlockCat 5 and its >1-bin sub-block.
4551const RES_ONE: [usize; 11] = [0, 0, 10, 20, 30, 39, 199, 30, 30, 39, 39];
4552// res-property values (post GetMbResProperty, CABAC): the ctx-table index.
4553const RP_I16_DC: usize = 1;
4554const RP_I16_AC: usize = 2;
4555const RP_LUMA_4X4: usize = 3;
4556const RP_CHROMA_DC: usize = 7; // U (V=8, same offsets)
4557const RP_CHROMA_AC: usize = 9; // U (V=10, same offsets)
4558/// Luma 8×8 (ctxBlockCat 5). Its RES_MAP/RES_CBF entries stay 0: cat 5 does NOT
4559/// share the `105 + off` / `166 + off` context bases the 4×4 categories use — it
4560/// has its own absolute bases (402 sig, 417 last) and its own per-position
4561/// ctxIdxInc maps below. RES_ONE[6] = 199 IS used, because 227 + 199 = 426 and
4562/// 232 + 199 = 431 reproduce the spec's coeff_abs_level_minus1 base exactly, so
4563/// the level loop needs no special case at all.
4564const RP_LUMA_8X8: usize = 6;
4565
4566/// significant_coeff_flag ctxIdxInc for ctxBlockCat 5, frame-coded (spec Table 9-43).
4567/// Unlike the 4×4 categories — where ctxIdxInc is simply the scan position — the
4568/// 8×8 map folds 63 positions onto 15 contexts.
4569const SIG8X8: [u8; 64] = [
4570    0, 1, 2, 3, 4, 5, 5, 4, 4, 3, 3, 4, 4, 4, 5, 5, //
4571    4, 4, 4, 4, 3, 3, 6, 7, 7, 7, 8, 9, 10, 9, 8, 7, //
4572    7, 6, 11, 12, 13, 11, 6, 7, 8, 9, 14, 10, 9, 8, 6, 11, //
4573    12, 13, 11, 6, 9, 14, 10, 9, 11, 12, 13, 11, 14, 10, 12, 14,
4574];
4575/// last_significant_coeff_flag ctxIdxInc for ctxBlockCat 5 (spec Table 9-43):
4576/// 63 positions onto 5 contexts.
4577const LAST8X8: [u8; 64] = [
4578    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, //
4579    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, //
4580    3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, //
4581    5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8,
4582];
4583
4584/// One residual block (openh264 `ParseResidualBlockCabac`), generic over the 5 CABAC
4585/// block categories. `rp` selects the context offsets. DC categories (I16 luma DC,
4586/// chroma DC) take the cbf context from the per-MB `cbf_dc` bitmask + neighbour MB DC
4587/// cbf; AC categories from the padded nzc cache. Returns totalCoeffNum.
4588#[allow(clippy::too_many_arguments)]
4589fn parse_residual_cabac(
4590    cab: &mut crate::cabac::Cabac,
4591    nzc: &mut [u8; 48],
4592    cbf_dc: &mut u16,
4593    iz: usize,
4594    rp: usize,
4595    is_intra: bool,
4596    ndc: (Option<u16>, Option<u16>), // (top MB cbf_dc, left MB cbf_dc); None = unavailable
4597    out: &mut [i32],                 // scan-order coefficients written here (len ≥ maxPos+1)
4598) -> u32 {
4599    // The CABAC residual parse IS the decoder's entropy stage on Main-profile
4600    // streams — it was invisible (a ~47% residue) until this scope named it.
4601    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Entropy);
4602    // ---- coded_block_flag ----
4603    // ctxBlockCat 5 is the ONLY category with no coded_block_flag: its presence is
4604    // inferred from CodedBlockPatternLuma, so parsing one here would desync.
4605    let is8 = rp == RP_LUMA_8X8;
4606    let is_dc = rp == RP_I16_DC || rp == RP_CHROMA_DC || rp == RP_CHROMA_DC + 1;
4607    let (mut na, mut nb) = (is_intra as u8, is_intra as u8);
4608    let scan = NZC_CACHE[iz.min(23)];
4609    if is_dc {
4610        if let Some(t) = ndc.0 {
4611            nb = ((t >> rp) & 1) as u8;
4612        }
4613        if let Some(l) = ndc.1 {
4614            na = ((l >> rp) & 1) as u8;
4615        }
4616    } else {
4617        if nzc[scan - 8] != 0xff {
4618            nb = (nzc[scan - 8] != 0) as u8;
4619        }
4620        if nzc[scan - 1] != 0xff {
4621            na = (nzc[scan - 1] != 0) as u8;
4622        }
4623    }
4624    if !is8 {
4625        let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntCbf);
4626        let cbf = cab.decode_decision(85 + RES_CBF[rp] + (na + (nb << 1)) as usize);
4627        if cbf == 0 {
4628            if !is_dc {
4629                nzc[scan] = 0;
4630            }
4631            return 0;
4632        }
4633        if is_dc {
4634            *cbf_dc |= 1 << rp;
4635        }
4636    }
4637    // ---- significance map ----
4638    let maxpos = RES_MAXPOS[rp] as usize;
4639    // cat 5 uses its own absolute bases; the 4×4 categories share 105/166 + offset.
4640    let (map, last) = if is8 { (402, 417) } else { (105 + RES_MAP[rp], 166 + RES_MAP[rp]) };
4641    // SPARSE significance map: record each significant POSITION in `pos[..n]`
4642    // instead of marking a dense 64-entry array. Three costs disappear — the
4643    // 256-byte `sig` zeroing per call, the level loop's data-dependent
4644    // `sig[i] != 0` re-scan of every position (a branch mispredict per
4645    // transition on typical 2-4-coeff blocks), and the final dense copy into
4646    // `out`. Bin ORDER is unchanged: levels were decoded at descending
4647    // significant positions, which is exactly `pos[..n]` reversed.
4648    //
4649    // CONTRACT with the callers (all 10 sites): `out` is freshly zeroed, so
4650    // writing only the significant entries leaves the same contents the dense
4651    // copy produced. A reused non-zero `out` would be a correctness bug.
4652    let mut pos = [0u8; 64];
4653    let mut n = 0usize;
4654    let mut last_hit = false;
4655    let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntSig);
4656    for i in 0..maxpos {
4657        // 4×4: ctxIdxInc IS the scan position. 8×8: it comes from the folded maps.
4658        let (mi, li) = if is8 { (SIG8X8[i] as usize, LAST8X8[i] as usize) } else { (i, i) };
4659        if cab.decode_decision(map + mi) != 0 {
4660            pos[n] = i as u8;
4661            n += 1;
4662            if cab.decode_decision(last + li) != 0 {
4663                last_hit = true;
4664                break;
4665            }
4666        }
4667    }
4668    if !last_hit {
4669        pos[n] = maxpos as u8;
4670        n += 1;
4671    }
4672    let coeff_num = n as u32;
4673    // ---- levels ----
4674    let one = 227 + RES_ONE[rp];
4675    let abs = 232 + RES_ONE[rp];
4676    let maxc2 = RES_MAXC2[rp];
4677    let (mut c1, mut c2) = (1i32, 0i32);
4678    drop(_sg);
4679    let _lg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntLvl);
4680    for k in (0..n).rev() {
4681        let mut level = 1 + cab.decode_decision(one + c1 as usize) as i32;
4682        if level == 2 {
4683            level += cabac_ueg_level(cab, abs + c2 as usize) as i32;
4684            c2 = (c2 + 1).min(maxc2);
4685            c1 = 0;
4686        } else if c1 != 0 {
4687            c1 = (c1 + 1).min(4);
4688        }
4689        if cab.decode_bypass() != 0 {
4690            level = -level;
4691        }
4692        out[pos[k] as usize] = level;
4693    }
4694    if is8 {
4695        // One 8×8 covers four consecutive z-order 4×4 cells. Every later
4696        // coded_block_flag ctxIdxInc reads this cache, so all four must carry the
4697        // count — writing only `scan` would corrupt the NEXT macroblock's contexts.
4698        for k in 0..4 {
4699            nzc[NZC_CACHE[(iz + k).min(23)]] = coeff_num as u8;
4700        }
4701    } else if !is_dc {
4702        nzc[scan] = coeff_num as u8;
4703    }
4704    coeff_num
4705}
4706
4707/// One deferred pixel-reconstruction job (entropy-decouple E1 seam).
4708enum EdcJob {
4709    Skip { mbx: usize, mby: usize, mv: (i32, i32) },
4710    Inter(Box<PInterJob>),
4711}
4712
4713/// The compact inputs of one CABAC P inter macroblock's reconstruction.
4714struct PInterJob {
4715    mbx: usize,
4716    mby: usize,
4717    t8: bool,
4718    qp: u8,
4719    cbp_chroma: u32,
4720    luma_scan: [[i32; 16]; 16],
4721    luma8: [[i32; 64]; 4],
4722    cdc: [[i32; 4]; 2],
4723    cac: [[[i32; 16]; 4]; 2],
4724    nnzs: [u8; 24],
4725}
4726
4727/// Entropy-decouple master knob — DEFAULT ON since 2026-08-05 (`RS_H264_EDC=0`
4728/// opts out). E1 was expected to be cost-neutral scaffolding for the E2
4729/// thread; it BANKED on its own: 13/15 pairs, z=2.84, median +4.0% (pooled
4730/// 19/24, z=2.86). Mechanism: LOOP FISSION — batching a row's parsing and
4731/// then a row's reconstruction keeps each large code path's I-cache and
4732/// branch state hot, instead of alternating two giant bodies per macroblock.
4733fn edc_on() -> bool {
4734    use std::sync::atomic::{AtomicU8, Ordering};
4735    static ON: AtomicU8 = AtomicU8::new(0);
4736    match ON.load(Ordering::Relaxed) {
4737        0 => {
4738            let v = !std::env::var_os("RS_H264_EDC").is_some_and(|v| v == "0");
4739            ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
4740            v
4741        }
4742        n => n == 1,
4743    }
4744}
4745
4746/// Row-interleaved deblocking master knob: `RS_H264_ROWDB=0` opts out,
4747/// restoring the picture-end pipeline (WHYS Part 17) as the A/B comparator.
4748fn rowdb_on() -> bool {
4749    use std::sync::atomic::{AtomicU8, Ordering};
4750    static ON: AtomicU8 = AtomicU8::new(0);
4751    match ON.load(Ordering::Relaxed) {
4752        0 => {
4753            let v = !std::env::var_os("RS_H264_ROWDB").is_some_and(|v| v == "0");
4754            ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
4755            v
4756        }
4757        n => n == 1,
4758    }
4759}
4760
4761/// 4×4-block (z-order) → 30-entry (6-stride) mv/ref/mvd cache index (openh264
4762/// g_kCache30ScanIdx). Top neighbour = cache[idx-6], left = cache[idx-1].
4763const CACHE30: [usize; 16] = [7, 8, 13, 14, 9, 10, 15, 16, 19, 20, 25, 26, 21, 22, 27, 28];
4764
4765/// z-order 4×4-block → raster index (openh264 g_kuiScan4). Per-MB mvd/ref state is
4766/// stored raster-indexed (matching how neighbour blocks 3/7/11/15 and 12..15 are read).
4767const G_SCAN4: [usize; 16] = [0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15];
4768
4769/// P `sub_mb_type` CABAC (openh264 `ParseSubMBTypeCabac`, ctx 21). 0=8×8, 1=8×4, 2=4×8, 3=4×4.
4770fn parse_sub_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
4771    const S: usize = 21;
4772    if cab.decode_decision(S) != 0 {
4773        return 0;
4774    }
4775    if cab.decode_decision(S + 1) != 0 {
4776        3 - cab.decode_decision(S + 2)
4777    } else {
4778        1
4779    }
4780}
4781
4782/// Intra `mb_type` sub-parse for P/B slices (openh264 `DecodeCabacIntraMbType`, `base`=32
4783/// for B). Returns 0 = I_4x4, 1..=24 = I_16x16, 25 = I_PCM (in the intra numbering).
4784fn parse_intra_mb_type_cabac(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
4785    if cab.decode_decision(base) == 0 {
4786        return 0; // I_4x4
4787    }
4788    if cab.decode_terminate() {
4789        return 25; // I_PCM
4790    }
4791    let mut t = 1 + 12 * cab.decode_decision(base + 1) as u32; // cbp_luma != 0
4792    if cab.decode_decision(base + 2) != 0 {
4793        t += 4 + 4 * cab.decode_decision(base + 2) as u32;
4794    }
4795    t += 2 * cab.decode_decision(base + 3) as u32;
4796    t += cab.decode_decision(base + 3) as u32;
4797    t
4798}
4799
4800/// B `mb_type` CABAC (openh264 `ParseMBTypeBSliceCabac`, ctx base 27). `ctx_inc` = (left
4801/// avail & !direct) + (top avail & !direct). Returns 0 = B_Direct_16x16, 1..=21 = the
4802/// L0/L1/Bi 16×16/16×8/8×16 shapes, 22 = B_8x8, 23.. = intra (mb_type − 23).
4803/// Test-only alias so the ENCODER crate can gate `cb_mb_type_b` against this
4804/// parser directly — they are exact inverses, so a round-trip is a complete gate.
4805#[doc(hidden)]
4806pub fn parse_mb_type_b(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
4807    parse_mb_type_b_cabac(cab, ctx_inc)
4808}
4809
4810fn parse_mb_type_b_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
4811    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
4812    const B: usize = 27;
4813    if cab.decode_decision(B + ctx_inc) == 0 {
4814        return 0; // B_Direct_16x16
4815    }
4816    if cab.decode_decision(B + 3) == 0 {
4817        return 1 + cab.decode_decision(B + 5) as u32; // 16×16 L0 / L1
4818    }
4819    let mut m = (cab.decode_decision(B + 4) as u32) << 3;
4820    m |= (cab.decode_decision(B + 5) as u32) << 2;
4821    m |= (cab.decode_decision(B + 5) as u32) << 1;
4822    m |= cab.decode_decision(B + 5) as u32;
4823    if m < 8 {
4824        return m + 3;
4825    }
4826    if m == 13 {
4827        return parse_intra_mb_type_cabac(cab, 32) + 23;
4828    }
4829    if m == 14 {
4830        return 11; // B_Bi_8x16
4831    }
4832    if m == 15 {
4833        return 22; // B_8x8
4834    }
4835    m = (m << 1) | cab.decode_decision(B + 5) as u32;
4836    m - 4
4837}
4838
4839/// B `sub_mb_type` CABAC (openh264 `ParseBSubMBTypeCabac`, ctx base 36). Returns 0..=12
4840/// per spec Table 7-18 (0 = B_Direct_8x8, 1 = B_L0_8x8, …, 12 = B_Bi_4x4).
4841fn parse_sub_mb_type_b_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
4842    const B: usize = 36;
4843    if cab.decode_decision(B) == 0 {
4844        return 0; // B_Direct_8x8
4845    }
4846    if cab.decode_decision(B + 1) == 0 {
4847        return 1 + cab.decode_decision(B + 3) as u32; // B_L0_8x8 / B_L1_8x8
4848    }
4849    let mut st = 3u32;
4850    if cab.decode_decision(B + 2) != 0 {
4851        if cab.decode_decision(B + 3) != 0 {
4852            return 11 + cab.decode_decision(B + 3) as u32; // B_L1_4x4 / B_Bi_4x4
4853        }
4854        st += 4;
4855    }
4856    st += 2 * cab.decode_decision(B + 3) as u32;
4857    st += cab.decode_decision(B + 3) as u32;
4858    st
4859}
4860
4861/// Parse one motion partition's `mvd` (x,y) and splat it into the 30-entry cache + the
4862/// per-MB raster mvd/ref state. `part_idx` = the partition's top-left z-order block (for
4863/// the ctxInc neighbour lookup); `zblocks` = every z-order 4×4 block the partition covers.
4864fn parse_mvd_partition(
4865    cab: &mut crate::cabac::Cabac,
4866    part_idx: usize,
4867    zblocks: &[usize],
4868    mvdc: &mut [[i16; 2]; 30],
4869    refc: &mut [i8; 30],
4870    mmvd: &mut [[i16; 2]; 16],
4871    mref: &mut [i8; 16],
4872    ref_idx: i8,
4873) -> (i32, i32) {
4874    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
4875    let s = CACHE30[part_idx];
4876    let ctx = |comp: usize| -> usize {
4877        let mut a = 0i32;
4878        if refc[s - 6] >= 0 {
4879            a += mvdc[s - 6][comp].unsigned_abs() as i32;
4880        }
4881        if refc[s - 1] >= 0 {
4882            a += mvdc[s - 1][comp].unsigned_abs() as i32;
4883        }
4884        if a >= 3 {
4885            1 + (a > 32) as usize
4886        } else {
4887            0
4888        }
4889    };
4890    let (cx, cy) = (ctx(0), ctx(1));
4891    let mvx = parse_mvd_cabac(cab, 0, cx);
4892    let mvy = parse_mvd_cabac(cab, 1, cy);
4893    for &zb in zblocks {
4894        mvdc[CACHE30[zb]] = [mvx, mvy];
4895        refc[CACHE30[zb]] = ref_idx;
4896        mmvd[G_SCAN4[zb]] = [mvx, mvy];
4897        mref[G_SCAN4[zb]] = ref_idx;
4898    }
4899    (mvx as i32, mvy as i32)
4900}
4901
4902/// `ref_idx_l0` (P) CABAC — mirror of the encoder `cb_ref_idx`. Unary, ctxIdxOffset
4903/// 54: binIdx 0 → `ctx0` (condTermFlagA + 2·condTermFlagB), binIdx 1 → 4, binIdx ≥2 → 5.
4904fn parse_ref_idx_cabac(cab: &mut crate::cabac::Cabac, ctx0: usize) -> i8 {
4905    const B: usize = 54;
4906    let mut r = 0i8;
4907    let mut bin_idx = 0u32;
4908    // Cap the unary length: valid ref_idx ≤ 15 (16 refs max); the cap keeps a corrupt
4909    // stream from looping unboundedly. The MC clamps the index, so an over-range value
4910    // is decoded as garbage (never a panic) — the robustness contract, not correctness.
4911    while bin_idx < 32 {
4912        let ctx = match bin_idx {
4913            0 => ctx0,
4914            1 => 4,
4915            _ => 5,
4916        };
4917        if cab.decode_decision(B + ctx) == 0 {
4918            break;
4919        }
4920        r += 1;
4921        bin_idx += 1;
4922    }
4923    r
4924}
4925
4926/// UEG3 mvd suffix (openh264 `DecodeUEGMvCabac`): TU prefix at `base + {0,1,2,3,3,..}`
4927/// (≤7), then EG3 bypass.
4928fn decode_ueg_mv(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
4929    const P2C: [usize; 8] = [0, 1, 2, 3, 3, 3, 3, 3];
4930    if cab.decode_decision(base) == 0 {
4931        return 0;
4932    }
4933    let mut code = 0u32;
4934    let mut count = 1usize;
4935    let mut tmp;
4936    loop {
4937        tmp = cab.decode_decision(base + P2C[count]);
4938        code += 1;
4939        count += 1;
4940        if tmp == 0 || count == 8 {
4941            break;
4942        }
4943    }
4944    if tmp != 0 {
4945        code += cabac_exp_bypass(cab, 3) + 1;
4946    }
4947    code
4948}
4949
4950/// One `mvd` component (openh264 `ParseMvdInfoCabac`). `ctx_inc` (0/1/2) from the
4951/// neighbour |mvd| sum. ctxIdxOffset 40 (x) / 47 (y).
4952fn parse_mvd_cabac(cab: &mut crate::cabac::Cabac, comp: usize, ctx_inc: usize) -> i16 {
4953    let base = 40 + comp * 7; // NEW_CTX_OFFSET_MVD + comp*CTX_NUM_MVD
4954    if cab.decode_decision(base + ctx_inc) == 0 {
4955        return 0;
4956    }
4957    let mag = (decode_ueg_mv(cab, base + 3) + 1) as i16;
4958    if cab.decode_bypass() != 0 {
4959        -mag
4960    } else {
4961        mag
4962    }
4963}
4964
4965/// `mb_skip_flag` CABAC (openh264 `ParseSkipFlagCabac`). `ctx_inc` = base 11 (P) or 24
4966/// (B) + (left avail & not-skip) + (top avail & not-skip). Returns true if skipped.
4967fn parse_mb_skip_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> bool {
4968    cab.decode_decision(ctx_inc) != 0
4969}
4970
4971/// P-slice `mb_type` CABAC (openh264 `ParseMBTypePSliceCabac`). Returns 0..3 = inter
4972/// (P_L0_16x16 / P_16x8 / P_8x16 / P_8x8), 5 = I_4x4, 6..29 = I_16x16, 30 = I_PCM.
4973fn parse_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
4974    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
4975    const S: usize = 11; // NEW_CTX_OFFSET_SKIP; P mb_type contexts hang off it
4976    if cab.decode_decision(S + 3) == 0 {
4977        // inter
4978        return if cab.decode_decision(S + 4) != 0 {
4979            if cab.decode_decision(S + 6) != 0 { 1 } else { 2 }
4980        } else if cab.decode_decision(S + 5) != 0 {
4981            3
4982        } else {
4983            0
4984        };
4985    }
4986    // intra (prefix bit was 1)
4987    if cab.decode_decision(S + 6) == 0 {
4988        return 5; // I_4x4
4989    }
4990    if cab.decode_terminate() {
4991        return 30; // I_PCM
4992    }
4993    let mut t = 6 + cab.decode_decision(S + 7) * 12;
4994    if cab.decode_decision(S + 8) != 0 {
4995        t += 4;
4996        if cab.decode_decision(S + 8) != 0 {
4997            t += 4;
4998        }
4999    }
5000    t += cab.decode_decision(S + 9) << 1;
5001    t += cab.decode_decision(S + 9);
5002    t
5003}
5004
5005/// I-slice `mb_type` CABAC parse (spec §9.3.2.5 / openh264 `ParseMBTypeISliceCabac`).
5006/// `ctx_inc` = (left MB is I_16x16/non-intra) + (top MB is …), i.e. 0..2; the corner
5007/// MB has no neighbours so `ctx_inc = 0`. Returns the raw mb_type: 0 = I_NxN (I_4x4/
5008/// I_8x8), 1..24 = I_16x16 (pred-mode/cbp packed), 25 = I_PCM.
5009fn parse_mb_type_i_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
5010    const O: usize = 3; // ctxIdxOffset for I-slice mb_type
5011    if cab.decode_decision(O + ctx_inc) == 0 {
5012        return 0; // I_NxN
5013    }
5014    if cab.decode_terminate() {
5015        return 25; // I_PCM
5016    }
5017    let mut t = 1 + cab.decode_decision(O + 3) * 12; // CBP luma: 0 or 12
5018    if cab.decode_decision(O + 4) != 0 {
5019        t += 4; // CBP chroma 1 or 2
5020        if cab.decode_decision(O + 5) != 0 {
5021            t += 4;
5022        }
5023    }
5024    t += cab.decode_decision(O + 6) << 1; // I_16x16 pred mode (2 bins)
5025    t += cab.decode_decision(O + 7);
5026    t
5027}
5028
5029/// One `Intra_4x4` (or `8x8`) pred-mode CABAC parse (openh264 `ParseIntraPredModeLuma
5030/// Cabac`): `prev_intra4x4_pred_mode_flag` (ctx 68) then, if 0, `rem_intra4x4_pred_mode`
5031/// (3 bins at ctx 69). Returns `-1` for "use predicted mode", else the 0..7 remainder.
5032fn parse_intra4x4_pred_mode_cabac(cab: &mut crate::cabac::Cabac) -> i32 {
5033    const IPR: usize = 68;
5034    if cab.decode_decision(IPR) == 1 {
5035        return -1; // prev_intra4x4_pred_mode_flag = 1
5036    }
5037    let mut m = cab.decode_decision(IPR + 1) as i32;
5038    m |= (cab.decode_decision(IPR + 1) as i32) << 1;
5039    m |= (cab.decode_decision(IPR + 1) as i32) << 2;
5040    m
5041}
5042
5043/// `intra_chroma_pred_mode` CABAC parse (openh264 `ParseIntraPredModeChromaCabac`):
5044/// TU(cMax=3) — bin0 at ctx `64 + ctx_inc` (ctx_inc from neighbour chroma modes, 0 for
5045/// the corner MB), the rest at ctx 67. Returns the mode 0..3.
5046fn parse_intra_chroma_pred_mode_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
5047    const CIPR: usize = 64;
5048    if cab.decode_decision(CIPR + ctx_inc) == 0 {
5049        return 0;
5050    }
5051    if cab.decode_decision(CIPR + 3) == 0 {
5052        return 1;
5053    }
5054    if cab.decode_decision(CIPR + 3) == 0 {
5055        return 2;
5056    }
5057    3
5058}
5059
5060/// `coded_block_pattern` CABAC parse (openh264 `ParseCbpInfoCabac`), corner-MB variant
5061/// (top/left neighbours unavailable → their terms are 0). ctxIdxOffset 73 (luma) with 4
5062/// z-order 8×8 bins whose ctxInc uses the EARLIER-decoded bits within this MB, then
5063/// chroma bits at 77/81. Returns cbp: bits 0-3 = luma 8×8, bits 4-5 = chroma pattern.
5064fn parse_cbp_cabac(cab: &mut crate::cabac::Cabac, top: Option<u8>, left: Option<u8>) -> u32 {
5065    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
5066    const CBP: usize = 73;
5067    let t = |m: u32| top.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
5068    let l = |m: u32| left.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
5069    let nb = |x: u32| (x == 0) as u32; // earlier 8×8 bin within this MB was NOT coded
5070    // Luma, 4 8×8 blocks in z-order. Top uses cbp bits 2/3, left uses 1/3.
5071    let b0 = cab.decode_decision(CBP + (l(1 << 1) + (t(1 << 2) << 1)) as usize);
5072    let b1 = cab.decode_decision(CBP + (nb(b0) + (t(1 << 3) << 1)) as usize);
5073    let b2 = cab.decode_decision(CBP + (l(1 << 3) + (nb(b0) << 1)) as usize);
5074    let b3 = cab.decode_decision(CBP + (nb(b2) + (nb(b1) << 1)) as usize);
5075    let mut cbp = b0 | (b1 << 1) | (b2 << 2) | (b3 << 3);
5076    // Chroma (4:2:0). ctxInc from neighbour chroma cbp (>>4).
5077    let ct = top.map_or(0u32, |c| ((c >> 4) != 0) as u32);
5078    let cl = left.map_or(0u32, |c| ((c >> 4) != 0) as u32);
5079    if cab.decode_decision(CBP + 4 + (cl + (ct << 1)) as usize) != 0 {
5080        let ct2 = top.map_or(0u32, |c| ((c >> 4) == 2) as u32);
5081        let cl2 = left.map_or(0u32, |c| ((c >> 4) == 2) as u32);
5082        let c1 = cab.decode_decision(CBP + 8 + (cl2 + (ct2 << 1)) as usize);
5083        cbp |= 1 << (4 + c1);
5084    }
5085    cbp
5086}
5087
5088fn read_ref_idx(r: &mut BitReader, num_ref_active: usize) -> Result<i32, OutOfData> {
5089    if num_ref_active == 2 {
5090        Ok(if r.read_bit()? { 0 } else { 1 }) // te(v): value = !bit
5091    } else {
5092        Ok(r.read_ue()? as i32)
5093    }
5094}
5095
5096/// B-partition prediction direction.
5097#[derive(Clone, Copy, PartialEq)]
5098enum BPred {
5099    L0,
5100    L1,
5101    Bi,
5102}
5103impl BPred {
5104    /// Whether this direction uses reference list `list` (0 or 1).
5105    fn uses(self, list: usize) -> bool {
5106        matches!(
5107            (self, list),
5108            (BPred::L0, 0) | (BPred::L1, 1) | (BPred::Bi, 0) | (BPred::Bi, 1)
5109        )
5110    }
5111}
5112
5113const B16X16: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 16)];
5114const B16X8: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 8), (0, 8, 16, 8)];
5115const B8X16: &[(usize, usize, usize, usize)] = &[(0, 0, 8, 16), (8, 0, 8, 16)];
5116
5117/// A partition region `(x, y, w, h)` in samples.
5118type Region = (usize, usize, usize, usize);
5119
5120/// B `mb_type` 1..=21 → (partition layout, MV-prediction mode 0/1/2 for 16×16/
5121/// 16×8/8×16, per-partition prediction direction) (spec Table 7-14).
5122/// Test-only view of [`b_inter_layout`] for the ENCODER crate: `(mvmode, p0, p1)`
5123/// with pred coded 1 = L0, 2 = L1, 3 = Bi — the encoder's `b_part_mb_type` is the
5124/// exact inverse, so a round-trip over 4..=21 gates the two tables against drift.
5125pub fn b_inter_shape(mb_type: u32) -> (u8, u8, u8) {
5126    let (_, mvmode, preds) = b_inter_layout(mb_type);
5127    let code = |p: BPred| match (p.uses(0), p.uses(1)) {
5128        (true, true) => 3,
5129        (true, false) => 1,
5130        _ => 2,
5131    };
5132    (mvmode, code(preds[0]), code(preds[1]))
5133}
5134
5135fn b_inter_layout(mb_type: u32) -> (&'static [Region], u8, [BPred; 2]) {
5136    use BPred::*;
5137    match mb_type {
5138        1 => (B16X16, 0, [L0, L0]),
5139        2 => (B16X16, 0, [L1, L1]),
5140        3 => (B16X16, 0, [Bi, Bi]),
5141        4 => (B16X8, 1, [L0, L0]),
5142        5 => (B8X16, 2, [L0, L0]),
5143        6 => (B16X8, 1, [L1, L1]),
5144        7 => (B8X16, 2, [L1, L1]),
5145        8 => (B16X8, 1, [L0, L1]),
5146        9 => (B8X16, 2, [L0, L1]),
5147        10 => (B16X8, 1, [L1, L0]),
5148        11 => (B8X16, 2, [L1, L0]),
5149        12 => (B16X8, 1, [L0, Bi]),
5150        13 => (B8X16, 2, [L0, Bi]),
5151        14 => (B16X8, 1, [L1, Bi]),
5152        15 => (B8X16, 2, [L1, Bi]),
5153        16 => (B16X8, 1, [Bi, L0]),
5154        17 => (B8X16, 2, [Bi, L0]),
5155        18 => (B16X8, 1, [Bi, L1]),
5156        19 => (B8X16, 2, [Bi, L1]),
5157        20 => (B16X8, 1, [Bi, Bi]),
5158        _ => (B8X16, 2, [Bi, Bi]), // 21
5159    }
5160}
5161
5162/// Whether a B `sub_mb_type` (1..=12) uses reference list `list`.
5163fn b_sub_uses(st: u32, list: usize) -> bool {
5164    let pred = match st {
5165        1 | 4 | 5 | 10 => 0,  // L0
5166        2 | 6 | 7 | 11 => 1,  // L1
5167        _ => 2,               // Bi (3, 8, 9, 12)
5168    };
5169    (list == 0 && pred != 1) || (list == 1 && pred != 0)
5170}
5171
5172/// Sub-partition shapes within an 8×8 for a B `sub_mb_type` (1..=12).
5173fn b_sub_parts(st: u32) -> &'static [(usize, usize, usize, usize)] {
5174    match st {
5175        1..=3 => &[(0, 0, 8, 8)],
5176        4 | 6 | 8 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
5177        5 | 7 | 9 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
5178        _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)], // 10/11/12
5179    }
5180}
5181
5182/// Sub-macroblock partition layout `(x, y, w, h)` in samples within an 8×8, for
5183/// a P-slice `sub_mb_type` (0 = 8×8, 1 = 8×4, 2 = 4×8, 3 = 4×4).
5184fn sub_mb_partitions(sub_type: u32) -> &'static [(usize, usize, usize, usize)] {
5185    match sub_type {
5186        0 => &[(0, 0, 8, 8)],
5187        1 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
5188        2 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
5189        _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)],
5190    }
5191}
5192
5193/// Copy a contiguous `w`x`h` block into a strided destination at `(x0, y0)`.
5194///
5195/// The width is SPECIALISED. Written as a per-pixel loop bounded by a runtime `w`,
5196/// this lowers to a bounds-checked store per pixel — and where it is a row copy of
5197/// runtime length, to a variable-length `memcpy` CALL per row. Both are the same
5198/// codegen trap the ENCODER fixed long ago ("H-17"); the decoder's copy of it was
5199/// never fixed, and it costs the most on exactly the streams a real encoder emits,
5200/// because x264's sub-16x16 partitions call it far more often than our own
5201/// 16x16-dominated bitstreams ever did. Byte-identical to the scalar form.
5202#[inline]
5203fn restride(dst: &mut [u8], dst_stride: usize, x0: usize, y0: usize, src: &[u8], w: usize, h: usize) {
5204    macro_rules! rows {
5205        ($n:expr) => {{
5206            for dy in 0..h {
5207                dst[(y0 + dy) * dst_stride + x0..][..$n].copy_from_slice(&src[dy * $n..][..$n]);
5208            }
5209        }};
5210    }
5211    match w {
5212        16 => rows!(16),
5213        8 => rows!(8),
5214        4 => rows!(4),
5215        2 => rows!(2),
5216        _ => {
5217            for dy in 0..h {
5218                dst[(y0 + dy) * dst_stride + x0..][..w].copy_from_slice(&src[dy * w..][..w]);
5219            }
5220        }
5221    }
5222}
5223
5224fn store(plane: &mut [u8], stride: usize, x0: usize, y0: usize, s: &[u8; 16]) {
5225    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Scatter);
5226    for dy in 0..4 {
5227        for dx in 0..4 {
5228            plane[(y0 + dy) * stride + (x0 + dx)] = s[dy * 4 + dx];
5229        }
5230    }
5231}
5232
5233/// Un-scans an 8×8 block from frame zig-zag scan order to raster (spec Table 8-12).
5234fn un_scan_8x8(scan: &[i32; 64]) -> [i32; 64] {
5235    const ZZ8: [usize; 64] = [
5236        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,
5237        20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
5238        58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
5239    ];
5240    let mut out = [0i32; 64];
5241    for k in 0..64 {
5242        out[ZZ8[k]] = scan[k];
5243    }
5244    out
5245}
5246
5247#[cfg(test)]
5248mod tests {
5249    use super::*;
5250
5251    fn fd(qp: u8, offset: i32) -> FrameDecoder {
5252        FrameDecoder::new(1, 1, qp, offset, Vec::new(), 1, false, false, true)
5253    }
5254
5255    #[test]
5256    fn mb_qp_delta_accumulates_mod_52() {
5257        let mut d = fd(26, 0);
5258        assert_eq!(d.cur_qp, 26, "QPy starts at the slice QP");
5259        d.step_qp(4);
5260        assert_eq!(d.cur_qp, 30); // 26 + 4
5261        d.step_qp(-10);
5262        assert_eq!(d.cur_qp, 20); // carries from the previous MB, not the slice
5263        // Wrap-around: (20 + 40 + 52) % 52 = 112 % 52 = 8.
5264        d.step_qp(40);
5265        assert_eq!(d.cur_qp, 8);
5266        // Negative wrap: (8 - 20 + 52) % 52 = 40.
5267        d.step_qp(-20);
5268        assert_eq!(d.cur_qp, 40);
5269    }
5270
5271    #[test]
5272    fn chroma_qp_index_offset_applied_and_clamped() {
5273        // Offset 0 reproduces the bare luma->chroma table (QP30 -> 29).
5274        assert_eq!(fd(0, 0).chroma_qp_for(30), 29);
5275        // Positive offset shifts the table lookup (QP30 + 2 -> table[2] = 31).
5276        assert_eq!(fd(0, 2).chroma_qp_for(30), 31);
5277        // The qPi index is clamped into 0..=51 before the lookup.
5278        assert_eq!(fd(0, -12).chroma_qp_for(5), chroma_qp(0));
5279        assert_eq!(fd(0, 99).chroma_qp_for(40), chroma_qp(51));
5280    }
5281}