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