Skip to main content

rusty_h264_decoder/
mb16.rs

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