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