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 = {
2148            let _gw = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBWeights);
2149            self.implicit_weights(refi0, refi1)
2150        };
2151        // Bi-prediction blend of two MC samples `p` (L0) and `q` (L1).
2152        let blend = |p: i32, q: i32| -> u8 {
2153            match weights {
2154                Some((w0, w1)) => (((p * w0 + q * w1 + 32) >> 6).clamp(0, 255)) as u8,
2155                None => ((p + q + 1) >> 1) as u8,
2156            }
2157        };
2158        let (mut a, mut b) = ([0u8; 256], [0u8; 256]);
2159        let _gl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBLuma);
2160        if refi0 >= 0 {
2161            let rf = &self.refs[refi0 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, mv0.0, mv0.1, &mut a);
2163        }
2164        if refi1 >= 0 {
2165            let rf = &self.refs1[refi1 as usize];
2166            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);
2167        }
2168        drop(_gl);
2169        let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
2170        // Hoist the loop-invariant L0/L1 branch out of the inner loop: uni-pred is
2171        // a row copy (memcpy), bi-pred a branchless blend (both autovectorize).
2172        match (refi0 >= 0, refi1 >= 0) {
2173            (true, true) => {
2174                for dy in 0..rh {
2175                    for dx in 0..rw {
2176                        let (p, q) = (a[dy * rw + dx] as i32, b[dy * rw + dx] as i32);
2177                        pred_y[(py + dy) * 16 + (px + dx)] = blend(p, q);
2178                    }
2179                }
2180            }
2181            (true, false) => {
2182                for dy in 0..rh {
2183                    let d = (py + dy) * 16 + px;
2184                    pred_y[d..d + rw].copy_from_slice(&a[dy * rw..dy * rw + rw]);
2185                }
2186            }
2187            _ => {
2188                for dy in 0..rh {
2189                    let d = (py + dy) * 16 + px;
2190                    pred_y[d..d + rw].copy_from_slice(&b[dy * rw..dy * rw + rw]);
2191                }
2192            }
2193        }
2194        drop(_gbl);
2195        let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
2196        let (crx, cry, crw, crh) = (px / 2, py / 2, rw / 2, rh / 2);
2197        for c in 0..2 {
2198            let (mut ca, mut cb) = ([0u8; 64], [0u8; 64]);
2199            if refi0 >= 0 {
2200                let rf = &self.refs[refi0 as usize];
2201                let pl = if c == 0 { &rf.pu } else { &rf.pv };
2202                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);
2203            }
2204            if refi1 >= 0 {
2205                let rf = &self.refs1[refi1 as usize];
2206                let pl = if c == 0 { &rf.pu } else { &rf.pv };
2207                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);
2208            }
2209            match (refi0 >= 0, refi1 >= 0) {
2210                (true, true) => {
2211                    for dy in 0..crh {
2212                        for dx in 0..crw {
2213                            let (p, q) = (ca[dy * crw + dx] as i32, cb[dy * crw + dx] as i32);
2214                            c_pred[c][(cry + dy) * 8 + (crx + dx)] = blend(p, q);
2215                        }
2216                    }
2217                }
2218                (true, false) => {
2219                    for dy in 0..crh {
2220                        let d = (cry + dy) * 8 + crx;
2221                        c_pred[c][d..d + crw].copy_from_slice(&ca[dy * crw..dy * crw + crw]);
2222                    }
2223                }
2224                _ => {
2225                    for dy in 0..crh {
2226                        let d = (cry + dy) * 8 + crx;
2227                        c_pred[c][d..d + crw].copy_from_slice(&cb[dy * crw..dy * crw + crw]);
2228                    }
2229                }
2230            }
2231        }
2232    }
2233
2234    /// Commits a region's per-list motion to the 4×4 grids (and marks coded).
2235    #[allow(clippy::too_many_arguments)]
2236    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)) {
2237        let w4 = self.mb_w * 4;
2238        for by in py / 4..(py + rh) / 4 {
2239            for bx in px / 4..(px + rw) / 4 {
2240                let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
2241                self.ref_idx_y[idx] = refi0;
2242                self.mv_y[idx] = if refi0 >= 0 { mv0 } else { (0, 0) };
2243                self.ref_idx1[idx] = refi1;
2244                self.mv1[idx] = if refi1 >= 0 { mv1 } else { (0, 0) };
2245                self.inter_y[idx] = true;
2246                self.coded_y[idx] = true;
2247                self.modes_y[idx] = 2;
2248            }
2249        }
2250    }
2251
2252    /// Spatial direct prediction for a region (whole MB or an 8×8): derives the
2253    /// per-list reference indices and base MVs, then motion-compensates each 4×4
2254    /// sub-block (applying `colZeroFlag`) and commits the motion (spec §8.4.1.2.2).
2255    #[allow(clippy::too_many_arguments)]
2256    /// Splits a `w`×`h` block region (4×4-block units) into the fewest rectangles
2257    /// whose contents are `uniform`, preferring partition-shaped cuts (whole →
2258    /// horizontal halves → vertical halves → quadrants). Emits at most w·h rects
2259    /// (the all-different worst case degenerates to per-block, i.e. the old loop).
2260    fn coalesce_region(
2261        x: usize,
2262        y: usize,
2263        w: usize,
2264        h: usize,
2265        uniform: &dyn Fn(usize, usize, usize, usize) -> bool,
2266        emit: &mut dyn FnMut(usize, usize, usize, usize),
2267    ) {
2268        if uniform(x, y, w, h) {
2269            emit(x, y, w, h);
2270            return;
2271        }
2272        if h > 1 && uniform(x, y, w, h / 2) && uniform(x, y + h / 2, w, h / 2) {
2273            emit(x, y, w, h / 2);
2274            emit(x, y + h / 2, w, h / 2);
2275            return;
2276        }
2277        if w > 1 && uniform(x, y, w / 2, h) && uniform(x + w / 2, y, w / 2, h) {
2278            emit(x, y, w / 2, h);
2279            emit(x + w / 2, y, w / 2, h);
2280            return;
2281        }
2282        match (w > 1, h > 1) {
2283            (true, true) => {
2284                for q in 0..4usize {
2285                    Self::coalesce_region(x + (q % 2) * (w / 2), y + (q / 2) * (h / 2), w / 2, h / 2, uniform, emit);
2286                }
2287            }
2288            (true, false) => {
2289                Self::coalesce_region(x, y, w / 2, h, uniform, emit);
2290                Self::coalesce_region(x + w / 2, y, w / 2, h, uniform, emit);
2291            }
2292            (false, true) => {
2293                Self::coalesce_region(x, y, w, h / 2, uniform, emit);
2294                Self::coalesce_region(x, y + h / 2, w, h / 2, uniform, emit);
2295            }
2296            (false, false) => emit(x, y, 1, 1),
2297        }
2298    }
2299
2300    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]) {
2301        let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDirect);
2302        if !self.direct_spatial {
2303            return self.decode_b_direct_temporal(mb_x, mb_y, px, py, rw, rh, pred_y, c_pred);
2304        }
2305        // MB-level neighbors drive the direct reference indices and base MVs.
2306        let (nbx, nby) = ((mb_x * 4) as isize, (mb_y * 4) as isize);
2307        let n0 = self.mv_neighbors_list(nbx, nby, 4, 0);
2308        let n1 = self.mv_neighbors_list(nbx, nby, 4, 1);
2309        let min_pos = |a: i32, b: i32| if a < 0 { b } else if b < 0 { a } else { a.min(b) };
2310        let rid = |n: &[MvNeighbor; 3]| min_pos(min_pos(n[0].ref_idx, n[1].ref_idx), n[2].ref_idx);
2311        let (mut refi0, mut refi1) = (rid(&n0), rid(&n1));
2312        let direct_zero = refi0 < 0 && refi1 < 0;
2313        if direct_zero {
2314            refi0 = 0;
2315            refi1 = 0;
2316        }
2317        let mv0 = if refi0 >= 0 && !direct_zero { predict_mv(n0[0], n0[1], n0[2], refi0) } else { (0, 0) };
2318        let mv1 = if refi1 >= 0 && !direct_zero { predict_mv(n1[0], n1[1], n1[2], refi1) } else { (0, 0) };
2319        // Per 4×4 sub-block: colZeroFlag zeroes the ref-0 motion vector. cz is the
2320        // ONLY per-block variable (two possible (m0,m1) values for the region), and
2321        // the MC filters + bi-blend are per-output-pixel — so sub-blocks with equal
2322        // cz coalesce into one wider `b_mc`, BIT-IDENTICAL. A 16×16 direct MB paid
2323        // 16 bi-pred b_mc calls (~96 MC kernel entries) before this; typically 1 now.
2324        let (bx0, by0, bw, bh) = (px / 4, py / 4, rw / 4, rh / 4);
2325        let mut czg = [[false; 4]; 4]; // region-local, [dy][dx]
2326        for dy in 0..bh {
2327            for dx in 0..bw {
2328                czg[dy][dx] =
2329                    !direct_zero && self.col_zero(mb_x * 4 + bx0 + dx, mb_y * 4 + by0 + dy);
2330            }
2331        }
2332        let uniform = |x: usize, y: usize, w: usize, h: usize| -> bool {
2333            let t = czg[y][x];
2334            (y..y + h).all(|dy| (x..x + w).all(|dx| czg[dy][dx] == t))
2335        };
2336        let mut rects: [(usize, usize, usize, usize); 16] = [(0, 0, 0, 0); 16];
2337        let mut n = 0usize;
2338        Self::coalesce_region(0, 0, bw, bh, &uniform, &mut |x, y, w, h| {
2339            rects[n] = (x, y, w, h);
2340            n += 1;
2341        });
2342        for &(x, y, w, h) in &rects[..n] {
2343            let cz = czg[y][x];
2344            let m0 = if refi0 == 0 && cz { (0, 0) } else { mv0 };
2345            let m1 = if refi1 == 0 && cz { (0, 0) } else { mv1 };
2346            let (lx, ly, lw, lh) = ((bx0 + x) * 4, (by0 + y) * 4, w * 4, h * 4);
2347            self.b_mc(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1, pred_y, c_pred);
2348            self.b_set_motion(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1);
2349        }
2350    }
2351
2352    /// Temporal direct prediction for a region (spec §8.4.1.2.3): for each 4×4
2353    /// (or per-8×8 corner under `direct_8x8_inference`), take the co-located
2354    /// List-0 motion from `RefPicList1[0]`, map its reference into the current
2355    /// List-0 by POC, and scale the motion vector by the POC distances.
2356    #[allow(clippy::too_many_arguments)]
2357    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]) {
2358        let poc1 = self.refs1.first().map_or(0, |f| f.poc);
2359        let infer = self.direct_8x8_inference;
2360        // Under direct_8x8_inference every 4×4 in an 8×8 takes the same MB-corner
2361        // co-located motion, so motion-compensate the whole 8×8 in one call — this
2362        // hits the width-8 MC asm and pays the per-call tile/blend setup 4× less.
2363        // Without inference, motion is genuinely per-4×4. Bit-identical either way
2364        // (MC of an 8×8 with one MV == four 4×4 MCs with that same MV).
2365        let step = if infer { 8 } else { 4 };
2366        let mut sy = py;
2367        while sy < py + rh {
2368            let mut sx = px;
2369            while sx < px + rw {
2370                let (cx4, cy4) = (sx / 4, sy / 4);
2371                // Co-located 4×4 (the 8×8's MB-corner under inference).
2372                let (colx, coly) = if infer {
2373                    ((cx4 / 2) * 3, (cy4 / 2) * 3)
2374                } else {
2375                    (cx4, cy4)
2376                };
2377                let (mvcol, refpoc) = {
2378                    let col = &self.refs1[0];
2379                    let idx = (mb_y * 4 + coly) * col.w4 + (mb_x * 4 + colx);
2380                    if col.w4 != 0 && idx < col.mv.len() && col.ref_poc[idx] != i32::MIN {
2381                        (col.mv[idx], col.ref_poc[idx])
2382                    } else {
2383                        ((0, 0), i32::MIN) // intra co-located → zero motion, refIdxL0 = 0
2384                    }
2385                };
2386                // MapColToList0: the current-list index of the co-located reference.
2387                let (refi0, mvc) = if refpoc == i32::MIN {
2388                    (0, (0, 0))
2389                } else {
2390                    let r = self.refs.iter().position(|f| f.poc == refpoc).unwrap_or(0) as i32;
2391                    (r, mvcol)
2392                };
2393                let poc0 = self.refs[refi0 as usize].poc;
2394                let td = (poc1 - poc0).clamp(-128, 127);
2395                let tb = (self.cur_poc - poc0).clamp(-128, 127);
2396                let (mv0, mv1) = if td == 0 || self.refs[refi0 as usize].long_term {
2397                    (mvc, (0, 0))
2398                } else {
2399                    let tx = (16384 + td.abs() / 2) / td;
2400                    let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
2401                    let m0 = ((dsf * mvc.0 + 128) >> 8, (dsf * mvc.1 + 128) >> 8);
2402                    (m0, (m0.0 - mvc.0, m0.1 - mvc.1))
2403                };
2404                self.b_mc(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1, pred_y, c_pred);
2405                self.b_set_motion(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1);
2406                sx += step;
2407            }
2408            sy += step;
2409        }
2410    }
2411
2412    /// Reads `ref_idx_lX` for a B partition (te(v)/ue(v) by the list's active
2413    /// count), bounds-checked against the available reference count.
2414    fn read_b_ref(&self, r: &mut BitReader, list: usize) -> Result<i32, MbError> {
2415        let (active, avail) = if list == 0 {
2416            (self.num_ref_active, self.refs.len())
2417        } else {
2418            (self.num_ref_active1, self.refs1.len())
2419        };
2420        let v = if active > 1 { read_ref_idx(r, active)? } else { 0 };
2421        if v as usize >= avail {
2422            return Err(MbError::Truncated);
2423        }
2424        Ok(v)
2425    }
2426
2427    /// Reconstructs a `B_Skip` macroblock: spatial-direct prediction, no residual.
2428    fn decode_b_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2429        if self.refs.is_empty() || self.refs1.is_empty() {
2430            return Err(MbError::Unsupported("B without references"));
2431        }
2432        let mut pred_y = [0u8; 256];
2433        let mut c_pred = [[0u8; 64]; 2];
2434        self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
2435        // Zero residual: the prediction is the reconstruction — copy it row-wise.
2436        for dy in 0..16 {
2437            let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
2438            self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
2439        }
2440        for c in 0..2 {
2441            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2442            for dy in 0..8 {
2443                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
2444                plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
2445            }
2446        }
2447        // nnz stays 0 (no residual) — clear the grids for neighbor context.
2448        let w4 = self.mb_w * 4;
2449        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2450            self.nnz_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 0;
2451        }
2452        Ok(())
2453    }
2454
2455    /// Reconstructs a B macroblock (spec Table 7-14): direct, L0/L1/Bi partitions,
2456    /// `B_8x8`, or intra.
2457    fn decode_b_mb(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2458        let mb_type = r.read_ue()?;
2459        if mb_type >= 23 {
2460            return self.decode_intra_mb(r, mb_x, mb_y, mb_type - 23);
2461        }
2462        if self.refs.is_empty() || self.refs1.is_empty() {
2463            return Err(MbError::Unsupported("B without references"));
2464        }
2465        let mut pred_y = [0u8; 256];
2466        let mut c_pred = [[0u8; 64]; 2];
2467
2468        if mb_type == 0 {
2469            // B_Direct_16x16 — 8×8 transform allowed only with direct_8x8_inference.
2470            self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
2471            return self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, self.direct_8x8_inference);
2472        }
2473        if mb_type == 22 {
2474            return self.decode_b_8x8(r, mb_x, mb_y);
2475        }
2476
2477        // 16x16 / 16x8 / 8x16 partitions with per-partition L0/L1/Bi.
2478        let (layout, mvmode, preds) = b_inter_layout(mb_type);
2479        // mb_pred order: ref_idx_l0 (all L0 parts), ref_idx_l1, mvd_l0, mvd_l1.
2480        let mut refi = [[-1i32; 2]; 2]; // [part][list]
2481        for (p, &(_, _, _, _)) in layout.iter().enumerate() {
2482            if preds[p].uses(0) {
2483                refi[p][0] = self.read_b_ref(r, 0)?;
2484            }
2485        }
2486        for (p, _) in layout.iter().enumerate() {
2487            if preds[p].uses(1) {
2488                refi[p][1] = self.read_b_ref(r, 1)?;
2489            }
2490        }
2491        let mut mvd = [[(0i32, 0i32); 2]; 2];
2492        for (p, _) in layout.iter().enumerate() {
2493            if preds[p].uses(0) {
2494                mvd[p][0] = (r.read_se()?, r.read_se()?);
2495            }
2496        }
2497        for (p, _) in layout.iter().enumerate() {
2498            if preds[p].uses(1) {
2499                mvd[p][1] = (r.read_se()?, r.read_se()?);
2500            }
2501        }
2502        // Per partition: predict + commit each list's MV, then motion-compensate.
2503        for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
2504            let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
2505            let pwb = (rw / 4) as isize;
2506            let mut mv = [(0i32, 0i32); 2];
2507            for list in 0..2 {
2508                if refi[p][list] >= 0 {
2509                    let n = self.mv_neighbors_list(pbx, pby, pwb, list);
2510                    let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], refi[p][list]);
2511                    mv[list] = (pmv.0 + mvd[p][list].0, pmv.1 + mvd[p][list].1);
2512                }
2513            }
2514            self.b_set_motion(mb_x, mb_y, rx, ry, rw, rh, refi[p][0], mv[0], refi[p][1], mv[1]);
2515            // Bug-for-bug compatibility with openh264 (the conformance oracle): its
2516            // 16x8/8x16 B macroblock path mis-handles a Bi partition's destination
2517            // buffer. Partition 0 has its List-0 prediction overwritten by List-1
2518            // (result = List-1 only); partition 1's List-1 prediction lands at a
2519            // doubly-offset address, leaving List-0 in place (result = List-0 only).
2520            // 16x16 Bi averages correctly; only the partitioned path is affected.
2521            let (mc_r0, mc_r1) = if mvmode != 0 && refi[p][0] >= 0 && refi[p][1] >= 0 {
2522                if p == 0 {
2523                    (-1, refi[p][1])
2524                } else {
2525                    (refi[p][0], -1)
2526                }
2527            } else {
2528                (refi[p][0], refi[p][1])
2529            };
2530            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);
2531        }
2532        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true)
2533    }
2534
2535    /// Reconstructs a `B_8x8` macroblock: four 8×8 sub-macroblock partitions, each
2536    /// direct or L0/L1/Bi with its own sub-partitioning (spec Table 7-18).
2537    fn decode_b_8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2538        let mut sub = [0u32; 4];
2539        for s in sub.iter_mut() {
2540            let v = r.read_ue()?;
2541            if v > 12 {
2542                return Err(MbError::Unsupported("invalid B sub_mb_type"));
2543            }
2544            *s = v;
2545        }
2546        let mut pred_y = [0u8; 256];
2547        let mut c_pred = [[0u8; 64]; 2];
2548        // ref_idx for all 8×8 partitions (L0 batch, then L1 batch), for the
2549        // non-direct sub-partitions.
2550        let mut refi = [[-1i32; 2]; 4];
2551        for (p, &st) in sub.iter().enumerate() {
2552            if st != 0 && b_sub_uses(st, 0) {
2553                refi[p][0] = self.read_b_ref(r, 0)?;
2554            }
2555        }
2556        for (p, &st) in sub.iter().enumerate() {
2557            if st != 0 && b_sub_uses(st, 1) {
2558                refi[p][1] = self.read_b_ref(r, 1)?;
2559            }
2560        }
2561        // mvd: all mvd_l0 (partition-major, sub-partition order), then all mvd_l1.
2562        let mut mvd0: Vec<(i32, i32)> = Vec::new();
2563        let mut mvd1: Vec<(i32, i32)> = Vec::new();
2564        for &st in &sub {
2565            if st != 0 && b_sub_uses(st, 0) {
2566                for _ in b_sub_parts(st) {
2567                    mvd0.push((r.read_se()?, r.read_se()?));
2568                }
2569            }
2570        }
2571        for &st in &sub {
2572            if st != 0 && b_sub_uses(st, 1) {
2573                for _ in b_sub_parts(st) {
2574                    mvd1.push((r.read_se()?, r.read_se()?));
2575                }
2576            }
2577        }
2578        // Decode each 8×8 partition.
2579        let (mut i0, mut i1) = (0usize, 0usize);
2580        for (p, &st) in sub.iter().enumerate() {
2581            let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
2582            if st == 0 {
2583                self.decode_b_direct(mb_x, mb_y, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred);
2584                continue;
2585            }
2586            for &(sx, sy, sw, sh) in b_sub_parts(st) {
2587                let (px, py) = (b8x + sx, b8y + sy);
2588                let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
2589                let pwb = (sw / 4) as isize;
2590                let mut mv = [(0i32, 0i32); 2];
2591                if b_sub_uses(st, 0) {
2592                    let n = self.mv_neighbors_list(pbx, pby, pwb, 0);
2593                    let pmv = predict_mv(n[0], n[1], n[2], refi[p][0]);
2594                    let d = mvd0[i0];
2595                    i0 += 1;
2596                    mv[0] = (pmv.0 + d.0, pmv.1 + d.1);
2597                }
2598                if b_sub_uses(st, 1) {
2599                    let n = self.mv_neighbors_list(pbx, pby, pwb, 1);
2600                    let pmv = predict_mv(n[0], n[1], n[2], refi[p][1]);
2601                    let d = mvd1[i1];
2602                    i1 += 1;
2603                    mv[1] = (pmv.0 + d.0, pmv.1 + d.1);
2604                }
2605                self.b_set_motion(mb_x, mb_y, px, py, sw, sh, refi[p][0], mv[0], refi[p][1], mv[1]);
2606                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);
2607            }
2608        }
2609        // noSubMbPartSizeLessThan8x8: each sub-partition must be ≥ 8×8 (direct
2610        // counts only with the 8×8 inference flag).
2611        let allow_8x8 = sub
2612            .iter()
2613            .all(|&st| if st == 0 { self.direct_8x8_inference } else { st <= 3 });
2614        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8)
2615    }
2616
2617    /// Reconstructs a `P_8x8` macroblock: four 8×8 sub-macroblock partitions,
2618    /// each independently split (8×8 / 8×4 / 4×8 / 4×4) with its own motion
2619    /// vector(s). `ref0` is `P_8x8ref0` (every `ref_idx` forced to 0, not coded).
2620    fn decode_p8x8(
2621        &mut self,
2622        r: &mut BitReader,
2623        mb_x: usize,
2624        mb_y: usize,
2625        ref0: bool,
2626    ) -> Result<(), MbError> {
2627        if self.refs.is_empty() {
2628            return Err(MbError::Unsupported("inter without reference"));
2629        }
2630        let w4 = self.mb_w * 4;
2631        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
2632        let num_refs = self.refs.len();
2633
2634        // mb_pred order (spec §7.3.5.2): all sub_mb_type, then all ref_idx_l0,
2635        // then all mvd_l0 (partition-major, sub-partition order within each).
2636        let mut sub_types = [0u32; 4];
2637        for st in sub_types.iter_mut() {
2638            let v = r.read_ue()?;
2639            if v > 3 {
2640                return Err(MbError::Unsupported("B-slice / invalid sub_mb_type"));
2641            }
2642            *st = v;
2643        }
2644        let mut ref_idxs = [0i32; 4];
2645        if self.num_ref_active > 1 && !ref0 {
2646            for ri in ref_idxs.iter_mut() {
2647                *ri = read_ref_idx(r, self.num_ref_active)?;
2648                if *ri as usize >= num_refs {
2649                    return Err(MbError::Truncated); // references a non-existent picture
2650                }
2651            }
2652        }
2653
2654        // Per sub-partition (in decoding order): median MV prediction from the
2655        // committed neighbor grid, mvd, commit, then motion-compensate. Committing
2656        // before the next prediction is what lets sub-partitions chain correctly.
2657        let mut pred_y = [0u8; 256];
2658        let mut c_pred = [[0u8; 64]; 2];
2659        for part in 0..4usize {
2660            let refi = ref_idxs[part];
2661            let (b8x, b8y) = ((part % 2) * 8, (part / 2) * 8);
2662            for &(srx, sry, srw, srh) in sub_mb_partitions(sub_types[part]) {
2663                let (px, py) = (b8x + srx, b8y + sry);
2664                let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
2665                let [a, b, c] = self.mv_neighbors_block(pbx, pby, (srw / 4) as isize);
2666                let pmv = predict_mv(a, b, c, refi);
2667                let mvd_x = r.read_se()?;
2668                let mvd_y = r.read_se()?;
2669                let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
2670                for by in py / 4..py / 4 + srh / 4 {
2671                    for bx in px / 4..px / 4 + srw / 4 {
2672                        let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
2673                        self.mv_y[idx] = mv;
2674                        self.inter_y[idx] = true;
2675                        self.ref_idx_y[idx] = refi;
2676                        self.coded_y[idx] = true;
2677                    }
2678                }
2679                let reference = &self.refs[refi as usize];
2680                let mut tmp = [0u8; 256];
2681                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);
2682                for dy in 0..srh {
2683                    for dx in 0..srw {
2684                        pred_y[(py + dy) * 16 + (px + dx)] = tmp[dy * srw + dx];
2685                    }
2686                }
2687                let (crx, cry, crw, crh) = (px / 2, py / 2, srw / 2, srh / 2);
2688                for cc in 0..2 {
2689                    let rc = if cc == 0 { &reference.pu } else { &reference.pv };
2690                    let mut tc = [0u8; 64];
2691                    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);
2692                    for dy in 0..crh {
2693                        for dx in 0..crw {
2694                            c_pred[cc][(cry + dy) * 8 + (crx + dx)] = tc[dy * crw + dx];
2695                        }
2696                    }
2697                }
2698                self.weight_partition(
2699                    &mut pred_y, &mut c_pred, 0, refi as usize, px, py, srw, srh,
2700                );
2701            }
2702        }
2703
2704        // P_8x8 allows the 8×8 transform only when every sub-partition is 8×8.
2705        let allow_8x8 = sub_types.iter().all(|&t| t == 0);
2706        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8)
2707    }
2708
2709    /// Reconstructs a `P_Skip` macroblock: motion-compensate from the reference
2710    /// at the skip MV, with no residual.
2711    fn decode_p_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2712        // P_Skip always references index 0 (the most recent picture). Borrow it —
2713        // a full-frame `.cloned()` here was ~86% of total decode time (one ~3 MB
2714        // plane copy per skip MB, thousands per frame).
2715        if self.refs.is_empty() {
2716            return Err(MbError::Unsupported("P_Skip without reference"));
2717        }
2718        let mv = self.skip_mv(mb_x, mb_y);
2719        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
2720
2721        let mut pred = [0u8; 256];
2722        let rf0 = &self.refs[0];
2723        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);
2724        if let Some(wt) = &self.weights {
2725            for p in pred.iter_mut() {
2726                *p = wt.apply_luma(*p, 0, 0);
2727            }
2728        }
2729        {
2730            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
2731            for dy in 0..16 {
2732                let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
2733                self.rec_y[d..d + 16].copy_from_slice(&pred[dy * 16..dy * 16 + 16]);
2734            }
2735        }
2736        for c in 0..2 {
2737            let mut pc = [0u8; 64];
2738            let rf0 = &self.refs[0];
2739            let rc = if c == 0 { &rf0.pu } else { &rf0.pv };
2740            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);
2741            if let Some(wt) = &self.weights {
2742                for p in pc.iter_mut() {
2743                    *p = wt.apply_chroma(*p, 0, 0, c);
2744                }
2745            }
2746            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2747            for dy in 0..8 {
2748                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
2749                plane[d..d + 8].copy_from_slice(&pc[dy * 8..dy * 8 + 8]);
2750            }
2751        }
2752        {
2753            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
2754            self.set_mb_mv(mb_x, mb_y, mv, true, 0);
2755            // Mark blocks coded; inter blocks count as DC (not I_4x4) for mode pred.
2756            let w4 = self.mb_w * 4;
2757            for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2758                self.coded_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = true;
2759                self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
2760            }
2761        }
2762        Ok(())
2763    }
2764
2765    /// Predicted `Intra_4x4` mode for the block at absolute coords `(bx, by)`.
2766    /// If either the left or top neighbor is outside the frame or in another
2767    /// slice, the prediction is DC (mode 2) (spec §8.3.1.1).
2768    fn predict_i4_mode(&self, bx: usize, by: usize) -> u8 {
2769        if bx == 0 || by == 0 {
2770            return 2;
2771        }
2772        // Left neighbor block (bx-1,by); top neighbor block (bx,by-1). A neighbor
2773        // in another slice — or, under constrained_intra, an inter neighbor — is
2774        // unavailable, forcing the predicted mode to DC.
2775        if !self.nbr_in_slice((bx - 1) / 4, by / 4)
2776            || !self.nbr_in_slice(bx / 4, (by - 1) / 4)
2777            || !self.intra_nbr_ok(bx - 1, by)
2778            || !self.intra_nbr_ok(bx, by - 1)
2779        {
2780            return 2;
2781        }
2782        let w4 = self.mb_w * 4;
2783        self.modes_y[by * w4 + (bx - 1)].min(self.modes_y[(by - 1) * w4 + bx])
2784    }
2785
2786    /// Gathers 4×4 luma intra neighbors at pixel `(px, py)` from `rec_y`.
2787    fn gather_i4(
2788        &self,
2789        px: usize,
2790        py: usize,
2791        avail_top: bool,
2792        avail_left: bool,
2793        bx: usize,
2794        by: usize,
2795    ) -> ([u8; 8], [u8; 4], u8) {
2796        let (cw, w4) = (self.cw, self.mb_w * 4);
2797        let mut top = [0u8; 8];
2798        let mut left = [0u8; 4];
2799        let mut corner = 0;
2800        if avail_top {
2801            for i in 0..4 {
2802                top[i] = self.rec_y[(py - 1) * cw + px + i];
2803            }
2804            let tr_avail = bx + 1 < w4
2805                && self.coded_y[(by - 1) * w4 + (bx + 1)]
2806                && self.nbr_in_slice((bx + 1) / 4, (by - 1) / 4)
2807                && self.intra_nbr_ok(bx + 1, by - 1);
2808            for i in 0..4 {
2809                top[4 + i] = if tr_avail {
2810                    self.rec_y[(py - 1) * cw + px + 4 + i]
2811                } else {
2812                    top[3]
2813                };
2814            }
2815        }
2816        if avail_left {
2817            for i in 0..4 {
2818                left[i] = self.rec_y[(py + i) * cw + px - 1];
2819            }
2820        }
2821        // The above-left corner has its own availability (block D); under
2822        // constrained_intra it is gone if that block is inter.
2823        if avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1) {
2824            corner = self.rec_y[(py - 1) * cw + px - 1];
2825        }
2826        (top, left, corner)
2827    }
2828
2829    /// Reconstructs an `I_PCM` macroblock: byte-aligned raw 8-bit samples, no
2830    /// prediction/transform/quant (spec §7.3.5, §8.3.5).
2831    fn decode_ipcm(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2832        r.align_to_byte()?;
2833        let (lx, ly) = (mb_x * 16, mb_y * 16);
2834        for dy in 0..16 {
2835            for dx in 0..16 {
2836                self.rec_y[(ly + dy) * self.cw + (lx + dx)] = r.read_bits(8)? as u8;
2837            }
2838        }
2839        let (cx, cy) = (mb_x * 8, mb_y * 8);
2840        for plane in [&mut self.rec_u, &mut self.rec_v] {
2841            for dy in 0..8 {
2842                for dx in 0..8 {
2843                    plane[(cy + dy) * self.ccw + (cx + dx)] = r.read_bits(8)? as u8;
2844                }
2845            }
2846        }
2847        // Neighbor context: an I_PCM block contributes TotalCoeff = 16, counts as
2848        // intra with DC mode for prediction, and has no motion (§9.2.1, §8.3.1.2.2).
2849        let (w4, w2) = (self.mb_w * 4, self.mb_w * 2);
2850        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2851            let idx = (mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx);
2852            self.nnz_y[idx] = 16;
2853            self.modes_y[idx] = 2;
2854            self.inter_y[idx] = false;
2855            self.ref_idx_y[idx] = -1;
2856            self.mv_y[idx] = (0, 0);
2857        }
2858        for c in 0..2 {
2859            for by in 0..2 {
2860                for bx in 0..2 {
2861                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = 16;
2862                }
2863            }
2864        }
2865        Ok(())
2866    }
2867
2868    fn decode_i4x4(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2869        let w4 = self.mb_w * 4;
2870
2871        // intra4x4 mode signalling
2872        let mut modes = [2u8; 16]; // raster [lby*4+lbx]
2873        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2874            let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
2875            let predicted = self.predict_i4_mode(bx, by);
2876            let actual = if r.read_bit()? {
2877                predicted
2878            } else {
2879                let rem = r.read_bits(3)? as u8;
2880                if rem < predicted {
2881                    rem
2882                } else {
2883                    rem + 1
2884                }
2885            };
2886            self.modes_y[by * w4 + bx] = actual;
2887            modes[lby * 4 + lbx] = actual;
2888        }
2889
2890        let chroma_mode = r.read_ue()? as u8;
2891        let cbp = read_cbp_intra(r)?;
2892        let cbp_luma = cbp & 15;
2893        let cbp_chroma = cbp >> 4;
2894        if cbp != 0 {
2895            self.step_qp(r.read_se()?);
2896        }
2897        let qp = self.cur_qp;
2898
2899        // luma residuals + serial reconstruction. Cross-MB neighbors are only
2900        // available when the adjacent macroblock is in this slice (and, under
2901        // constrained_intra_pred, is itself intra-coded).
2902        let top_mb_avail = mb_y > 0
2903            && self.nbr_in_slice(mb_x, mb_y - 1)
2904            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
2905        let left_mb_avail = mb_x > 0
2906            && self.nbr_in_slice(mb_x - 1, mb_y)
2907            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
2908        self.nnz_cache_load(mb_x, mb_y);
2909        for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
2910            let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
2911            let (px, py) = (bx * 4, by * 4);
2912            let avail_top = lby > 0 || top_mb_avail;
2913            let avail_left = lbx > 0 || left_mb_avail;
2914            let mut qb = [0i32; 16];
2915            let total = if cbp_luma & (1 << (blk / 4)) != 0 {
2916                let nc = self.nc_pred(lbx, lby);
2917                let scan16 = decode_residual_block(r, 16, nc)?;
2918                qb = un_scan_4x4_dcac(&scan16);
2919                scan16.iter().filter(|&&v| v != 0).count() as u8
2920            } else {
2921                0
2922            };
2923            self.nnz_cache_set(lbx, lby, total);
2924            self.nnz_y[by * w4 + bx] = total;
2925            let (top, left, corner) = self.gather_i4(px, py, avail_top, avail_left, bx, by);
2926            let pred = intra4x4_pred(modes[lby * 4 + lbx], avail_top, avail_left, &top, &left, corner);
2927            let mut predb = [0i32; 16];
2928            for i in 0..16 {
2929                predb[i] = pred[i] as i32;
2930            }
2931            let s = reconstruct_4x4(&self.dequant(&qb, qp, 0), &predb);
2932            store(&mut self.rec_y, self.cw, px, py, &s);
2933            self.coded_y[by * w4 + bx] = true;
2934        }
2935
2936        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
2937    }
2938
2939    /// Decodes an `I_8x8` macroblock (High profile): four 8×8 luma blocks, each
2940    /// with its own intra mode, 8×8 transform residual (CAVLC = four interleaved
2941    /// 4×4 blocks), and 8×8 intra prediction.
2942    fn decode_i8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
2943        let w4 = self.mb_w * 4;
2944        self.mb_t8x8[mb_y * self.mb_w + mb_x] = true;
2945
2946        // intra8x8 mode signalling — one mode per 8×8 block (raster 0..3),
2947        // stored into all four of its 4×4 cells so neighbors can read it.
2948        let mut modes8 = [2u8; 4];
2949        for (b8, mode) in modes8.iter_mut().enumerate() {
2950            let (b8x, b8y) = (b8 % 2, b8 / 2);
2951            let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
2952            let predicted = self.predict_i4_mode(bx, by);
2953            let actual = if r.read_bit()? {
2954                predicted
2955            } else {
2956                let rem = r.read_bits(3)? as u8;
2957                if rem < predicted { rem } else { rem + 1 }
2958            };
2959            *mode = actual;
2960            for sy in 0..2 {
2961                for sx in 0..2 {
2962                    self.modes_y[(by + sy) * w4 + (bx + sx)] = actual;
2963                }
2964            }
2965        }
2966
2967        let chroma_mode = r.read_ue()? as u8;
2968        let cbp = read_cbp_intra(r)?;
2969        let cbp_luma = cbp & 15;
2970        let cbp_chroma = cbp >> 4;
2971        if cbp != 0 {
2972            self.step_qp(r.read_se()?);
2973        }
2974        let qp = self.cur_qp;
2975
2976        let top_mb_avail = mb_y > 0
2977            && self.nbr_in_slice(mb_x, mb_y - 1)
2978            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
2979        let left_mb_avail = mb_x > 0
2980            && self.nbr_in_slice(mb_x - 1, mb_y)
2981            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
2982        self.nnz_cache_load(mb_x, mb_y);
2983
2984        for b8 in 0..4 {
2985            let (b8x, b8y) = (b8 % 2, b8 / 2);
2986            let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
2987            let (px, py) = (bx * 4, by * 4);
2988
2989            // residual: 8×8 CAVLC = four 4×4 sub-blocks, coeff k of sub-block s
2990            // mapping to 8×8 scan position 4·k + s (spec §7.3.5.3.2).
2991            let mut res8 = [0i32; 64];
2992            if cbp_luma & (1 << b8) != 0 {
2993                let mut scan8 = [0i32; 64];
2994                for sub in 0..4 {
2995                    let (sx, sy) = (sub % 2, sub / 2);
2996                    let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
2997                    let nc = self.nc_pred(cx, cy);
2998                    let blk = decode_residual_block(r, 16, nc)?;
2999                    let total = blk.iter().filter(|&&v| v != 0).count() as u8;
3000                    self.nnz_cache_set(cx, cy, total);
3001                    self.nnz_y[(by + sy) * w4 + (bx + sx)] = total;
3002                    for k in 0..16 {
3003                        scan8[4 * k + sub] = blk[k];
3004                    }
3005                }
3006                let raster = un_scan_8x8(&scan8);
3007                res8 = self.inv_quant8(&raster, qp, 0);
3008            } else {
3009                for sub in 0..4 {
3010                    let (sx, sy) = (sub % 2, sub / 2);
3011                    self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
3012                    self.nnz_y[(by + sy) * w4 + (bx + sx)] = 0;
3013                }
3014            }
3015
3016            let avail_top = b8y > 0 || top_mb_avail;
3017            let avail_left = b8x > 0 || left_mb_avail;
3018            let (top, left, corner, avail_corner) =
3019                self.gather_i8(px, py, avail_top, avail_left, bx, by);
3020            let pred = intra8x8_pred(
3021                modes8[b8], avail_top, avail_left, avail_corner, &top, &left, corner,
3022            );
3023            let mut predb = [0i32; 64];
3024            for i in 0..64 {
3025                predb[i] = pred[i] as i32;
3026            }
3027            let recon = add_residual_8x8(&res8, &predb);
3028            for dy in 0..8 {
3029                for dx in 0..8 {
3030                    self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
3031                }
3032            }
3033            for sy in 0..2 {
3034                for sx in 0..2 {
3035                    self.coded_y[(by + sy) * w4 + (bx + sx)] = true;
3036                }
3037            }
3038        }
3039
3040        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
3041    }
3042
3043    /// Dequantizes + inverse-transforms an 8×8 luma block, applying the scaling
3044    /// matrix `list` (0 = intra, 1 = inter) or flat weights.
3045    fn inv_quant8(&self, raster: &[i32; 64], qp: u8, list: usize) -> [i32; 64] {
3046        match &self.scaling8 {
3047            Some(s) => inverse_quant_8x8(raster, qp, &s[list]),
3048            None => inverse_quant_8x8(raster, qp, &[16i32; 64]),
3049        }
3050    }
3051
3052    /// Gathers the 8×8 luma intra reference samples at pixel `(px, py)`: the 16
3053    /// top samples (8..15 substituted from the last when no top-right), 8 left
3054    /// samples, the above-left corner, and whether the corner is available.
3055    #[allow(clippy::too_many_arguments)]
3056    fn gather_i8(
3057        &self,
3058        px: usize,
3059        py: usize,
3060        avail_top: bool,
3061        avail_left: bool,
3062        bx: usize,
3063        by: usize,
3064    ) -> ([u8; 16], [u8; 8], u8, bool) {
3065        let (cw, w4) = (self.cw, self.mb_w * 4);
3066        let mut top = [0u8; 16];
3067        let mut left = [0u8; 8];
3068        let mut corner = 0;
3069        if avail_top {
3070            for i in 0..8 {
3071                top[i] = self.rec_y[(py - 1) * cw + px + i];
3072            }
3073            let tr_avail = bx + 2 < w4
3074                && self.coded_y[(by - 1) * w4 + (bx + 2)]
3075                && self.nbr_in_slice((bx + 2) / 4, (by - 1) / 4)
3076                && self.intra_nbr_ok(bx + 2, by - 1);
3077            for i in 0..8 {
3078                top[8 + i] = if tr_avail {
3079                    self.rec_y[(py - 1) * cw + px + 8 + i]
3080                } else {
3081                    top[7]
3082                };
3083            }
3084        }
3085        if avail_left {
3086            for i in 0..8 {
3087                left[i] = self.rec_y[(py + i) * cw + px - 1];
3088            }
3089        }
3090        let avail_corner = avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1);
3091        if avail_corner {
3092            corner = self.rec_y[(py - 1) * cw + px - 1];
3093        }
3094        (top, left, corner, avail_corner)
3095    }
3096
3097    fn decode_i16(
3098        &mut self,
3099        r: &mut BitReader,
3100        mb_x: usize,
3101        mb_y: usize,
3102        mt: u32,
3103    ) -> Result<(), MbError> {
3104        let pred_mode = I16Mode::from_id(mt % 4);
3105        let cbp_chroma = (mt % 12) / 4;
3106        let cbp_luma_15 = mt / 12 == 1;
3107        let chroma_mode = r.read_ue()? as u8;
3108        self.step_qp(r.read_se()?);
3109        let qp = self.cur_qp;
3110        let w4 = self.mb_w * 4;
3111
3112        // luma DC
3113        self.nnz_cache_load(mb_x, mb_y);
3114        let nc_dc = self.nc_pred(0, 0);
3115        let dc_scan = decode_residual_block(r, 16, nc_dc)?;
3116        let dc_levels = un_scan_4x4_dcac(&dc_scan);
3117        let recon_dc = self.dequant_luma_dc(&dc_levels, qp, 0);
3118
3119        // luma AC (nnz set for all 16 blocks: 0 when DC-only, matching the encoder)
3120        let mut q_blocks = [[0i32; 16]; 16];
3121        for &(bx, by) in &LUMA_4X4_SCAN_XY {
3122            let total = if cbp_luma_15 {
3123                let nc = self.nc_pred(bx, by);
3124                let ac = decode_residual_block(r, 15, nc)?;
3125                un_scan_4x4_ac_into(&ac, &mut q_blocks[by * 4 + bx]);
3126                ac.iter().filter(|&&v| v != 0).count() as u8
3127            } else {
3128                0
3129            };
3130            self.nnz_cache_set(bx, by, total);
3131            self.nnz_y[(mb_y * 4 + by) * w4 + (mb_x * 4 + bx)] = total;
3132        }
3133
3134        // prediction + reconstruction
3135        let avail_top = mb_y > 0
3136            && self.nbr_in_slice(mb_x, mb_y - 1)
3137            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
3138        let avail_left = mb_x > 0
3139            && self.nbr_in_slice(mb_x - 1, mb_y)
3140            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
3141        let (lx, ly) = (mb_x * 16, mb_y * 16);
3142        let mut top = [0u8; 16];
3143        let mut left = [0u8; 16];
3144        if avail_top {
3145            for i in 0..16 {
3146                top[i] = self.rec_y[(ly - 1) * self.cw + lx + i];
3147            }
3148        }
3149        if avail_left {
3150            for i in 0..16 {
3151                left[i] = self.rec_y[(ly + i) * self.cw + lx - 1];
3152            }
3153        }
3154        let corner = if avail_top && avail_left {
3155            self.rec_y[(ly - 1) * self.cw + lx - 1]
3156        } else {
3157            0
3158        };
3159        let pred_l = luma16x16_pred(pred_mode, avail_top, avail_left, &top, &left, corner);
3160        for by in 0..4 {
3161            for bx in 0..4 {
3162                let mut deq = self.dequant(&q_blocks[by * 4 + bx], qp, 0);
3163                deq[0] = recon_dc[by * 4 + bx];
3164                let mut predb = [0i32; 16];
3165                for dy in 0..4 {
3166                    for dx in 0..4 {
3167                        predb[dy * 4 + dx] = pred_l[(by * 4 + dy) * 16 + (bx * 4 + dx)] as i32;
3168                    }
3169                }
3170                let s = reconstruct_4x4(&deq, &predb);
3171                store(&mut self.rec_y, self.cw, lx + bx * 4, ly + by * 4, &s);
3172            }
3173        }
3174        // I_16x16 blocks are treated as DC for neighbor mode prediction.
3175        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3176            self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
3177        }
3178
3179        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
3180    }
3181
3182    /// Reads and reconstructs the chroma residual (shared by both luma types).
3183    fn decode_chroma(
3184        &mut self,
3185        r: &mut BitReader,
3186        mb_x: usize,
3187        mb_y: usize,
3188        cbp_chroma: u32,
3189        chroma_mode: u8,
3190    ) -> Result<(), MbError> {
3191        let qpc = self.chroma_qp_for(self.cur_qp);
3192        let (cx, cy) = (mb_x * 8, mb_y * 8);
3193        let avail_top = mb_y > 0
3194            && self.nbr_in_slice(mb_x, mb_y - 1)
3195            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
3196        let avail_left = mb_x > 0
3197            && self.nbr_in_slice(mb_x - 1, mb_y)
3198            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
3199
3200        let mut c_recon_dc = [[0i32; 4]; 2];
3201        if cbp_chroma != 0 {
3202            for (c, slot) in c_recon_dc.iter_mut().enumerate() {
3203                let dc = decode_residual_block(r, 4, -1)?;
3204                *slot = self.dequant_chroma_dc(&[dc[0], dc[1], dc[2], dc[3]], qpc, 1 + c);
3205            }
3206        }
3207        let mut c_q_blocks = [[[0i32; 16]; 4]; 2];
3208        if cbp_chroma == 2 {
3209            self.chroma_cache_load(mb_x, mb_y);
3210            let w2 = self.mb_w * 2;
3211            for c in 0..2 {
3212                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
3213                    let nc = self.chroma_nc_pred(c, bx, by);
3214                    let ac = decode_residual_block(r, 15, nc)?;
3215                    let total = ac.iter().filter(|&&v| v != 0).count() as u8;
3216                    self.chroma_nnz_cache_set(c, bx, by, total);
3217                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = total;
3218                    un_scan_4x4_ac_into(&ac, &mut c_q_blocks[c][by * 2 + bx]);
3219                }
3220            }
3221        }
3222        for c in 0..2 {
3223            let mut ctop = [0u8; 8];
3224            let mut cleft = [0u8; 8];
3225            let mut ccorner = 0u8;
3226            {
3227                let rec_c = if c == 0 { &self.rec_u } else { &self.rec_v };
3228                if avail_top {
3229                    for i in 0..8 {
3230                        ctop[i] = rec_c[(cy - 1) * self.ccw + cx + i];
3231                    }
3232                }
3233                if avail_left {
3234                    for i in 0..8 {
3235                        cleft[i] = rec_c[(cy + i) * self.ccw + cx - 1];
3236                    }
3237                }
3238                if avail_top && avail_left {
3239                    ccorner = rec_c[(cy - 1) * self.ccw + cx - 1];
3240                }
3241            }
3242            let pred8 = chroma8x8_pred(chroma_mode, avail_top, avail_left, &ctop, &cleft, ccorner);
3243            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
3244                let mut predb = [0i32; 16];
3245                for dy in 0..4 {
3246                    for dx in 0..4 {
3247                        predb[dy * 4 + dx] = pred8[(by * 4 + dy) * 8 + (bx * 4 + dx)] as i32;
3248                    }
3249                }
3250                let mut deq = self.dequant(&c_q_blocks[c][by * 2 + bx], qpc, 1 + c);
3251                deq[0] = c_recon_dc[c][by * 2 + bx];
3252                let s = reconstruct_4x4(&deq, &predb);
3253                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3254                store(plane, self.ccw, cx + bx * 4, cy + by * 4, &s);
3255            }
3256        }
3257        Ok(())
3258    }
3259
3260    /// Applies the in-loop deblocking filter to the reconstructed frame, with
3261    /// the slice's `FilterOffsetA`/`FilterOffsetB` (each = the coded `*_div2`
3262    /// value × 2).
3263    pub fn deblock(&mut self, offset_a: i32, offset_b: i32) {
3264        // Deblock boundary strength uses the *transform block's* coded status. For
3265        // an 8×8-transform macroblock the unit is the whole 8×8, so every 4×4 cell
3266        // shares the 8×8's coefficient presence (OR of its four sub-block counts)
3267        // — distinct from the per-sub-block `nnz_y` used for the CAVLC nC context.
3268        // Only differs from `nnz_y` when some MB uses the 8×8 transform (High
3269        // profile). On Baseline (no 8×8) it's identical — skip the clone + rewrite.
3270        let nnz_db_storage;
3271        let nnz_db: &[u8] = if self.mb_t8x8.iter().any(|&t| t) {
3272            let mut n = self.nnz_y.clone();
3273            let w4 = self.mb_w * 4;
3274            for mb_y in 0..self.mb_h {
3275                for mb_x in 0..self.mb_w {
3276                    if !self.mb_t8x8[mb_y * self.mb_w + mb_x] {
3277                        continue;
3278                    }
3279                    for b8 in 0..4 {
3280                        let (bx, by) = (mb_x * 4 + (b8 % 2) * 2, mb_y * 4 + (b8 / 2) * 2);
3281                        let any = (0..2).any(|sy| (0..2).any(|sx| self.nnz_y[(by + sy) * w4 + (bx + sx)] > 0));
3282                        for sy in 0..2 {
3283                            for sx in 0..2 {
3284                                n[(by + sy) * w4 + (bx + sx)] = u8::from(any);
3285                            }
3286                        }
3287                    }
3288                }
3289            }
3290            nnz_db_storage = n;
3291            &nnz_db_storage
3292        } else {
3293            &self.nnz_y
3294        };
3295        // Map per-block reference indices to a stable picture identity (POC) so
3296        // the boundary-strength comparison recognises the same picture across lists.
3297        let ref_id: Vec<i32> = self
3298            .ref_idx_y
3299            .iter()
3300            .map(|&r| if r >= 0 { self.refs.get(r as usize).map_or(i32::MIN, |f| f.poc) } else { i32::MIN })
3301            .collect();
3302        // List-1 identities are read only by B bi-pred edges; on P frames `refs1` is
3303        // empty (every entry would be NO_REF), so skip the whole per-block collect.
3304        let ref_id1: Vec<i32> = if self.refs1.is_empty() {
3305            Vec::new()
3306        } else {
3307            self.ref_idx1
3308                .iter()
3309                .map(|&r| if r >= 0 { self.refs1.get(r as usize).map_or(i32::MIN, |f| f.poc) } else { i32::MIN })
3310                .collect()
3311        };
3312        let info = rusty_h264_common::deblock::BlockInfo {
3313            inter: &self.inter_y,
3314            nnz: nnz_db,
3315            mv: &self.mv_y,
3316            ref_id: &ref_id,
3317            mv1: &self.mv1,
3318            ref_id1: &ref_id1,
3319            w4: self.mb_w * 4,
3320            t8x8: &self.mb_t8x8,
3321            bs: &[],
3322        };
3323        rusty_h264_common::deblock::filter_frame(
3324            &mut self.rec_y,
3325            &mut self.rec_u,
3326            &mut self.rec_v,
3327            self.mb_w,
3328            self.mb_h,
3329            &self.mb_qp,
3330            self.chroma_qp_offset,
3331            offset_a,
3332            offset_b,
3333            &info,
3334        );
3335    }
3336
3337    /// Crops the reconstructed coded-size planes to the display window.
3338    pub fn into_frame(self, crop_r: usize, crop_b: usize) -> YuvFrame {
3339        // No cropping (the common case): the reconstruction planes ARE the output —
3340        // move them out instead of allocating + copying three full planes per frame.
3341        if crop_r == 0 && crop_b == 0 {
3342            return YuvFrame {
3343                width: self.cw,
3344                height: self.ch,
3345                y: self.rec_y,
3346                u: self.rec_u,
3347                v: self.rec_v,
3348            };
3349        }
3350        let dw = self.cw - 2 * crop_r;
3351        let dh = self.ch - 2 * crop_b;
3352        let mut y = vec![0u8; dw * dh];
3353        for row in 0..dh {
3354            y[row * dw..row * dw + dw].copy_from_slice(&self.rec_y[row * self.cw..row * self.cw + dw]);
3355        }
3356        let (cdw, cdh) = (dw / 2, dh / 2);
3357        let mut u = vec![0u8; cdw * cdh];
3358        let mut v = vec![0u8; cdw * cdh];
3359        for row in 0..cdh {
3360            u[row * cdw..row * cdw + cdw]
3361                .copy_from_slice(&self.rec_u[row * self.ccw..row * self.ccw + cdw]);
3362            v[row * cdw..row * cdw + cdw]
3363                .copy_from_slice(&self.rec_v[row * self.ccw..row * self.ccw + cdw]);
3364        }
3365        let _ = self.cch;
3366        YuvFrame {
3367            width: dw,
3368            height: dh,
3369            y,
3370            u,
3371            v,
3372        }
3373    }
3374}
3375
3376/// Reads `ref_idx_l0` as `te(v)` with range `num_ref_active - 1`: a single flag
3377/// when exactly two references are active (cMax == 1), else `ue(v)`.
3378// ---- CABAC binarization engine helpers (openh264 cabac_decoder.cpp) ----
3379
3380/// Unary bin (`DecodeUnaryBinCabac`): bin0 at `ctx`; if 1, count bins at `ctx+off`
3381/// (including the terminating 0) until a 0.
3382fn cabac_unary(cab: &mut crate::cabac::Cabac, ctx: usize, off: usize) -> u32 {
3383    if cab.decode_decision(ctx) == 0 {
3384        return 0;
3385    }
3386    let mut sym = 0;
3387    loop {
3388        let bin = cab.decode_decision(ctx + off);
3389        sym += 1;
3390        // Cap the unary run: no valid H.264 element coded through this helper
3391        // (mb_qp_delta) exceeds a few dozen bins, but on malformed / buffer-exhausted
3392        // input the arithmetic engine keeps yielding 1s (it zero-fills past the end),
3393        // which would loop forever. 512 is far beyond any legal value.
3394        if bin == 0 || sym >= 512 {
3395            break;
3396        }
3397    }
3398    sym
3399}
3400
3401/// k-th order Exp-Golomb in bypass (`DecodeExpBypassCabac`).
3402fn cabac_exp_bypass(cab: &mut crate::cabac::Cabac, mut count: i32) -> u32 {
3403    let mut sym = 0u32;
3404    loop {
3405        let c = cab.decode_bypass();
3406        if c == 1 {
3407            sym += 1 << count;
3408            count += 1;
3409        }
3410        if c == 0 || count == 16 {
3411            break;
3412        }
3413    }
3414    let mut sym2 = 0u32;
3415    while count > 0 {
3416        count -= 1;
3417        if cab.decode_bypass() != 0 {
3418            sym2 |= 1 << count;
3419        }
3420    }
3421    sym + sym2
3422}
3423
3424/// UEG0 coeff-level suffix (`DecodeUEGLevelCabac`): TU prefix at `ctx` (≤13) then an
3425/// EG0 bypass suffix.
3426fn cabac_ueg_level(cab: &mut crate::cabac::Cabac, ctx: usize) -> u32 {
3427    if cab.decode_decision(ctx) == 0 {
3428        return 0;
3429    }
3430    let mut code = 0u32;
3431    let mut count = 1;
3432    let mut tmp;
3433    loop {
3434        tmp = cab.decode_decision(ctx);
3435        code += 1;
3436        count += 1;
3437        if tmp == 0 || count == 13 {
3438            break;
3439        }
3440    }
3441    if tmp != 0 {
3442        code += cabac_exp_bypass(cab, 0) + 1;
3443    }
3444    code
3445}
3446
3447/// `mb_qp_delta` CABAC (`ParseDeltaQpCabac`): ctxIdxOffset 60, ctxInc = (prev delta ≠ 0).
3448fn parse_mb_qp_delta_cabac(cab: &mut crate::cabac::Cabac, last_delta_qp: &mut i32) -> i32 {
3449    const O: usize = 60;
3450    let ctx_inc = (*last_delta_qp != 0) as usize;
3451    let mut qp_delta = 0;
3452    if cab.decode_decision(O + ctx_inc) != 0 {
3453        let code = cabac_unary(cab, O + 2, 1) + 1;
3454        qp_delta = ((code + 1) >> 1) as i32;
3455        if code & 1 == 0 {
3456            qp_delta = -qp_delta;
3457        }
3458    }
3459    *last_delta_qp = qp_delta;
3460    qp_delta
3461}
3462
3463/// z-order block → padded (8-stride) nzc-cache index (openh264 g_kCacheNzcScanIdx):
3464/// 16 luma, 4 Cb, 4 Cr. Top neighbour = cache[idx-8], left = cache[idx-1].
3465const NZC_CACHE: [usize; 24] = [
3466    9, 10, 17, 18, 11, 12, 19, 20, 25, 26, 33, 34, 27, 28, 35, 36, // luma
3467    14, 15, 22, 23, // Cb
3468    38, 39, 46, 47, // Cr
3469];
3470
3471// g_kBlockCat2CtxOffset* + maxPos/maxC2, indexed by CABAC res-property (1..10; 0 unused).
3472const RES_MAXPOS: [i32; 11] = [0, 15, 14, 15, 3, 14, 63, 3, 3, 14, 14];
3473const RES_MAXC2: [i32; 11] = [0, 4, 4, 4, 3, 4, 4, 3, 3, 4, 4];
3474const RES_CBF: [usize; 11] = [0, 0, 4, 8, 12, 16, 0, 12, 12, 16, 16];
3475const RES_MAP: [usize; 11] = [0, 0, 15, 29, 44, 47, 0, 44, 44, 47, 47];
3476const RES_ONE: [usize; 11] = [0, 0, 10, 20, 30, 39, 0, 30, 30, 39, 39];
3477// res-property values (post GetMbResProperty, CABAC): the ctx-table index.
3478const RP_I16_DC: usize = 1;
3479const RP_I16_AC: usize = 2;
3480const RP_LUMA_4X4: usize = 3;
3481const RP_CHROMA_DC: usize = 7; // U (V=8, same offsets)
3482const RP_CHROMA_AC: usize = 9; // U (V=10, same offsets)
3483
3484/// One residual block (openh264 `ParseResidualBlockCabac`), generic over the 5 CABAC
3485/// block categories. `rp` selects the context offsets. DC categories (I16 luma DC,
3486/// chroma DC) take the cbf context from the per-MB `cbf_dc` bitmask + neighbour MB DC
3487/// cbf; AC categories from the padded nzc cache. Returns totalCoeffNum.
3488#[allow(clippy::too_many_arguments)]
3489fn parse_residual_cabac(
3490    cab: &mut crate::cabac::Cabac,
3491    nzc: &mut [u8; 48],
3492    cbf_dc: &mut u16,
3493    iz: usize,
3494    rp: usize,
3495    is_intra: bool,
3496    ndc: (Option<u16>, Option<u16>), // (top MB cbf_dc, left MB cbf_dc); None = unavailable
3497    out: &mut [i32],                 // scan-order coefficients written here (len ≥ maxPos+1)
3498) -> u32 {
3499    // The CABAC residual parse IS the decoder's entropy stage on Main-profile
3500    // streams — it was invisible (a ~47% residue) until this scope named it.
3501    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Entropy);
3502    // ---- coded_block_flag ----
3503    let is_dc = rp == RP_I16_DC || rp == RP_CHROMA_DC || rp == RP_CHROMA_DC + 1;
3504    let (mut na, mut nb) = (is_intra as u8, is_intra as u8);
3505    let scan = NZC_CACHE[iz.min(23)];
3506    if is_dc {
3507        if let Some(t) = ndc.0 {
3508            nb = ((t >> rp) & 1) as u8;
3509        }
3510        if let Some(l) = ndc.1 {
3511            na = ((l >> rp) & 1) as u8;
3512        }
3513    } else {
3514        if nzc[scan - 8] != 0xff {
3515            nb = (nzc[scan - 8] != 0) as u8;
3516        }
3517        if nzc[scan - 1] != 0xff {
3518            na = (nzc[scan - 1] != 0) as u8;
3519        }
3520    }
3521    let cbf = cab.decode_decision(85 + RES_CBF[rp] + (na + (nb << 1)) as usize);
3522    if cbf == 0 {
3523        if !is_dc {
3524            nzc[scan] = 0;
3525        }
3526        return 0;
3527    }
3528    if is_dc {
3529        *cbf_dc |= 1 << rp;
3530    }
3531    // ---- significance map ----
3532    let maxpos = RES_MAXPOS[rp] as usize;
3533    let map = 105 + RES_MAP[rp];
3534    let last = 166 + RES_MAP[rp];
3535    let mut sig = [0i32; 64];
3536    let mut coeff_num = 0u32;
3537    let mut last_hit = false;
3538    for i in 0..maxpos {
3539        if cab.decode_decision(map + i) != 0 {
3540            sig[i] = 1;
3541            coeff_num += 1;
3542            if cab.decode_decision(last + i) != 0 {
3543                last_hit = true;
3544                break;
3545            }
3546        }
3547    }
3548    if !last_hit {
3549        sig[maxpos] = 1;
3550        coeff_num += 1;
3551    }
3552    // ---- levels ----
3553    let one = 227 + RES_ONE[rp];
3554    let abs = 232 + RES_ONE[rp];
3555    let maxc2 = RES_MAXC2[rp];
3556    let (mut c1, mut c2) = (1i32, 0i32);
3557    for i in (0..=maxpos).rev() {
3558        if sig[i] != 0 {
3559            let mut level = sig[i] + cab.decode_decision(one + c1 as usize) as i32;
3560            if level == 2 {
3561                level += cabac_ueg_level(cab, abs + c2 as usize) as i32;
3562                c2 = (c2 + 1).min(maxc2);
3563                c1 = 0;
3564            } else if c1 != 0 {
3565                c1 = (c1 + 1).min(4);
3566            }
3567            if cab.decode_bypass() != 0 {
3568                level = -level;
3569            }
3570            sig[i] = level;
3571        }
3572    }
3573    out[..=maxpos].copy_from_slice(&sig[..=maxpos]);
3574    if !is_dc {
3575        nzc[scan] = coeff_num as u8;
3576    }
3577    coeff_num
3578}
3579
3580/// 4×4-block (z-order) → 30-entry (6-stride) mv/ref/mvd cache index (openh264
3581/// g_kCache30ScanIdx). Top neighbour = cache[idx-6], left = cache[idx-1].
3582const CACHE30: [usize; 16] = [7, 8, 13, 14, 9, 10, 15, 16, 19, 20, 25, 26, 21, 22, 27, 28];
3583
3584/// z-order 4×4-block → raster index (openh264 g_kuiScan4). Per-MB mvd/ref state is
3585/// stored raster-indexed (matching how neighbour blocks 3/7/11/15 and 12..15 are read).
3586const G_SCAN4: [usize; 16] = [0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15];
3587
3588/// P `sub_mb_type` CABAC (openh264 `ParseSubMBTypeCabac`, ctx 21). 0=8×8, 1=8×4, 2=4×8, 3=4×4.
3589fn parse_sub_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
3590    const S: usize = 21;
3591    if cab.decode_decision(S) != 0 {
3592        return 0;
3593    }
3594    if cab.decode_decision(S + 1) != 0 {
3595        3 - cab.decode_decision(S + 2)
3596    } else {
3597        1
3598    }
3599}
3600
3601/// Intra `mb_type` sub-parse for P/B slices (openh264 `DecodeCabacIntraMbType`, `base`=32
3602/// for B). Returns 0 = I_4x4, 1..=24 = I_16x16, 25 = I_PCM (in the intra numbering).
3603fn parse_intra_mb_type_cabac(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
3604    if cab.decode_decision(base) == 0 {
3605        return 0; // I_4x4
3606    }
3607    if cab.decode_terminate() {
3608        return 25; // I_PCM
3609    }
3610    let mut t = 1 + 12 * cab.decode_decision(base + 1) as u32; // cbp_luma != 0
3611    if cab.decode_decision(base + 2) != 0 {
3612        t += 4 + 4 * cab.decode_decision(base + 2) as u32;
3613    }
3614    t += 2 * cab.decode_decision(base + 3) as u32;
3615    t += cab.decode_decision(base + 3) as u32;
3616    t
3617}
3618
3619/// B `mb_type` CABAC (openh264 `ParseMBTypeBSliceCabac`, ctx base 27). `ctx_inc` = (left
3620/// avail & !direct) + (top avail & !direct). Returns 0 = B_Direct_16x16, 1..=21 = the
3621/// L0/L1/Bi 16×16/16×8/8×16 shapes, 22 = B_8x8, 23.. = intra (mb_type − 23).
3622fn parse_mb_type_b_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
3623    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
3624    const B: usize = 27;
3625    if cab.decode_decision(B + ctx_inc) == 0 {
3626        return 0; // B_Direct_16x16
3627    }
3628    if cab.decode_decision(B + 3) == 0 {
3629        return 1 + cab.decode_decision(B + 5) as u32; // 16×16 L0 / L1
3630    }
3631    let mut m = (cab.decode_decision(B + 4) as u32) << 3;
3632    m |= (cab.decode_decision(B + 5) as u32) << 2;
3633    m |= (cab.decode_decision(B + 5) as u32) << 1;
3634    m |= cab.decode_decision(B + 5) as u32;
3635    if m < 8 {
3636        return m + 3;
3637    }
3638    if m == 13 {
3639        return parse_intra_mb_type_cabac(cab, 32) + 23;
3640    }
3641    if m == 14 {
3642        return 11; // B_Bi_8x16
3643    }
3644    if m == 15 {
3645        return 22; // B_8x8
3646    }
3647    m = (m << 1) | cab.decode_decision(B + 5) as u32;
3648    m - 4
3649}
3650
3651/// B `sub_mb_type` CABAC (openh264 `ParseBSubMBTypeCabac`, ctx base 36). Returns 0..=12
3652/// per spec Table 7-18 (0 = B_Direct_8x8, 1 = B_L0_8x8, …, 12 = B_Bi_4x4).
3653fn parse_sub_mb_type_b_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
3654    const B: usize = 36;
3655    if cab.decode_decision(B) == 0 {
3656        return 0; // B_Direct_8x8
3657    }
3658    if cab.decode_decision(B + 1) == 0 {
3659        return 1 + cab.decode_decision(B + 3) as u32; // B_L0_8x8 / B_L1_8x8
3660    }
3661    let mut st = 3u32;
3662    if cab.decode_decision(B + 2) != 0 {
3663        if cab.decode_decision(B + 3) != 0 {
3664            return 11 + cab.decode_decision(B + 3) as u32; // B_L1_4x4 / B_Bi_4x4
3665        }
3666        st += 4;
3667    }
3668    st += 2 * cab.decode_decision(B + 3) as u32;
3669    st += cab.decode_decision(B + 3) as u32;
3670    st
3671}
3672
3673/// Parse one motion partition's `mvd` (x,y) and splat it into the 30-entry cache + the
3674/// per-MB raster mvd/ref state. `part_idx` = the partition's top-left z-order block (for
3675/// the ctxInc neighbour lookup); `zblocks` = every z-order 4×4 block the partition covers.
3676fn parse_mvd_partition(
3677    cab: &mut crate::cabac::Cabac,
3678    part_idx: usize,
3679    zblocks: &[usize],
3680    mvdc: &mut [[i16; 2]; 30],
3681    refc: &mut [i8; 30],
3682    mmvd: &mut [[i16; 2]; 16],
3683    mref: &mut [i8; 16],
3684    ref_idx: i8,
3685) -> (i32, i32) {
3686    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
3687    let s = CACHE30[part_idx];
3688    let ctx = |comp: usize| -> usize {
3689        let mut a = 0i32;
3690        if refc[s - 6] >= 0 {
3691            a += mvdc[s - 6][comp].unsigned_abs() as i32;
3692        }
3693        if refc[s - 1] >= 0 {
3694            a += mvdc[s - 1][comp].unsigned_abs() as i32;
3695        }
3696        if a >= 3 {
3697            1 + (a > 32) as usize
3698        } else {
3699            0
3700        }
3701    };
3702    let (cx, cy) = (ctx(0), ctx(1));
3703    let mvx = parse_mvd_cabac(cab, 0, cx);
3704    let mvy = parse_mvd_cabac(cab, 1, cy);
3705    for &zb in zblocks {
3706        mvdc[CACHE30[zb]] = [mvx, mvy];
3707        refc[CACHE30[zb]] = ref_idx;
3708        mmvd[G_SCAN4[zb]] = [mvx, mvy];
3709        mref[G_SCAN4[zb]] = ref_idx;
3710    }
3711    (mvx as i32, mvy as i32)
3712}
3713
3714/// `ref_idx_l0` (P) CABAC — mirror of the encoder `cb_ref_idx`. Unary, ctxIdxOffset
3715/// 54: binIdx 0 → `ctx0` (condTermFlagA + 2·condTermFlagB), binIdx 1 → 4, binIdx ≥2 → 5.
3716fn parse_ref_idx_cabac(cab: &mut crate::cabac::Cabac, ctx0: usize) -> i8 {
3717    const B: usize = 54;
3718    let mut r = 0i8;
3719    let mut bin_idx = 0u32;
3720    // Cap the unary length: valid ref_idx ≤ 15 (16 refs max); the cap keeps a corrupt
3721    // stream from looping unboundedly. The MC clamps the index, so an over-range value
3722    // is decoded as garbage (never a panic) — the robustness contract, not correctness.
3723    while bin_idx < 32 {
3724        let ctx = match bin_idx {
3725            0 => ctx0,
3726            1 => 4,
3727            _ => 5,
3728        };
3729        if cab.decode_decision(B + ctx) == 0 {
3730            break;
3731        }
3732        r += 1;
3733        bin_idx += 1;
3734    }
3735    r
3736}
3737
3738/// UEG3 mvd suffix (openh264 `DecodeUEGMvCabac`): TU prefix at `base + {0,1,2,3,3,..}`
3739/// (≤7), then EG3 bypass.
3740fn decode_ueg_mv(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
3741    const P2C: [usize; 8] = [0, 1, 2, 3, 3, 3, 3, 3];
3742    if cab.decode_decision(base) == 0 {
3743        return 0;
3744    }
3745    let mut code = 0u32;
3746    let mut count = 1usize;
3747    let mut tmp;
3748    loop {
3749        tmp = cab.decode_decision(base + P2C[count]);
3750        code += 1;
3751        count += 1;
3752        if tmp == 0 || count == 8 {
3753            break;
3754        }
3755    }
3756    if tmp != 0 {
3757        code += cabac_exp_bypass(cab, 3) + 1;
3758    }
3759    code
3760}
3761
3762/// One `mvd` component (openh264 `ParseMvdInfoCabac`). `ctx_inc` (0/1/2) from the
3763/// neighbour |mvd| sum. ctxIdxOffset 40 (x) / 47 (y).
3764fn parse_mvd_cabac(cab: &mut crate::cabac::Cabac, comp: usize, ctx_inc: usize) -> i16 {
3765    let base = 40 + comp * 7; // NEW_CTX_OFFSET_MVD + comp*CTX_NUM_MVD
3766    if cab.decode_decision(base + ctx_inc) == 0 {
3767        return 0;
3768    }
3769    let mag = (decode_ueg_mv(cab, base + 3) + 1) as i16;
3770    if cab.decode_bypass() != 0 {
3771        -mag
3772    } else {
3773        mag
3774    }
3775}
3776
3777/// `mb_skip_flag` CABAC (openh264 `ParseSkipFlagCabac`). `ctx_inc` = base 11 (P) or 24
3778/// (B) + (left avail & not-skip) + (top avail & not-skip). Returns true if skipped.
3779fn parse_mb_skip_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> bool {
3780    cab.decode_decision(ctx_inc) != 0
3781}
3782
3783/// P-slice `mb_type` CABAC (openh264 `ParseMBTypePSliceCabac`). Returns 0..3 = inter
3784/// (P_L0_16x16 / P_16x8 / P_8x16 / P_8x8), 5 = I_4x4, 6..29 = I_16x16, 30 = I_PCM.
3785fn parse_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
3786    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
3787    const S: usize = 11; // NEW_CTX_OFFSET_SKIP; P mb_type contexts hang off it
3788    if cab.decode_decision(S + 3) == 0 {
3789        // inter
3790        return if cab.decode_decision(S + 4) != 0 {
3791            if cab.decode_decision(S + 6) != 0 { 1 } else { 2 }
3792        } else if cab.decode_decision(S + 5) != 0 {
3793            3
3794        } else {
3795            0
3796        };
3797    }
3798    // intra (prefix bit was 1)
3799    if cab.decode_decision(S + 6) == 0 {
3800        return 5; // I_4x4
3801    }
3802    if cab.decode_terminate() {
3803        return 30; // I_PCM
3804    }
3805    let mut t = 6 + cab.decode_decision(S + 7) * 12;
3806    if cab.decode_decision(S + 8) != 0 {
3807        t += 4;
3808        if cab.decode_decision(S + 8) != 0 {
3809            t += 4;
3810        }
3811    }
3812    t += cab.decode_decision(S + 9) << 1;
3813    t += cab.decode_decision(S + 9);
3814    t
3815}
3816
3817/// I-slice `mb_type` CABAC parse (spec §9.3.2.5 / openh264 `ParseMBTypeISliceCabac`).
3818/// `ctx_inc` = (left MB is I_16x16/non-intra) + (top MB is …), i.e. 0..2; the corner
3819/// MB has no neighbours so `ctx_inc = 0`. Returns the raw mb_type: 0 = I_NxN (I_4x4/
3820/// I_8x8), 1..24 = I_16x16 (pred-mode/cbp packed), 25 = I_PCM.
3821fn parse_mb_type_i_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
3822    const O: usize = 3; // ctxIdxOffset for I-slice mb_type
3823    if cab.decode_decision(O + ctx_inc) == 0 {
3824        return 0; // I_NxN
3825    }
3826    if cab.decode_terminate() {
3827        return 25; // I_PCM
3828    }
3829    let mut t = 1 + cab.decode_decision(O + 3) * 12; // CBP luma: 0 or 12
3830    if cab.decode_decision(O + 4) != 0 {
3831        t += 4; // CBP chroma 1 or 2
3832        if cab.decode_decision(O + 5) != 0 {
3833            t += 4;
3834        }
3835    }
3836    t += cab.decode_decision(O + 6) << 1; // I_16x16 pred mode (2 bins)
3837    t += cab.decode_decision(O + 7);
3838    t
3839}
3840
3841/// One `Intra_4x4` (or `8x8`) pred-mode CABAC parse (openh264 `ParseIntraPredModeLuma
3842/// Cabac`): `prev_intra4x4_pred_mode_flag` (ctx 68) then, if 0, `rem_intra4x4_pred_mode`
3843/// (3 bins at ctx 69). Returns `-1` for "use predicted mode", else the 0..7 remainder.
3844fn parse_intra4x4_pred_mode_cabac(cab: &mut crate::cabac::Cabac) -> i32 {
3845    const IPR: usize = 68;
3846    if cab.decode_decision(IPR) == 1 {
3847        return -1; // prev_intra4x4_pred_mode_flag = 1
3848    }
3849    let mut m = cab.decode_decision(IPR + 1) as i32;
3850    m |= (cab.decode_decision(IPR + 1) as i32) << 1;
3851    m |= (cab.decode_decision(IPR + 1) as i32) << 2;
3852    m
3853}
3854
3855/// `intra_chroma_pred_mode` CABAC parse (openh264 `ParseIntraPredModeChromaCabac`):
3856/// TU(cMax=3) — bin0 at ctx `64 + ctx_inc` (ctx_inc from neighbour chroma modes, 0 for
3857/// the corner MB), the rest at ctx 67. Returns the mode 0..3.
3858fn parse_intra_chroma_pred_mode_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
3859    const CIPR: usize = 64;
3860    if cab.decode_decision(CIPR + ctx_inc) == 0 {
3861        return 0;
3862    }
3863    if cab.decode_decision(CIPR + 3) == 0 {
3864        return 1;
3865    }
3866    if cab.decode_decision(CIPR + 3) == 0 {
3867        return 2;
3868    }
3869    3
3870}
3871
3872/// `coded_block_pattern` CABAC parse (openh264 `ParseCbpInfoCabac`), corner-MB variant
3873/// (top/left neighbours unavailable → their terms are 0). ctxIdxOffset 73 (luma) with 4
3874/// z-order 8×8 bins whose ctxInc uses the EARLIER-decoded bits within this MB, then
3875/// chroma bits at 77/81. Returns cbp: bits 0-3 = luma 8×8, bits 4-5 = chroma pattern.
3876fn parse_cbp_cabac(cab: &mut crate::cabac::Cabac, top: Option<u8>, left: Option<u8>) -> u32 {
3877    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
3878    const CBP: usize = 73;
3879    let t = |m: u32| top.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
3880    let l = |m: u32| left.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
3881    let nb = |x: u32| (x == 0) as u32; // earlier 8×8 bin within this MB was NOT coded
3882    // Luma, 4 8×8 blocks in z-order. Top uses cbp bits 2/3, left uses 1/3.
3883    let b0 = cab.decode_decision(CBP + (l(1 << 1) + (t(1 << 2) << 1)) as usize);
3884    let b1 = cab.decode_decision(CBP + (nb(b0) + (t(1 << 3) << 1)) as usize);
3885    let b2 = cab.decode_decision(CBP + (l(1 << 3) + (nb(b0) << 1)) as usize);
3886    let b3 = cab.decode_decision(CBP + (nb(b2) + (nb(b1) << 1)) as usize);
3887    let mut cbp = b0 | (b1 << 1) | (b2 << 2) | (b3 << 3);
3888    // Chroma (4:2:0). ctxInc from neighbour chroma cbp (>>4).
3889    let ct = top.map_or(0u32, |c| ((c >> 4) != 0) as u32);
3890    let cl = left.map_or(0u32, |c| ((c >> 4) != 0) as u32);
3891    if cab.decode_decision(CBP + 4 + (cl + (ct << 1)) as usize) != 0 {
3892        let ct2 = top.map_or(0u32, |c| ((c >> 4) == 2) as u32);
3893        let cl2 = left.map_or(0u32, |c| ((c >> 4) == 2) as u32);
3894        let c1 = cab.decode_decision(CBP + 8 + (cl2 + (ct2 << 1)) as usize);
3895        cbp |= 1 << (4 + c1);
3896    }
3897    cbp
3898}
3899
3900fn read_ref_idx(r: &mut BitReader, num_ref_active: usize) -> Result<i32, OutOfData> {
3901    if num_ref_active == 2 {
3902        Ok(if r.read_bit()? { 0 } else { 1 }) // te(v): value = !bit
3903    } else {
3904        Ok(r.read_ue()? as i32)
3905    }
3906}
3907
3908/// B-partition prediction direction.
3909#[derive(Clone, Copy, PartialEq)]
3910enum BPred {
3911    L0,
3912    L1,
3913    Bi,
3914}
3915impl BPred {
3916    /// Whether this direction uses reference list `list` (0 or 1).
3917    fn uses(self, list: usize) -> bool {
3918        matches!(
3919            (self, list),
3920            (BPred::L0, 0) | (BPred::L1, 1) | (BPred::Bi, 0) | (BPred::Bi, 1)
3921        )
3922    }
3923}
3924
3925const B16X16: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 16)];
3926const B16X8: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 8), (0, 8, 16, 8)];
3927const B8X16: &[(usize, usize, usize, usize)] = &[(0, 0, 8, 16), (8, 0, 8, 16)];
3928
3929/// A partition region `(x, y, w, h)` in samples.
3930type Region = (usize, usize, usize, usize);
3931
3932/// B `mb_type` 1..=21 → (partition layout, MV-prediction mode 0/1/2 for 16×16/
3933/// 16×8/8×16, per-partition prediction direction) (spec Table 7-14).
3934fn b_inter_layout(mb_type: u32) -> (&'static [Region], u8, [BPred; 2]) {
3935    use BPred::*;
3936    match mb_type {
3937        1 => (B16X16, 0, [L0, L0]),
3938        2 => (B16X16, 0, [L1, L1]),
3939        3 => (B16X16, 0, [Bi, Bi]),
3940        4 => (B16X8, 1, [L0, L0]),
3941        5 => (B8X16, 2, [L0, L0]),
3942        6 => (B16X8, 1, [L1, L1]),
3943        7 => (B8X16, 2, [L1, L1]),
3944        8 => (B16X8, 1, [L0, L1]),
3945        9 => (B8X16, 2, [L0, L1]),
3946        10 => (B16X8, 1, [L1, L0]),
3947        11 => (B8X16, 2, [L1, L0]),
3948        12 => (B16X8, 1, [L0, Bi]),
3949        13 => (B8X16, 2, [L0, Bi]),
3950        14 => (B16X8, 1, [L1, Bi]),
3951        15 => (B8X16, 2, [L1, Bi]),
3952        16 => (B16X8, 1, [Bi, L0]),
3953        17 => (B8X16, 2, [Bi, L0]),
3954        18 => (B16X8, 1, [Bi, L1]),
3955        19 => (B8X16, 2, [Bi, L1]),
3956        20 => (B16X8, 1, [Bi, Bi]),
3957        _ => (B8X16, 2, [Bi, Bi]), // 21
3958    }
3959}
3960
3961/// Whether a B `sub_mb_type` (1..=12) uses reference list `list`.
3962fn b_sub_uses(st: u32, list: usize) -> bool {
3963    let pred = match st {
3964        1 | 4 | 5 | 10 => 0,  // L0
3965        2 | 6 | 7 | 11 => 1,  // L1
3966        _ => 2,               // Bi (3, 8, 9, 12)
3967    };
3968    (list == 0 && pred != 1) || (list == 1 && pred != 0)
3969}
3970
3971/// Sub-partition shapes within an 8×8 for a B `sub_mb_type` (1..=12).
3972fn b_sub_parts(st: u32) -> &'static [(usize, usize, usize, usize)] {
3973    match st {
3974        1..=3 => &[(0, 0, 8, 8)],
3975        4 | 6 | 8 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
3976        5 | 7 | 9 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
3977        _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)], // 10/11/12
3978    }
3979}
3980
3981/// Sub-macroblock partition layout `(x, y, w, h)` in samples within an 8×8, for
3982/// a P-slice `sub_mb_type` (0 = 8×8, 1 = 8×4, 2 = 4×8, 3 = 4×4).
3983fn sub_mb_partitions(sub_type: u32) -> &'static [(usize, usize, usize, usize)] {
3984    match sub_type {
3985        0 => &[(0, 0, 8, 8)],
3986        1 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
3987        2 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
3988        _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)],
3989    }
3990}
3991
3992fn store(plane: &mut [u8], stride: usize, x0: usize, y0: usize, s: &[u8; 16]) {
3993    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Scatter);
3994    for dy in 0..4 {
3995        for dx in 0..4 {
3996            plane[(y0 + dy) * stride + (x0 + dx)] = s[dy * 4 + dx];
3997        }
3998    }
3999}
4000
4001/// Un-scans an 8×8 block from frame zig-zag scan order to raster (spec Table 8-12).
4002fn un_scan_8x8(scan: &[i32; 64]) -> [i32; 64] {
4003    const ZZ8: [usize; 64] = [
4004        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,
4005        20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
4006        58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
4007    ];
4008    let mut out = [0i32; 64];
4009    for k in 0..64 {
4010        out[ZZ8[k]] = scan[k];
4011    }
4012    out
4013}
4014
4015#[cfg(test)]
4016mod tests {
4017    use super::*;
4018
4019    fn fd(qp: u8, offset: i32) -> FrameDecoder {
4020        FrameDecoder::new(1, 1, qp, offset, Vec::new(), 1, false, false, true)
4021    }
4022
4023    #[test]
4024    fn mb_qp_delta_accumulates_mod_52() {
4025        let mut d = fd(26, 0);
4026        assert_eq!(d.cur_qp, 26, "QPy starts at the slice QP");
4027        d.step_qp(4);
4028        assert_eq!(d.cur_qp, 30); // 26 + 4
4029        d.step_qp(-10);
4030        assert_eq!(d.cur_qp, 20); // carries from the previous MB, not the slice
4031        // Wrap-around: (20 + 40 + 52) % 52 = 112 % 52 = 8.
4032        d.step_qp(40);
4033        assert_eq!(d.cur_qp, 8);
4034        // Negative wrap: (8 - 20 + 52) % 52 = 40.
4035        d.step_qp(-20);
4036        assert_eq!(d.cur_qp, 40);
4037    }
4038
4039    #[test]
4040    fn chroma_qp_index_offset_applied_and_clamped() {
4041        // Offset 0 reproduces the bare luma->chroma table (QP30 -> 29).
4042        assert_eq!(fd(0, 0).chroma_qp_for(30), 29);
4043        // Positive offset shifts the table lookup (QP30 + 2 -> table[2] = 31).
4044        assert_eq!(fd(0, 2).chroma_qp_for(30), 31);
4045        // The qPi index is clamped into 0..=51 before the lookup.
4046        assert_eq!(fd(0, -12).chroma_qp_for(5), chroma_qp(0));
4047        assert_eq!(fd(0, 99).chroma_qp_for(40), chroma_qp(51));
4048    }
4049}