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