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, reconstruct_4x4_dc, reconstruct_4x4_dc_into, reconstruct_4x4_into, I16Mode,
19    CHROMA_4X4_SCAN_XY, LUMA_4X4_SCAN_XY,
20};
21use rusty_h264_common::transform::{
22    dequant_scatter_4x4, dequantize, dequantize_weighted, inverse_quant_8x8,
23    inverse_quant_chroma_dc,
24    inverse_quant_chroma_dc_weighted, inverse_quant_luma_dc, inverse_quant_luma_dc_weighted,
25};
26use rusty_h264_common::{BitReader, YuvFrame};
27
28/// One frame's motion field, in 4x4-block raster (`mb_w*4` wide).
29///
30/// Captured from any conformant stream this decoder parses — including x264's —
31/// so a harness can compare motion fields between encoders without depending on
32/// external MV-export tooling.
33pub struct MvField {
34    pub mb_w: usize,
35    pub mb_h: usize,
36    pub mv: Vec<(i32, i32)>,
37    pub ref_idx: Vec<i32>,
38    pub inter: Vec<bool>,
39}
40
41/// Frames captured in decode order when `RFF_MV_DUMP=1`. Diagnostic only.
42pub static MV_DUMP: std::sync::Mutex<Vec<MvField>> = std::sync::Mutex::new(Vec::new());
43
44pub fn mv_dump_on() -> bool {
45    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
46    *ON.get_or_init(|| std::env::var("RFF_MV_DUMP").map_or(false, |v| v != "0"))
47}
48
49/// Reconstructed coded-size planes plus CAVLC `nnz` context grids.
50pub struct FrameDecoder {
51    mb_w: usize,
52    mb_h: usize,
53    /// Slice QP (`SliceQPy`) — the deblock filter's frame-level QP.
54    qp: u8,
55    /// Running luma QP (`QPy`), carried across macroblocks and stepped by each
56    /// `mb_qp_delta` (spec §7.4.5). Equals `qp` on constant-QP streams.
57    cur_qp: u8,
58    /// `chroma_qp_index_offset` from the active PPS (§8.5.8).
59    chroma_qp_offset: i32,
60    cw: usize,
61    ch: usize,
62    ccw: usize,
63    cch: usize,
64    rec_y: Vec<u8>,
65    rec_u: Vec<u8>,
66    rec_v: Vec<u8>,
67    /// Per-macroblock luma QP (`QPy`), for per-edge deblock strength.
68    mb_qp: Vec<u8>,
69    /// First macroblock address of the slice currently being decoded. Neighbors
70    /// with a lower address belong to an earlier slice and are "not available"
71    /// for prediction (spec §8.3/§8.4). Slices are contiguous raster ranges (we
72    /// reject FMO/slice-groups), so address ≥ this ⇔ same slice.
73    slice_first_mb: usize,
74    nnz_y: Vec<u8>,
75    nnz_c: [Vec<u8>; 2],
76    modes_y: Vec<u8>,
77    coded_y: Vec<bool>,
78    /// Per-4×4-block List-0 motion (mv + ref index, `-1` = no L0). For P slices
79    /// this is the only motion; B slices add the List-1 grids below.
80    mv_y: Vec<(i32, i32)>,
81    inter_y: Vec<bool>,
82    ref_idx_y: Vec<i32>,
83    /// Per-4×4-block List-1 motion for B slices (`ref_idx1 = -1` = no L1).
84    mv1: Vec<(i32, i32)>,
85    ref_idx1: Vec<i32>,
86    /// `RefPicList1` and B-slice flags (unused outside B slices).
87    refs1: Vec<crate::Ref>,
88    num_ref_active1: usize,
89    is_b: bool,
90    /// True if the stream's profile permits B-slices (`profile_idc != 66`). When
91    /// false (Baseline / Constrained Baseline), `as_reference` skips the per-block
92    /// motion (mv/ref_idx/ref_poc) that only B temporal/spatial direct ever reads.
93    b_possible: bool,
94    direct_spatial: bool,
95    nnz_l_cache: [u8; 25],
96    nnz_c_cache: [[u8; 9]; 2],
97    /// Decoded-picture buffer (most-recent first); empty in I-slices. `ref_idx`
98    /// indexes into this list.
99    refs: Vec<crate::Ref>,
100    /// `num_ref_idx_l0_active` for the current slice — drives whether `ref_idx`
101    /// is coded (active > 1) and its te(v)/ue(v) form, independently of how many
102    /// reference pictures actually exist (spec §7.4.5.1, §9.1).
103    num_ref_active: usize,
104    /// `constrained_intra_pred_flag`: when set, intra prediction may only use
105    /// samples from intra-coded neighbors (inter neighbors are "not available").
106    constrained_intra: bool,
107    /// High-profile 4×4 scaling matrices in **raster** order, indexed by
108    /// `[Y-intra, Cb-intra, Cr-intra, Y-inter, Cb-inter, Cr-inter]`. `None` = flat.
109    scaling: Option<[[i32; 16]; 6]>,
110    /// High-profile 8×8 luma scaling matrices in raster order `[Y-intra, Y-inter]`
111    /// (4:2:0 has only these two). `None` = flat.
112    scaling8: Option<[[i32; 64]; 2]>,
113    /// `transform_8x8_mode_flag` from the PPS: enables `transform_size_8x8_flag`.
114    transform_8x8_mode: bool,
115    /// Per-macroblock `transform_size_8x8_flag` (for deblocking: internal 4×4
116    /// luma edges of 8×8-transform MBs are not filtered).
117    mb_t8x8: Vec<bool>,
118    // ---- Row-interleaved deblocking state (docs/row-interleave-plan.md) ----
119    /// Per-MB boundary strengths, filled row-by-row as decode completes rows.
120    bs_frame: Vec<rusty_h264_common::deblock::MbBs>,
121    /// Rows whose bS is derived (watermark).
122    bs_rows: usize,
123    /// Rows already deblock-FILTERED (watermark; R3).
124    flt_rows: usize,
125    /// Two-row rolling window of packed records (prev = row r-1, cur = row r).
126    pk_prev: Vec<rusty_h264_common::deblock::MbPack>,
127    pk_cur: Vec<rusty_h264_common::deblock::MbPack>,
128    /// Transform-block coded mask (nnz with the 8x8 OR applied), filled per row.
129    nnz_dbr: Vec<u8>,
130    /// Unfiltered bottom rows of the last-filtered MB row (intra reads these).
131    bak_y: Vec<u8>,
132    bak_u: Vec<u8>,
133    bak_v: Vec<u8>,
134    /// Entropy-decouple: deferred pixel jobs + the per-slice activation flag
135    /// (CABAC slices only — the CAVLC loop has no flush hooks).
136    edc_jobs: Vec<EdcJob>,
137    edc_active: bool,
138    // ---- E2: the worker-thread plumbing (all None outside a threaded slice) ----
139    edc_tx: Option<std::sync::mpsc::SyncSender<EdcMsg>>,
140    edc_ctx_rx: Option<std::sync::mpsc::Receiver<PixelCtx>>,
141    edc_back_tx: Option<std::sync::mpsc::Sender<PixelCtx>>,
142    /// While the parse thread holds the pixel context for an intra macroblock
143    /// (planes moved into `self`), the rest of the context parks here.
144    edc_parked: Option<PixelCtx>,
145    /// E3: while parsing a B macroblock in threaded mode, its MC regions
146    /// accumulate here instead of executing (the pixel side is the worker's).
147    edc_regions: Option<Vec<BRegion>>,
148    /// D10: jobs accumulated for the current row, sent as ONE message.
149    edc_batch: Vec<EdcJob>,
150    /// D12: bits/MB carried in from previous slices (0 = not yet known).
151    bits_per_mb: f64,
152    /// Current slice's deblock parameters (set per slice by the caller).
153    db_ena: bool,
154    db_oa: i32,
155    db_ob: i32,
156    /// Per-macroblock deblock derivation CLASS (`MB_KIND_*`), so the loop filter
157    /// can skip the 24-block neighbourhood gather on macroblocks whose strengths
158    /// are determined by syntax alone. Starts UNSET; anything left UNSET simply
159    /// takes the blind path, so a missed producer site costs speed, not
160    /// correctness. Only classes that are uniform BY SYNTAX are written — notably
161    /// NOT `B_Skip`/`B_Direct`, whose direct-derived motion varies per 4×4.
162    mb_kind: Vec<u8>,
163    /// Explicit weighted-prediction tables, when active for this slice.
164    weights: Option<WeightTable>,
165    /// Current picture's `PicOrderCnt` (for temporal direct + implicit weighting).
166    cur_poc: i32,
167    /// `weighted_bipred_idc` (0 = none/average, 1 = explicit, 2 = implicit).
168    weighted_bipred_idc: u8,
169    /// `direct_8x8_inference_flag` (B direct co-located sub-block selection).
170    direct_8x8_inference: bool,
171}
172
173/// Explicit weighted-prediction tables (spec §7.4.3.2 / §8.4.2.3.2). Per
174/// reference list, per ref index: a luma `(weight, offset)` and two chroma
175/// `(weight, offset)` (Cb, Cr). `log2` denominators are shared.
176#[derive(Clone, Default)]
177pub struct WeightTable {
178    pub luma_log2_denom: i32,
179    pub chroma_log2_denom: i32,
180    /// `[list][ref_idx] = (weight, offset)`.
181    pub luma: [Vec<(i32, i32)>; 2],
182    /// `[list][ref_idx][cb=0/cr=1] = (weight, offset)`.
183    pub chroma: [Vec<[(i32, i32); 2]>; 2],
184}
185
186impl WeightTable {
187    /// Applies a single-list (uni-prediction) luma weight (spec §8.4.2.3.2).
188    fn apply_luma(&self, sample: u8, list: usize, refi: usize) -> u8 {
189        let (w, o) = self.luma[list][refi];
190        let lwd = self.luma_log2_denom;
191        let v = if lwd >= 1 {
192            ((sample as i32 * w + (1 << (lwd - 1))) >> lwd) + o
193        } else {
194            sample as i32 * w + o
195        };
196        v.clamp(0, 255) as u8
197    }
198
199    /// Applies a single-list (uni-prediction) chroma weight for component `cc`.
200    fn apply_chroma(&self, sample: u8, list: usize, refi: usize, cc: usize) -> u8 {
201        let (w, o) = self.chroma[list][refi][cc];
202        let cwd = self.chroma_log2_denom;
203        let v = if cwd >= 1 {
204            ((sample as i32 * w + (1 << (cwd - 1))) >> cwd) + o
205        } else {
206            sample as i32 * w + o
207        };
208        v.clamp(0, 255) as u8
209    }
210}
211
212/// Why a macroblock could not be decoded.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub enum MbError {
215    Truncated,
216    Unsupported(&'static str),
217}
218
219impl From<OutOfData> for MbError {
220    fn from(_: OutOfData) -> Self {
221        MbError::Truncated
222    }
223}
224
225/// Recycled per-picture scratch grids.
226///
227/// `FrameDecoder::new` used to allocate ~1.65 MB of frame-wide grids for EVERY
228/// coded picture and drop them when the picture finished. The sampled profiler
229/// prices that (stage `dec-setup`) at 6.7% of decode — larger than dequant,
230/// reconstruct and intra prediction combined, and none of it is codec work.
231///
232/// Two costs are being paid, and the allocation is the bigger one. A ~460 KB
233/// `Vec` goes straight to the OS, so every page is a fresh zero page and the
234/// decoder takes a soft page fault on FIRST TOUCH of each 4 KB — a cost charged
235/// to whatever per-macroblock stage happens to touch it first, not to the
236/// allocation. Handing the same buffers back keeps the pages mapped and warm.
237///
238/// The initialising fill is NOT skipped: these grids are read as neighbour
239/// context (`modes_y` must read 2/DC, `ref_idx_y` must read -1) before every
240/// block that writes them, so a stale value from the previous picture is a
241/// correctness bug, not a performance trade. `clear()` + `resize()` keeps the
242/// fill and drops only the allocation.
243///
244/// The reconstruction planes are deliberately NOT pooled: `into_frame` MOVES
245/// them out as the caller's output frame, so there is nothing to hand back.
246#[derive(Default)]
247pub struct GridPool {
248    /// D12: running bits-per-macroblock of decoded slices, the E2 dispatch's
249    /// density signal. Lives here because `GridPool` is the only state that
250    /// survives a picture (`FrameDecoder` is rebuilt per picture).
251    bits_per_mb: f64,
252    mb_qp: Vec<u8>,
253    bs_frame: Vec<rusty_h264_common::deblock::MbBs>,
254    pk_prev: Vec<rusty_h264_common::deblock::MbPack>,
255    pk_cur: Vec<rusty_h264_common::deblock::MbPack>,
256    nnz_dbr: Vec<u8>,
257    bak_y: Vec<u8>,
258    bak_u: Vec<u8>,
259    bak_v: Vec<u8>,
260    nnz_y: Vec<u8>,
261    nnz_c0: Vec<u8>,
262    nnz_c1: Vec<u8>,
263    modes_y: Vec<u8>,
264    coded_y: Vec<bool>,
265    mv_y: Vec<(i32, i32)>,
266    inter_y: Vec<bool>,
267    ref_idx_y: Vec<i32>,
268    mv1: Vec<(i32, i32)>,
269    ref_idx1: Vec<i32>,
270    mb_t8x8: Vec<bool>,
271    mb_kind: Vec<u8>,
272}
273
274/// Reuse `v`'s allocation for `n` copies of `val`. Identical OBSERVABLE result to
275/// `vec![val; n]`; differs only in that it reuses the existing allocation when the
276/// capacity already suffices.
277#[inline]
278fn refill<T: Clone>(mut v: Vec<T>, n: usize, val: T) -> Vec<T> {
279    v.clear();
280    v.resize(n, val);
281    v
282}
283
284impl FrameDecoder {
285    pub fn new(
286        mb_w: usize,
287        mb_h: usize,
288        qp: u8,
289        chroma_qp_offset: i32,
290        refs: Vec<crate::Ref>,
291        num_ref_active: usize,
292        constrained_intra: bool,
293        transform_8x8_mode: bool,
294        b_possible: bool,
295    ) -> Self {
296        Self::with_pool(
297            mb_w,
298            mb_h,
299            qp,
300            chroma_qp_offset,
301            refs,
302            num_ref_active,
303            constrained_intra,
304            transform_8x8_mode,
305            b_possible,
306            GridPool::default(),
307        )
308    }
309
310    /// As `new`, but reusing a previous picture's grid allocations. See `GridPool`.
311    #[allow(clippy::too_many_arguments)]
312    pub fn with_pool(
313        mb_w: usize,
314        mb_h: usize,
315        qp: u8,
316        chroma_qp_offset: i32,
317        refs: Vec<crate::Ref>,
318        num_ref_active: usize,
319        constrained_intra: bool,
320        transform_8x8_mode: bool,
321        b_possible: bool,
322        pool: GridPool,
323    ) -> Self {
324        let (cw, ch) = (mb_w * 16, mb_h * 16);
325        let (ccw, cch) = (cw / 2, ch / 2);
326        let bits_per_mb = pool.bits_per_mb;
327        Self {
328            mb_w,
329            mb_h,
330            qp,
331            cur_qp: qp,
332            chroma_qp_offset,
333            cw,
334            ch,
335            ccw,
336            cch,
337            rec_y: vec![0; cw * ch],
338            rec_u: vec![0; ccw * cch],
339            rec_v: vec![0; ccw * cch],
340            mb_qp: refill(pool.mb_qp, mb_w * mb_h, qp),
341            slice_first_mb: 0,
342            nnz_y: refill(pool.nnz_y, (mb_w * 4) * (mb_h * 4), 0),
343            nnz_c: [
344                refill(pool.nnz_c0, (mb_w * 2) * (mb_h * 2), 0),
345                refill(pool.nnz_c1, (mb_w * 2) * (mb_h * 2), 0),
346            ],
347            modes_y: refill(pool.modes_y, (mb_w * 4) * (mb_h * 4), 2),
348            coded_y: refill(pool.coded_y, (mb_w * 4) * (mb_h * 4), false),
349            mv_y: refill(pool.mv_y, (mb_w * 4) * (mb_h * 4), (0, 0)),
350            inter_y: refill(pool.inter_y, (mb_w * 4) * (mb_h * 4), false),
351            ref_idx_y: refill(pool.ref_idx_y, (mb_w * 4) * (mb_h * 4), -1),
352            mv1: refill(pool.mv1, (mb_w * 4) * (mb_h * 4), (0, 0)),
353            ref_idx1: refill(pool.ref_idx1, (mb_w * 4) * (mb_h * 4), -1),
354            refs1: Vec::new(),
355            num_ref_active1: 0,
356            is_b: false,
357            b_possible,
358            direct_spatial: true,
359            nnz_l_cache: [0x80; 25],
360            nnz_c_cache: [[0x80; 9]; 2],
361            refs,
362            num_ref_active,
363            constrained_intra,
364            scaling: None,
365            scaling8: None,
366            transform_8x8_mode,
367            mb_t8x8: refill(pool.mb_t8x8, mb_w * mb_h, false),
368            bs_frame: refill(pool.bs_frame, mb_w * mb_h, Default::default()),
369            bs_rows: 0,
370            flt_rows: 0,
371            pk_prev: {
372                let mut v = pool.pk_prev;
373                v.clear();
374                v
375            },
376            pk_cur: {
377                let mut v = pool.pk_cur;
378                v.clear();
379                v
380            },
381            nnz_dbr: refill(pool.nnz_dbr, (mb_w * 4) * (mb_h * 4), 0),
382            bak_y: refill(pool.bak_y, cw, 0),
383            bak_u: refill(pool.bak_u, ccw, 0),
384            bak_v: refill(pool.bak_v, ccw, 0),
385            edc_jobs: Vec::new(),
386            edc_active: false,
387            edc_tx: None,
388            edc_ctx_rx: None,
389            edc_back_tx: None,
390            edc_parked: None,
391            edc_regions: None,
392            edc_batch: Vec::new(),
393            bits_per_mb,
394            db_ena: false,
395            db_oa: 0,
396            db_ob: 0,
397            mb_kind: refill(
398                pool.mb_kind,
399                mb_w * mb_h,
400                rusty_h264_common::deblock::MB_KIND_UNSET,
401            ),
402            weights: None,
403            cur_poc: 0,
404            weighted_bipred_idc: 0,
405            direct_8x8_inference: false,
406        }
407    }
408
409    /// Sets the explicit weighted-prediction tables for this slice.
410    pub fn set_weights(&mut self, weights: WeightTable) {
411        self.weights = Some(weights);
412    }
413
414    /// Applies explicit uni-prediction weighting to a motion-compensated partition
415    /// (luma `pred_y` region + the two chroma planes), if weighting is active.
416    /// `list` is the reference list and `refi` the partition's reference index.
417    fn weight_partition(
418        &self,
419        pred_y: &mut [u8; 256],
420        c_pred: &mut [[u8; 64]; 2],
421        list: usize,
422        refi: usize,
423        rx: usize,
424        ry: usize,
425        rw: usize,
426        rh: usize,
427    ) {
428        let Some(wt) = &self.weights else { return };
429        for dy in 0..rh {
430            for dx in 0..rw {
431                let i = (ry + dy) * 16 + (rx + dx);
432                pred_y[i] = wt.apply_luma(pred_y[i], list, refi);
433            }
434        }
435        let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
436        for cc in 0..2 {
437            for dy in 0..crh {
438                for dx in 0..crw {
439                    let i = (cry + dy) * 8 + (crx + dx);
440                    c_pred[cc][i] = wt.apply_chroma(c_pred[cc][i], list, refi, cc);
441                }
442            }
443        }
444    }
445
446    /// Sets the High-profile scaling matrices (raster order: six 4×4 lists, two
447    /// 8×8 luma lists). The caller un-zig-zags the SPS lists. Flat is the default.
448    pub fn set_scaling(&mut self, scaling: [[i32; 16]; 6], scaling8: [[i32; 64]; 2]) {
449        self.scaling = Some(scaling);
450        self.scaling8 = Some(scaling8);
451    }
452
453    /// Dequantizes a 4×4 AC block with scaling list `list` (flat if none active).
454    fn dequant(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
455        match &self.scaling {
456            Some(s) => dequantize_weighted(levels, qp, &s[list]),
457            None => dequantize(levels, qp),
458        }
459    }
460
461    /// Single-coefficient twin of `dequant` for position 0 (DC-only fast path).
462    fn dequant_dc4(&self, level: i32, qp: u8, list: usize) -> i32 {
463        rusty_h264_common::transform::dequantize_dc4(
464            level,
465            qp,
466            self.scaling.as_ref().map(|s| s[list][0]),
467        )
468    }
469
470    /// Inverse-quantizes the I_16x16 luma DC with scaling list `list`'s DC weight.
471    fn dequant_luma_dc(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
472        match &self.scaling {
473            Some(s) => inverse_quant_luma_dc_weighted(levels, qp, s[list][0]),
474            None => inverse_quant_luma_dc(levels, qp),
475        }
476    }
477
478    /// Inverse-quantizes a chroma DC block with scaling list `list`'s DC weight.
479    fn dequant_chroma_dc(&self, levels: &[i32; 4], qp: u8, list: usize) -> [i32; 4] {
480        match &self.scaling {
481            Some(s) => inverse_quant_chroma_dc_weighted(levels, qp, s[list][0]),
482            None => inverse_quant_chroma_dc(levels, qp),
483        }
484    }
485
486    /// Sets the B-slice context for the slice about to be decoded: `RefPicList1`,
487    /// its active count, and the direct-mode flag.
488    #[allow(clippy::too_many_arguments)]
489    pub fn set_b_context(
490        &mut self,
491        refs1: Vec<crate::Ref>,
492        num_ref_active1: usize,
493        direct_spatial: bool,
494        cur_poc: i32,
495        weighted_bipred_idc: u8,
496        direct_8x8_inference: bool,
497    ) {
498        self.is_b = true;
499        self.refs1 = refs1;
500        self.num_ref_active1 = num_ref_active1;
501        self.direct_spatial = direct_spatial;
502        self.cur_poc = cur_poc;
503        self.weighted_bipred_idc = weighted_bipred_idc;
504        self.direct_8x8_inference = direct_8x8_inference;
505    }
506
507    /// Steps the running luma QP by a `mb_qp_delta` (spec §7.4.5, 8-bit depth):
508    /// `QPy = (QPy_prev + delta + 52) % 52`.
509    fn step_qp(&mut self, delta: i32) {
510        self.cur_qp = (self.cur_qp as i32 + delta + 52).rem_euclid(52) as u8;
511    }
512
513    /// Maps a luma QP to its chroma QP, applying `chroma_qp_index_offset`
514    /// (spec §8.5.8): `QPc = qpc_table(Clip3(0, 51, QPy + offset))`.
515    fn chroma_qp_for(&self, qp_y: u8) -> u8 {
516        let qpi = (qp_y as i32 + self.chroma_qp_offset).clamp(0, 51) as u8;
517        chroma_qp(qpi)
518    }
519
520    /// Resets per-slice state before decoding a continuation slice of the same
521    /// picture: the running QP (each slice carries its own `slice_qp`) and the
522    /// reference list (each slice may reorder it).
523    pub fn begin_slice(&mut self, slice_qp: u8, refs: Vec<crate::Ref>, num_ref_active: usize) {
524        self.cur_qp = slice_qp;
525        self.qp = slice_qp;
526        self.refs = refs;
527        self.num_ref_active = num_ref_active;
528        self.weights = None; // re-set per slice if a pred_weight_table is present
529    }
530
531    /// Whether the neighbor macroblock at `(nbx, nby)` is in the slice currently
532    /// being decoded (address ≥ the slice's first MB). For single-slice pictures
533    /// `slice_first_mb == 0`, so this is always true and prediction is unchanged.
534    #[inline]
535    fn nbr_in_slice(&self, nbx: usize, nby: usize) -> bool {
536        nby * self.mb_w + nbx >= self.slice_first_mb
537    }
538
539    /// Whether the neighbor 4×4 block at `(nbx, nby)` may contribute to intra
540    /// prediction. With `constrained_intra_pred`, an inter-coded neighbor is
541    /// treated as unavailable (spec §8.3.1.2.{1,2}); otherwise always usable.
542    #[inline]
543    fn intra_nbr_ok(&self, nbx: usize, nby: usize) -> bool {
544        !self.constrained_intra || !self.inter_y[nby * (self.mb_w * 4) + nbx]
545    }
546
547    fn mv_neighbors(&self, mb_x: usize, mb_y: usize) -> [MvNeighbor; 3] {
548        let w4 = self.mb_w * 4;
549        let get = |avail: bool, bx: isize, by: isize| {
550            if avail {
551                let idx = by as usize * w4 + bx as usize;
552                MvNeighbor {
553                    available: true,
554                    mv: self.mv_y[idx],
555                    ref_idx: self.ref_idx_y[idx],
556                }
557            } else {
558                MvNeighbor::NONE
559            }
560        };
561        let (bx, by) = (mb_x as isize * 4, mb_y as isize * 4);
562        let a = get(mb_x > 0 && self.nbr_in_slice(mb_x - 1, mb_y), bx - 1, by);
563        let b = get(mb_y > 0 && self.nbr_in_slice(mb_x, mb_y - 1), bx, by - 1);
564        let c = if mb_y > 0 && mb_x + 1 < self.mb_w && self.nbr_in_slice(mb_x + 1, mb_y - 1) {
565            get(true, bx + 4, by - 1)
566        } else {
567            get(mb_x > 0 && mb_y > 0 && self.nbr_in_slice(mb_x - 1, mb_y - 1), bx - 1, by - 1)
568        };
569        [a, b, c]
570    }
571
572    fn mv_neighbors_block(&self, pbx: isize, pby: isize, pwb: isize) -> [MvNeighbor; 3] {
573        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Neighbors);
574        let (w4, h4) = ((self.mb_w * 4) as isize, (self.mb_h * 4) as isize);
575        let get = |bx: isize, by: isize| -> MvNeighbor {
576            // Available iff inside the frame, decoded, and in the current slice.
577            if bx < 0
578                || by < 0
579                || bx >= w4
580                || by >= h4
581                || !self.coded_y[(by * w4 + bx) as usize]
582                || !self.nbr_in_slice(bx as usize / 4, by as usize / 4)
583            {
584                MvNeighbor::NONE
585            } else {
586                let idx = (by * w4 + bx) as usize;
587                MvNeighbor { available: true, mv: self.mv_y[idx], ref_idx: self.ref_idx_y[idx] }
588            }
589        };
590        let a = get(pbx - 1, pby);
591        let b = get(pbx, pby - 1);
592        let mut c = get(pbx + pwb, pby - 1);
593        if !c.available {
594            c = get(pbx - 1, pby - 1);
595        }
596        [a, b, c]
597    }
598
599    fn skip_mv(&self, mb_x: usize, mb_y: usize) -> (i32, i32) {
600        let [a, b, c] = self.mv_neighbors(mb_x, mb_y);
601        if !a.available
602            || !b.available
603            || (a.ref_idx == 0 && a.mv == (0, 0))
604            || (b.ref_idx == 0 && b.mv == (0, 0))
605        {
606            (0, 0)
607        } else {
608            predict_mv(a, b, c, 0)
609        }
610    }
611
612    fn set_mb_mv(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32), inter: bool, refi: i32) {
613        let w4 = self.mb_w * 4;
614        for dy in 0..4 {
615            for dx in 0..4 {
616                let idx = (mb_y * 4 + dy) * w4 + (mb_x * 4 + dx);
617                self.mv_y[idx] = mv;
618                self.inter_y[idx] = inter;
619                self.ref_idx_y[idx] = if inter { refi } else { -1 };
620            }
621        }
622    }
623
624    /// Commit one inter partition's motion into the 4×4 grid (ref 0, 1-ref P).
625    /// `(rx,ry,rw,rh)` are MB-relative luma pixels; committing before the next
626    /// partition's prediction is what lets a later partition predict from it.
627    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) {
628        let w4 = self.mb_w * 4;
629        for by in ry / 4..ry / 4 + rh / 4 {
630            for bx in rx / 4..rx / 4 + rw / 4 {
631                let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
632                self.mv_y[idx] = mv;
633                self.inter_y[idx] = true;
634                self.ref_idx_y[idx] = refi as i32;
635                self.coded_y[idx] = true;
636            }
637        }
638    }
639
640    /// Per-slice deblock parameters, needed DURING decode by the row-interleave
641    /// path. `ena` is already resolved against `RFF_ABL_DEBLOCK` by the caller.
642    pub fn set_deblock_params(&mut self, ena: bool, oa: i32, ob: i32) {
643        // Latch: the FIRST disabling slice turns row filtering off for the rest
644        // of the picture (see `row_hook`); rows already filtered stay counted
645        // in `flt_rows` and the picture-end tail handles the remainder.
646        self.db_ena = ena && (self.flt_rows == 0 || self.db_ena);
647        self.db_oa = oa;
648        self.db_ob = ob;
649    }
650
651    /// Derives bS for macroblock row `r` from the just-decoded (hot) grids into
652    /// `bs_frame`, maintaining the two-row rolling record window (R2 of
653    /// docs/row-interleave-plan.md).
654    fn derive_bs_row(&mut self, r: usize) {
655        // Same stage label the in-filter derivation used, so profiles keep
656        // pricing bS derivation wherever it lives.
657        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DebDerive);
658        use rusty_h264_common::deblock::{derive_mb_records, pack_mb, BlockInfo, MbBs};
659        let (mb_w, w4) = (self.mb_w, self.mb_w * 4);
660        // Transform-block coded mask for this row: raw nnz, then the 8x8 OR for
661        // t8 macroblocks (spec §8.7: the 8x8 transform's coded status is per 8x8).
662        for br in r * 4..r * 4 + 4 {
663            let a = br * w4;
664            self.nnz_dbr[a..a + w4].copy_from_slice(&self.nnz_y[a..a + w4]);
665        }
666        for mb_x in 0..mb_w {
667            if !self.mb_t8x8[r * mb_w + mb_x] {
668                continue;
669            }
670            for b8 in 0..4usize {
671                let (bx, by) = (mb_x * 4 + (b8 % 2) * 2, r * 4 + (b8 / 2) * 2);
672                let any = (0..2).any(|sy| (0..2).any(|sx| self.nnz_y[(by + sy) * w4 + bx + sx] > 0));
673                for sy in 0..2 {
674                    for sx in 0..2 {
675                        self.nnz_dbr[(by + sy) * w4 + bx + sx] = any as u8;
676                    }
677                }
678            }
679        }
680        let poc0: Vec<i32> = self.refs.iter().map(|f| f.poc).collect();
681        let poc1: Vec<i32> = self.refs1.iter().map(|f| f.poc).collect();
682        let info = BlockInfo {
683            inter: &self.inter_y,
684            nnz: &self.nnz_dbr,
685            mv: &self.mv_y,
686            ref_id: &self.ref_idx_y,
687            mv1: &self.mv1,
688            ref_id1: if poc1.is_empty() { &[] } else { &self.ref_idx1 },
689            w4,
690            t8x8: &self.mb_t8x8,
691            bs: &[],
692            poc0: &poc0,
693            poc1: &poc1,
694            kind: &[],
695        };
696        let has1 = !info.ref_id1.is_empty();
697        std::mem::swap(&mut self.pk_prev, &mut self.pk_cur);
698        self.pk_cur.clear();
699        for mb_x in 0..mb_w {
700            self.pk_cur.push(pack_mb(&info, has1, mb_x, r));
701            let cur = &self.pk_cur[mb_x];
702            let left = if mb_x > 0 { Some(&self.pk_cur[mb_x - 1]) } else { None };
703            let top = if r > 0 { Some(&self.pk_prev[mb_x]) } else { None };
704            let mb_t8 = self.mb_t8x8[r * mb_w + mb_x];
705            let (mut bv, mut bh) = ([[0i32; 4]; 4], [[0i32; 4]; 4]);
706            derive_mb_records(cur, left, top, mb_t8, &mut bv, &mut bh);
707            let mut m = MbBs::default();
708            for e in 0..4 {
709                for sg in 0..4 {
710                    m.v[e][sg] = bv[e][sg] as u8;
711                    m.h[e][sg] = bh[e][sg] as u8;
712                }
713            }
714            self.bs_frame[r * mb_w + mb_x] = m;
715        }
716    }
717
718    /// Decode-loop hook: called at each MB-loop head with the NEXT address to be
719    /// decoded; derives AND FILTERS (R3) every fully-decoded row. Filtering a
720    /// row here preserves the spec's raster per-MB filter order exactly (every
721    /// MB the row's edges touch is already decoded; bottom-adjacent edges
722    /// belong to the NEXT row's MBs, which filter later).
723    #[inline]
724    fn row_hook(&mut self, addr: usize) {
725        let _rh = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecRowHook);
726        edcstat::bump(&edcstat::MBS, 1); // called once per MB-loop head
727        if !rowdb_on() {
728            // Even without row deblocking, completed rows' deferred pixel jobs
729            // must not pile up past a row boundary indefinitely; flush here so
730            // the queue stays row-sized.
731            self.edc_flush();
732            return;
733        }
734        let done = addr / self.mb_w;
735        if self.edc_tx.is_some() {
736            // E2: derivation stays here (it reads the syntax grids); filtering
737            // is the worker's, fed the row's bs/qp/t8 snapshot. `flt_rows`
738            // advances on the worker and comes home with the context.
739            if self.bs_rows < done {
740                self.edc_giveback();
741            }
742            while self.bs_rows < done {
743                let r = self.bs_rows;
744                self.derive_bs_row(r);
745                self.bs_rows += 1;
746                let base = r * self.mb_w;
747                // ORDER: this row's pixel jobs must reach the worker BEFORE the
748                // filter message for the same row.
749                self.edc_flush_batch();
750                edcstat::bump(&edcstat::ROWS, 1);
751                edcstat::bump(
752                    &edcstat::ROWBYTES,
753                    (self.mb_w
754                        * (std::mem::size_of::<rusty_h264_common::deblock::MbBs>() + 2))
755                        as u64,
756                );
757                let msg = EdcMsg::Row {
758                    r,
759                    bs: self.bs_frame[base..base + self.mb_w].to_vec(),
760                    qp: self.mb_qp[base..base + self.mb_w].to_vec(),
761                    t8: self.mb_t8x8[base..base + self.mb_w].to_vec(),
762                };
763                self.edc_tx.as_ref().unwrap().send(msg).expect("worker alive");
764            }
765            return;
766        }
767        if self.bs_rows < done {
768            self.edc_flush();
769        }
770        while self.bs_rows < done {
771            let r = self.bs_rows;
772            self.derive_bs_row(r);
773            self.bs_rows += 1;
774            // Row filtering requires deblock enabled on EVERY slice so far
775            // (`db_ena` latches false once any slice disables it): a mixed
776            // picture falls back to the picture-end tail so "latest slice
777            // wins" semantics are preserved.
778            if self.db_ena {
779                self.save_bak(r);
780                self.filter_row(r);
781                self.flt_rows = r + 1;
782            }
783        }
784    }
785
786    /// Saves the UNFILTERED bottom pixel rows of MB row `r` before filtering
787    /// modifies them: the next row's intra prediction must read pre-deblock
788    /// samples (spec §8.3), and filtering touches the bottom three rows while
789    /// intra reads exactly the bottom ONE (+ the corner) — so one backup row
790    /// per plane suffices, overwritten per row.
791    fn save_bak(&mut self, r: usize) {
792        let y0 = (r * 16 + 15) * self.cw;
793        self.bak_y.copy_from_slice(&self.rec_y[y0..y0 + self.cw]);
794        let c0 = (r * 8 + 7) * self.ccw;
795        self.bak_u.copy_from_slice(&self.rec_u[c0..c0 + self.ccw]);
796        self.bak_v.copy_from_slice(&self.rec_v[c0..c0 + self.ccw]);
797    }
798
799    /// Filters one MB row against the stored strengths, using the CURRENT
800    /// slice's alpha/beta offsets (single-offset streams — the whole corpus —
801    /// are bit-identical to the picture-end call; the plan's risk register
802    /// documents the multi-offset divergence).
803    fn filter_row(&mut self, r: usize) {
804        let info = rusty_h264_common::deblock::BlockInfo {
805            inter: &self.inter_y,
806            nnz: &self.nnz_dbr,
807            mv: &self.mv_y,
808            ref_id: &self.ref_idx_y,
809            mv1: &self.mv1,
810            ref_id1: &self.ref_idx1,
811            w4: self.mb_w * 4,
812            t8x8: &self.mb_t8x8,
813            bs: &self.bs_frame,
814            poc0: &[],
815            poc1: &[],
816            kind: &self.mb_kind,
817        };
818        rusty_h264_common::deblock::filter_frame_rows(
819            &mut self.rec_y,
820            &mut self.rec_u,
821            &mut self.rec_v,
822            self.mb_w,
823            self.mb_h,
824            r..r + 1,
825            &self.mb_qp,
826            self.chroma_qp_offset,
827            self.db_oa,
828            self.db_ob,
829            &info,
830        );
831    }
832
833    /// Top-neighbour LUMA pixel for intra prediction: reads the unfiltered
834    /// backup row when the row above has already been deblock-filtered by the
835    /// row-interleave (flt_rows gates it; 0 when the interleave is off, so
836    /// this compiles to the plain read on the fallback path).
837    #[inline]
838    fn top_y_px(&self, py: usize, x: usize) -> u8 {
839        if py % 16 == 0 && self.flt_rows * 16 >= py {
840            self.bak_y[x]
841        } else {
842            self.rec_y[(py - 1) * self.cw + x]
843        }
844    }
845
846    /// Slice form of [`Self::top_y_px`] for the contiguous 16-wide I16 gather.
847    #[inline]
848    fn top_y_row(&self, py: usize, x: usize, n: usize) -> &[u8] {
849        if py % 16 == 0 && self.flt_rows * 16 >= py {
850            &self.bak_y[x..x + n]
851        } else {
852            &self.rec_y[(py - 1) * self.cw + x..][..n]
853        }
854    }
855
856    /// Top-neighbour CHROMA pixel (plane `c`: 0 = U, 1 = V).
857    #[inline]
858    fn top_c_px(&self, c: usize, cy: usize, x: usize) -> u8 {
859        if cy % 8 == 0 && self.flt_rows * 8 >= cy {
860            if c == 0 { self.bak_u[x] } else { self.bak_v[x] }
861        } else {
862            let rec = if c == 0 { &self.rec_u } else { &self.rec_v };
863            rec[(cy - 1) * self.ccw + x]
864        }
865    }
866
867    /// Slice form of [`Self::top_c_px`] for the 8-wide chroma gather.
868    #[inline]
869    fn top_c_row(&self, c: usize, cy: usize, x: usize, n: usize) -> &[u8] {
870        if cy % 8 == 0 && self.flt_rows * 8 >= cy {
871            if c == 0 { &self.bak_u[x..x + n] } else { &self.bak_v[x..x + n] }
872        } else {
873            let rec = if c == 0 { &self.rec_u } else { &self.rec_v };
874            &rec[(cy - 1) * self.ccw + x..][..n]
875        }
876    }
877
878    /// Snapshots the (deblocked) reconstruction as a reference picture.
879    pub fn as_reference(&self) -> crate::RefFrame {
880        self.as_reference_pooled(&mut Vec::new())
881    }
882
883    /// `as_reference` drawing its padded-plane allocations from `pool` (recycled
884    /// planes of evicted DPB frames — see `Decoder::reclaim_retired`). ~1.9 MB of
885    /// fresh allocation per reference picture otherwise (`dpb-clone` stage, 3-4%
886    /// of decode, mostly first-touch page faults).
887    pub fn as_reference_pooled(&self, pool: &mut Vec<Vec<u8>>) -> crate::RefFrame {
888        // MV CAPTURE (`RFF_MV_DUMP=1`) — lets a harness read the motion field any
889        // conformant H.264 stream carries, including x264's, using this decoder as
890        // the parser. Diagnostic only; inert unless the env var is set.
891        if mv_dump_on() {
892            MV_DUMP.lock().unwrap().push(MvField {
893                mb_w: self.mb_w,
894                mb_h: self.mb_h,
895                mv: self.mv_y.clone(),
896                ref_idx: self.ref_idx_y.clone(),
897                inter: self.inter_y.clone(),
898            });
899        }
900
901        // The per-block motion (mv/ref_idx/ref_poc) is read ONLY by B temporal/spatial
902        // direct (`col.mv/ref_idx/ref_poc`, guarded on `w4 != 0` + `idx < len`). On
903        // Baseline/Constrained-Baseline streams (no B) it's pure waste — skip the two
904        // grid clones + the per-block ref_poc resolve/alloc. `w4 = 0` makes the B
905        // readers no-op even on malformed input.
906        let (mv, ref_idx, mv1, ref_idx1, ref_poc, w4) = if self.b_possible {
907            (
908                self.mv_y.clone(),
909                self.ref_idx_y.clone(),
910                self.mv1.clone(),
911                self.ref_idx1.clone(),
912                // Resolve each block's List-0 ref index to the referenced picture's
913                // POC, so temporal direct can map it into the current list.
914                self.ref_idx_y
915                    .iter()
916                    .map(|&r| {
917                        if r >= 0 {
918                            self.refs.get(r as usize).map_or(i32::MIN, |f| f.poc)
919                        } else {
920                            i32::MIN
921                        }
922                    })
923                    .collect(),
924                self.mb_w * 4,
925            )
926        } else {
927            (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), 0)
928        };
929        // Pop an exact-size recycled buffer per plane; a miss falls back to a
930        // fresh allocation inside `pad_plane_into`.
931        let mut take = |len: usize| -> Vec<u8> {
932            match pool.iter().position(|v| v.len() == len) {
933                Some(i) => pool.swap_remove(i),
934                None => Vec::new(),
935            }
936        };
937        let (lpw, lph) = (self.cw + 2 * crate::LPAD, self.ch + 2 * crate::LPAD);
938        let (cpw, cph) = (self.ccw + 2 * crate::CPAD, self.ch / 2 + 2 * crate::CPAD);
939        crate::RefFrame {
940            // Pad once here (ExpandPicture) instead of extracting a clamped tile
941            // on every MC call — same copy class as the old plane clone.
942            py: rusty_h264_common::inter::pad_plane_into(take(lpw * lph), &self.rec_y, self.cw, self.ch, crate::LPAD),
943            pu: rusty_h264_common::inter::pad_plane_into(take(cpw * cph), &self.rec_u, self.ccw, self.ch / 2, crate::CPAD),
944            pv: rusty_h264_common::inter::pad_plane_into(take(cpw * cph), &self.rec_v, self.ccw, self.ch / 2, crate::CPAD),
945            cw: self.cw,
946            ch: self.ch,
947            frame_num: 0, // set by the caller (decode_slice knows frame_num)
948            poc: 0,       // set by the caller
949            mv,
950            ref_idx,
951            mv1,
952            ref_idx1,
953            ref_poc,
954            w4,
955            long_term: false,
956            long_term_idx: 0,
957        }
958    }
959
960    fn nnz_cache_load(&mut self, mb_x: usize, mb_y: usize) {
961        let w4 = self.mb_w * 4;
962        let top_unavail = mb_y == 0 || !self.nbr_in_slice(mb_x, mb_y - 1);
963        let left_unavail = mb_x == 0 || !self.nbr_in_slice(mb_x - 1, mb_y);
964        for lbx in 0..4 {
965            self.nnz_l_cache[1 + lbx] =
966                if top_unavail { 0x80 } else { self.nnz_y[(mb_y * 4 - 1) * w4 + (mb_x * 4 + lbx)] };
967        }
968        for lby in 0..4 {
969            self.nnz_l_cache[(lby + 1) * 5] =
970                if left_unavail { 0x80 } else { self.nnz_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 - 1)] };
971        }
972    }
973    #[inline]
974    fn nc_pred(&self, lbx: usize, lby: usize) -> i32 {
975        let left = self.nnz_l_cache[(lby + 1) * 5 + lbx] as i32;
976        let top = self.nnz_l_cache[lby * 5 + (lbx + 1)] as i32;
977        let r = left + top;
978        if r < 0x80 { (r + 1) >> 1 } else { r & 0x7f }
979    }
980    #[inline]
981    fn nnz_cache_set(&mut self, lbx: usize, lby: usize, total: u8) {
982        self.nnz_l_cache[(lby + 1) * 5 + (lbx + 1)] = total;
983    }
984    fn chroma_cache_load(&mut self, mb_x: usize, mb_y: usize) {
985        let w2 = self.mb_w * 2;
986        let top_unavail = mb_y == 0 || !self.nbr_in_slice(mb_x, mb_y - 1);
987        let left_unavail = mb_x == 0 || !self.nbr_in_slice(mb_x - 1, mb_y);
988        for c in 0..2 {
989            for bx in 0..2 {
990                self.nnz_c_cache[c][1 + bx] =
991                    if top_unavail { 0x80 } else { self.nnz_c[c][(mb_y * 2 - 1) * w2 + (mb_x * 2 + bx)] };
992            }
993            for by in 0..2 {
994                self.nnz_c_cache[c][(by + 1) * 3] =
995                    if left_unavail { 0x80 } else { self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 - 1)] };
996            }
997        }
998    }
999    #[inline]
1000    fn chroma_nc_pred(&self, c: usize, bx: usize, by: usize) -> i32 {
1001        let left = self.nnz_c_cache[c][(by + 1) * 3 + bx] as i32;
1002        let top = self.nnz_c_cache[c][by * 3 + (bx + 1)] as i32;
1003        let r = left + top;
1004        if r < 0x80 { (r + 1) >> 1 } else { r & 0x7f }
1005    }
1006    #[inline]
1007    fn chroma_nnz_cache_set(&mut self, c: usize, bx: usize, by: usize, total: u8) {
1008        self.nnz_c_cache[c][(by + 1) * 3 + (bx + 1)] = total;
1009    }
1010
1011    /// Decodes one slice's macroblocks (raster order) starting at `first_mb`,
1012    /// until `more_rbsp_data()` is exhausted or the picture is full. Returns the
1013    /// next macroblock address (= total when the picture is complete). In a
1014    /// P-slice each macroblock is preceded by `mb_skip_run`.
1015    /// CABAC slice-data decode (docs/cabac-decode-plan.md), brought up brick by brick
1016    /// against the instrumented openh264 oracle. Phase 1: verify engine init; the
1017    /// syntax layer (Phase 2+) is WIP.
1018    #[allow(clippy::too_many_arguments)]
1019    pub fn decode_slice_data_cabac(
1020        &mut self,
1021        rbsp: &[u8],
1022        start_byte: usize,
1023        slice_qp: u8,
1024        cabac_init_idc: u32,
1025        is_i: bool,
1026        is_p: bool,
1027        first_mb: usize,
1028    ) -> Result<usize, MbError> {
1029        // E2: overlap parse (this thread) with pixel reconstruction (a scoped
1030        // worker owning the planes) for P slices. I slices and B slices keep
1031        // the inline path (their pixel coupling is per-MB); the ownership
1032        // ping-pong around intra-in-P macroblocks is `edc_intra_sync`.
1033        let eligible = edc_on() && rowdb_on() && !is_i && (is_p || self.is_b);
1034        let threaded = eligible
1035            && edc_mt()
1036                .unwrap_or_else(|| edc_dispatch(self.mb_w, self.mb_h, self.bits_per_mb, true));
1037        edcstat::bump(&edcstat::DISPATCH_ON, threaded as u64);
1038        edcstat::bump(&edcstat::DISPATCH_SEEN, eligible as u64);
1039        if !threaded {
1040            let r = self.decode_slice_cabac_inner(rbsp, start_byte, slice_qp, cabac_init_idc, is_i, is_p, first_mb);
1041            self.note_slice_density(rbsp.len().saturating_sub(start_byte), first_mb, &r);
1042            return r;
1043        }
1044        let ctx = self.edc_take_ctx();
1045        // D7 PROBE: is the CPU overhead PAYLOAD (alloc/copy per job) or
1046        // SYNCHRONISATION (blocking on a full queue, park/unpark)? The bound
1047        // separates them: raising it removes send-blocking without changing a
1048        // single byte copied. `RS_H264_EDC_BOUND` sweeps it.
1049        let (tx, rx) = std::sync::mpsc::sync_channel::<EdcMsg>(edc_bound());
1050        let (ctx_tx, ctx_rx) = std::sync::mpsc::channel::<PixelCtx>();
1051        let (back_tx, back_rx) = std::sync::mpsc::channel::<PixelCtx>();
1052        let (res, ctx, panicked) = std::thread::scope(|sc| {
1053            let h = sc.spawn(move || edc_worker(ctx, rx, ctx_tx, back_rx));
1054            self.edc_tx = Some(tx);
1055            self.edc_ctx_rx = Some(ctx_rx);
1056            self.edc_back_tx = Some(back_tx);
1057            // UNWIND SAFETY (found by the fuzzer as a HANG, not a failure): a
1058            // panic inside the parse loop would skip the cleanup below — but
1059            // the sender lives in `self`, which outlives the unwind, so the
1060            // channel would never close, the worker would never exit, and the
1061            // scope's join would block forever, converting a diagnosable panic
1062            // into a silent deadlock under `catch_unwind` harnesses. Catch,
1063            // clean up, join, restore the planes, THEN resume the panic.
1064            let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1065                self.decode_slice_cabac_inner(rbsp, start_byte, slice_qp, cabac_init_idc, is_i, is_p, first_mb)
1066            }));
1067            self.edc_flush_batch(); // ORDER: no job may outlive the channel
1068            self.edc_giveback(); // if an intra macroblock left us holding
1069            self.edc_tx = None; // closes the channel -> worker drains + returns
1070            self.edc_ctx_rx = None;
1071            self.edc_back_tx = None;
1072            match (r, h.join()) {
1073                (Ok(res), Ok(ctx)) => (res, Some(ctx), None),
1074                (Err(p), Ok(ctx)) => (Err(MbError::Truncated), Some(ctx), Some(p)),
1075                (Ok(_), Err(p)) | (Err(_), Err(p)) => (Err(MbError::Truncated), None, Some(p)),
1076            }
1077        });
1078        if let Some(ctx) = ctx {
1079            self.edc_restore_ctx(ctx);
1080        }
1081        if let Some(p) = panicked {
1082            std::panic::resume_unwind(p);
1083        }
1084        self.note_slice_density(rbsp.len().saturating_sub(start_byte), first_mb, &res);
1085        res
1086    }
1087
1088    /// Feed the D12 dispatch its density signal from a slice just decoded.
1089    /// Exponentially smoothed so one atypical slice cannot flip the arm, and
1090    /// only ever read on the NEXT slice — the current one is already committed.
1091    fn note_slice_density(&mut self, bytes: usize, first_mb: usize, r: &Result<usize, MbError>) {
1092        let Ok(end) = r else { return };
1093        let mbs = end.saturating_sub(first_mb);
1094        if mbs == 0 {
1095            return;
1096        }
1097        let bpm = (bytes * 8) as f64 / mbs as f64;
1098        self.bits_per_mb = if self.bits_per_mb == 0.0 {
1099            bpm
1100        } else {
1101            0.75 * self.bits_per_mb + 0.25 * bpm
1102        };
1103    }
1104
1105    fn decode_slice_cabac_inner(
1106        &mut self,
1107        rbsp: &[u8],
1108        start_byte: usize,
1109        slice_qp: u8,
1110        cabac_init_idc: u32,
1111        is_i: bool,
1112        is_p: bool,
1113        first_mb: usize,
1114    ) -> Result<usize, MbError> {
1115        self.edc_active = edc_on();
1116        let mut cab = crate::cabac::Cabac::new(rbsp, start_byte, slice_qp as i32, cabac_init_idc, is_i);
1117        let (range, _offset) = cab.dbg_state();
1118        let trace = std::env::var_os("RH_CABAC_TRACE").is_some();
1119        debug_assert_eq!(range, 510, "CABAC init range must be 510");
1120
1121        const I16_CBP: [u32; 6] = [0, 16, 32, 15, 31, 47];
1122        let mbw = self.mb_w;
1123        let total = self.mb_w * self.mb_h;
1124        // Per-MB neighbour state (single-slice assumption: avail == in-bounds).
1125        // SCOPED: 11 zero-initialised allocations sized by MB count, once per slice.
1126        let _alloc_g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecSliceAlloc);
1127        let mut cat = vec![255u8; total]; // 0=I4x4, 2=I16, 255=unavailable
1128        let mut mb_cbp = vec![0u8; total];
1129        let mut cmode = vec![-1i32; total]; // chroma pred mode
1130        let mut mb_nzc = vec![[0u8; 24]; total]; // 16 luma raster + 8 chroma
1131        let mut cbf_dc = vec![0u16; total];
1132        let mut mb_skip = vec![false; total];
1133        let mut mb_ref = vec![[-1i8; 16]; total]; // per-4×4-block List-0 ref (-1 = intra)
1134        let mut mb_mvd = vec![[[0i16; 2]; 16]; total]; // per-block mvd (for mvd ctxInc)
1135        let mut mb_ref1 = vec![[-1i8; 16]; total]; // B: per-block List-1 ref (-1 = not in list)
1136        let mut mb_mvd1 = vec![[[0i16; 2]; 16]; total]; // B: per-block List-1 mvd (ctxInc)
1137        let mut mb_direct = vec![false; total]; // B: MB is (skip/)direct — for mb_type ctxInc
1138        drop(_alloc_g);
1139        let mut last_delta_qp = 0i32;
1140        let mut addr = first_mb;
1141
1142        let _mbloop_g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbLoop);
1143        loop {
1144            // BOUND the entropy-coded loop. `decode_terminate` is the only exit, and a
1145            // mutated stream can simply never produce it — the arithmetic decoder
1146            // zero-fills past the end of the buffer and keeps yielding symbols. Without
1147            // this the loop walks `addr` past the picture and indexes out of bounds.
1148            // (Surfaced by the fuzzer the moment CABAC became the default; the CAVLC
1149            // slice loop already had its own bound.)
1150            if addr >= total {
1151                return Err(MbError::Truncated);
1152            }
1153            self.row_hook(addr);
1154            let (mbx, mby) = (addr % mbw, addr / mbw);
1155            let left = (mbx > 0).then(|| addr - 1);
1156            let top = (mby > 0).then(|| addr - mbw);
1157
1158            // Brick 3.1/3.2: P-slice mb_skip_flag, then mb_type (P mb_type is neighbour-
1159            // independent; intra sub-types map to the I dispatch below).
1160            let mb_type;
1161            if is_p {
1162                let sctx = 11
1163                    + left.map_or(0, |a| (!mb_skip[a]) as usize)
1164                    + top.map_or(0, |a| (!mb_skip[a]) as usize);
1165                if parse_mb_skip_cabac(&mut cab, sctx) {
1166                    mb_skip[addr] = true;
1167                    cat[addr] = 100; // inter (not I16/PCM) for neighbour context
1168                    last_delta_qp = 0; // skip codes no mb_qp_delta → delta ctxInc resets
1169                    // P_Skip recon reuses the entropy-free CAVLC primitive verbatim: it
1170                    // takes no bit-reader (skip has no coded syntax past the flag), just
1171                    // predicts the skip MV, motion-compensates, and commits the grid.
1172                    self.decode_p_skip(mbx, mby)?;
1173                    self.mb_qp[addr] = self.cur_qp; // skip inherits QPy
1174                    let eos = cab.decode_terminate();
1175                    addr += 1;
1176                    if eos || addr >= total {
1177                        break;
1178                    }
1179                    continue;
1180                }
1181                let mbt = parse_mb_type_p_cabac(&mut cab);
1182                if mbt == 30 {
1183                    return Err(MbError::Unsupported("CABAC I_PCM (WIP)"));
1184                }
1185                if mbt <= 3 {
1186                    let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbP);
1187                    // noSubMbPartSizeLessThan8x8Flag (spec 7.3.5): P_8x8 permits the
1188                    // 8x8 transform only when every sub-partition is itself 8x8.
1189                    let mut allow8 = true;
1190                    // Inter MB (Bricks 3.3/3.4/3.5). 1-ref stream → ref_idx not coded (ref=0).
1191                    // Build the 30-entry mvd/ref neighbour cache (openh264 WelsFillCacheInterCabac).
1192                    let mut mvdc = [[0i16; 2]; 30];
1193                    let mut refc = [-1i8; 30];
1194                    if let Some(l) = left {
1195                        for (ci, bi) in [(6usize, 3usize), (12, 7), (18, 11), (24, 15)] {
1196                            refc[ci] = mb_ref[l][bi];
1197                            mvdc[ci] = mb_mvd[l][bi];
1198                        }
1199                    }
1200                    if let Some(t) = top {
1201                        for (ci, bi) in [(1usize, 12usize), (2, 13), (3, 14), (4, 15)] {
1202                            refc[ci] = mb_ref[t][bi];
1203                            mvdc[ci] = mb_mvd[t][bi];
1204                        }
1205                    }
1206                    if mbx > 0 && mby > 0 {
1207                        let a = addr - mbw - 1;
1208                        (refc[0], mvdc[0]) = (mb_ref[a][15], mb_mvd[a][15]);
1209                    }
1210                    if mby > 0 && mbx + 1 < mbw {
1211                        let a = addr - mbw + 1;
1212                        (refc[5], mvdc[5]) = (mb_ref[a][12], mb_mvd[a][12]);
1213                    }
1214                    let mut mmvd = [[0i16; 2]; 16];
1215                    let mut mref = [0i8; 16];
1216                    // mb_pred (spec 7.3.5.1): all ref_idx_l0 FIRST (only when >1 active
1217                    // ref), then all mvd + ref-aware predict + commit. `refidx!` parses one
1218                    // partition's ref_idx (ctxIdxOffset 54, ctx from neighbour refc) and
1219                    // seeds refc so a later partition's ref/mvd context sees it — mirror
1220                    // of the encoder's two-phase emit_mb_cabac_p_inter.
1221                    macro_rules! refidx {
1222                        ($pi:expr, $zb:expr) => {{
1223                            if self.num_ref_active > 1 {
1224                                let s = CACHE30[$pi];
1225                                let c0 = (refc[s - 1] > 0) as usize + 2 * (refc[s - 6] > 0) as usize;
1226                                let r = parse_ref_idx_cabac(&mut cab, c0);
1227                                for &zb in $zb.iter() {
1228                                    refc[CACHE30[zb]] = r;
1229                                }
1230                                r
1231                            } else {
1232                                0i8
1233                            }
1234                        }};
1235                    }
1236                    macro_rules! part {
1237                        ($pi:expr, $zb:expr, $pred:expr, $rx:expr, $ry:expr, $rw:expr, $rh:expr, $refi:expr) => {{
1238                            let (mvx, mvy) = parse_mvd_partition(&mut cab, $pi, $zb, &mut mvdc, &mut refc, &mut mmvd, &mut mref, $refi);
1239                            let [na, nb, nc] = self.mv_neighbors_block(
1240                                (mbx * 4 + $rx / 4) as isize,
1241                                (mby * 4 + $ry / 4) as isize,
1242                                ($rw / 4) as isize,
1243                            );
1244                            let pmv = $pred(na, nb, nc);
1245                            self.commit_inter_grid(mbx, mby, $rx, $ry, $rw, $rh, (pmv.0 + mvx, pmv.1 + mvy), $refi);
1246                        }};
1247                    }
1248                    match mbt {
1249                        0 => {
1250                            let r0 = refidx!(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
1251                            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);
1252                        }
1253                        1 => {
1254                            let r0 = refidx!(0, &[0, 1, 2, 3, 4, 5, 6, 7]);
1255                            let r1 = refidx!(8, &[8, 9, 10, 11, 12, 13, 14, 15]);
1256                            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);
1257                            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);
1258                        }
1259                        2 => {
1260                            let r0 = refidx!(0, &[0, 1, 2, 3, 8, 9, 10, 11]);
1261                            let r1 = refidx!(4, &[4, 5, 6, 7, 12, 13, 14, 15]);
1262                            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);
1263                            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);
1264                        }
1265                        _ => {
1266                            // P_8x8: 4 sub_mb_types, then 4 ref_idx (one per 8×8), then mvd.
1267                            let mut subt = [0u32; 4];
1268                            for st in &mut subt {
1269                                *st = parse_sub_mb_type_p_cabac(&mut cab);
1270                            }
1271                            allow8 = subt.iter().all(|&t| t == 0);
1272                            let mut pr = [0i8; 4];
1273                            for (i, r) in pr.iter_mut().enumerate() {
1274                                let b = i * 4;
1275                                *r = refidx!(b, &[b, b + 1, b + 2, b + 3]);
1276                            }
1277                            for i in 0..4usize {
1278                                let b = i * 4;
1279                                let (ox, oy) = ((i % 2) * 8, (i / 2) * 8); // 8×8 pixel origin in MB
1280                                let ri = pr[i];
1281                                match subt[i] {
1282                                    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),
1283                                    1 => {
1284                                        part!(b, &[b, b + 1], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 8, 4, ri);
1285                                        part!(b + 2, &[b + 2, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy + 4, 8, 4, ri);
1286                                    }
1287                                    2 => {
1288                                        part!(b, &[b, b + 2], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 4, 8, ri);
1289                                        part!(b + 1, &[b + 1, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox + 4, oy, 4, 8, ri);
1290                                    }
1291                                    _ => {
1292                                        for j in 0..4usize {
1293                                            let (sx, sy) = ((j % 2) * 4, (j / 2) * 4);
1294                                            part!(b + j, &[b + j], |a, b, c| predict_mv(a, b, c, ri as i32), ox + sx, oy + sy, 4, 4, ri);
1295                                        }
1296                                    }
1297                                }
1298                            }
1299                        }
1300                    }
1301                    mb_ref[addr] = mref;
1302                    mb_mvd[addr] = mmvd;
1303                    cat[addr] = 100;
1304
1305                    // Inter cbp + residual (is_intra = false → cbf default nA=nB=0).
1306                    let cbp = parse_cbp_cabac(&mut cab, top.map(|a| mb_cbp[a]), left.map(|a| mb_cbp[a]));
1307                    mb_cbp[addr] = cbp as u8;
1308                    // H-49: an INTER macroblock carries transform_size_8x8_flag AFTER cbp
1309                    // (spec 7.3.5), present only when CodedBlockPatternLuma > 0 and
1310                    // noSubMbPartSizeLessThan8x8Flag. Same context as the intra read.
1311                    let t8 = self.transform_8x8_mode && (cbp & 15) != 0 && allow8 && {
1312                        let a = left.map_or(0, |x| self.mb_t8x8[x] as usize);
1313                        let b = top.map_or(0, |x| self.mb_t8x8[x] as usize);
1314                        cab.decode_decision(399 + a + b) != 0
1315                    };
1316                    self.mb_t8x8[addr] = t8;
1317                    let mut luma8 = [[0i32; 64]; 4]; // per 8x8 block, 8x8 scan order (t8)
1318                    let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
1319                    let mut nzc = [0xffu8; 48];
1320                    if let Some(t) = top {
1321                        let tnz = mb_nzc[t];
1322                        nzc[1..5].copy_from_slice(&tnz[12..16]);
1323                        (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1324                        (nzc[6], nzc[7], nzc[30], nzc[31]) = (tnz[20], tnz[21], tnz[22], tnz[23]);
1325                    }
1326                    if let Some(l) = left {
1327                        let lnz = mb_nzc[l];
1328                        (nzc[8], nzc[16], nzc[24], nzc[32]) = (lnz[3], lnz[7], lnz[11], lnz[15]);
1329                        (nzc[13], nzc[21], nzc[37], nzc[45]) = (lnz[17], lnz[21], lnz[19], lnz[23]);
1330                    }
1331                    let mut cbfdc = 0u16;
1332                    let mut nnzs = [0u8; 24]; // parsed totalCoeff per block (see add_inter_residual)
1333                    let mut luma_scan = [[0i32; 16]; 16]; // per z-order 4×4 block (scan order)
1334                    let mut cdc = [[0i32; 4]; 2]; // chroma DC per plane (scan order)
1335                    let mut cac = [[[0i32; 16]; 4]; 2]; // chroma AC per plane, per 4×4 block
1336                    // A cbp==0 MB codes no mb_qp_delta → the next MB's delta ctxInc sees 0.
1337                    if cbp == 0 {
1338                        last_delta_qp = 0;
1339                    }
1340                    if cbp != 0 {
1341                        let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1342                        let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1343                        self.step_qp(qpd);
1344                        for id8 in 0..4usize {
1345                            if cbp_luma & (1 << id8) != 0 {
1346                                if t8 {
1347                                    // All four slots carry the 8x8 total: cat 5 has no per-4x4
1348                                    // counts, and the recon helper now reads one slot
1349                                    // per 4x4 cell.
1350                                    let n8 = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, false, ndc, &mut luma8[id8]) as u8;
1351                                    for k in 0..4 {
1352                                        nnzs[id8 * 4 + k] = n8;
1353                                    }
1354                                } else {
1355                                    for id4 in 0..4usize {
1356                                        let iz = id8 * 4 + id4;
1357                                        nnzs[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, false, ndc, &mut luma_scan[iz]) as u8;
1358                                    }
1359                                }
1360                            } else {
1361                                for k in 0..4 {
1362                                    nzc[NZC_CACHE[id8 * 4 + k]] = 0;
1363                                }
1364                            }
1365                        }
1366                        if cbp_chroma >= 1 {
1367                            for i in 0..2usize {
1368                                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, false, ndc, &mut cdc[i]);
1369                            }
1370                        }
1371                        if cbp_chroma == 2 {
1372                            for i in 0..2usize {
1373                                for id4 in 0..4usize {
1374                                    nnzs[16 + i * 4 + id4] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, false, ndc, &mut cac[i][id4]) as u8;
1375                                }
1376                            }
1377                        }
1378                    }
1379                    self.mb_qp[addr] = self.cur_qp;
1380                    cbf_dc[addr] = cbfdc;
1381                    let _sc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecStateCache);
1382                    let mut mn = [0u8; 24];
1383                    for k in 0..4 {
1384                        mn[k] = nzc[9 + k];
1385                        mn[4 + k] = nzc[17 + k];
1386                        mn[8 + k] = nzc[25 + k];
1387                        mn[12 + k] = nzc[33 + k];
1388                    }
1389                    (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
1390                    (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
1391                    // A block whose residual was skipped (cbp bit clear / no chroma AC)
1392                    // has 0 coeffs, not "unavailable" — export 0 so an intra neighbour's
1393                    // CBF ctxInc reads 0 (not the 0xff sentinel → is_intra default).
1394                    for v in mn.iter_mut() {
1395                        if *v == 0xff {
1396                            *v = 0;
1397                        }
1398                    }
1399                    mb_nzc[addr] = mn;
1400                    drop(_sc);
1401
1402                    if self.refs.is_empty() {
1403                        return Err(MbError::Unsupported("inter without reference"));
1404                    }
1405                    let (mut jgmv, mut jgref) = ([(0i32, 0i32); 16], [0u8; 16]);
1406                    {
1407                        let w4r = self.mb_w * 4;
1408                        for by in 0..4usize {
1409                            for bx in 0..4usize {
1410                                let bidx = (mby * 4 + by) * w4r + (mbx * 4 + bx);
1411                                jgmv[by * 4 + bx] = self.mv_y[bidx];
1412                                jgref[by * 4 + bx] =
1413                                    self.ref_idx_y[bidx].clamp(0, 15) as u8;
1414                            }
1415                        }
1416                    }
1417                    let job = PInterJob {
1418                        mbx,
1419                        mby,
1420                        t8,
1421                        qp: self.cur_qp,
1422                        cbp_chroma,
1423                        gmv: jgmv,
1424                        gref: jgref,
1425                        luma_scan,
1426                        luma8,
1427                        cdc,
1428                        cac,
1429                        nnzs,
1430                    };
1431                    // D9: `cbp == 0` means every coefficient array in `job` is
1432                    // ZERO — 2,592 of its 2,784 bytes. Ship the motion-only form
1433                    // instead of allocating, filling, channel-passing and freeing
1434                    // 2.6 KB of nothing (12.8-37.5% of inter macroblocks on the
1435                    // x264 corpus). The worker rebuilds the zeroed job and calls
1436                    // the SAME `recon_p_inter`, so no second implementation
1437                    // exists to drift.
1438                    let nores = cbp == 0 && nores_on();
1439                    if self.edc_tx.is_some() {
1440                        self.edc_giveback();
1441                        self.edc_commit_nnz(mbx, mby, t8, &nnzs, cbp_chroma);
1442                        if edcstat::on() {
1443                            edcstat::bump(&edcstat::J_INTER, 1);
1444                            let nores = job.cbp_chroma == 0
1445                                && job.luma_scan.iter().all(|b| b.iter().all(|&c| c == 0))
1446                                && job.cdc.iter().all(|p| p.iter().all(|&c| c == 0))
1447                                && job.cac.iter().all(|p| p.iter().all(|b| b.iter().all(|&c| c == 0)));
1448                            if nores {
1449                                edcstat::bump(&edcstat::J_INTER_NORES, 1);
1450                            }
1451                        }
1452                        let job_msg = if nores {
1453                            edcstat::bump(&edcstat::J_NORES_SENT, 1);
1454                            EdcJob::InterNoRes(Box::new(PInterNoResJob {
1455                                mbx: job.mbx,
1456                                mby: job.mby,
1457                                t8: job.t8,
1458                                qp: job.qp,
1459                                gmv: job.gmv,
1460                                gref: job.gref,
1461                            }))
1462                        } else {
1463                            EdcJob::Inter(Box::new(job))
1464                        };
1465                        self.edc_send_job(job_msg);
1466                    } else if self.edc_active {
1467                        if nores {
1468                            edcstat::bump(&edcstat::J_NORES_SENT, 1);
1469                            self.edc_jobs.push(EdcJob::InterNoRes(Box::new(PInterNoResJob {
1470                                mbx: job.mbx,
1471                                mby: job.mby,
1472                                t8: job.t8,
1473                                qp: job.qp,
1474                                gmv: job.gmv,
1475                                gref: job.gref,
1476                            })));
1477                        } else {
1478                            self.edc_jobs.push(EdcJob::Inter(Box::new(job)));
1479                        }
1480                    } else {
1481                        self.recon_p_inter(&job);
1482                        if double_recon() {
1483                            self.recon_p_inter(&job);
1484                        }
1485                    }
1486
1487                    let eos = cab.decode_terminate();
1488                    addr += 1;
1489                    if eos || addr >= total {
1490                        break;
1491                    }
1492                    continue;
1493                }
1494                mb_type = mbt - 5; // 5→0 (I_4x4), 6..29→1..24 (I_16x16)
1495            } else if self.is_b {
1496                self.edc_flush(); // (E1 single-thread mode: B stays inline)
1497                if self.edc_tx.is_some() {
1498                    // E3: this B macroblock's MC regions record instead of executing.
1499                    self.edc_regions = Some(Vec::with_capacity(8));
1500                }
1501                let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbB);
1502                // noSubMbPartSizeLessThan8x8Flag for B: direct MBs qualify only under
1503                // direct_8x8_inference_flag; B_8x8 needs every sub-partition 8x8.
1504                let mut allow8 = true;
1505                // B-slice: mb_skip_flag (ctx 24 + neighbour-not-skip), then B mb_type.
1506                let sctx = 24
1507                    + left.map_or(0, |a| (!mb_skip[a]) as usize)
1508                    + top.map_or(0, |a| (!mb_skip[a]) as usize);
1509                if parse_mb_skip_cabac(&mut cab, sctx) {
1510                    mb_skip[addr] = true;
1511                    cat[addr] = 100;
1512                    mb_direct[addr] = true;
1513                    last_delta_qp = 0; // skip codes no mb_qp_delta → delta ctxInc resets
1514                    // B_Skip recon reuses the entropy-free CAVLC primitive (spatial/temporal
1515                    // direct with no residual), which also commits the motion grid.
1516                    self.decode_b_skip(mbx, mby)?;
1517                    self.mb_qp[addr] = self.cur_qp;
1518                    // Skip/direct blocks contribute mvd 0 to a later MB's mvd ctxInc; the
1519                    // ref stays in-list so |mvd|=0 is summed (same result either way).
1520                    mb_ref[addr] = [0i8; 16];
1521                    mb_ref1[addr] = [0i8; 16];
1522                    let eos = cab.decode_terminate();
1523                    addr += 1;
1524                    if eos || addr >= total {
1525                        break;
1526                    }
1527                    continue;
1528                }
1529                let bci = left.map_or(0, |a| (!mb_direct[a]) as usize)
1530                    + top.map_or(0, |a| (!mb_direct[a]) as usize);
1531                let bmt = parse_mb_type_b_cabac(&mut cab, bci);
1532                if bmt < 23 {
1533                    // ---- B inter: parse motion (mvd L0/L1; ref not coded on this 1-ref
1534                    // stream) + residual. Recon (b_mc/direct) deferred to B.3. ----
1535                    let mut mvdc0 = [[0i16; 2]; 30];
1536                    let mut refc0 = [-1i8; 30];
1537                    let mut mvdc1 = [[0i16; 2]; 30];
1538                    let mut refc1 = [-1i8; 30];
1539                    // WelsFillCacheInterCabac, per list (L0 = mb_ref/mb_mvd, L1 = mb_ref1/mb_mvd1).
1540                    macro_rules! fill {
1541                        ($mrf:expr, $mmv:expr, $rc:expr, $mc:expr) => {{
1542                            if let Some(l) = left {
1543                                for (ci, bi) in [(6usize, 3usize), (12, 7), (18, 11), (24, 15)] {
1544                                    $rc[ci] = $mrf[l][bi];
1545                                    $mc[ci] = $mmv[l][bi];
1546                                }
1547                            }
1548                            if let Some(t) = top {
1549                                for (ci, bi) in [(1usize, 12usize), (2, 13), (3, 14), (4, 15)] {
1550                                    $rc[ci] = $mrf[t][bi];
1551                                    $mc[ci] = $mmv[t][bi];
1552                                }
1553                            }
1554                            if mbx > 0 && mby > 0 {
1555                                let a = addr - mbw - 1;
1556                                ($rc[0], $mc[0]) = ($mrf[a][15], $mmv[a][15]);
1557                            }
1558                            if mby > 0 && mbx + 1 < mbw {
1559                                let a = addr - mbw + 1;
1560                                ($rc[5], $mc[5]) = ($mrf[a][12], $mmv[a][12]);
1561                            }
1562                        }};
1563                    }
1564                    fill!(mb_ref, mb_mvd, refc0, mvdc0);
1565                    fill!(mb_ref1, mb_mvd1, refc1, mvdc1);
1566                    let mut mmvd0 = [[0i16; 2]; 16];
1567                    let mut mref0 = [-1i8; 16];
1568                    let mut mmvd1 = [[0i16; 2]; 16];
1569                    let mut mref1 = [-1i8; 16];
1570                    if self.refs.is_empty() || self.refs1.is_empty() {
1571                        return Err(MbError::Unsupported("B without references"));
1572                    }
1573                    // Recon (mirrors CAVLC decode_b_mb / decode_b_8x8): predict each list's
1574                    // MV off the committed grid + the CABAC-parsed mvd, commit, MC (bi-pred
1575                    // blend), then add the residual. Prediction reads mmvd0/mmvd1 (the mvd
1576                    // per raster block, splatted during the parse above).
1577                    let mut pred_y = [0u8; 256];
1578                    let mut c_pred = [[0u8; 64]; 2];
1579
1580                    if bmt == 0 {
1581                        // B_Direct_16x16: no coded motion. A direct block contributes mvd 0
1582                        // to a later MB's mvd ctxInc with its ref in-list (|0| summed).
1583                        mb_direct[addr] = true;
1584                        allow8 = self.direct_8x8_inference;
1585                        (mref0, mref1) = ([0i8; 16], [0i8; 16]);
1586                        self.decode_b_direct(mbx, mby, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
1587                    } else if bmt == 22 {
1588                        // B_8x8: 4 sub_mb_types, (ref not coded on 1-ref), then mvd
1589                        // list-major → sub-MB → sub-partition (openh264 order).
1590                        let mut subt = [0u32; 4];
1591                        for s in &mut subt {
1592                            *s = parse_sub_mb_type_b_cabac(&mut cab);
1593                        }
1594                        allow8 = subt.iter().all(|&t| if t == 0 { self.direct_8x8_inference } else { (1..=3).contains(&t) });
1595                        // A direct sub-partition contributes mvd 0 / ref in-list to the
1596                        // ctxInc — both the per-MB export and the within-MB 30-cache that a
1597                        // later (non-direct) sub in this MB reads.
1598                        for i in 0..4usize {
1599                            if subt[i] == 0 {
1600                                let b = i * 4;
1601                                for &zb in &[b, b + 1, b + 2, b + 3] {
1602                                    (mref0[G_SCAN4[zb]], mref1[G_SCAN4[zb]]) = (0, 0);
1603                                    (refc0[CACHE30[zb]], refc1[CACHE30[zb]]) = (0, 0);
1604                                }
1605                            }
1606                        }
1607                        // ref_idx_l0 for all four 8x8s, then ref_idx_l1, then the mvds
1608                        // (spec 7.3.5.2 sub_mb_pred). ONE ref per 8x8 -- never per
1609                        // sub-partition -- and B_Direct_8x8 codes none.
1610                        let mut sref = [[0i8; 2]; 4]; // [sub-MB][list]
1611                        for list in 0..2usize {
1612                            let active = if list == 0 { self.num_ref_active } else { self.num_ref_active1 };
1613                            if active <= 1 {
1614                                continue;
1615                            }
1616                            let rc = if list == 0 { &mut refc0 } else { &mut refc1 };
1617                            for i in 0..4usize {
1618                                let st = subt[i];
1619                                if st == 0 || !b_sub_uses(st, list) {
1620                                    continue;
1621                                }
1622                                let b = i * 4;
1623                                let s = CACHE30[b];
1624                                let c0 = (rc[s - 1] > 0) as usize + 2 * (rc[s - 6] > 0) as usize;
1625                                let r = parse_ref_idx_cabac(&mut cab, c0);
1626                                for &zb in &[b, b + 1, b + 2, b + 3] {
1627                                    rc[CACHE30[zb]] = r;
1628                                }
1629                                sref[i][list] = r;
1630                            }
1631                        }
1632                        for list in 0..2usize {
1633                            let (mmv, mrf, mc, rc) = if list == 0 {
1634                                (&mut mmvd0, &mut mref0, &mut mvdc0, &mut refc0)
1635                            } else {
1636                                (&mut mmvd1, &mut mref1, &mut mvdc1, &mut refc1)
1637                            };
1638                            for i in 0..4usize {
1639                                let st = subt[i];
1640                                if st == 0 || !b_sub_uses(st, list) {
1641                                    continue;
1642                                }
1643                                let b = i * 4;
1644                                for &(sx, sy, sw, sh) in b_sub_parts(st) {
1645                                    let mut zb = [0usize; 4];
1646                                    let mut n = 0;
1647                                    for ly in sy / 4..sy / 4 + sh / 4 {
1648                                        for lx in sx / 4..sx / 4 + sw / 4 {
1649                                            zb[n] = b + ly * 2 + lx;
1650                                            n += 1;
1651                                        }
1652                                    }
1653                                    parse_mvd_partition(&mut cab, zb[0], &zb[..n], mc, rc, mmv, mrf, sref[i][list]);
1654                                }
1655                            }
1656                        }
1657                        // Recon each 8×8: direct sub → decode_b_direct; else per sub-part
1658                        // predict (median) + commit + MC.
1659                        for (p, &st) in subt.iter().enumerate() {
1660                            let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
1661                            if st == 0 {
1662                                self.decode_b_direct(mbx, mby, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred);
1663                                continue;
1664                            }
1665                            for &(sx, sy, sw, sh) in b_sub_parts(st) {
1666                                let (px, py) = (b8x + sx, b8y + sy);
1667                                let mut mv = [(0i32, 0i32); 2];
1668                                for list in 0..2usize {
1669                                    if b_sub_uses(st, list) {
1670                                        let d = if list == 0 { mmvd0 } else { mmvd1 }[(py / 4) * 4 + px / 4];
1671                                        let n = self.mv_neighbors_list((mbx * 4 + px / 4) as isize, (mby * 4 + py / 4) as isize, (sw / 4) as isize, list);
1672                                        let pmv = predict_mv(n[0], n[1], n[2], sref[p][list] as i32);
1673                                        mv[list] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
1674                                    }
1675                                }
1676                                let refi0 = if b_sub_uses(st, 0) { sref[p][0] as i32 } else { -1 };
1677                                let refi1 = if b_sub_uses(st, 1) { sref[p][1] as i32 } else { -1 };
1678                                self.b_set_motion(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1]);
1679                                self.b_mc_or_record(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
1680                            }
1681                        }
1682                    } else {
1683                        let (layout, mvmode, preds) = b_inter_layout(bmt);
1684                        let parts: &[(usize, &[usize])] = match mvmode {
1685                            0 => &[(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])],
1686                            1 => &[(0, &[0, 1, 2, 3, 4, 5, 6, 7]), (8, &[8, 9, 10, 11, 12, 13, 14, 15])],
1687                            _ => &[(0, &[0, 1, 2, 3, 8, 9, 10, 11]), (4, &[4, 5, 6, 7, 12, 13, 14, 15])],
1688                        };
1689                        // ref_idx_l0 for EVERY partition, then ref_idx_l1, then the mvds
1690                        // (spec 7.3.5.1 macroblock_prediction). This was missing entirely
1691                        // -- the B path assumed a single reference -- so any B slice with
1692                        // more than one active reference in either list desynced the
1693                        // arithmetic decoder at the first partition that codes a ref_idx,
1694                        // and the slice ended early at a phantom end_of_slice_flag.
1695                        let mut pref = [[0i8; 2]; 2]; // [partition][list]
1696                        for list in 0..2usize {
1697                            let active = if list == 0 { self.num_ref_active } else { self.num_ref_active1 };
1698                            if active <= 1 {
1699                                continue;
1700                            }
1701                            let rc = if list == 0 { &mut refc0 } else { &mut refc1 };
1702                            for (p, &(pidx, zb)) in parts.iter().enumerate() {
1703                                if !preds[p].uses(list) {
1704                                    continue;
1705                                }
1706                                let s = CACHE30[pidx];
1707                                let c0 = (rc[s - 1] > 0) as usize + 2 * (rc[s - 6] > 0) as usize;
1708                                let r = parse_ref_idx_cabac(&mut cab, c0);
1709                                // Seed the cache so a later partition's ref/mvd ctxInc sees it.
1710                                for &zbi in zb.iter() {
1711                                    rc[CACHE30[zbi]] = r;
1712                                }
1713                                pref[p][list] = r;
1714                            }
1715                        }
1716                        // mvd parse order: list-major, partition-minor (openh264
1717                        // ParseInterBMotionInfoCabac); the ctxInc reads the same-list cache.
1718                        for list in 0..2usize {
1719                            let (mmv, mrf, mc, rc) = if list == 0 {
1720                                (&mut mmvd0, &mut mref0, &mut mvdc0, &mut refc0)
1721                            } else {
1722                                (&mut mmvd1, &mut mref1, &mut mvdc1, &mut refc1)
1723                            };
1724                            for (p, &(pidx, zb)) in parts.iter().enumerate() {
1725                                if preds[p].uses(list) {
1726                                    parse_mvd_partition(&mut cab, pidx, zb, mc, rc, mmv, mrf, pref[p][list]);
1727                                }
1728                            }
1729                        }
1730                        // Per-partition recon: predict each list's MV, commit, MC.
1731                        for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
1732                            let mut mv = [(0i32, 0i32); 2];
1733                            for list in 0..2usize {
1734                                if preds[p].uses(list) {
1735                                    let d = if list == 0 { mmvd0 } else { mmvd1 }[(ry / 4) * 4 + rx / 4];
1736                                    let n = self.mv_neighbors_list((mbx * 4 + rx / 4) as isize, (mby * 4 + ry / 4) as isize, (rw / 4) as isize, list);
1737                                    let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], pref[p][list] as i32);
1738                                    mv[list] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
1739                                }
1740                            }
1741                            let refi0 = if preds[p].uses(0) { pref[p][0] as i32 } else { -1 };
1742                            let refi1 = if preds[p].uses(1) { pref[p][1] as i32 } else { -1 };
1743                            self.b_set_motion(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1]);
1744                            // Proper spec bi-prediction (average of L0+L1). NOTE: the CAVLC
1745                            // decode_b_mb replicates an openh264 bug here for a Bi 16×8/8×16
1746                            // partition; our pixel gate is ffmpeg (spec-correct), so we do NOT.
1747                            self.b_mc_or_record(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
1748                        }
1749                    }
1750                    mb_ref[addr] = mref0;
1751                    mb_mvd[addr] = mmvd0;
1752                    mb_ref1[addr] = mref1;
1753                    mb_mvd1[addr] = mmvd1;
1754                    cat[addr] = 100;
1755
1756                    // Inter cbp + residual (identical to the P path).
1757                    let cbp = parse_cbp_cabac(&mut cab, top.map(|a| mb_cbp[a]), left.map(|a| mb_cbp[a]));
1758                    mb_cbp[addr] = cbp as u8;
1759                    // H-49: an INTER macroblock carries transform_size_8x8_flag AFTER cbp
1760                    // (spec 7.3.5), present only when CodedBlockPatternLuma > 0 and
1761                    // noSubMbPartSizeLessThan8x8Flag. Same context as the intra read.
1762                    let t8 = self.transform_8x8_mode && (cbp & 15) != 0 && allow8 && {
1763                        let a = left.map_or(0, |x| self.mb_t8x8[x] as usize);
1764                        let b = top.map_or(0, |x| self.mb_t8x8[x] as usize);
1765                        cab.decode_decision(399 + a + b) != 0
1766                    };
1767                    self.mb_t8x8[addr] = t8;
1768                    let mut luma8 = [[0i32; 64]; 4]; // per 8x8 block, 8x8 scan order (t8)
1769                    let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
1770                    let mut nzc = [0xffu8; 48];
1771                    if let Some(t) = top {
1772                        let tnz = mb_nzc[t];
1773                        nzc[1..5].copy_from_slice(&tnz[12..16]);
1774                        (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1775                        (nzc[6], nzc[7], nzc[30], nzc[31]) = (tnz[20], tnz[21], tnz[22], tnz[23]);
1776                    }
1777                    if let Some(l) = left {
1778                        let lnz = mb_nzc[l];
1779                        (nzc[8], nzc[16], nzc[24], nzc[32]) = (lnz[3], lnz[7], lnz[11], lnz[15]);
1780                        (nzc[13], nzc[21], nzc[37], nzc[45]) = (lnz[17], lnz[21], lnz[19], lnz[23]);
1781                    }
1782                    let mut cbfdc = 0u16;
1783                    let mut nnzs = [0u8; 24]; // parsed totalCoeff per block
1784                    let mut luma_scan = [[0i32; 16]; 16];
1785                    let mut cdc = [[0i32; 4]; 2];
1786                    let mut cac = [[[0i32; 16]; 4]; 2];
1787                    if cbp == 0 {
1788                        last_delta_qp = 0;
1789                    }
1790                    if cbp != 0 {
1791                        let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1792                        let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1793                        self.step_qp(qpd);
1794                        for id8 in 0..4usize {
1795                            if cbp_luma & (1 << id8) != 0 {
1796                                if t8 {
1797                                    // All four slots carry the 8x8 total: cat 5 has no per-4x4
1798                                    // counts, and the recon helper now reads one slot
1799                                    // per 4x4 cell.
1800                                    let n8 = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, false, ndc, &mut luma8[id8]) as u8;
1801                                    for k in 0..4 {
1802                                        nnzs[id8 * 4 + k] = n8;
1803                                    }
1804                                } else {
1805                                    for id4 in 0..4usize {
1806                                        let iz = id8 * 4 + id4;
1807                                        nnzs[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, false, ndc, &mut luma_scan[iz]) as u8;
1808                                    }
1809                                }
1810                            } else {
1811                                for k in 0..4 {
1812                                    nzc[NZC_CACHE[id8 * 4 + k]] = 0;
1813                                }
1814                            }
1815                        }
1816                        if cbp_chroma >= 1 {
1817                            for i in 0..2usize {
1818                                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, false, ndc, &mut cdc[i]);
1819                            }
1820                        }
1821                        if cbp_chroma == 2 {
1822                            for i in 0..2usize {
1823                                for id4 in 0..4usize {
1824                                    nnzs[16 + i * 4 + id4] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, false, ndc, &mut cac[i][id4]) as u8;
1825                                }
1826                            }
1827                        }
1828                    }
1829                    self.mb_qp[addr] = self.cur_qp;
1830                    cbf_dc[addr] = cbfdc;
1831                    let _sc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecStateCache);
1832                    let mut mn = [0u8; 24];
1833                    for k in 0..4 {
1834                        mn[k] = nzc[9 + k];
1835                        mn[4 + k] = nzc[17 + k];
1836                        mn[8 + k] = nzc[25 + k];
1837                        mn[12 + k] = nzc[33 + k];
1838                    }
1839                    (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
1840                    (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
1841                    // A block whose residual was skipped (cbp bit clear / no chroma AC)
1842                    // has 0 coeffs, not "unavailable" — export 0 so an intra neighbour's
1843                    // CBF ctxInc reads 0 (not the 0xff sentinel → is_intra default).
1844                    for v in mn.iter_mut() {
1845                        if *v == 0xff {
1846                            *v = 0;
1847                        }
1848                    }
1849                    mb_nzc[addr] = mn;
1850                    drop(_sc);
1851                    if let Some(regions) = self.edc_regions.take() {
1852                        self.edc_giveback();
1853                        self.edc_commit_nnz(mbx, mby, t8, &nnzs, cbp_chroma);
1854                        let job = BJob {
1855                            mbx,
1856                            mby,
1857                            t8,
1858                            qp: self.cur_qp,
1859                            cbp_chroma,
1860                            skip: false,
1861                            regions,
1862                            luma_scan,
1863                            luma8,
1864                            cdc,
1865                            cac,
1866                            nnzs,
1867                        };
1868                        self.edc_send_job(EdcJob::B(Box::new(job)));
1869                    } else {
1870                        self.add_inter_residual(mbx, mby, &pred_y, &c_pred, &luma_scan, if t8 { Some(&luma8) } else { None }, &cdc, &cac, cbp_chroma, &nnzs);
1871                    }
1872
1873                    let eos = cab.decode_terminate();
1874                    addr += 1;
1875                    if eos || addr >= total {
1876                        break;
1877                    }
1878                    continue;
1879                }
1880                mb_type = bmt - 23; // 23→0 (I_4x4), 24..=47→1..24 (I_16x16), 48→25 (PCM)
1881                if mb_type == 25 {
1882                    return Err(MbError::Unsupported("CABAC I_PCM (WIP)"));
1883                }
1884            } else {
1885                let li = left.map_or(0, |a| (cat[a] >= 2) as usize);
1886                let ti = top.map_or(0, |a| (cat[a] >= 2) as usize);
1887                mb_type = parse_mb_type_i_cabac(&mut cab, li + ti);
1888                if mb_type == 25 {
1889                    return Err(MbError::Unsupported("CABAC I_PCM (WIP)"));
1890                }
1891            }
1892            // H-48: the CABAC intra path is INLINED in this loop, not routed through
1893            // `decode_intra_mb` (which only the CAVLC readers call) — wiring the scope
1894            // there reported ZERO calls against 480,510 intra-pred calls. All three
1895            // intra entries (I-slice, P-slice mb_type>3, B-slice bmt>=23) converge
1896            // here, so this is the one point that sees every intra MB.
1897            self.edc_intra_sync(); // intra reconstruction reads neighbour PIXELS
1898            let _gi = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbI);
1899            // chroma-pred-mode ctxInc from neighbour chroma modes (1..=3).
1900            let cci = left.map_or(0, |a| (1..=3).contains(&cmode[a]) as usize)
1901                + top.map_or(0, |a| (1..=3).contains(&cmode[a]) as usize);
1902
1903            if mb_type != 0 {
1904                // ---- I_16x16 (mb_type 1..=24): pred mode & cbp DERIVED from mb_type;
1905                // luma DC always coded. Syntax order: intra_chroma_pred_mode, mb_qp_delta,
1906                // luma DC (Hadamard), luma AC (if cbp_luma), chroma DC/AC. Mirrors the CAVLC
1907                // decode_i16, driven by the CABAC residual. ----
1908                let mt = mb_type - 1;
1909                let pred_mode = I16Mode::from_id(mt % 4);
1910                let cbp_chroma = (mt % 12) / 4;
1911                let cbp_luma_15 = mt / 12 == 1;
1912                let chroma_mode = parse_intra_chroma_pred_mode_cabac(&mut cab, cci) as u8;
1913                cmode[addr] = chroma_mode as i32;
1914                cat[addr] = 2;
1915                mb_cbp[addr] = ((cbp_chroma as u8) << 4) | if cbp_luma_15 { 15 } else { 0 };
1916                let w4 = self.mb_w * 4;
1917
1918                let mut nzc = [0xffu8; 48];
1919                if let Some(t) = top {
1920                    let tn = mb_nzc[t];
1921                    nzc[1..5].copy_from_slice(&tn[12..16]);
1922                    (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
1923                    (nzc[6], nzc[7]) = (tn[20], tn[21]);
1924                    (nzc[30], nzc[31]) = (tn[22], tn[23]);
1925                }
1926                if let Some(l) = left {
1927                    let ln = mb_nzc[l];
1928                    (nzc[8], nzc[16], nzc[24], nzc[32]) = (ln[3], ln[7], ln[11], ln[15]);
1929                    (nzc[13], nzc[21], nzc[37], nzc[45]) = (ln[17], ln[21], ln[19], ln[23]);
1930                }
1931
1932                let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
1933                let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
1934                self.step_qp(qpd);
1935                let qp = self.cur_qp;
1936                let mut cbfdc = 0u16;
1937
1938                // Luma DC (iz=0, category I16_LUMA_DC, 16 coeffs) → Hadamard dequant.
1939                let mut dc_scan = [0i32; 16];
1940                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 0, RP_I16_DC, true, ndc, &mut dc_scan);
1941                let recon_dc = self.dequant_luma_dc(&un_scan_4x4_dcac(&dc_scan), qp, 0);
1942
1943                // Luma AC (iz 0..15, category I16_LUMA_AC, 15 coeffs) when cbp_luma set.
1944                let mut q_blocks = [[0i32; 16]; 16];
1945                for (iz, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
1946                    let total = if cbp_luma_15 {
1947                        let mut ac = [0i32; 16];
1948                        let t = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_I16_AC, true, ndc, &mut ac);
1949                        un_scan_4x4_ac_into(&ac, &mut q_blocks[lby * 4 + lbx]);
1950                        t as u8
1951                    } else {
1952                        nzc[NZC_CACHE[iz]] = 0;
1953                        0
1954                    };
1955                    self.nnz_y[(mby * 4 + lby) * w4 + (mbx * 4 + lbx)] = total;
1956                }
1957
1958                let mut cdc = [[0i32; 4]; 2];
1959                let mut cac = [[[0i32; 16]; 4]; 2];
1960                if cbp_chroma >= 1 {
1961                    for i in 0..2usize {
1962                        parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, true, ndc, &mut cdc[i]);
1963                    }
1964                }
1965                if cbp_chroma == 2 {
1966                    for i in 0..2usize {
1967                        for id4 in 0..4usize {
1968                            parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, true, ndc, &mut cac[i][id4]);
1969                        }
1970                    }
1971                }
1972
1973                // Luma recon: 16×16 intra prediction, then per-4×4 (dequant AC + injected DC).
1974                let top_ok = mby > 0 && self.nbr_in_slice(mbx, mby - 1) && self.intra_nbr_ok(mbx * 4, mby * 4 - 1);
1975                let left_ok = mbx > 0 && self.nbr_in_slice(mbx - 1, mby) && self.intra_nbr_ok(mbx * 4 - 1, mby * 4);
1976                let (lx, ly) = (mbx * 16, mby * 16);
1977                let mut t16 = [0u8; 16];
1978                let mut l16 = [0u8; 16];
1979                if top_ok {
1980                    t16.copy_from_slice(self.top_y_row(ly, lx, 16));
1981                }
1982                if left_ok {
1983                    for i in 0..16 {
1984                        l16[i] = self.rec_y[(ly + i) * self.cw + lx - 1];
1985                    }
1986                }
1987                let corner = if top_ok && left_ok { self.top_y_px(ly, lx - 1) } else { 0 };
1988                let pred_l = luma16x16_pred(pred_mode, top_ok, left_ok, &t16, &l16, corner);
1989                for by in 0..4 {
1990                    for bx in 0..4 {
1991                        let mut deq = self.dequant(&q_blocks[by * 4 + bx], qp, 0);
1992                        deq[0] = recon_dc[by * 4 + bx];
1993                        let predb: [i32; 16] = std::array::from_fn(|i| pred_l[(by * 4 + i / 4) * 16 + (bx * 4 + i % 4)] as i32);
1994                        let s = reconstruct_4x4(&deq, &predb);
1995                        store(&mut self.rec_y, self.cw, lx + bx * 4, ly + by * 4, &s);
1996                        // I_16x16 blocks predict as DC for neighbour mode-prediction, and
1997                        // must be marked coded so a later I_4x4 MB's top-right availability
1998                        // (gather_i4 reads coded_y) sees this block as present.
1999                        self.modes_y[(mby * 4 + by) * w4 + (mbx * 4 + bx)] = 2;
2000                        self.coded_y[(mby * 4 + by) * w4 + (mbx * 4 + bx)] = true;
2001                    }
2002                }
2003                self.recon_chroma_cabac(mbx, mby, chroma_mode, &cdc, &cac, cbp_chroma, top_ok, left_ok);
2004
2005                self.mb_qp[addr] = self.cur_qp;
2006                cbf_dc[addr] = cbfdc;
2007                let mut mn = [0u8; 24];
2008                for k in 0..4 {
2009                    mn[k] = nzc[9 + k];
2010                    mn[4 + k] = nzc[17 + k];
2011                    mn[8 + k] = nzc[25 + k];
2012                    mn[12 + k] = nzc[33 + k];
2013                }
2014                (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
2015                (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
2016                for v in mn.iter_mut() {
2017                    if *v == 0xff {
2018                        *v = 0;
2019                    }
2020                }
2021                mb_nzc[addr] = mn;
2022
2023                let eos = cab.decode_terminate();
2024                addr += 1;
2025                if eos || addr >= total {
2026                    break;
2027                }
2028                continue;
2029            }
2030            cat[addr] = 0;
2031            let w4 = self.mb_w * 4;
2032            // H-49: transform_size_8x8_flag. For I_NxN it precedes the intra pred
2033            // modes (spec §7.3.5); ctxIdx = 399 + condTermFlagA + condTermFlagB,
2034            // each 1 when that neighbour MB carries the flag. Omitting this read is
2035            // what desynced every High-profile stream.
2036            let t8 = self.transform_8x8_mode && {
2037                let a = left.map_or(0, |x| self.mb_t8x8[x] as usize);
2038                let b = top.map_or(0, |x| self.mb_t8x8[x] as usize);
2039                cab.decode_decision(399 + a + b) != 0
2040            };
2041            self.mb_t8x8[addr] = t8;
2042            // Brick 2.4 + recon: derive & store each intra mode (prev-flag → the
2043            // neighbour-predicted mode, else rem), exactly as the CAVLC path.
2044            let mut modes = [2u8; 16]; // raster [lby*4+lbx]
2045            let mut modes8 = [2u8; 4]; // one per 8×8 when t8
2046            if t8 {
2047                // One mode per 8×8, broadcast to its four 4×4 cells so neighbour
2048                // mode prediction keeps working unchanged.
2049                for b8 in 0..4usize {
2050                    let (b8x, b8y) = (b8 % 2, b8 / 2);
2051                    let (bx, by) = (mbx * 4 + b8x * 2, mby * 4 + b8y * 2);
2052                    let predicted = self.predict_i4_mode(bx, by);
2053                    let rr = parse_intra4x4_pred_mode_cabac(&mut cab);
2054                    let actual = if rr < 0 {
2055                        predicted
2056                    } else {
2057                        let rem = rr as u8;
2058                        if rem < predicted { rem } else { rem + 1 }
2059                    };
2060                    modes8[b8] = actual;
2061                    for dy in 0..2 {
2062                        for dx in 0..2 {
2063                            self.modes_y[(by + dy) * w4 + (bx + dx)] = actual;
2064                            modes[(b8y * 2 + dy) * 4 + (b8x * 2 + dx)] = actual;
2065                        }
2066                    }
2067                }
2068            } else {
2069                for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2070                    let (bx, by) = (mbx * 4 + lbx, mby * 4 + lby);
2071                    let predicted = self.predict_i4_mode(bx, by);
2072                    let rr = parse_intra4x4_pred_mode_cabac(&mut cab);
2073                    let actual = if rr < 0 {
2074                        predicted
2075                    } else {
2076                        let rem = rr as u8;
2077                        if rem < predicted { rem } else { rem + 1 }
2078                    };
2079                    self.modes_y[by * w4 + bx] = actual;
2080                    modes[lby * 4 + lbx] = actual;
2081                }
2082            }
2083            let chroma_mode = parse_intra_chroma_pred_mode_cabac(&mut cab, cci) as u8;
2084            cmode[addr] = chroma_mode as i32;
2085            let cbp = parse_cbp_cabac(&mut cab, top.map(|a| mb_cbp[a]), left.map(|a| mb_cbp[a]));
2086            mb_cbp[addr] = cbp as u8;
2087            let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
2088
2089            // Build the padded nzc cache from neighbours (openh264 WelsFillCacheNonZeroCount).
2090            let mut nzc = [0xffu8; 48];
2091            if let Some(t) = top {
2092                let tn = mb_nzc[t];
2093                nzc[1..5].copy_from_slice(&tn[12..16]);
2094                (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
2095                (nzc[6], nzc[7]) = (tn[20], tn[21]);
2096                (nzc[30], nzc[31]) = (tn[22], tn[23]);
2097            }
2098            if let Some(l) = left {
2099                let ln = mb_nzc[l];
2100                (nzc[8], nzc[16], nzc[24], nzc[32]) = (ln[3], ln[7], ln[11], ln[15]);
2101                (nzc[13], nzc[21], nzc[37], nzc[45]) = (ln[17], ln[21], ln[19], ln[23]);
2102            }
2103
2104            // Bricks 2.6 + 2.7: mb_qp_delta + residual (I_4x4 luma 4×4 + chroma DC/AC),
2105            // storing scan-order coefficients for recon.
2106            let mut cbfdc = 0u16;
2107            let mut luma_scan = [[0i32; 16]; 16]; // per z-order 4×4 block
2108            let mut luma8 = [[0i32; 64]; 4]; // per 8×8 block, 8×8 scan order (t8)
2109            let mut cdc = [[0i32; 4]; 2]; // chroma DC per plane
2110            let mut cac = [[[0i32; 16]; 4]; 2]; // chroma AC per plane, per 4×4 block
2111            if cbp == 0 {
2112                last_delta_qp = 0;
2113            }
2114            if cbp != 0 {
2115                let ndc = (top.map(|a| cbf_dc[a]), left.map(|a| cbf_dc[a]));
2116                let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
2117                self.step_qp(qpd);
2118                for id8 in 0..4usize {
2119                    if cbp_luma & (1 << id8) != 0 {
2120                        if t8 {
2121                            // ctxBlockCat 5: ONE 64-coefficient block per 8×8, and no
2122                            // coded_block_flag — presence comes from cbp_luma alone.
2123                            let n = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, true, ndc, &mut luma8[id8]);
2124                            let (b8x, b8y) = (id8 % 2, id8 / 2);
2125                            for sy in 0..2 {
2126                                for sx in 0..2 {
2127                                    self.nnz_y[(mby * 4 + b8y * 2 + sy) * w4 + (mbx * 4 + b8x * 2 + sx)] = n as u8;
2128                                }
2129                            }
2130                        } else {
2131                            for id4 in 0..4usize {
2132                                let iz = id8 * 4 + id4;
2133                                parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, true, ndc, &mut luma_scan[iz]);
2134                            }
2135                        }
2136                    } else {
2137                        for k in 0..4 {
2138                            nzc[NZC_CACHE[id8 * 4 + k]] = 0;
2139                        }
2140                        if t8 {
2141                            let (b8x, b8y) = (id8 % 2, id8 / 2);
2142                            for sy in 0..2 {
2143                                for sx in 0..2 {
2144                                    self.nnz_y[(mby * 4 + b8y * 2 + sy) * w4 + (mbx * 4 + b8x * 2 + sx)] = 0;
2145                                }
2146                            }
2147                        }
2148                    }
2149                }
2150                if cbp_chroma >= 1 {
2151                    for i in 0..2usize {
2152                        parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, true, ndc, &mut cdc[i]);
2153                    }
2154                }
2155                if cbp_chroma == 2 {
2156                    for i in 0..2usize {
2157                        for id4 in 0..4usize {
2158                            parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, true, ndc, &mut cac[i][id4]);
2159                        }
2160                    }
2161                }
2162            }
2163            self.mb_qp[addr] = self.cur_qp;
2164            cbf_dc[addr] = cbfdc;
2165            // Extract the MB's nzc (raster luma + chroma) for future neighbours.
2166            let mut mn = [0u8; 24];
2167            for k in 0..4 {
2168                mn[k] = nzc[9 + k];
2169                mn[4 + k] = nzc[17 + k];
2170                mn[8 + k] = nzc[25 + k];
2171                mn[12 + k] = nzc[33 + k];
2172            }
2173            (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
2174            (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
2175            for v in mn.iter_mut() {
2176                if *v == 0xff {
2177                    *v = 0;
2178                }
2179            }
2180            mb_nzc[addr] = mn;
2181
2182            // ---- Brick 4.3a: recon (I_4x4 luma + chroma) via the CAVLC-proven primitives.
2183            let qp = self.cur_qp;
2184            let top_ok = mby > 0 && self.nbr_in_slice(mbx, mby - 1) && self.intra_nbr_ok(mbx * 4, mby * 4 - 1);
2185            let left_ok = mbx > 0 && self.nbr_in_slice(mbx - 1, mby) && self.intra_nbr_ok(mbx * 4 - 1, mby * 4);
2186            if t8 {
2187                // I_8x8 recon, reusing the CAVLC-proven primitives verbatim
2188                // (un_scan_8x8 / inv_quant8 / gather_i8 / intra8x8_pred /
2189                // add_residual_8x8). Only the ENTROPY half differed.
2190                for b8 in 0..4usize {
2191                    let (b8x, b8y) = (b8 % 2, b8 / 2);
2192                    let (bx, by) = (mbx * 4 + b8x * 2, mby * 4 + b8y * 2);
2193                    let (px, py) = (bx * 4, by * 4);
2194                    let res8 = if cbp_luma & (1 << b8) != 0 {
2195                        let raster = un_scan_8x8(&luma8[b8]);
2196                        self.inv_quant8(&raster, qp, 0)
2197                    } else {
2198                        [0i32; 64]
2199                    };
2200                    let avail_top = b8y > 0 || top_ok;
2201                    let avail_left = b8x > 0 || left_ok;
2202                    let (t, l, corner, avail_corner) =
2203                        self.gather_i8(px, py, avail_top, avail_left, bx, by);
2204                    let pred =
2205                        intra8x8_pred(modes8[b8], avail_top, avail_left, avail_corner, &t, &l, corner);
2206                    let mut predb = [0i32; 64];
2207                    for i in 0..64 {
2208                        predb[i] = pred[i] as i32;
2209                    }
2210                    let recon = add_residual_8x8(&res8, &predb);
2211                    for dy in 0..8 {
2212                        for dx in 0..8 {
2213                            self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
2214                        }
2215                    }
2216                    for sy in 0..2 {
2217                        for sx in 0..2 {
2218                            self.coded_y[(by + sy) * w4 + (bx + sx)] = true;
2219                        }
2220                    }
2221                }
2222            }
2223            for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
2224                if t8 {
2225                    break;
2226                }
2227                let (bx, by) = (mbx * 4 + lbx, mby * 4 + lby);
2228                let (px, py) = (bx * 4, by * 4);
2229                let at = lby > 0 || top_ok;
2230                let al = lbx > 0 || left_ok;
2231                let qb = un_scan_4x4_dcac(&luma_scan[blk]);
2232                self.nnz_y[by * w4 + bx] = luma_scan[blk].iter().filter(|&&v| v != 0).count() as u8;
2233                let (t, l, corner) = self.gather_i4(px, py, at, al, bx, by);
2234                let pred = intra4x4_pred(modes[lby * 4 + lbx], at, al, &t, &l, corner);
2235                let predb = std::array::from_fn(|i| pred[i] as i32);
2236                let s = reconstruct_4x4(&self.dequant(&qb, qp, 0), &predb);
2237                store(&mut self.rec_y, self.cw, px, py, &s);
2238                self.coded_y[by * w4 + bx] = true;
2239            }
2240            self.recon_chroma_cabac(mbx, mby, chroma_mode, &cdc, &cac, cbp_chroma, top_ok, left_ok);
2241
2242            // Brick 2.1: end_of_slice_flag.
2243            let eos = cab.decode_terminate();
2244            addr += 1;
2245            if eos || addr >= total {
2246                break;
2247            }
2248        }
2249        if trace {
2250            eprintln!("# CABAC decoded {} MBs (of {total})", addr - first_mb);
2251        }
2252        self.edc_flush(); // slice end: no job crosses a slice boundary
2253        Ok(addr)
2254    }
2255
2256    /// CABAC chroma recon (mirrors `decode_chroma`'s reconstruction, driven by the
2257    /// CABAC-parsed DC/AC coefficients). `cdc[c]` = 2×2 DC (scan order); `cac[c][blk]`
2258    /// = 15 AC per 4×4 block (scan order).
2259    #[allow(clippy::too_many_arguments)]
2260    /// Add a CABAC-parsed inter residual to an already-built motion-comp prediction
2261    /// (`pred_y`/`c_pred`), writing the reconstruction. Shared by the P and B inter
2262    /// paths — same `reconstruct_4x4` as intra, MC output as the prediction, inter
2263    /// scaling lists (luma 3 / chroma 4+c). `luma_scan[z]`/`cdc`/`cac` are the
2264    /// scan-order coefficients; uncoded blocks are zero so recon == prediction.
2265    #[allow(clippy::too_many_arguments)]
2266    fn add_inter_residual(
2267        &mut self,
2268        mb_x: usize,
2269        mb_y: usize,
2270        pred_y: &[u8; 256],
2271        c_pred: &[[u8; 64]; 2],
2272        luma_scan: &[[i32; 16]; 16],
2273        // `Some` when the macroblock carries transform_size_8x8_flag: four 8x8
2274        // blocks in 8x8 scan order, replacing the sixteen 4x4 luma blocks.
2275        luma8: Option<&[[i32; 64]; 4]>,
2276        cdc: &[[i32; 4]; 2],
2277        cac: &[[[i32; 16]; 4]; 2],
2278        cbp_chroma: u32,
2279        // Parsed totalCoeff per block, indexed exactly as the parse's `iz`:
2280        // [0..16] luma 4x4 z-order (for t8, the 8x8 count sits at `id8*4`),
2281        // [16..24] chroma AC as `16 + c*4 + id4`. The parser already counted
2282        // every significant coefficient; re-deriving the counts here scanned
2283        // 16-64 array elements per block (~400 loads/MB) for information the
2284        // caller was holding — the diagnosis's stage-boundary re-derivation tax.
2285        nnzs: &[u8; 24],
2286    ) {
2287        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecResidAdd);
2288        let qp = self.cur_qp;
2289        let qpc = self.chroma_qp_for(qp);
2290        let (w4r, w2r) = (self.mb_w * 4, self.mb_w * 2);
2291        if let Some(l8) = luma8 {
2292            // INTER 8x8 luma: same primitives the I_8x8 and CAVLC paths use.
2293            for b8 in 0..4usize {
2294                let (b8x, b8y) = (b8 % 2, b8 / 2);
2295                // PER-CELL, not one aggregate broadcast over all four cells. CAVLC
2296                // codes an 8x8 block as four 4x4 sub-blocks and its nC predictor
2297                // reads these per-4x4 counts from `nnz_y`, so the broadcast
2298                // corrupted the NEXT macroblock's nC and desynced the parse -- which
2299                // is why CAVLC 8x8 streams ffmpeg accepts would not decode here. The
2300                // worker copy of this function never wrote `nnz_y` at all, so the
2301                // threaded path was unaffected and hid the defect. CABAC has no
2302                // per-4x4 counts, so its callers put the 8x8 total in all four slots.
2303                let nnz: u32 = (0..4).map(|k| nnzs[b8 * 4 + k] as u32).sum();
2304                for sy in 0..2 {
2305                    for sx in 0..2 {
2306                        self.nnz_y[(mb_y * 4 + b8y * 2 + sy) * w4r + (mb_x * 4 + b8x * 2 + sx)] =
2307                            nnzs[b8 * 4 + sy * 2 + sx];
2308                    }
2309                }
2310                let res8 = if nnz == 0 {
2311                    [0i32; 64]
2312                } else {
2313                    let raster = un_scan_8x8(&l8[b8]);
2314                    // list 1 = INTER 8x8 luma scaling list (0 is the intra one).
2315                    self.inv_quant8(&raster, qp, 1)
2316                };
2317                // The 4x4 inter path marks coded_y per block; the 8x8 branch must too,
2318                // or a later intra macroblock's neighbour availability is wrong.
2319                for sy in 0..2 {
2320                    for sx in 0..2 {
2321                        self.coded_y[(mb_y * 4 + b8y * 2 + sy) * w4r + (mb_x * 4 + b8x * 2 + sx)] = true;
2322                    }
2323                }
2324                let predb: [i32; 64] =
2325                    std::array::from_fn(|i| pred_y[(b8y * 8 + i / 8) * 16 + (b8x * 8 + i % 8)] as i32);
2326                let recon = add_residual_8x8(&res8, &predb);
2327                let (px, py) = (mb_x * 16 + b8x * 8, mb_y * 16 + b8y * 8);
2328                for dy in 0..8 {
2329                    for dx in 0..8 {
2330                        self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
2331                    }
2332                }
2333            }
2334        }
2335        for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
2336            if luma8.is_some() {
2337                break;
2338            }
2339            let nnz = nnzs[blk];
2340            self.nnz_y[(mb_y * 4 + lby) * w4r + (mb_x * 4 + lbx)] = nnz;
2341            let cw = self.cw;
2342            let p_off = (lby * 4) * 16 + lbx * 4;
2343            let r_off = (mb_y * 4 + lby) * 4 * cw + (mb_x * 4 + lbx) * 4;
2344            if nnz == 0 {
2345                // Zero residual → recon == prediction EXACTLY (the integer IDCT is
2346                // linear so zeros map to zeros, and pred is already 0..=255) — copy
2347                // the pred rows straight into the plane. On real (sparse-cbp)
2348                // streams this is MOST of the 4×4 blocks.
2349                for r in 0..4 {
2350                    self.rec_y[r_off + r * cw..r_off + r * cw + 4]
2351                        .copy_from_slice(&pred_y[p_off + r * 16..p_off + r * 16 + 4]);
2352                }
2353                continue;
2354            }
2355            // DC-ONLY: the sole significant coefficient is scan position 0 (the
2356            // zig-zag starts at DC, and un_scan keeps it at raster 0), so the
2357            // whole dequant + IDCT collapses to one multiply and a flat add.
2358            if nnz == 1 && luma_scan[blk][0] != 0 {
2359                let f = self.dequant_dc4(luma_scan[blk][0], qp, 3);
2360                reconstruct_4x4_dc_into((f + 32) >> 6, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
2361            } else {
2362                // Fused un-scan + dequant over ONLY the significant coefficients,
2363                // then IDCT + add + clip straight into the plane — no `qb`, no
2364                // `deq`-from-dense, no `predb` gather, no `s`, no `store` call.
2365                //
2366                // HYBRID: the scatter walks scan positions with a data-dependent
2367                // branch per slot, which beats the branchless dense 16-multiply
2368                // loop only while the block is SPARSE. The DC/zero fast paths
2369                // already removed the sparsest blocks, so the population here
2370                // skews denser — above ~6 coefficients the dense loop wins.
2371                let deq = if nnz <= 6 {
2372                    dequant_scatter_4x4(&luma_scan[blk], nnz, 0, qp, self.scaling.as_ref().map(|sc| &sc[3]))
2373                } else {
2374                    self.dequant(&un_scan_4x4_dcac(&luma_scan[blk]), qp, 3)
2375                };
2376                reconstruct_4x4_into(&deq, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
2377            }
2378        }
2379        let mut c_dc = [[0i32; 4]; 2];
2380        if cbp_chroma != 0 {
2381            for c in 0..2 {
2382                c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 4 + c);
2383            }
2384        }
2385        let ccw = self.ccw;
2386        for c in 0..2 {
2387            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2388                let mut ac_nz = false;
2389                if cbp_chroma == 2 {
2390                    let n = nnzs[16 + c * 4 + by * 2 + bx];
2391                    self.nnz_c[c][(mb_y * 2 + by) * w2r + (mb_x * 2 + bx)] = n;
2392                    ac_nz = n != 0;
2393                }
2394                let dc = c_dc[c][by * 2 + bx];
2395                let p_off = (by * 4) * 8 + bx * 4;
2396                let r_off = (mb_y * 2 + by) * 4 * ccw + (mb_x * 2 + bx) * 4;
2397                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2398                if dc == 0 && !ac_nz {
2399                    // Zero residual (no AC, zero DC) → recon == prediction exactly.
2400                    for r in 0..4 {
2401                        plane[r_off + r * ccw..r_off + r * ccw + 4]
2402                            .copy_from_slice(&c_pred[c][p_off + r * 8..p_off + r * 8 + 4]);
2403                    }
2404                    continue;
2405                }
2406                // DC-ONLY (no coded AC — covers every cbp_chroma==1 block and the
2407                // AC-empty blocks of cbp_chroma==2): the chroma DC arrives ALREADY
2408                // dequantized, so the residual is `(dc + 32) >> 6` flat.
2409                if !ac_nz {
2410                    reconstruct_4x4_dc_into((dc + 32) >> 6, &c_pred[c], p_off, 8, plane, r_off, ccw);
2411                    continue;
2412                }
2413                // AC-only scan: index i is overall scan position i+1 (ac_shift=1).
2414                // Same sparse/dense hybrid as luma.
2415                let n = nnzs[16 + c * 4 + by * 2 + bx];
2416                let mut deq = if n <= 6 {
2417                    dequant_scatter_4x4(&cac[c][by * 2 + bx], n, 1, qpc, self.scaling.as_ref().map(|sc| &sc[4 + c]))
2418                } else {
2419                    let mut ac = [0i32; 16];
2420                    un_scan_4x4_ac_into(&cac[c][by * 2 + bx], &mut ac);
2421                    // Free-fn dequant: `self.dequant` borrows all of `self`, which
2422                    // conflicts with the live `plane` (&mut self.rec_u/v) borrow.
2423                    match &self.scaling {
2424                        Some(sc) => dequantize_weighted(&ac, qpc, &sc[4 + c]),
2425                        None => dequantize(&ac, qpc),
2426                    }
2427                };
2428                deq[0] = dc;
2429                reconstruct_4x4_into(&deq, &c_pred[c], p_off, 8, plane, r_off, ccw);
2430            }
2431        }
2432    }
2433
2434    fn recon_chroma_cabac(
2435        &mut self,
2436        mb_x: usize,
2437        mb_y: usize,
2438        chroma_mode: u8,
2439        cdc: &[[i32; 4]; 2],
2440        cac: &[[[i32; 16]; 4]; 2],
2441        cbp_chroma: u32,
2442        avail_top: bool,
2443        avail_left: bool,
2444    ) {
2445        let qpc = self.chroma_qp_for(self.cur_qp);
2446        let (cx, cy) = (mb_x * 8, mb_y * 8);
2447        let mut c_dc = [[0i32; 4]; 2];
2448        if cbp_chroma != 0 {
2449            for c in 0..2 {
2450                c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 1 + c);
2451            }
2452        }
2453        let w2 = self.mb_w * 2;
2454        for c in 0..2 {
2455            let mut ctop = [0u8; 8];
2456            let mut cleft = [0u8; 8];
2457            let mut ccorner = 0u8;
2458            {
2459                let rec_c = if c == 0 { &self.rec_u } else { &self.rec_v };
2460                if avail_top {
2461                    ctop.copy_from_slice(self.top_c_row(c, cy, cx, 8));
2462                }
2463                if avail_left {
2464                    for i in 0..8 {
2465                        cleft[i] = rec_c[(cy + i) * self.ccw + cx - 1];
2466                    }
2467                }
2468                if avail_top && avail_left {
2469                    ccorner = self.top_c_px(c, cy, cx - 1);
2470                }
2471            }
2472            let pred8 = chroma8x8_pred(chroma_mode, avail_top, avail_left, &ctop, &cleft, ccorner);
2473            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2474                let mut ac = [0i32; 16];
2475                if cbp_chroma == 2 {
2476                    un_scan_4x4_ac_into(&cac[c][by * 2 + bx], &mut ac);
2477                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] =
2478                        cac[c][by * 2 + bx].iter().filter(|&&v| v != 0).count() as u8;
2479                }
2480                let mut deq = self.dequant(&ac, qpc, 1 + c);
2481                deq[0] = c_dc[c][by * 2 + bx];
2482                let predb: [i32; 16] =
2483                    std::array::from_fn(|i| pred8[(by * 4 + i / 4) * 8 + (bx * 4 + i % 4)] as i32);
2484                let s = reconstruct_4x4(&deq, &predb);
2485                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2486                store(plane, self.ccw, cx + bx * 4, cy + by * 4, &s);
2487            }
2488        }
2489    }
2490
2491    /// D14 — the CAVLC E-seam (P3 item 5). Mirrors `decode_slice_data_cabac`:
2492    /// overlap parse (this thread) with pixel reconstruction (a scoped worker
2493    /// owning the planes). Now possible because the CAVLC inter recon was
2494    /// converged onto `add_inter_residual`, so both entropy coders emit the SAME
2495    /// `PInterJob` and share one worker recon.
2496    pub fn decode_slice_data(
2497        &mut self,
2498        r: &mut BitReader,
2499        is_p: bool,
2500        first_mb: usize,
2501    ) -> Result<usize, MbError> {
2502        let eligible = edc_on() && rowdb_on() && (is_p || self.is_b);
2503        let threaded = eligible
2504            && edc_mt().unwrap_or_else(|| edc_dispatch(self.mb_w, self.mb_h, self.bits_per_mb, false));
2505        edcstat::bump(&edcstat::DISPATCH_ON, threaded as u64);
2506        edcstat::bump(&edcstat::DISPATCH_SEEN, eligible as u64);
2507        if !threaded {
2508            return self.decode_slice_cavlc_inner(r, is_p, first_mb);
2509        }
2510        let ctx = self.edc_take_ctx();
2511        let (tx, rx) = std::sync::mpsc::sync_channel::<EdcMsg>(edc_bound());
2512        let (ctx_tx, ctx_rx) = std::sync::mpsc::channel::<PixelCtx>();
2513        let (back_tx, back_rx) = std::sync::mpsc::channel::<PixelCtx>();
2514        let (res, ctx, panicked) = std::thread::scope(|sc| {
2515            let h = sc.spawn(move || edc_worker(ctx, rx, ctx_tx, back_rx));
2516            self.edc_tx = Some(tx);
2517            self.edc_ctx_rx = Some(ctx_rx);
2518            self.edc_back_tx = Some(back_tx);
2519            // UNWIND SAFETY (same trap the CABAC wrapper documents): the sender
2520            // lives in `self`, which outlives an unwind, so a panic in the parse
2521            // loop would leave the channel open, the worker alive and the scope
2522            // join blocking forever — turning a diagnosable panic into a silent
2523            // deadlock. Catch, clean up, join, restore, THEN resume.
2524            let r2 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2525                self.decode_slice_cavlc_inner(r, is_p, first_mb)
2526            }));
2527            self.edc_flush_batch();
2528            self.edc_giveback();
2529            self.edc_tx = None;
2530            self.edc_ctx_rx = None;
2531            self.edc_back_tx = None;
2532            match (r2, h.join()) {
2533                (Ok(res), Ok(ctx)) => (res, Some(ctx), None),
2534                (Err(pn), Ok(ctx)) => (Err(MbError::Truncated), Some(ctx), Some(pn)),
2535                (Ok(_), Err(pn)) | (Err(_), Err(pn)) => (Err(MbError::Truncated), None, Some(pn)),
2536            }
2537        });
2538        if let Some(ctx) = ctx {
2539            self.edc_restore_ctx(ctx);
2540        }
2541        if let Some(pn) = panicked {
2542            std::panic::resume_unwind(pn);
2543        }
2544        res
2545    }
2546
2547    fn decode_slice_cavlc_inner(
2548        &mut self,
2549        r: &mut BitReader,
2550        is_p: bool,
2551        first_mb: usize,
2552    ) -> Result<usize, MbError> {
2553        let total = self.mb_w * self.mb_h;
2554        self.slice_first_mb = first_mb;
2555        self.edc_active = edc_on();
2556        let mut addr = first_mb;
2557        while addr < total {
2558            self.row_hook(addr);
2559            if is_p || self.is_b {
2560                let skip_run = {
2561                    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
2562                    r.read_ue()?
2563                } as usize;
2564                for _ in 0..skip_run {
2565                    if addr >= total {
2566                        break;
2567                    }
2568                    if self.is_b {
2569                        self.decode_b_skip(addr % self.mb_w, addr / self.mb_w)?;
2570                    } else {
2571                        self.decode_p_skip(addr % self.mb_w, addr / self.mb_w)?;
2572                    }
2573                    self.mb_qp[addr] = self.cur_qp; // skip inherits QPy
2574                    addr += 1;
2575                }
2576                if addr >= total {
2577                    break;
2578                }
2579                // A trailing skip run with no following macroblock ends the slice.
2580                if skip_run > 0 && !r.more_rbsp_data() {
2581                    break;
2582                }
2583            }
2584            if self.is_b {
2585                // ORDER: B reconstructs inline (not seam-ready).
2586                self.edc_intra_sync();
2587                self.decode_b_mb(r, addr % self.mb_w, addr / self.mb_w)?;
2588            } else {
2589                self.decode_mb(r, addr % self.mb_w, addr / self.mb_w, is_p)?;
2590            }
2591            self.mb_qp[addr] = self.cur_qp;
2592            addr += 1;
2593            // CAVLC slice end: no more data after this macroblock.
2594            if !r.more_rbsp_data() {
2595                break;
2596            }
2597        }
2598        self.edc_flush(); // slice end: no job crosses a slice boundary
2599        Ok(addr)
2600    }
2601
2602    fn decode_mb(
2603        &mut self,
2604        r: &mut BitReader,
2605        mb_x: usize,
2606        mb_y: usize,
2607        is_p: bool,
2608    ) -> Result<(), MbError> {
2609        let mut mb_type = {
2610            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
2611            r.read_ue()?
2612        };
2613        if is_p {
2614            // In P-slices, mb_type 0/1/2 are inter (16×16, 16×8, 8×16),
2615            // 3 = P_8x8, 4 = P_8x8ref0 (ref_idx forced 0), 5+ intra.
2616            if mb_type <= 2 {
2617                return self.decode_inter(r, mb_x, mb_y, mb_type as u8);
2618            }
2619            if mb_type == 3 || mb_type == 4 {
2620                return self.decode_p8x8(r, mb_x, mb_y, mb_type == 4);
2621            }
2622            mb_type -= 5;
2623        }
2624        // ORDER: intra reconstruction reads neighbour PIXELS, so the worker
2625        // must have applied every deferred job before this point.
2626        self.edc_intra_sync();
2627        self.decode_intra_mb(r, mb_x, mb_y, mb_type)
2628    }
2629
2630    /// Decodes an intra macroblock given its intra `mb_type` (0 = I_4x4,
2631    /// 1..=24 = I_16x16, 25 = I_PCM) — shared by I-, P- and B-slice paths.
2632    fn decode_intra_mb(
2633        &mut self,
2634        r: &mut BitReader,
2635        mb_x: usize,
2636        mb_y: usize,
2637        mb_type: u32,
2638    ) -> Result<(), MbError> {
2639        // H-48: this scope was DECLARED and never wired, which is precisely why the
2640        // stage table left 19.8% unaccounted — 66,120 of 475,200 macroblocks on the
2641        // reference stream are I-type and had no scope at all.
2642        let _gi = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbI);
2643        if mb_type == 0 {
2644            // I_NxN: transform_size_8x8_flag (when enabled) selects I_8x8 vs I_4x4.
2645            if self.transform_8x8_mode && r.read_bit()? {
2646                self.decode_i8x8(r, mb_x, mb_y)?;
2647            } else {
2648                self.decode_i4x4(r, mb_x, mb_y)?;
2649            }
2650        } else if (1..=24).contains(&mb_type) {
2651            self.decode_i16(r, mb_x, mb_y, mb_type - 1)?;
2652        } else if mb_type == 25 {
2653                        // ORDER: I_PCM writes pixels directly.
2654            self.edc_intra_sync();
2655            self.decode_ipcm(r, mb_x, mb_y)?;
2656        } else {
2657            return Err(MbError::Unsupported("only I_4x4 / I_16x16 / I_PCM macroblocks"));
2658        }
2659        // Mark all luma blocks coded for the next macroblock's top-right.
2660        let w4 = self.mb_w * 4;
2661        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2662            self.coded_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = true;
2663        }
2664        Ok(())
2665    }
2666
2667    /// Reconstructs an inter macroblock (`mode` 0 = P_L0_16x16, 1 = P_16x8,
2668    /// 2 = P_8x16): parse the per-partition motion vectors and residual,
2669    /// motion-compensate each partition, and add the residual.
2670    fn decode_inter(
2671        &mut self,
2672        r: &mut BitReader,
2673        mb_x: usize,
2674        mb_y: usize,
2675        mode: u8,
2676    ) -> Result<(), MbError> {
2677        if self.refs.is_empty() {
2678            return Err(MbError::Unsupported("inter without reference"));
2679        }
2680        // DEBLOCK CLASS: mode 0 is P_L0_16x16 — ONE partition, so all 16 blocks
2681        // share a reference and motion vector and no internal edge can reach
2682        // strength 1. Internal strengths then follow from coefficients alone, i.e.
2683        // 16 nnz bytes instead of a 24-block gather across 5-7 grids. Modes 1/2
2684        // (P_16x8 / P_8x16) have two partitions with independent motion and stay
2685        // UNSET (blind path).
2686        if mode == 0 {
2687            self.mb_kind[mb_y * self.mb_w + mb_x] =
2688                rusty_h264_common::deblock::MB_KIND_INTER_UNIFORM;
2689        }
2690        // QP (qp/qpc) is bound after mb_qp_delta is read below.
2691        let w4 = self.mb_w * 4;
2692        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
2693        let num_refs = self.refs.len();
2694        let layout = inter_partitions(mode);
2695
2696        // mb_pred order (spec 7.3.5.1): all ref_idx_l0 first (only when more than
2697        // one reference is active), then all mvd_l0.
2698        let nparts = layout.len();
2699        let mut ref_idxs = [0i32; 4];
2700        if self.num_ref_active > 1 {
2701            for ri in ref_idxs[..nparts].iter_mut() {
2702                *ri = read_ref_idx(r, self.num_ref_active)?;
2703                if *ri as usize >= num_refs {
2704                    return Err(MbError::Truncated); // references a non-existent picture
2705                }
2706            }
2707        }
2708
2709        // Phase 1: per partition, ref-aware MV prediction + mvd, committing the
2710        // motion grid so a later partition predicts from an earlier one.
2711        let mut part_mv = [(0i32, (0i32, 0i32)); 4];
2712        {
2713            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::MvGrid);
2714            for (part, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
2715                let refi = ref_idxs[part];
2716                let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
2717                let [a, b, c] = self.mv_neighbors_block(pbx, pby, (rw / 4) as isize);
2718                let pmv = predict_partition_mv(mode, part, a, b, c, refi);
2719                let mvd_x = r.read_se()?;
2720                let mvd_y = r.read_se()?;
2721                let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
2722                part_mv[part] = (refi, mv);
2723                for by in ry / 4..ry / 4 + rh / 4 {
2724                    for bx in rx / 4..rx / 4 + rw / 4 {
2725                        let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
2726                        self.mv_y[idx] = mv;
2727                        self.inter_y[idx] = true;
2728                        self.ref_idx_y[idx] = refi;
2729                        self.coded_y[idx] = true;
2730                    }
2731                }
2732            }
2733        }
2734
2735        // Phase 2: motion-compensate each partition from its reference.
2736        let mut pred_y = [0u8; 256];
2737        let mut c_pred = [[0u8; 64]; 2];
2738        // D8-CAVLC: the double-stage ablation, extended to the CAVLC loop. The
2739        // CABAC measurement could not run here at all (`edc_active` is false on
2740        // this path, so nothing replays through `edc_flush` and `doubled` read
2741        // 0) — yet CAVLC is exactly the population P3 item 5 targets, and its
2742        // cheaper parse should make the PIXEL share larger. Doubling the whole
2743        // partition loop is idempotent: pass 2 overwrites `pred_y` with fresh MC
2744        // BEFORE `weight_partition` runs, so weighting cannot apply twice.
2745        // This doubles MC only (not the residual add inside `inter_finish`), so
2746        // it is a LOWER BOUND on the CAVLC pixel share.
2747        // D14 (CAVLC E-seam): when the seam is live the WORKER motion-compensates
2748        // from the committed MV grids, so skip MC here rather than computing a
2749        // prediction that would be discarded.
2750        let defer = self.edc_tx.is_some() || self.edc_active;
2751        let mc_passes = if defer { 0 } else if double_recon() { 2 } else { 1 };
2752        for _pass in 0..mc_passes {
2753        if _pass > 0 {
2754            edcstat::bump(&edcstat::DOUBLED, 1);
2755        }
2756        for (part, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
2757            let (refi, mv) = part_mv[part];
2758            let reference = &self.refs[refi as usize];
2759            let mut tmp = [0u8; 256];
2760            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);
2761            {
2762                let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
2763                restride(&mut pred_y, 16, rx, ry, &tmp, rw, rh);
2764            }
2765            let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
2766            for cc in 0..2 {
2767                let rc = if cc == 0 { &reference.pu } else { &reference.pv };
2768                let mut tc = [0u8; 64];
2769                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);
2770                {
2771                    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
2772                    restride(&mut c_pred[cc], 8, crx, cry, &tc, crw, crh);
2773                }
2774            }
2775            self.weight_partition(&mut pred_y, &mut c_pred, 0, refi as usize, rx, ry, rw, rh);
2776        }
2777        }
2778
2779        // 16×16/16×8/8×16 partitions are all ≥ 8×8, so the 8×8 transform is allowed.
2780        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true, defer)
2781    }
2782
2783    /// Shared inter tail: parse `coded_block_pattern` + `mb_qp_delta`, decode the
2784    /// luma/chroma residual, and add it to the already-built motion-compensated
2785    /// prediction. Used by both the 16×16/16×8/8×16 path and `P_8x8`.
2786    fn inter_finish(
2787        &mut self,
2788        r: &mut BitReader,
2789        mb_x: usize,
2790        mb_y: usize,
2791        pred_y: &[u8; 256],
2792        c_pred: &[[u8; 64]; 2],
2793        allow_8x8: bool,
2794        // D14: emit a worker job instead of reconstructing. Only the CAVLC
2795        // 16x16/16x8/8x16 path sets this; B and P_8x8 stay inline.
2796        defer: bool,
2797    ) -> Result<(), MbError> {
2798        let w4 = self.mb_w * 4;
2799        let cbp = {
2800            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
2801            read_cbp_inter(r)?
2802        };
2803        let cbp_luma = cbp & 15;
2804        let cbp_chroma = cbp >> 4;
2805        // transform_size_8x8_flag follows cbp (before mb_qp_delta) when luma has
2806        // coefficients, the 8×8 transform is enabled, and every partition ≥ 8×8.
2807        let t8x8 = cbp_luma > 0 && self.transform_8x8_mode && allow_8x8 && r.read_bit()?;
2808        if t8x8 {
2809            self.mb_t8x8[mb_y * self.mb_w + mb_x] = true;
2810        }
2811        if cbp != 0 {
2812            self.step_qp(r.read_se()?);
2813        }
2814        let (qp, qpc) = (self.cur_qp, self.chroma_qp_for(self.cur_qp));
2815
2816        // ---- luma residual ----
2817        self.nnz_cache_load(mb_x, mb_y);
2818        let mut luma_scan = [[0i32; 16]; 16];
2819        let mut nnzs = [0u8; 24];
2820        let mut luma8 = [[0i32; 64]; 4]; // 8×8-transform residuals (when t8x8)
2821        if t8x8 {
2822            for b8 in 0..4 {
2823                let (b8x, b8y) = (b8 % 2, b8 / 2);
2824                let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
2825                if cbp_luma & (1 << b8) != 0 {
2826                    let mut scan8 = [0i32; 64];
2827                    for sub in 0..4 {
2828                        let (sx, sy) = (sub % 2, sub / 2);
2829                        let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
2830                        let nc = self.nc_pred(cx, cy);
2831                        let blk = decode_residual_block(r, 16, nc)?;
2832                        let total = blk.iter().filter(|&&v| v != 0).count() as u8;
2833                        self.nnz_cache_set(cx, cy, total);
2834                        self.nnz_y[(by + sy) * w4 + (bx + sx)] = total;
2835                        // The PER-SUB-BLOCK count the next macroblock's nC prediction
2836                        // depends on -- summing these into one slot and letting the
2837                        // recon helper broadcast it back is what broke CAVLC 8x8.
2838                        nnzs[b8 * 4 + sub] = total;
2839                        for k in 0..16 {
2840                            scan8[4 * k + sub] = blk[k];
2841                        }
2842                    }
2843                    // RAW: `add_inter_residual` applies un_scan_8x8 + inv_quant8
2844                    // itself, exactly as it does for the CABAC path.
2845                    luma8[b8] = scan8;
2846                } else {
2847                    for sub in 0..4 {
2848                        let (sx, sy) = (sub % 2, sub / 2);
2849                        self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
2850                        self.nnz_y[(by + sy) * w4 + (bx + sx)] = 0;
2851                    }
2852                }
2853            }
2854        } else {
2855            for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
2856                let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
2857                let total = if cbp_luma & (1 << (blk / 4)) != 0 {
2858                    let nc = self.nc_pred(lbx, lby);
2859                    let scan16 = decode_residual_block(r, 16, nc)?;
2860                    luma_scan[blk] = scan16; // RAW scan order, like CABAC
2861                    scan16.iter().filter(|&&v| v != 0).count() as u8
2862                } else {
2863                    0
2864                };
2865                self.nnz_cache_set(lbx, lby, total);
2866                self.nnz_y[by * w4 + bx] = total;
2867                nnzs[blk] = total;
2868            }
2869        }
2870
2871        // ---- chroma residual ----
2872        let mut c_recon_dc = [[0i32; 4]; 2];
2873        if cbp_chroma != 0 {
2874            for (c, slot) in c_recon_dc.iter_mut().enumerate() {
2875                let dc = decode_residual_block(r, 4, -1)?;
2876                *slot = [dc[0], dc[1], dc[2], dc[3]]; // RAW; dequantised in the helper
2877            }
2878        }
2879        let mut c_q = [[[0i32; 16]; 4]; 2];
2880        if cbp_chroma == 2 {
2881            self.chroma_cache_load(mb_x, mb_y);
2882            let w2 = self.mb_w * 2;
2883            for c in 0..2 {
2884                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
2885                    let nc = self.chroma_nc_pred(c, bx, by);
2886                    let ac = decode_residual_block(r, 15, nc)?;
2887                    let total = ac.iter().filter(|&&v| v != 0).count() as u8;
2888                    self.chroma_nnz_cache_set(c, bx, by, total);
2889                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = total;
2890                    c_q[c][by * 2 + bx] = ac; // RAW scan order
2891                    nnzs[16 + c * 4 + by * 2 + bx] = total;
2892                }
2893            }
2894        }
2895
2896        // ---- reconstruction ----
2897        //
2898        // D13: this used to be a 109-line hand-rolled copy of the residual add.
2899        // It now calls the SAME `add_inter_residual` the CABAC path uses, which
2900        // is what makes the CAVLC E-seam possible at all: the two paths had
2901        // different residual representations (CAVLC pre-applied un_scan and
2902        // inv_quant at PARSE time; CABAC carries raw scan-order coefficients and
2903        // dequantises inside the helper), so no job could be shared. Carrying the
2904        // raw forms — which CAVLC already had in hand — converges them, deletes
2905        // a duplicate implementation, and lets a deferred job reuse the existing
2906        // worker recon instead of needing a second copy that could drift.
2907        if defer {
2908            // The residual representation now matches CABAC exactly (the
2909            // convergence commit), so the SAME `PInterJob` and the SAME worker
2910            // `recon_p_inter` serve both entropy coders — no second recon
2911            // implementation exists that could drift.
2912            let (mut gmv, mut gref) = ([(0i32, 0i32); 16], [0u8; 16]);
2913            let w4r = self.mb_w * 4;
2914            for by in 0..4usize {
2915                for bx in 0..4usize {
2916                    let bi = (mb_y * 4 + by) * w4r + (mb_x * 4 + bx);
2917                    gmv[by * 4 + bx] = self.mv_y[bi];
2918                    gref[by * 4 + bx] = self.ref_idx_y[bi].clamp(0, 15) as u8;
2919                }
2920            }
2921            // D9 applies here too: `cbp == 0` means all 2,592 coefficient bytes
2922            // of the 2,784-byte job are ZERO, so ship the 176-byte motion-only
2923            // form. Discovered on the CABAC path; it transfers for free because
2924            // the CAVLC arm now emits the SAME job type.
2925            let ej = if cbp == 0 && nores_on() {
2926                edcstat::bump(&edcstat::J_NORES_SENT, 1);
2927                EdcJob::InterNoRes(Box::new(PInterNoResJob {
2928                    mbx: mb_x, mby: mb_y, t8: t8x8, qp, gmv, gref,
2929                }))
2930            } else {
2931                EdcJob::Inter(Box::new(PInterJob {
2932                    mbx: mb_x, mby: mb_y, t8: t8x8, qp,
2933                    cbp_chroma, gmv, gref,
2934                    luma_scan, luma8, cdc: c_recon_dc, cac: c_q, nnzs,
2935                }))
2936            };
2937            if self.edc_tx.is_some() {
2938                self.edc_giveback();
2939                self.edc_send_job(ej);
2940            } else {
2941                self.edc_jobs.push(ej);
2942            }
2943        } else {
2944            self.add_inter_residual(
2945                mb_x, mb_y, pred_y, c_pred, &luma_scan,
2946                if t8x8 { Some(&luma8) } else { None },
2947                &c_recon_dc, &c_q, cbp_chroma, &nnzs,
2948            );
2949        }
2950
2951        // MV grid + coded flags were set per partition; mark modes as DC.
2952        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
2953            self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
2954        }
2955        Ok(())
2956    }
2957
2958    // ---------------------------------------------------------------------
2959    // B-slice macroblock decoding
2960    // ---------------------------------------------------------------------
2961
2962    /// Per-list (`list` 0 or 1) MV-prediction neighbors for the block region at
2963    /// `(pbx, pby)` of width `pwb` blocks — the L0/L1 analogue of
2964    /// `mv_neighbors_block`.
2965    fn mv_neighbors_list(&self, pbx: isize, pby: isize, pwb: isize, list: usize) -> [MvNeighbor; 3] {
2966        let (w4, h4) = ((self.mb_w * 4) as isize, (self.mb_h * 4) as isize);
2967        let (mvg, refg) = if list == 0 {
2968            (&self.mv_y, &self.ref_idx_y)
2969        } else {
2970            (&self.mv1, &self.ref_idx1)
2971        };
2972        let get = |bx: isize, by: isize| -> MvNeighbor {
2973            if bx < 0
2974                || by < 0
2975                || bx >= w4
2976                || by >= h4
2977                || !self.coded_y[(by * w4 + bx) as usize]
2978                || !self.nbr_in_slice(bx as usize / 4, by as usize / 4)
2979            {
2980                MvNeighbor::NONE
2981            } else {
2982                let idx = (by * w4 + bx) as usize;
2983                MvNeighbor { available: true, mv: mvg[idx], ref_idx: refg[idx] }
2984            }
2985        };
2986        let a = get(pbx - 1, pby);
2987        let b = get(pbx, pby - 1);
2988        let mut c = get(pbx + pwb, pby - 1);
2989        if !c.available {
2990            c = get(pbx - 1, pby - 1);
2991        }
2992        [a, b, c]
2993    }
2994
2995    /// `colZeroFlag` for the 4×4 block at absolute block coords `(bx, by)`: true
2996    /// when `RefPicList1[0]` is a short-term picture whose co-located block uses
2997    /// reference 0 with a near-zero motion vector (spec §8.4.1.2.2).
2998    /// Co-located 4x4 block coords for the current block's `(bx4, by4)` within the
2999    /// macroblock, per spec 8.4.1.2.1. Under `direct_8x8_inference_flag` every 4x4
3000    /// in an 8x8 takes that 8x8's OUTER CORNER (`luma4x4BlkIdx = 5 * mbPartIdx`,
3001    /// i.e. (0,0) (3,0) (0,3) (3,3)); otherwise motion is genuinely per-4x4.
3002    ///
3003    /// 8.4.1.2.1 is SHARED by both direct modes, so spatial and temporal must map
3004    /// identically. They did not: temporal mapped the corner and spatial read the
3005    /// block's own coords, which is invisible while every 4x4 in the co-located 8x8
3006    /// carries the same motion -- true of every stream until sub-8x8 P partitions
3007    /// (x264 `--partitions p4x4`) make them differ. Hence one function.
3008    #[inline]
3009    fn col_block(&self, bx4: usize, by4: usize) -> (usize, usize) {
3010        if self.direct_8x8_inference {
3011            ((bx4 / 2) * 3, (by4 / 2) * 3)
3012        } else {
3013            (bx4, by4)
3014        }
3015    }
3016
3017    fn col_zero(&self, bx: usize, by: usize) -> bool {
3018        let Some(col) = self.refs1.first() else { return false };
3019        if col.long_term || col.w4 == 0 {
3020            return false;
3021        }
3022        let idx = by * col.w4 + bx;
3023        if idx >= col.ref_idx.len() {
3024            return false;
3025        }
3026        // Spec 8.4.1.2.1: the co-located motion is List-0's when the co-located
3027        // block HAS a List-0 prediction, and List-1's otherwise (predFlagL0Col == 0).
3028        // Reading List-0 unconditionally treats an L1-only block as intra
3029        // (ref_idx -1), which silently suppresses colZeroFlag. An L1-only
3030        // co-located block can only exist when the co-located picture is itself a
3031        // B picture, i.e. only under b-pyramid -- which is why this survived every
3032        // non-pyramid B stream.
3033        let (cref, cmv) = if col.ref_idx[idx] >= 0 {
3034            (col.ref_idx[idx], col.mv[idx])
3035        } else if idx < col.ref_idx1.len() && col.ref_idx1[idx] >= 0 {
3036            (col.ref_idx1[idx], col.mv1[idx])
3037        } else {
3038            return false;
3039        };
3040        cref == 0 && cmv.0.abs() <= 1 && cmv.1.abs() <= 1
3041    }
3042
3043    /// Implicit bi-prediction weights `(w0, w1)` from POC distances (spec
3044    /// §8.4.2.3.2), or `None` for the plain average (idc≠2, uni-pred, or the
3045    /// equidistant / out-of-range fall-back to 32:32 which equals the average).
3046    fn implicit_weights(&self, refi0: i32, refi1: i32) -> Option<(i32, i32)> {
3047        if self.weighted_bipred_idc != 2 || refi0 < 0 || refi1 < 0 {
3048            return None;
3049        }
3050        let r0 = &self.refs[refi0 as usize];
3051        let r1 = &self.refs1[refi1 as usize];
3052        let td = (r1.poc - r0.poc).clamp(-128, 127);
3053        let tb = (self.cur_poc - r0.poc).clamp(-128, 127);
3054        if td == 0 || r0.long_term || r1.long_term {
3055            return None; // 32:32 → identical to the average
3056        }
3057        let tx = (16384 + td.abs() / 2) / td;
3058        let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
3059        let w1 = dsf >> 2;
3060        if !(-64..=128).contains(&w1) {
3061            return None; // out of range → 32:32 average
3062        }
3063        Some((64 - w1, w1))
3064    }
3065
3066    /// Motion-compensates a region with the given per-list refs/MVs. Bi-prediction
3067    /// is the simple `(a+b+1)>>1` average, or POC-weighted when implicit weighting
3068    /// (idc 2) is active. Writes into `pred_y`/`c_pred`.
3069    #[allow(clippy::too_many_arguments)]
3070    fn b_mc(
3071        &self,
3072        mb_x: usize,
3073        mb_y: usize,
3074        px: usize,
3075        py: usize,
3076        rw: usize,
3077        rh: usize,
3078        refi0: i32,
3079        mv0: (i32, i32),
3080        refi1: i32,
3081        mv1: (i32, i32),
3082        pred_y: &mut [u8; 256],
3083        c_pred: &mut [[u8; 64]; 2],
3084    ) {
3085        let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBMc);
3086        // Malformed-stream armor, mirroring the P path: now that B slices actually
3087        // PARSE ref_idx (they used to be hardcoded to 0), a mutated stream can hand
3088        // us an index past the end of either list. Clamp rather than panic — the
3089        // crate is `forbid(unsafe_code)` and fuzz-gated to never panic, and a
3090        // wrong picture on garbage input carries no conformance duty.
3091        let refi0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
3092        let refi1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
3093        if (refi0 >= 0 && self.refs.is_empty()) || (refi1 >= 0 && self.refs1.is_empty()) {
3094            return;
3095        }
3096        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
3097        let weights = {
3098            let _gw = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBWeights);
3099            self.implicit_weights(refi0, refi1)
3100        };
3101        // Bi-prediction blend: the weights decision is LOOP-INVARIANT, so every
3102        // blend site below matches on `weights` ONCE and runs a branch-free
3103        // pixel loop — the unweighted `(p+q+1)>>1` average then autovectorizes
3104        // (the per-pixel closure this replaces hid the invariant behind a
3105        // capture, and its chroma form was a &dyn call PER PIXEL).
3106        // FULL-WIDTH regions (px == 0, rw == 16 — every 16×16/16×8 partition and
3107        // most direct regions) occupy contiguous rows of `pred_y`, so MC writes
3108        // the destination DIRECTLY: uni-pred needs no staging at all, bi-pred
3109        // stages only the second list and blends in place. The staging arrays
3110        // (512 B zeroed per call before this) now exist only on the branches
3111        // that read them. Same fusion as the P path's mc_rect (WHYS Part 8).
3112        let full = px == 0 && rw == 16;
3113        let _gl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBLuma);
3114        // One scratch borrow for the whole region — both bi-pred passes included.
3115        // The closure yields whether the arm already ran the chroma half (the
3116        // bi-pred full-width arm does, to keep its staging alive) — a plain
3117        // `return` inside would exit the CLOSURE only and chroma would run twice.
3118        let chroma_done = rusty_h264_common::inter::with_mc_scratch(|scr| match (refi0 >= 0, refi1 >= 0, full) {
3119            (true, false, true) => {
3120                let rf = &self.refs[refi0 as usize];
3121                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
3122                false
3123            }
3124            (false, true, true) => {
3125                let rf = &self.refs1[refi1 as usize];
3126                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
3127                false
3128            }
3129            (true, true, true) => {
3130                let rf = &self.refs[refi0 as usize];
3131                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
3132                let mut b = [0u8; 256];
3133                let rf = &self.refs1[refi1 as usize];
3134                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut b[..rw * rh]);
3135                drop(_gl);
3136                let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
3137                // SLICE-then-zip: proving the bounds ONCE lets rustc emit the whole
3138                // 256-byte average as 8 straight-line vpavgb ops (verified in
3139                // isolation, x86-64-v3); the indexed form kept a per-iteration
3140                // bounds check and a loop. A hand AVX2 kernel is refuted — the
3141                // compiler already emits the ideal instruction.
3142                let dst = &mut pred_y[py * 16..py * 16 + rw * rh];
3143                match weights {
3144                    None => {
3145                        for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
3146                            *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
3147                        }
3148                    }
3149                    Some((w0, w1)) => {
3150                        for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
3151                            *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
3152                        }
3153                    }
3154                }
3155                let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
3156                self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
3157                true
3158            }
3159            _ => {
3160                // Narrow region — rows are strided in `pred_y`; stage and copy.
3161                let (mut a, mut b) = ([0u8; 256], [0u8; 256]);
3162                if refi0 >= 0 {
3163                    let rf = &self.refs[refi0 as usize];
3164                    rusty_h264_common::inter::mc_luma_padded_pre(scr, &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[..rw * rh]);
3165                }
3166                if refi1 >= 0 {
3167                    let rf = &self.refs1[refi1 as usize];
3168                    rusty_h264_common::inter::mc_luma_padded_pre(scr, &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[..rw * rh]);
3169                }
3170                drop(_gl);
3171                let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
3172                match (refi0 >= 0, refi1 >= 0) {
3173                    (true, true) => {
3174                        for dy in 0..rh {
3175                            let (ar, br) = (&a[dy * rw..dy * rw + rw], &b[dy * rw..dy * rw + rw]);
3176                            let base = (py + dy) * 16 + px;
3177                            let dst = &mut pred_y[base..base + rw];
3178                            match weights {
3179                                None => {
3180                                    for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
3181                                        *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
3182                                    }
3183                                }
3184                                Some((w0, w1)) => {
3185                                    for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
3186                                        *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
3187                                    }
3188                                }
3189                            }
3190                        }
3191                    }
3192                    (true, false) => {
3193                        for dy in 0..rh {
3194                            let d = (py + dy) * 16 + px;
3195                            pred_y[d..d + rw].copy_from_slice(&a[dy * rw..dy * rw + rw]);
3196                        }
3197                    }
3198                    _ => {
3199                        for dy in 0..rh {
3200                            let d = (py + dy) * 16 + px;
3201                            pred_y[d..d + rw].copy_from_slice(&b[dy * rw..dy * rw + rw]);
3202                        }
3203                    }
3204                }
3205                false
3206            }
3207        });
3208        if chroma_done {
3209            return;
3210        }
3211        let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
3212        self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
3213    }
3214
3215    /// Chroma half of `b_mc`, with the same full-width direct-write fusion
3216    /// (crw == 8 rows are contiguous in the 8-wide `c_pred` planes).
3217    #[allow(clippy::too_many_arguments)]
3218    fn b_mc_chroma(
3219        &self,
3220        mb_x: usize,
3221        mb_y: usize,
3222        px: usize,
3223        py: usize,
3224        rw: usize,
3225        rh: usize,
3226        refi0: i32,
3227        mv0: (i32, i32),
3228        refi1: i32,
3229        mv1: (i32, i32),
3230        c_pred: &mut [[u8; 64]; 2],
3231        weights: Option<(i32, i32)>,
3232        cch: usize,
3233    ) {
3234        let (crx, cry, crw, crh) = (px / 2, py / 2, rw / 2, rh / 2);
3235        let full = crx == 0 && crw == 8;
3236        for c in 0..2 {
3237            match (refi0 >= 0, refi1 >= 0, full) {
3238                (true, false, true) => {
3239                    let rf = &self.refs[refi0 as usize];
3240                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
3241                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
3242                }
3243                (false, true, true) => {
3244                    let rf = &self.refs1[refi1 as usize];
3245                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
3246                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
3247                }
3248                (true, true, true) => {
3249                    let rf = &self.refs[refi0 as usize];
3250                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
3251                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
3252                    let mut cb = [0u8; 64];
3253                    let rf = &self.refs1[refi1 as usize];
3254                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
3255                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut cb[..crw * crh]);
3256                    let dst = &mut c_pred[c][cry * 8..cry * 8 + crw * crh];
3257                    match weights {
3258                        None => {
3259                            for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
3260                                *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
3261                            }
3262                        }
3263                        Some((w0, w1)) => {
3264                            for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
3265                                *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
3266                            }
3267                        }
3268                    }
3269                }
3270                _ => {
3271                    let (mut ca, mut cb) = ([0u8; 64], [0u8; 64]);
3272                    if refi0 >= 0 {
3273                        let rf = &self.refs[refi0 as usize];
3274                        let pl = if c == 0 { &rf.pu } else { &rf.pv };
3275                        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[..crw * crh]);
3276                    }
3277                    if refi1 >= 0 {
3278                        let rf = &self.refs1[refi1 as usize];
3279                        let pl = if c == 0 { &rf.pu } else { &rf.pv };
3280                        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[..crw * crh]);
3281                    }
3282                    match (refi0 >= 0, refi1 >= 0) {
3283                        (true, true) => {
3284                            for dy in 0..crh {
3285                                let (pr, qr) = (&ca[dy * crw..dy * crw + crw], &cb[dy * crw..dy * crw + crw]);
3286                                let base = (cry + dy) * 8 + crx;
3287                                let dst = &mut c_pred[c][base..base + crw];
3288                                match weights {
3289                                    None => {
3290                                        for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
3291                                            *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
3292                                        }
3293                                    }
3294                                    Some((w0, w1)) => {
3295                                        for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
3296                                            *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
3297                                        }
3298                                    }
3299                                }
3300                            }
3301                        }
3302                        (true, false) => {
3303                            for dy in 0..crh {
3304                                let d = (cry + dy) * 8 + crx;
3305                                c_pred[c][d..d + crw].copy_from_slice(&ca[dy * crw..dy * crw + crw]);
3306                            }
3307                        }
3308                        _ => {
3309                            for dy in 0..crh {
3310                                let d = (cry + dy) * 8 + crx;
3311                                c_pred[c][d..d + crw].copy_from_slice(&cb[dy * crw..dy * crw + crw]);
3312                            }
3313                        }
3314                    }
3315                }
3316            }
3317        }
3318    }
3319
3320    /// Commits a region's per-list motion to the 4×4 grids (and marks coded).
3321    #[allow(clippy::too_many_arguments)]
3322    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)) {
3323        let _gs = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBSet);
3324        let w4 = self.mb_w * 4;
3325        for by in py / 4..(py + rh) / 4 {
3326            for bx in px / 4..(px + rw) / 4 {
3327                let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
3328                self.ref_idx_y[idx] = refi0;
3329                self.mv_y[idx] = if refi0 >= 0 { mv0 } else { (0, 0) };
3330                self.ref_idx1[idx] = refi1;
3331                self.mv1[idx] = if refi1 >= 0 { mv1 } else { (0, 0) };
3332                self.inter_y[idx] = true;
3333                self.coded_y[idx] = true;
3334                self.modes_y[idx] = 2;
3335            }
3336        }
3337    }
3338
3339    /// Spatial direct prediction for a region (whole MB or an 8×8): derives the
3340    /// per-list reference indices and base MVs, then motion-compensates each 4×4
3341    /// sub-block (applying `colZeroFlag`) and commits the motion (spec §8.4.1.2.2).
3342    #[allow(clippy::too_many_arguments)]
3343    /// Splits a `w`×`h` block region (4×4-block units) into the fewest rectangles
3344    /// whose contents are `uniform`, preferring partition-shaped cuts (whole →
3345    /// horizontal halves → vertical halves → quadrants). Emits at most w·h rects
3346    /// (the all-different worst case degenerates to per-block, i.e. the old loop).
3347    fn coalesce_region(
3348        x: usize,
3349        y: usize,
3350        w: usize,
3351        h: usize,
3352        uniform: &dyn Fn(usize, usize, usize, usize) -> bool,
3353        emit: &mut dyn FnMut(usize, usize, usize, usize),
3354    ) {
3355        if uniform(x, y, w, h) {
3356            emit(x, y, w, h);
3357            return;
3358        }
3359        if h > 1 && uniform(x, y, w, h / 2) && uniform(x, y + h / 2, w, h / 2) {
3360            emit(x, y, w, h / 2);
3361            emit(x, y + h / 2, w, h / 2);
3362            return;
3363        }
3364        if w > 1 && uniform(x, y, w / 2, h) && uniform(x + w / 2, y, w / 2, h) {
3365            emit(x, y, w / 2, h);
3366            emit(x + w / 2, y, w / 2, h);
3367            return;
3368        }
3369        match (w > 1, h > 1) {
3370            (true, true) => {
3371                for q in 0..4usize {
3372                    Self::coalesce_region(x + (q % 2) * (w / 2), y + (q / 2) * (h / 2), w / 2, h / 2, uniform, emit);
3373                }
3374            }
3375            (true, false) => {
3376                Self::coalesce_region(x, y, w / 2, h, uniform, emit);
3377                Self::coalesce_region(x + w / 2, y, w / 2, h, uniform, emit);
3378            }
3379            (false, true) => {
3380                Self::coalesce_region(x, y, w, h / 2, uniform, emit);
3381                Self::coalesce_region(x, y + h / 2, w, h / 2, uniform, emit);
3382            }
3383            (false, false) => emit(x, y, 1, 1),
3384        }
3385    }
3386
3387    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]) {
3388        let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDirect);
3389        if !self.direct_spatial {
3390            return self.decode_b_direct_temporal(mb_x, mb_y, px, py, rw, rh, pred_y, c_pred);
3391        }
3392        // H-48: DERIVATION-ONLY scope, dropped before the MC loop below. DecBDirect
3393        // wraps this function whole and therefore INCLUDES the `b_mc` calls it makes,
3394        // so its 1460 ns/call was never "MV derivation is slow" — that read was wrong.
3395        // This guard is what separates the two.
3396        let gd = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDeriv);
3397        // MB-level neighbors drive the direct reference indices and base MVs.
3398        let (nbx, nby) = ((mb_x * 4) as isize, (mb_y * 4) as isize);
3399        let n0 = self.mv_neighbors_list(nbx, nby, 4, 0);
3400        let n1 = self.mv_neighbors_list(nbx, nby, 4, 1);
3401        let min_pos = |a: i32, b: i32| if a < 0 { b } else if b < 0 { a } else { a.min(b) };
3402        let rid = |n: &[MvNeighbor; 3]| min_pos(min_pos(n[0].ref_idx, n[1].ref_idx), n[2].ref_idx);
3403        let (mut refi0, mut refi1) = (rid(&n0), rid(&n1));
3404        let direct_zero = refi0 < 0 && refi1 < 0;
3405        if direct_zero {
3406            refi0 = 0;
3407            refi1 = 0;
3408        }
3409        let mv0 = if refi0 >= 0 && !direct_zero { predict_mv(n0[0], n0[1], n0[2], refi0) } else { (0, 0) };
3410        let mv1 = if refi1 >= 0 && !direct_zero { predict_mv(n1[0], n1[1], n1[2], refi1) } else { (0, 0) };
3411        // Per 4×4 sub-block: colZeroFlag zeroes the ref-0 motion vector. cz is the
3412        // ONLY per-block variable (two possible (m0,m1) values for the region), and
3413        // the MC filters + bi-blend are per-output-pixel — so sub-blocks with equal
3414        // cz coalesce into one wider `b_mc`, BIT-IDENTICAL. A 16×16 direct MB paid
3415        // 16 bi-pred b_mc calls (~96 MC kernel entries) before this; typically 1 now.
3416        let (bx0, by0, bw, bh) = (px / 4, py / 4, rw / 4, rh / 4);
3417        let mut czg = [[false; 4]; 4]; // region-local, [dy][dx]
3418        for dy in 0..bh {
3419            for dx in 0..bw {
3420                let (colx, coly) = self.col_block(bx0 + dx, by0 + dy);
3421                czg[dy][dx] = !direct_zero && self.col_zero(mb_x * 4 + colx, mb_y * 4 + coly);
3422            }
3423        }
3424        let uniform = |x: usize, y: usize, w: usize, h: usize| -> bool {
3425            let t = czg[y][x];
3426            (y..y + h).all(|dy| (x..x + w).all(|dx| czg[dy][dx] == t))
3427        };
3428        let mut rects: [(usize, usize, usize, usize); 16] = [(0, 0, 0, 0); 16];
3429        let mut n = 0usize;
3430        Self::coalesce_region(0, 0, bw, bh, &uniform, &mut |x, y, w, h| {
3431            rects[n] = (x, y, w, h);
3432            n += 1;
3433        });
3434        drop(gd); // derivation ends; everything below is MC + motion-grid commit
3435        for &(x, y, w, h) in &rects[..n] {
3436            let cz = czg[y][x];
3437            let m0 = if refi0 == 0 && cz { (0, 0) } else { mv0 };
3438            let m1 = if refi1 == 0 && cz { (0, 0) } else { mv1 };
3439            let (lx, ly, lw, lh) = ((bx0 + x) * 4, (by0 + y) * 4, w * 4, h * 4);
3440            self.b_mc_or_record(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1, pred_y, c_pred);
3441            self.b_set_motion(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1);
3442        }
3443    }
3444
3445    /// Temporal direct prediction for a region (spec §8.4.1.2.3): for each 4×4
3446    /// (or per-8×8 corner under `direct_8x8_inference`), take the co-located
3447    /// List-0 motion from `RefPicList1[0]`, map its reference into the current
3448    /// List-0 by POC, and scale the motion vector by the POC distances.
3449    #[allow(clippy::too_many_arguments)]
3450    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]) {
3451        let poc1 = self.refs1.first().map_or(0, |f| f.poc);
3452        let infer = self.direct_8x8_inference;
3453        // Under direct_8x8_inference every 4×4 in an 8×8 takes the same MB-corner
3454        // co-located motion, so motion-compensate the whole 8×8 in one call — this
3455        // hits the width-8 MC asm and pays the per-call tile/blend setup 4× less.
3456        // Without inference, motion is genuinely per-4×4. Bit-identical either way
3457        // (MC of an 8×8 with one MV == four 4×4 MCs with that same MV).
3458        let step = if infer { 8 } else { 4 };
3459        let mut sy = py;
3460        while sy < py + rh {
3461            let mut sx = px;
3462            while sx < px + rw {
3463                // Co-located 4×4 (the 8×8's MB-corner under inference) — shared with
3464                // the spatial path's colZeroFlag, which must map identically.
3465                let (colx, coly) = self.col_block(sx / 4, sy / 4);
3466                let (mvcol, refpoc) = {
3467                    let col = &self.refs1[0];
3468                    let idx = (mb_y * 4 + coly) * col.w4 + (mb_x * 4 + colx);
3469                    if col.w4 != 0 && idx < col.mv.len() && col.ref_poc[idx] != i32::MIN {
3470                        (col.mv[idx], col.ref_poc[idx])
3471                    } else {
3472                        ((0, 0), i32::MIN) // intra co-located → zero motion, refIdxL0 = 0
3473                    }
3474                };
3475                // MapColToList0: the current-list index of the co-located reference.
3476                let (refi0, mvc) = if refpoc == i32::MIN {
3477                    (0, (0, 0))
3478                } else {
3479                    let r = self.refs.iter().position(|f| f.poc == refpoc).unwrap_or(0) as i32;
3480                    (r, mvcol)
3481                };
3482                let poc0 = self.refs[refi0 as usize].poc;
3483                let td = (poc1 - poc0).clamp(-128, 127);
3484                let tb = (self.cur_poc - poc0).clamp(-128, 127);
3485                let (mv0, mv1) = if td == 0 || self.refs[refi0 as usize].long_term {
3486                    (mvc, (0, 0))
3487                } else {
3488                    let tx = (16384 + td.abs() / 2) / td;
3489                    let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
3490                    let m0 = ((dsf * mvc.0 + 128) >> 8, (dsf * mvc.1 + 128) >> 8);
3491                    (m0, (m0.0 - mvc.0, m0.1 - mvc.1))
3492                };
3493                self.b_mc_or_record(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1, pred_y, c_pred);
3494                self.b_set_motion(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1);
3495                sx += step;
3496            }
3497            sy += step;
3498        }
3499    }
3500
3501    /// Reads `ref_idx_lX` for a B partition (te(v)/ue(v) by the list's active
3502    /// count), bounds-checked against the available reference count.
3503    fn read_b_ref(&self, r: &mut BitReader, list: usize) -> Result<i32, MbError> {
3504        let (active, avail) = if list == 0 {
3505            (self.num_ref_active, self.refs.len())
3506        } else {
3507            (self.num_ref_active1, self.refs1.len())
3508        };
3509        let v = if active > 1 { read_ref_idx(r, active)? } else { 0 };
3510        if v as usize >= avail {
3511            return Err(MbError::Truncated);
3512        }
3513        Ok(v)
3514    }
3515
3516    /// Reconstructs a `B_Skip` macroblock: spatial-direct prediction, no residual.
3517    fn decode_b_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3518        if self.refs.is_empty() || self.refs1.is_empty() {
3519            return Err(MbError::Unsupported("B without references"));
3520        }
3521        if self.edc_tx.is_some() {
3522            self.edc_regions = Some(Vec::with_capacity(4));
3523        }
3524        let mut pred_y = [0u8; 256];
3525        let mut c_pred = [[0u8; 64]; 2];
3526        self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
3527        if let Some(regions) = self.edc_regions.take() {
3528            // nnz clears are PARSE state; the pixel copy is the worker's.
3529            let w4 = self.mb_w * 4;
3530            for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3531                self.nnz_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 0;
3532            }
3533            self.edc_giveback();
3534            self.edc_send_job(EdcJob::BSkip { mbx: mb_x, mby: mb_y, regions });
3535            return Ok(());
3536        }
3537        // Zero residual: the prediction is the reconstruction — copy it row-wise.
3538        for dy in 0..16 {
3539            let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
3540            self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
3541        }
3542        for c in 0..2 {
3543            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3544            for dy in 0..8 {
3545                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
3546                plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
3547            }
3548        }
3549        // nnz stays 0 (no residual) — clear the grids for neighbor context.
3550        let w4 = self.mb_w * 4;
3551        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3552            self.nnz_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 0;
3553        }
3554        Ok(())
3555    }
3556
3557    /// Reconstructs a B macroblock (spec Table 7-14): direct, L0/L1/Bi partitions,
3558    /// `B_8x8`, or intra.
3559    fn decode_b_mb(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3560        let mb_type = r.read_ue()?;
3561        if mb_type >= 23 {
3562            return self.decode_intra_mb(r, mb_x, mb_y, mb_type - 23);
3563        }
3564        if self.refs.is_empty() || self.refs1.is_empty() {
3565            return Err(MbError::Unsupported("B without references"));
3566        }
3567        let mut pred_y = [0u8; 256];
3568        let mut c_pred = [[0u8; 64]; 2];
3569
3570        if mb_type == 0 {
3571            // B_Direct_16x16 — 8×8 transform allowed only with direct_8x8_inference.
3572            self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
3573            return self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, self.direct_8x8_inference, false);
3574        }
3575        if mb_type == 22 {
3576            return self.decode_b_8x8(r, mb_x, mb_y);
3577        }
3578
3579        // 16x16 / 16x8 / 8x16 partitions with per-partition L0/L1/Bi.
3580        let (layout, mvmode, preds) = b_inter_layout(mb_type);
3581        // mb_pred order: ref_idx_l0 (all L0 parts), ref_idx_l1, mvd_l0, mvd_l1.
3582        let mut refi = [[-1i32; 2]; 2]; // [part][list]
3583        for (p, &(_, _, _, _)) in layout.iter().enumerate() {
3584            if preds[p].uses(0) {
3585                refi[p][0] = self.read_b_ref(r, 0)?;
3586            }
3587        }
3588        for (p, _) in layout.iter().enumerate() {
3589            if preds[p].uses(1) {
3590                refi[p][1] = self.read_b_ref(r, 1)?;
3591            }
3592        }
3593        let mut mvd = [[(0i32, 0i32); 2]; 2];
3594        for (p, _) in layout.iter().enumerate() {
3595            if preds[p].uses(0) {
3596                mvd[p][0] = (r.read_se()?, r.read_se()?);
3597            }
3598        }
3599        for (p, _) in layout.iter().enumerate() {
3600            if preds[p].uses(1) {
3601                mvd[p][1] = (r.read_se()?, r.read_se()?);
3602            }
3603        }
3604        // Per partition: predict + commit each list's MV, then motion-compensate.
3605        for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
3606            let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
3607            let pwb = (rw / 4) as isize;
3608            let mut mv = [(0i32, 0i32); 2];
3609            for list in 0..2 {
3610                if refi[p][list] >= 0 {
3611                    let n = self.mv_neighbors_list(pbx, pby, pwb, list);
3612                    let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], refi[p][list]);
3613                    mv[list] = (pmv.0 + mvd[p][list].0, pmv.1 + mvd[p][list].1);
3614                }
3615            }
3616            self.b_set_motion(mb_x, mb_y, rx, ry, rw, rh, refi[p][0], mv[0], refi[p][1], mv[1]);
3617            // Spec-correct bi-prediction (average of L0 and L1), matching the CABAC
3618            // path. This used to replicate an openh264 bug for a Bi 16x8/8x16
3619            // partition -- openh264 mis-handles the destination buffer there, so
3620            // partition 0 came out List-1-only and partition 1 List-0-only. That was
3621            // deliberate when openh264's h264dec WAS the conformance oracle, but the
3622            // gate is ffmpeg now and the CABAC path already went spec-correct; the
3623            // CAVLC path was simply left behind. Measured: mb_type 12..21 (every B
3624            // 16x8/8x16 with at least one Bi partition) were 100% wrong vs ffmpeg,
3625            // while 1..11 (no Bi partition) were only collaterally damaged.
3626            self.b_mc_or_record(mb_x, mb_y, rx, ry, rw, rh, refi[p][0], mv[0], refi[p][1], mv[1], &mut pred_y, &mut c_pred);
3627        }
3628        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true, false)
3629    }
3630
3631    /// Reconstructs a `B_8x8` macroblock: four 8×8 sub-macroblock partitions, each
3632    /// direct or L0/L1/Bi with its own sub-partitioning (spec Table 7-18).
3633    fn decode_b_8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
3634        let mut sub = [0u32; 4];
3635        for s in sub.iter_mut() {
3636            let v = r.read_ue()?;
3637            if v > 12 {
3638                return Err(MbError::Unsupported("invalid B sub_mb_type"));
3639            }
3640            *s = v;
3641        }
3642        let mut pred_y = [0u8; 256];
3643        let mut c_pred = [[0u8; 64]; 2];
3644        // ref_idx for all 8×8 partitions (L0 batch, then L1 batch), for the
3645        // non-direct sub-partitions.
3646        let mut refi = [[-1i32; 2]; 4];
3647        for (p, &st) in sub.iter().enumerate() {
3648            if st != 0 && b_sub_uses(st, 0) {
3649                refi[p][0] = self.read_b_ref(r, 0)?;
3650            }
3651        }
3652        for (p, &st) in sub.iter().enumerate() {
3653            if st != 0 && b_sub_uses(st, 1) {
3654                refi[p][1] = self.read_b_ref(r, 1)?;
3655            }
3656        }
3657        // mvd: all mvd_l0 (partition-major, sub-partition order), then all mvd_l1.
3658        //
3659        // FIXED ARRAYS, NOT `Vec::new()` + push. These are per-MACROBLOCK on every
3660        // B_8x8, and a growing Vec allocated (and reallocated) twice per MB — on a
3661        // B-heavy stream that is thousands of allocations per frame for data whose
3662        // maximum size is a compile-time constant: 4 partitions x at most 4
3663        // sub-partitions = 16 entries. Indexing a fixed array cannot exceed that, and
3664        // an out-of-range index would panic rather than misbehave, so the bound is
3665        // enforced either way and this crate stays forbid(unsafe).
3666        const MAX_MVD: usize = 16;
3667        let mut mvd0 = [(0i32, 0i32); MAX_MVD];
3668        let mut mvd1 = [(0i32, 0i32); MAX_MVD];
3669        let (mut n0, mut n1) = (0usize, 0usize);
3670        for &st in &sub {
3671            if st != 0 && b_sub_uses(st, 0) {
3672                for _ in b_sub_parts(st) {
3673                    mvd0[n0] = (r.read_se()?, r.read_se()?);
3674                    n0 += 1;
3675                }
3676            }
3677        }
3678        for &st in &sub {
3679            if st != 0 && b_sub_uses(st, 1) {
3680                for _ in b_sub_parts(st) {
3681                    mvd1[n1] = (r.read_se()?, r.read_se()?);
3682                    n1 += 1;
3683                }
3684            }
3685        }
3686        // Decode each 8×8 partition.
3687        let (mut i0, mut i1) = (0usize, 0usize);
3688        for (p, &st) in sub.iter().enumerate() {
3689            let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
3690            if st == 0 {
3691                self.decode_b_direct(mb_x, mb_y, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred);
3692                continue;
3693            }
3694            for &(sx, sy, sw, sh) in b_sub_parts(st) {
3695                let (px, py) = (b8x + sx, b8y + sy);
3696                let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
3697                let pwb = (sw / 4) as isize;
3698                let mut mv = [(0i32, 0i32); 2];
3699                if b_sub_uses(st, 0) {
3700                    let n = self.mv_neighbors_list(pbx, pby, pwb, 0);
3701                    let pmv = predict_mv(n[0], n[1], n[2], refi[p][0]);
3702                    let d = mvd0[i0];
3703                    i0 += 1;
3704                    mv[0] = (pmv.0 + d.0, pmv.1 + d.1);
3705                }
3706                if b_sub_uses(st, 1) {
3707                    let n = self.mv_neighbors_list(pbx, pby, pwb, 1);
3708                    let pmv = predict_mv(n[0], n[1], n[2], refi[p][1]);
3709                    let d = mvd1[i1];
3710                    i1 += 1;
3711                    mv[1] = (pmv.0 + d.0, pmv.1 + d.1);
3712                }
3713                self.b_set_motion(mb_x, mb_y, px, py, sw, sh, refi[p][0], mv[0], refi[p][1], mv[1]);
3714                self.b_mc_or_record(mb_x, mb_y, px, py, sw, sh, refi[p][0], mv[0], refi[p][1], mv[1], &mut pred_y, &mut c_pred);
3715            }
3716        }
3717        // noSubMbPartSizeLessThan8x8: each sub-partition must be ≥ 8×8 (direct
3718        // counts only with the 8×8 inference flag).
3719        let allow_8x8 = sub
3720            .iter()
3721            .all(|&st| if st == 0 { self.direct_8x8_inference } else { st <= 3 });
3722        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8, false)
3723    }
3724
3725    /// Reconstructs a `P_8x8` macroblock: four 8×8 sub-macroblock partitions,
3726    /// each independently split (8×8 / 8×4 / 4×8 / 4×4) with its own motion
3727    /// vector(s). `ref0` is `P_8x8ref0` (every `ref_idx` forced to 0, not coded).
3728    fn decode_p8x8(
3729        &mut self,
3730        r: &mut BitReader,
3731        mb_x: usize,
3732        mb_y: usize,
3733        ref0: bool,
3734    ) -> Result<(), MbError> {
3735        if self.refs.is_empty() {
3736            return Err(MbError::Unsupported("inter without reference"));
3737        }
3738        let w4 = self.mb_w * 4;
3739        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
3740        let num_refs = self.refs.len();
3741
3742        // mb_pred order (spec §7.3.5.2): all sub_mb_type, then all ref_idx_l0,
3743        // then all mvd_l0 (partition-major, sub-partition order within each).
3744        let mut sub_types = [0u32; 4];
3745        for st in sub_types.iter_mut() {
3746            let v = r.read_ue()?;
3747            if v > 3 {
3748                return Err(MbError::Unsupported("B-slice / invalid sub_mb_type"));
3749            }
3750            *st = v;
3751        }
3752        let mut ref_idxs = [0i32; 4];
3753        if self.num_ref_active > 1 && !ref0 {
3754            for ri in ref_idxs.iter_mut() {
3755                *ri = read_ref_idx(r, self.num_ref_active)?;
3756                if *ri as usize >= num_refs {
3757                    return Err(MbError::Truncated); // references a non-existent picture
3758                }
3759            }
3760        }
3761
3762        // Per sub-partition (in decoding order): median MV prediction from the
3763        // committed neighbor grid, mvd, commit, then motion-compensate. Committing
3764        // before the next prediction is what lets sub-partitions chain correctly.
3765        // D14b: P_8x8 defers too. Syncing before it instead cost 9,772 pipeline
3766        // drains per stream (45 per 1000 MBs, vs CABAC's 3.3) because P_8x8 is
3767        // common in CAVLC P slices — and the seam measured 1.65-1.97x SLOWER
3768        // for it. Deferring is byte-identical for sub-partitions: the worker
3769        // motion-compensates per 4x4 from the committed grids, which is exactly
3770        // how the CABAC path already handles P_8x8, and a 6-tap filter applied
3771        // per-4x4 with the same MV gives the same pixels as one 8x8 call.
3772        let defer = self.edc_tx.is_some() || self.edc_active;
3773
3774        // ── PHASE 1: PARSE + COMMIT. Must always run to completion. ──────────
3775        //
3776        // These two are interleaved BY NECESSITY: each sub-partition's MV
3777        // prediction reads the grids the previous one committed, so they cannot
3778        // be separated from each other. But they consume BITSTREAM, so nothing
3779        // here may ever be skipped conditionally.
3780        //
3781        // This phase split is a STRUCTURAL guard, not a tidy-up. When MC lived
3782        // inside this loop, deferring it was written as a `break` — which also
3783        // skipped the `read_se` mvd reads, desynced the bitstream, and
3784        // mis-parsed as a B-slice sub_mb_type on a Baseline stream. It happened
3785        // to crash; a desync that stayed IN RANGE would have produced plausible
3786        // garbage instead. With MC in its own pass below, skipping pixel work
3787        // cannot reach the bitstream at all — the mistake is unavailable.
3788        let mut regions: [(usize, usize, usize, usize, i32, (i32, i32)); 16] =
3789            [(0, 0, 0, 0, 0, (0, 0)); 16];
3790        let mut nreg = 0usize;
3791        for part in 0..4usize {
3792            let refi = ref_idxs[part];
3793            let (b8x, b8y) = ((part % 2) * 8, (part / 2) * 8);
3794            for &(srx, sry, srw, srh) in sub_mb_partitions(sub_types[part]) {
3795                let (px, py) = (b8x + srx, b8y + sry);
3796                let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
3797                let [a, b, c] = self.mv_neighbors_block(pbx, pby, (srw / 4) as isize);
3798                let pmv = predict_mv(a, b, c, refi);
3799                let mvd_x = r.read_se()?;
3800                let mvd_y = r.read_se()?;
3801                let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
3802                for by in py / 4..py / 4 + srh / 4 {
3803                    for bx in px / 4..px / 4 + srw / 4 {
3804                        let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
3805                        self.mv_y[idx] = mv;
3806                        self.inter_y[idx] = true;
3807                        self.ref_idx_y[idx] = refi;
3808                        self.coded_y[idx] = true;
3809                    }
3810                }
3811                regions[nreg] = (px, py, srw, srh, refi, mv);
3812                nreg += 1;
3813            }
3814        }
3815
3816        // ── PHASE 2: PIXEL WORK ONLY. Reads no bitstream; safe to skip. ──────
3817        let mut pred_y = [0u8; 256];
3818        let mut c_pred = [[0u8; 64]; 2];
3819        if !defer {
3820            for &(px, py, srw, srh, refi, mv) in &regions[..nreg] {
3821                let reference = &self.refs[refi as usize];
3822                let mut tmp = [0u8; 256];
3823                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);
3824                restride(&mut pred_y, 16, px, py, &tmp, srw, srh);
3825                let (crx, cry, crw, crh) = (px / 2, py / 2, srw / 2, srh / 2);
3826                for cc in 0..2 {
3827                    let rc = if cc == 0 { &reference.pu } else { &reference.pv };
3828                    let mut tc = [0u8; 64];
3829                    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);
3830                    restride(&mut c_pred[cc], 8, crx, cry, &tc, crw, crh);
3831                }
3832                self.weight_partition(
3833                    &mut pred_y, &mut c_pred, 0, refi as usize, px, py, srw, srh,
3834                );
3835            }
3836        }
3837
3838        // P_8x8 allows the 8×8 transform only when every sub-partition is 8×8.
3839        let allow_8x8 = sub_types.iter().all(|&t| t == 0);
3840        self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8, defer)
3841    }
3842
3843    /// Reconstructs a `P_Skip` macroblock: motion-compensate from the reference
3844    /// at the skip MV, with no residual.
3845    /// Records a B MC region (threaded mode) or executes it inline — the SAME
3846    /// arguments as `b_mc`; the recorded arm resolves the implicit weights at
3847    /// parse time (identical function, parse-side data).
3848    #[allow(clippy::too_many_arguments)]
3849    fn b_mc_or_record(&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), pred_y: &mut [u8; 256], c_pred: &mut [[u8; 64]; 2]) {
3850        if self.edc_regions.is_some() {
3851            // Mirror `b_mc`'s malformed-stream armor EXACTLY before touching the
3852            // ref lists: the inline path clamps the indices and bails on empty
3853            // lists BEFORE computing weights; calling `implicit_weights` with the
3854            // raw indices re-introduced the panic the armor exists to prevent
3855            // (found by the fuzzer, via the unwind-guard that turned the
3856            // resulting worker deadlock back into a diagnosable failure).
3857            let cr0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
3858            let cr1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
3859            let w = if (cr0 >= 0 && self.refs.is_empty()) || (cr1 >= 0 && self.refs1.is_empty()) {
3860                None // the worker's port returns before reading the weights
3861            } else {
3862                self.implicit_weights(cr0, cr1)
3863            };
3864            self.edc_regions.as_mut().unwrap().push(BRegion { px, py, rw, rh, refi0, refi1, mv0, mv1, w });
3865            return;
3866        }
3867        self.b_mc(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, pred_y, c_pred);
3868    }
3869
3870    /// Builds the worker's owned pixel context from `self` (planes MOVED out,
3871    /// shared read-only state cloned, filter inputs snapshotted).
3872    fn edc_take_ctx(&mut self) -> PixelCtx {
3873        PixelCtx {
3874            rec_y: std::mem::take(&mut self.rec_y),
3875            rec_u: std::mem::take(&mut self.rec_u),
3876            rec_v: std::mem::take(&mut self.rec_v),
3877            bak_y: std::mem::take(&mut self.bak_y),
3878            bak_u: std::mem::take(&mut self.bak_u),
3879            bak_v: std::mem::take(&mut self.bak_v),
3880            refs: self.refs.clone(),
3881            refs1: self.refs1.clone(),
3882            weights: self.weights.clone(),
3883            scaling: self.scaling,
3884            scaling8: self.scaling8,
3885            cw: self.cw,
3886            ccw: self.ccw,
3887            mb_w: self.mb_w,
3888            mb_h: self.mb_h,
3889            chroma_qp_offset: self.chroma_qp_offset,
3890            flt_rows: self.flt_rows,
3891            db_ena: self.db_ena,
3892            db_oa: self.db_oa,
3893            db_ob: self.db_ob,
3894            cur_qp: self.cur_qp,
3895            qp_grid: self.mb_qp.clone(),
3896            t8_grid: self.mb_t8x8.clone(),
3897            bs_store: self.bs_frame.clone(),
3898        }
3899    }
3900
3901    /// Restores the planes (and the filter watermark) from a returned context.
3902    fn edc_restore_ctx(&mut self, ctx: PixelCtx) {
3903        self.rec_y = ctx.rec_y;
3904        self.rec_u = ctx.rec_u;
3905        self.rec_v = ctx.rec_v;
3906        self.bak_y = ctx.bak_y;
3907        self.bak_u = ctx.bak_u;
3908        self.bak_v = ctx.bak_v;
3909        self.flt_rows = ctx.flt_rows;
3910    }
3911
3912    /// Intra macroblocks read neighbour PIXELS: fetch the context from the
3913    /// worker (which drains all prior jobs first — the channel is FIFO) and
3914    /// install the planes so the inline intra path runs unchanged. The context
3915    /// is given back lazily at the next job/row/slice-end (`edc_giveback`), so
3916    /// consecutive intra macroblocks pay ONE round-trip.
3917    /// Queue a pixel job for the worker (D10). Batched per row; see `EdcMsg::Batch`.
3918    #[inline]
3919    fn edc_send_job(&mut self, job: EdcJob) {
3920        edcstat::bump(&edcstat::JOBS, 1);
3921        if !batch_on() {
3922            self.edc_tx
3923                .as_ref()
3924                .unwrap()
3925                .send(EdcMsg::Job(job))
3926                .expect("worker alive");
3927            return;
3928        }
3929        self.edc_batch.push(job);
3930    }
3931
3932    /// Ship the accumulated row batch. MUST be called before anything that
3933    /// depends on those jobs having been applied: the row's `Row` filter
3934    /// message, a `NeedCtx` handover, and slice end.
3935    fn edc_flush_batch(&mut self) {
3936        if self.edc_batch.is_empty() {
3937            return;
3938        }
3939        // Replace with a PRE-RESERVED buffer rather than the empty Vec
3940        // `mem::take` would leave: otherwise each row reallocs and regrows from
3941        // zero, trading 208k channel sends for ~7 reallocs x 3k rows.
3942        let cap = self.edc_batch.capacity().max(self.mb_w);
3943        let jobs = std::mem::replace(&mut self.edc_batch, Vec::with_capacity(cap));
3944        edcstat::bump(&edcstat::BATCHES, 1);
3945        self.edc_tx
3946            .as_ref()
3947            .unwrap()
3948            .send(EdcMsg::Batch(jobs))
3949            .expect("worker alive");
3950    }
3951
3952    fn edc_intra_sync(&mut self) {
3953        if self.edc_tx.is_none() {
3954            self.edc_flush();
3955            return;
3956        }
3957        if self.edc_parked.is_some() {
3958            return; // already holding
3959        }
3960        // ORDER: drain the batch before taking the planes, or those jobs would
3961        // be applied to a context the parse thread is concurrently holding.
3962        self.edc_flush_batch();
3963        edcstat::bump(&edcstat::NEEDCTX, 1);
3964        self.edc_tx.as_ref().unwrap().send(EdcMsg::NeedCtx).expect("worker alive");
3965        let mut ctx = self.edc_ctx_rx.as_ref().unwrap().recv().expect("worker ctx");
3966        self.rec_y = std::mem::take(&mut ctx.rec_y);
3967        self.rec_u = std::mem::take(&mut ctx.rec_u);
3968        self.rec_v = std::mem::take(&mut ctx.rec_v);
3969        self.bak_y = std::mem::take(&mut ctx.bak_y);
3970        self.bak_u = std::mem::take(&mut ctx.bak_u);
3971        self.bak_v = std::mem::take(&mut ctx.bak_v);
3972        self.flt_rows = ctx.flt_rows;
3973        self.edc_parked = Some(ctx);
3974    }
3975
3976    /// Returns a held context to the worker (inverse of `edc_intra_sync`).
3977    /// NOTE (D10): this is called per-macroblock on the job paths, so it must
3978    /// NOT flush the batch — that would undo the batching. It is safe: the
3979    /// parked state is only ever entered through `edc_intra_sync`, which
3980    /// flushes before taking the planes, so nothing can be queued-but-unsent
3981    /// while the parse thread holds them.
3982    fn edc_giveback(&mut self) {
3983        if let Some(mut parked) = self.edc_parked.take() {
3984            parked.rec_y = std::mem::take(&mut self.rec_y);
3985            parked.rec_u = std::mem::take(&mut self.rec_u);
3986            parked.rec_v = std::mem::take(&mut self.rec_v);
3987            parked.bak_y = std::mem::take(&mut self.bak_y);
3988            parked.bak_u = std::mem::take(&mut self.bak_u);
3989            parked.bak_v = std::mem::take(&mut self.bak_v);
3990            parked.flt_rows = self.flt_rows;
3991            // Fail-soft: on the unwind path the worker may already be gone;
3992            // dropping the parked context is acceptable there (the planes are
3993            // lost, but the panic is being propagated anyway).
3994            let _ = self.edc_back_tx.as_ref().unwrap().send(parked);
3995        }
3996    }
3997
3998    /// Parse-side twin of the nnz/coded grid writes `add_inter_residual` does
3999    /// inline — the worker's port omits them (they are PARSE state: deblock
4000    /// derivation and the CAVLC nC contexts read them), so the threaded path
4001    /// commits them here, from the same parsed counts (their equality with the
4002    /// recon-side recount is the Part 8 nnz-threading brick's own invariant).
4003    fn edc_commit_nnz(&mut self, mbx: usize, mby: usize, t8: bool, nnzs: &[u8; 24], cbp_chroma: u32) {
4004        let (w4r, w2r) = (self.mb_w * 4, self.mb_w * 2);
4005        if t8 {
4006            for b8 in 0..4usize {
4007                let (b8x, b8y) = (b8 % 2, b8 / 2);
4008                let n = nnzs[b8 * 4];
4009                for sy in 0..2 {
4010                    for sx in 0..2 {
4011                        self.nnz_y[(mby * 4 + b8y * 2 + sy) * w4r + (mbx * 4 + b8x * 2 + sx)] = n;
4012                    }
4013                }
4014            }
4015        } else {
4016            for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
4017                self.nnz_y[(mby * 4 + lby) * w4r + (mbx * 4 + lbx)] = nnzs[blk];
4018            }
4019        }
4020        if cbp_chroma == 2 {
4021            for c in 0..2usize {
4022                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
4023                    self.nnz_c[c][(mby * 2 + by) * w2r + (mbx * 2 + bx)] = nnzs[16 + c * 4 + by * 2 + bx];
4024                }
4025            }
4026        }
4027    }
4028
4029    /// Flush the entropy-decouple job queue: replay every deferred pixel job
4030    /// in parse order. Called before any intra macroblock (its reconstruction
4031    /// reads neighbour PIXELS), before row filtering, at B-branch entry, at
4032    /// slice end, and at `deblock()` as a backstop.
4033    fn edc_flush(&mut self) {
4034        if self.edc_jobs.is_empty() {
4035            return;
4036        }
4037        let jobs = std::mem::take(&mut self.edc_jobs);
4038        for j in &jobs {
4039            match j {
4040                EdcJob::Skip { mbx, mby, mv } => {
4041                    self.recon_p_skip(*mbx, *mby, *mv);
4042                    if double_recon() {
4043                        edcstat::bump(&edcstat::DOUBLED, 1);
4044                        self.recon_p_skip(*mbx, *mby, *mv);
4045                    }
4046                }
4047                EdcJob::Inter(job) => {
4048                    self.recon_p_inter(job);
4049                    if double_recon() {
4050                        edcstat::bump(&edcstat::DOUBLED, 1);
4051                        self.recon_p_inter(job);
4052                    }
4053                }
4054                EdcJob::InterNoRes(job) => {
4055                    let full = job.to_full();
4056                    self.recon_p_inter(&full);
4057                    if double_recon() {
4058                        edcstat::bump(&edcstat::DOUBLED, 1);
4059                        self.recon_p_inter(&full);
4060                    }
4061                }
4062                // B jobs exist only in worker (MT) mode and are never queued
4063                // here — the single-thread seam keeps B inline. Not reachable
4064                // from any input (the push sites are gated on `edc_tx`).
4065                EdcJob::B(_) | EdcJob::BSkip { .. } => unreachable!("B jobs are worker-only"),
4066            }
4067        }
4068        // Hand the (now empty) Vec back so its allocation is reused.
4069        self.edc_jobs = jobs;
4070        self.edc_jobs.clear();
4071    }
4072
4073    /// Reconstructs one CABAC P inter macroblock from its parse job — the
4074    /// pixel half of the entropy-decouple seam (docs/entropy-decouple-plan.md
4075    /// E1). Reads NOTHING from parse state except the frame grids this MB's
4076    /// parse already committed (its own block MVs/refs, re-gathered below —
4077    /// stable after commit) and the immutable DPB; called either inline
4078    /// (seam off / flush disabled) or in-order at a flush point. Byte-
4079    /// identical to the former inline block by construction: replay order
4080    /// equals inline order at every pixel-observable point (intra reads, row
4081    /// filtering) because flushes precede both.
4082    fn recon_p_inter(&mut self, j: &PInterJob) {
4083        let mbw = self.mb_w;
4084        // `add_inter_residual` (and anything under it) reads `self.cur_qp`,
4085        // which at FLUSH time belongs to a later macroblock — replay must
4086        // restore this MB's qp. The x264 corpus (near-constant QP) could not
4087        // see this; the encoder's delta-QP roundtrip stream caught it.
4088        let saved_qp = self.cur_qp;
4089        self.cur_qp = j.qp;
4090                    // ---- Recon: motion-comp (per 4×4 luma / co-located 2×2 chroma using the
4091                    // committed grid MV — the 6-tap/bilinear filter is per-output-pixel, so
4092                    // per-block MC is bit-identical to per-partition MC) + residual add via the
4093                    // SAME reconstruct_4x4 as intra, with the MC output as the prediction.
4094                    let qp = j.qp;
4095                    let qpc = self.chroma_qp_for(qp);
4096                    let (w4r, w2r) = (mbw * 4, mbw * 2);
4097                    let mut pred_y = [0u8; 256];
4098                    let mut c_pred = [[0u8; 64]; 2];
4099                    {
4100                        // MC-CALL COALESCING (side-by-side descent, dec target #2): the old
4101                        // loop paid 16 mc_luma(4×4) + 32 mc_chroma(2×2) per MB regardless of
4102                        // partitioning — 48 calls even for a single-MV 16×16 MB, and the
4103                        // per-call glue around 2.4M calls was ~40% of decoding real-world
4104                        // (x264) streams. The 6-tap/bilinear filters are per-output-pixel,
4105                        // so merging blocks with equal (mv, ref) into one wider MC call is
4106                        // BIT-IDENTICAL; the rect ladder mirrors the partition shapes.
4107                        let _ms = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMcStage);
4108                        let (rh16, cch) = (self.mb_h * 16, self.mb_h * 8);
4109                        let mut gmv = [(0i32, 0i32); 16];
4110                        let mut gref = [0usize; 16];
4111                        let _gg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::MvGrid);
4112                        for by in 0..4usize {
4113                            for bx in 0..4usize {
4114                                let bidx = (j.mby * 4 + by) * w4r + (j.mbx * 4 + bx);
4115                                gmv[by * 4 + bx] = self.mv_y[bidx];
4116                                // Per-block reference (multi-ref P): ref_idx_l0 committed to the
4117                                // grid. Clamp — a corrupt stream can over-range it (never panic).
4118                                gref[by * 4 + bx] =
4119                                    (self.ref_idx_y[bidx].max(0) as usize).min(self.refs.len() - 1);
4120                            }
4121                        }
4122                        drop(_gg);
4123                        // All blocks of the rect (in 4×4-block units) match its top-left?
4124                        let rect_eq = |x4: usize, y4: usize, w4: usize, h4: usize| -> bool {
4125                            let t = y4 * 4 + x4;
4126                            (0..h4).all(|dy| {
4127                                (0..w4).all(|dx| {
4128                                    let b = (y4 + dy) * 4 + (x4 + dx);
4129                                    gmv[b] == gmv[t] && gref[b] == gref[t]
4130                                })
4131                            })
4132                        };
4133                        let refs = &self.refs;
4134                        let (cw, ccw) = (self.cw, self.ccw);
4135                        let mut mc_rect = |x4: usize,
4136                                           y4: usize,
4137                                           w4: usize,
4138                                           h4: usize,
4139                                           pred_y: &mut [u8; 256],
4140                                           c_pred: &mut [[u8; 64]; 2]| {
4141                            let b = y4 * 4 + x4;
4142                            let (mv, reference) = (gmv[b], &refs[gref[b]]);
4143                            let (w, h) = (w4 * 4, h4 * 4);
4144                            // A FULL-WIDTH rect (w == 16, so x4 == 0) occupies contiguous
4145                            // whole rows of `pred_y` — the MC output layout and the
4146                            // destination layout coincide, so MC writes the prediction
4147                            // buffer DIRECTLY. The staging copy exists only for narrow
4148                            // rects, whose rows really are strided in `pred_y`. This is
4149                            // the diagnosis's "stage-boundary materialization" tax paid
4150                            // by the dominant 16×16/16×8 shapes: 256 B of `t` zeroing
4151                            // plus a 256 B copy per rect, for nothing.
4152                            if w == 16 {
4153                                rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &reference.py, reference.lstride(), crate::LPAD, cw, rh16, j.mbx * 16, j.mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut pred_y[y4 * 64..y4 * 64 + w * h]));
4154                            } else {
4155                                let mut t = [0u8; 256];
4156                                rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &reference.py, reference.lstride(), crate::LPAD, cw, rh16, j.mbx * 16 + x4 * 4, j.mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut t[..w * h]));
4157                                let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
4158                                for dy in 0..h {
4159                                    pred_y[(y4 * 4 + dy) * 16 + x4 * 4..][..w]
4160                                        .copy_from_slice(&t[dy * w..dy * w + w]);
4161                                }
4162                            }
4163                            let (cw4, ch4) = (w4 * 2, h4 * 2);
4164                            for cc in 0..2 {
4165                                let rc = if cc == 0 { &reference.pu } else { &reference.pv };
4166                                // Same full-width coincidence for chroma: cw4 == 8 rows
4167                                // are contiguous in the 8-wide `c_pred` plane.
4168                                if cw4 == 8 {
4169                                    mc_chroma_padded(rc, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut c_pred[cc][y4 * 16..y4 * 16 + cw4 * ch4]);
4170                                    continue;
4171                                }
4172                                let mut tc = [0u8; 64];
4173                                mc_chroma_padded(rc, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8 + x4 * 2, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut tc[..cw4 * ch4]);
4174                                let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
4175                                for dy in 0..ch4 {
4176                                    c_pred[cc][(y4 * 2 + dy) * 8 + x4 * 2..][..cw4]
4177                                        .copy_from_slice(&tc[dy * cw4..dy * cw4 + cw4]);
4178                                }
4179                            }
4180                        };
4181                        if rect_eq(0, 0, 4, 4) {
4182                            mc_rect(0, 0, 4, 4, &mut pred_y, &mut c_pred);
4183                        } else if rect_eq(0, 0, 4, 2) && rect_eq(0, 2, 4, 2) {
4184                            mc_rect(0, 0, 4, 2, &mut pred_y, &mut c_pred);
4185                            mc_rect(0, 2, 4, 2, &mut pred_y, &mut c_pred);
4186                        } else if rect_eq(0, 0, 2, 4) && rect_eq(2, 0, 2, 4) {
4187                            mc_rect(0, 0, 2, 4, &mut pred_y, &mut c_pred);
4188                            mc_rect(2, 0, 2, 4, &mut pred_y, &mut c_pred);
4189                        } else {
4190                            for q in 0..4usize {
4191                                let (qx, qy) = ((q % 2) * 2, (q / 2) * 2);
4192                                if rect_eq(qx, qy, 2, 2) {
4193                                    mc_rect(qx, qy, 2, 2, &mut pred_y, &mut c_pred);
4194                                } else if rect_eq(qx, qy, 2, 1) && rect_eq(qx, qy + 1, 2, 1) {
4195                                    mc_rect(qx, qy, 2, 1, &mut pred_y, &mut c_pred);
4196                                    mc_rect(qx, qy + 1, 2, 1, &mut pred_y, &mut c_pred);
4197                                } else if rect_eq(qx, qy, 1, 2) && rect_eq(qx + 1, qy, 1, 2) {
4198                                    mc_rect(qx, qy, 1, 2, &mut pred_y, &mut c_pred);
4199                                    mc_rect(qx + 1, qy, 1, 2, &mut pred_y, &mut c_pred);
4200                                } else {
4201                                    for j in 0..4usize {
4202                                        mc_rect(qx + (j % 2), qy + (j / 2), 1, 1, &mut pred_y, &mut c_pred);
4203                                    }
4204                                }
4205                            }
4206                        }
4207                        // EXPLICIT WEIGHTED PREDICTION (spec 8.4.2.3). The CAVLC inter
4208                        // path weights each partition after MC; the MC-call-coalescing
4209                        // rewrite of this CABAC path lost it, and nothing caught that
4210                        // because the effect is invisible unless a stream actually
4211                        // carries non-default weights. x264's `weightp` DUPLICATES a
4212                        // reference and distinguishes the copy ONLY by its weights, so
4213                        // every macroblock picking the weighted index decoded unweighted
4214                        // -- a silent, accumulating luma drift.
4215                        //
4216                        // Applied per 4x4 block rather than per partition: the weight
4217                        // depends solely on the block's reference index, so the two are
4218                        // equivalent, and `gref` already holds it for every block
4219                        // regardless of which rect ladder rung ran.
4220                        if self.weights.is_some() {
4221                            for by in 0..4usize {
4222                                for bx in 0..4usize {
4223                                    let refi = gref[by * 4 + bx];
4224                                    self.weight_partition(
4225                                        &mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4,
4226                                    );
4227                                }
4228                            }
4229                        }
4230                    }
4231                    // Residual add — the SAME helper the B path uses (this inline
4232                    // copy was a duplicate; deduped when the zero-block fast path
4233                    // landed so both paths share it).
4234                    self.add_inter_residual(j.mbx, j.mby, &pred_y, &c_pred, &j.luma_scan, if j.t8 { Some(&j.luma8) } else { None }, &j.cdc, &j.cac, j.cbp_chroma, &j.nnzs);
4235        self.cur_qp = saved_qp;
4236    }
4237
4238    fn decode_p_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
4239        // DEBLOCK CLASS: a P_Skip macroblock carries no coefficients and one
4240        // (ref, mv) for all 16 blocks, so every internal boundary strength is 0 by
4241        // §8.7.2.1 and the loop filter needs 9 block loads instead of 24. This is
4242        // the single highest-value classification: the MB-kind census measures Skip
4243        // at 36.4% (CAVLC) / 65.0% (main) / 57.8% (high) of real x264 corpora.
4244        // Written HERE because both the CAVLC and the CABAC slice loops funnel
4245        // through this one function.
4246        //
4247        // Deliberately NOT done for `B_Skip` — its motion is direct-derived and can
4248        // differ per 4×4 sub-block, so its internal edges can legally reach
4249        // strength 1. B_Skip stays UNSET and takes the blind path.
4250        self.mb_kind[mb_y * self.mb_w + mb_x] = rusty_h264_common::deblock::MB_KIND_SKIP;
4251        // P_Skip always references index 0 (the most recent picture). Borrow it —
4252        // a full-frame `.cloned()` here was ~86% of total decode time (one ~3 MB
4253        // plane copy per skip MB, thousands per frame).
4254        if self.refs.is_empty() {
4255            return Err(MbError::Unsupported("P_Skip without reference"));
4256        }
4257        let mv = self.skip_mv(mb_x, mb_y);
4258        // Grid commits are PARSE state (later macroblocks' MV prediction and
4259        // availability read them) — they run now; the pixel half reads only
4260        // the DPB + `mv`, so it defers cleanly (E1 seam).
4261        {
4262            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
4263            self.set_mb_mv(mb_x, mb_y, mv, true, 0);
4264            let w4 = self.mb_w * 4;
4265            for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
4266                self.coded_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = true;
4267                self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
4268            }
4269        }
4270        if self.edc_tx.is_some() {
4271            self.edc_giveback();
4272            self.edc_send_job(EdcJob::Skip { mbx: mb_x, mby: mb_y, mv });
4273            return Ok(());
4274        }
4275        if self.edc_active {
4276            self.edc_jobs.push(EdcJob::Skip { mbx: mb_x, mby: mb_y, mv });
4277            return Ok(());
4278        }
4279        self.recon_p_skip(mb_x, mb_y, mv);
4280        if double_recon() {
4281            self.recon_p_skip(mb_x, mb_y, mv);
4282        }
4283        Ok(())
4284    }
4285
4286    /// Pixel half of P_Skip (see the E1 seam note on `recon_p_inter`).
4287    fn recon_p_skip(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32)) {
4288        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
4289
4290        let mut pred = [0u8; 256];
4291        let rf0 = &self.refs[0];
4292        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);
4293        if let Some(wt) = &self.weights {
4294            for p in pred.iter_mut() {
4295                *p = wt.apply_luma(*p, 0, 0);
4296            }
4297        }
4298        {
4299            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
4300            for dy in 0..16 {
4301                let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
4302                self.rec_y[d..d + 16].copy_from_slice(&pred[dy * 16..dy * 16 + 16]);
4303            }
4304        }
4305        for c in 0..2 {
4306            let mut pc = [0u8; 64];
4307            let rf0 = &self.refs[0];
4308            let rc = if c == 0 { &rf0.pu } else { &rf0.pv };
4309            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);
4310            if let Some(wt) = &self.weights {
4311                for p in pc.iter_mut() {
4312                    *p = wt.apply_chroma(*p, 0, 0, c);
4313                }
4314            }
4315            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
4316            for dy in 0..8 {
4317                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
4318                plane[d..d + 8].copy_from_slice(&pc[dy * 8..dy * 8 + 8]);
4319            }
4320        }
4321    }
4322
4323    /// Predicted `Intra_4x4` mode for the block at absolute coords `(bx, by)`.
4324    /// If either the left or top neighbor is outside the frame or in another
4325    /// slice, the prediction is DC (mode 2) (spec §8.3.1.1).
4326    fn predict_i4_mode(&self, bx: usize, by: usize) -> u8 {
4327        if bx == 0 || by == 0 {
4328            return 2;
4329        }
4330        // Left neighbor block (bx-1,by); top neighbor block (bx,by-1). A neighbor
4331        // in another slice — or, under constrained_intra, an inter neighbor — is
4332        // unavailable, forcing the predicted mode to DC.
4333        if !self.nbr_in_slice((bx - 1) / 4, by / 4)
4334            || !self.nbr_in_slice(bx / 4, (by - 1) / 4)
4335            || !self.intra_nbr_ok(bx - 1, by)
4336            || !self.intra_nbr_ok(bx, by - 1)
4337        {
4338            return 2;
4339        }
4340        let w4 = self.mb_w * 4;
4341        self.modes_y[by * w4 + (bx - 1)].min(self.modes_y[(by - 1) * w4 + bx])
4342    }
4343
4344    /// Gathers 4×4 luma intra neighbors at pixel `(px, py)` from `rec_y`.
4345    fn gather_i4(
4346        &self,
4347        px: usize,
4348        py: usize,
4349        avail_top: bool,
4350        avail_left: bool,
4351        bx: usize,
4352        by: usize,
4353    ) -> ([u8; 8], [u8; 4], u8) {
4354        let (cw, w4) = (self.cw, self.mb_w * 4);
4355        let mut top = [0u8; 8];
4356        let mut left = [0u8; 4];
4357        let mut corner = 0;
4358        if avail_top {
4359            for i in 0..4 {
4360                top[i] = self.top_y_px(py, px + i);
4361            }
4362            let tr_avail = bx + 1 < w4
4363                && self.coded_y[(by - 1) * w4 + (bx + 1)]
4364                && self.nbr_in_slice((bx + 1) / 4, (by - 1) / 4)
4365                && self.intra_nbr_ok(bx + 1, by - 1);
4366            for i in 0..4 {
4367                top[4 + i] = if tr_avail {
4368                    self.top_y_px(py, px + 4 + i)
4369                } else {
4370                    top[3]
4371                };
4372            }
4373        }
4374        if avail_left {
4375            for i in 0..4 {
4376                left[i] = self.rec_y[(py + i) * cw + px - 1];
4377            }
4378        }
4379        // The above-left corner has its own availability (block D); under
4380        // constrained_intra it is gone if that block is inter.
4381        if avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1) {
4382            corner = self.top_y_px(py, px - 1);
4383        }
4384        (top, left, corner)
4385    }
4386
4387    /// Reconstructs an `I_PCM` macroblock: byte-aligned raw 8-bit samples, no
4388    /// prediction/transform/quant (spec §7.3.5, §8.3.5).
4389    fn decode_ipcm(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
4390        r.align_to_byte()?;
4391        let (lx, ly) = (mb_x * 16, mb_y * 16);
4392        for dy in 0..16 {
4393            for dx in 0..16 {
4394                self.rec_y[(ly + dy) * self.cw + (lx + dx)] = r.read_bits(8)? as u8;
4395            }
4396        }
4397        let (cx, cy) = (mb_x * 8, mb_y * 8);
4398        for plane in [&mut self.rec_u, &mut self.rec_v] {
4399            for dy in 0..8 {
4400                for dx in 0..8 {
4401                    plane[(cy + dy) * self.ccw + (cx + dx)] = r.read_bits(8)? as u8;
4402                }
4403            }
4404        }
4405        // Neighbor context: an I_PCM block contributes TotalCoeff = 16, counts as
4406        // intra with DC mode for prediction, and has no motion (§9.2.1, §8.3.1.2.2).
4407        let (w4, w2) = (self.mb_w * 4, self.mb_w * 2);
4408        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
4409            let idx = (mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx);
4410            self.nnz_y[idx] = 16;
4411            self.modes_y[idx] = 2;
4412            self.inter_y[idx] = false;
4413            self.ref_idx_y[idx] = -1;
4414            self.mv_y[idx] = (0, 0);
4415        }
4416        for c in 0..2 {
4417            for by in 0..2 {
4418                for bx in 0..2 {
4419                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = 16;
4420                }
4421            }
4422        }
4423        Ok(())
4424    }
4425
4426    fn decode_i4x4(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
4427        let w4 = self.mb_w * 4;
4428
4429        // intra4x4 mode signalling
4430        let mut modes = [2u8; 16]; // raster [lby*4+lbx]
4431        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
4432            let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
4433            let predicted = self.predict_i4_mode(bx, by);
4434            let actual = if r.read_bit()? {
4435                predicted
4436            } else {
4437                let rem = r.read_bits(3)? as u8;
4438                if rem < predicted {
4439                    rem
4440                } else {
4441                    rem + 1
4442                }
4443            };
4444            self.modes_y[by * w4 + bx] = actual;
4445            modes[lby * 4 + lbx] = actual;
4446        }
4447
4448        let chroma_mode = r.read_ue()? as u8;
4449        let cbp = read_cbp_intra(r)?;
4450        let cbp_luma = cbp & 15;
4451        let cbp_chroma = cbp >> 4;
4452        if cbp != 0 {
4453            self.step_qp(r.read_se()?);
4454        }
4455        let qp = self.cur_qp;
4456
4457        // luma residuals + serial reconstruction. Cross-MB neighbors are only
4458        // available when the adjacent macroblock is in this slice (and, under
4459        // constrained_intra_pred, is itself intra-coded).
4460        let top_mb_avail = mb_y > 0
4461            && self.nbr_in_slice(mb_x, mb_y - 1)
4462            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
4463        let left_mb_avail = mb_x > 0
4464            && self.nbr_in_slice(mb_x - 1, mb_y)
4465            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
4466        self.nnz_cache_load(mb_x, mb_y);
4467        for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
4468            let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
4469            let (px, py) = (bx * 4, by * 4);
4470            let avail_top = lby > 0 || top_mb_avail;
4471            let avail_left = lbx > 0 || left_mb_avail;
4472            let mut qb = [0i32; 16];
4473            let total = if cbp_luma & (1 << (blk / 4)) != 0 {
4474                let nc = self.nc_pred(lbx, lby);
4475                let scan16 = decode_residual_block(r, 16, nc)?;
4476                qb = un_scan_4x4_dcac(&scan16);
4477                scan16.iter().filter(|&&v| v != 0).count() as u8
4478            } else {
4479                0
4480            };
4481            self.nnz_cache_set(lbx, lby, total);
4482            self.nnz_y[by * w4 + bx] = total;
4483            let (top, left, corner) = self.gather_i4(px, py, avail_top, avail_left, bx, by);
4484            let pred = intra4x4_pred(modes[lby * 4 + lbx], avail_top, avail_left, &top, &left, corner);
4485            let mut predb = [0i32; 16];
4486            for i in 0..16 {
4487                predb[i] = pred[i] as i32;
4488            }
4489            let s = reconstruct_4x4(&self.dequant(&qb, qp, 0), &predb);
4490            store(&mut self.rec_y, self.cw, px, py, &s);
4491            self.coded_y[by * w4 + bx] = true;
4492        }
4493
4494        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
4495    }
4496
4497    /// Decodes an `I_8x8` macroblock (High profile): four 8×8 luma blocks, each
4498    /// with its own intra mode, 8×8 transform residual (CAVLC = four interleaved
4499    /// 4×4 blocks), and 8×8 intra prediction.
4500    fn decode_i8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
4501        let w4 = self.mb_w * 4;
4502        self.mb_t8x8[mb_y * self.mb_w + mb_x] = true;
4503
4504        // intra8x8 mode signalling — one mode per 8×8 block (raster 0..3),
4505        // stored into all four of its 4×4 cells so neighbors can read it.
4506        let mut modes8 = [2u8; 4];
4507        for (b8, mode) in modes8.iter_mut().enumerate() {
4508            let (b8x, b8y) = (b8 % 2, b8 / 2);
4509            let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
4510            let predicted = self.predict_i4_mode(bx, by);
4511            let actual = if r.read_bit()? {
4512                predicted
4513            } else {
4514                let rem = r.read_bits(3)? as u8;
4515                if rem < predicted { rem } else { rem + 1 }
4516            };
4517            *mode = actual;
4518            for sy in 0..2 {
4519                for sx in 0..2 {
4520                    self.modes_y[(by + sy) * w4 + (bx + sx)] = actual;
4521                }
4522            }
4523        }
4524
4525        let chroma_mode = r.read_ue()? as u8;
4526        let cbp = read_cbp_intra(r)?;
4527        let cbp_luma = cbp & 15;
4528        let cbp_chroma = cbp >> 4;
4529        if cbp != 0 {
4530            self.step_qp(r.read_se()?);
4531        }
4532        let qp = self.cur_qp;
4533
4534        let top_mb_avail = mb_y > 0
4535            && self.nbr_in_slice(mb_x, mb_y - 1)
4536            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
4537        let left_mb_avail = mb_x > 0
4538            && self.nbr_in_slice(mb_x - 1, mb_y)
4539            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
4540        self.nnz_cache_load(mb_x, mb_y);
4541
4542        for b8 in 0..4 {
4543            let (b8x, b8y) = (b8 % 2, b8 / 2);
4544            let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
4545            let (px, py) = (bx * 4, by * 4);
4546
4547            // residual: 8×8 CAVLC = four 4×4 sub-blocks, coeff k of sub-block s
4548            // mapping to 8×8 scan position 4·k + s (spec §7.3.5.3.2).
4549            let mut res8 = [0i32; 64];
4550            if cbp_luma & (1 << b8) != 0 {
4551                let mut scan8 = [0i32; 64];
4552                for sub in 0..4 {
4553                    let (sx, sy) = (sub % 2, sub / 2);
4554                    let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
4555                    let nc = self.nc_pred(cx, cy);
4556                    let blk = decode_residual_block(r, 16, nc)?;
4557                    let total = blk.iter().filter(|&&v| v != 0).count() as u8;
4558                    self.nnz_cache_set(cx, cy, total);
4559                    self.nnz_y[(by + sy) * w4 + (bx + sx)] = total;
4560                    for k in 0..16 {
4561                        scan8[4 * k + sub] = blk[k];
4562                    }
4563                }
4564                let raster = un_scan_8x8(&scan8);
4565                res8 = self.inv_quant8(&raster, qp, 0);
4566            } else {
4567                for sub in 0..4 {
4568                    let (sx, sy) = (sub % 2, sub / 2);
4569                    self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
4570                    self.nnz_y[(by + sy) * w4 + (bx + sx)] = 0;
4571                }
4572            }
4573
4574            let avail_top = b8y > 0 || top_mb_avail;
4575            let avail_left = b8x > 0 || left_mb_avail;
4576            let (top, left, corner, avail_corner) =
4577                self.gather_i8(px, py, avail_top, avail_left, bx, by);
4578            let pred = intra8x8_pred(
4579                modes8[b8], avail_top, avail_left, avail_corner, &top, &left, corner,
4580            );
4581            let mut predb = [0i32; 64];
4582            for i in 0..64 {
4583                predb[i] = pred[i] as i32;
4584            }
4585            let recon = add_residual_8x8(&res8, &predb);
4586            for dy in 0..8 {
4587                for dx in 0..8 {
4588                    self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
4589                }
4590            }
4591            for sy in 0..2 {
4592                for sx in 0..2 {
4593                    self.coded_y[(by + sy) * w4 + (bx + sx)] = true;
4594                }
4595            }
4596        }
4597
4598        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
4599    }
4600
4601    /// Dequantizes + inverse-transforms an 8×8 luma block, applying the scaling
4602    /// matrix `list` (0 = intra, 1 = inter) or flat weights.
4603    fn inv_quant8(&self, raster: &[i32; 64], qp: u8, list: usize) -> [i32; 64] {
4604        match &self.scaling8 {
4605            Some(s) => inverse_quant_8x8(raster, qp, &s[list]),
4606            None => inverse_quant_8x8(raster, qp, &[16i32; 64]),
4607        }
4608    }
4609
4610    /// Gathers the 8×8 luma intra reference samples at pixel `(px, py)`: the 16
4611    /// top samples (8..15 substituted from the last when no top-right), 8 left
4612    /// samples, the above-left corner, and whether the corner is available.
4613    #[allow(clippy::too_many_arguments)]
4614    fn gather_i8(
4615        &self,
4616        px: usize,
4617        py: usize,
4618        avail_top: bool,
4619        avail_left: bool,
4620        bx: usize,
4621        by: usize,
4622    ) -> ([u8; 16], [u8; 8], u8, bool) {
4623        let (cw, w4) = (self.cw, self.mb_w * 4);
4624        let mut top = [0u8; 16];
4625        let mut left = [0u8; 8];
4626        let mut corner = 0;
4627        if avail_top {
4628            for i in 0..8 {
4629                top[i] = self.top_y_px(py, px + i);
4630            }
4631            let tr_avail = bx + 2 < w4
4632                && self.coded_y[(by - 1) * w4 + (bx + 2)]
4633                && self.nbr_in_slice((bx + 2) / 4, (by - 1) / 4)
4634                && self.intra_nbr_ok(bx + 2, by - 1);
4635            for i in 0..8 {
4636                top[8 + i] = if tr_avail {
4637                    self.top_y_px(py, px + 8 + i)
4638                } else {
4639                    top[7]
4640                };
4641            }
4642        }
4643        if avail_left {
4644            for i in 0..8 {
4645                left[i] = self.rec_y[(py + i) * cw + px - 1];
4646            }
4647        }
4648        let avail_corner = avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1);
4649        if avail_corner {
4650            corner = self.top_y_px(py, px - 1);
4651        }
4652        (top, left, corner, avail_corner)
4653    }
4654
4655    fn decode_i16(
4656        &mut self,
4657        r: &mut BitReader,
4658        mb_x: usize,
4659        mb_y: usize,
4660        mt: u32,
4661    ) -> Result<(), MbError> {
4662        let pred_mode = I16Mode::from_id(mt % 4);
4663        let cbp_chroma = (mt % 12) / 4;
4664        let cbp_luma_15 = mt / 12 == 1;
4665        let chroma_mode = r.read_ue()? as u8;
4666        self.step_qp(r.read_se()?);
4667        let qp = self.cur_qp;
4668        let w4 = self.mb_w * 4;
4669
4670        // luma DC
4671        self.nnz_cache_load(mb_x, mb_y);
4672        let nc_dc = self.nc_pred(0, 0);
4673        let dc_scan = decode_residual_block(r, 16, nc_dc)?;
4674        let dc_levels = un_scan_4x4_dcac(&dc_scan);
4675        let recon_dc = self.dequant_luma_dc(&dc_levels, qp, 0);
4676
4677        // luma AC (nnz set for all 16 blocks: 0 when DC-only, matching the encoder)
4678        let mut q_blocks = [[0i32; 16]; 16];
4679        for &(bx, by) in &LUMA_4X4_SCAN_XY {
4680            let total = if cbp_luma_15 {
4681                let nc = self.nc_pred(bx, by);
4682                let ac = decode_residual_block(r, 15, nc)?;
4683                un_scan_4x4_ac_into(&ac, &mut q_blocks[by * 4 + bx]);
4684                ac.iter().filter(|&&v| v != 0).count() as u8
4685            } else {
4686                0
4687            };
4688            self.nnz_cache_set(bx, by, total);
4689            self.nnz_y[(mb_y * 4 + by) * w4 + (mb_x * 4 + bx)] = total;
4690        }
4691
4692        // prediction + reconstruction
4693        let avail_top = mb_y > 0
4694            && self.nbr_in_slice(mb_x, mb_y - 1)
4695            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
4696        let avail_left = mb_x > 0
4697            && self.nbr_in_slice(mb_x - 1, mb_y)
4698            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
4699        let (lx, ly) = (mb_x * 16, mb_y * 16);
4700        let mut top = [0u8; 16];
4701        let mut left = [0u8; 16];
4702        if avail_top {
4703            for i in 0..16 {
4704                top[i] = self.top_y_px(ly, lx + i);
4705            }
4706        }
4707        if avail_left {
4708            for i in 0..16 {
4709                left[i] = self.rec_y[(ly + i) * self.cw + lx - 1];
4710            }
4711        }
4712        let corner = if avail_top && avail_left {
4713            self.top_y_px(ly, lx - 1)
4714        } else {
4715            0
4716        };
4717        let pred_l = luma16x16_pred(pred_mode, avail_top, avail_left, &top, &left, corner);
4718        for by in 0..4 {
4719            for bx in 0..4 {
4720                let mut deq = self.dequant(&q_blocks[by * 4 + bx], qp, 0);
4721                deq[0] = recon_dc[by * 4 + bx];
4722                let mut predb = [0i32; 16];
4723                for dy in 0..4 {
4724                    for dx in 0..4 {
4725                        predb[dy * 4 + dx] = pred_l[(by * 4 + dy) * 16 + (bx * 4 + dx)] as i32;
4726                    }
4727                }
4728                let s = reconstruct_4x4(&deq, &predb);
4729                store(&mut self.rec_y, self.cw, lx + bx * 4, ly + by * 4, &s);
4730            }
4731        }
4732        // I_16x16 blocks are treated as DC for neighbor mode prediction.
4733        for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
4734            self.modes_y[(mb_y * 4 + lby) * w4 + (mb_x * 4 + lbx)] = 2;
4735        }
4736
4737        self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
4738    }
4739
4740    /// Reads and reconstructs the chroma residual (shared by both luma types).
4741    fn decode_chroma(
4742        &mut self,
4743        r: &mut BitReader,
4744        mb_x: usize,
4745        mb_y: usize,
4746        cbp_chroma: u32,
4747        chroma_mode: u8,
4748    ) -> Result<(), MbError> {
4749        let qpc = self.chroma_qp_for(self.cur_qp);
4750        let (cx, cy) = (mb_x * 8, mb_y * 8);
4751        let avail_top = mb_y > 0
4752            && self.nbr_in_slice(mb_x, mb_y - 1)
4753            && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
4754        let avail_left = mb_x > 0
4755            && self.nbr_in_slice(mb_x - 1, mb_y)
4756            && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
4757
4758        let mut c_recon_dc = [[0i32; 4]; 2];
4759        if cbp_chroma != 0 {
4760            for (c, slot) in c_recon_dc.iter_mut().enumerate() {
4761                let dc = decode_residual_block(r, 4, -1)?;
4762                *slot = self.dequant_chroma_dc(&[dc[0], dc[1], dc[2], dc[3]], qpc, 1 + c);
4763            }
4764        }
4765        let mut c_q_blocks = [[[0i32; 16]; 4]; 2];
4766        if cbp_chroma == 2 {
4767            self.chroma_cache_load(mb_x, mb_y);
4768            let w2 = self.mb_w * 2;
4769            for c in 0..2 {
4770                for &(bx, by) in &CHROMA_4X4_SCAN_XY {
4771                    let nc = self.chroma_nc_pred(c, bx, by);
4772                    let ac = decode_residual_block(r, 15, nc)?;
4773                    let total = ac.iter().filter(|&&v| v != 0).count() as u8;
4774                    self.chroma_nnz_cache_set(c, bx, by, total);
4775                    self.nnz_c[c][(mb_y * 2 + by) * w2 + (mb_x * 2 + bx)] = total;
4776                    un_scan_4x4_ac_into(&ac, &mut c_q_blocks[c][by * 2 + bx]);
4777                }
4778            }
4779        }
4780        for c in 0..2 {
4781            let mut ctop = [0u8; 8];
4782            let mut cleft = [0u8; 8];
4783            let mut ccorner = 0u8;
4784            {
4785                let rec_c = if c == 0 { &self.rec_u } else { &self.rec_v };
4786                if avail_top {
4787                    for i in 0..8 {
4788                        ctop[i] = self.top_c_px(c, cy, cx + i);
4789                    }
4790                }
4791                if avail_left {
4792                    for i in 0..8 {
4793                        cleft[i] = rec_c[(cy + i) * self.ccw + cx - 1];
4794                    }
4795                }
4796                if avail_top && avail_left {
4797                    ccorner = self.top_c_px(c, cy, cx - 1);
4798                }
4799            }
4800            let pred8 = chroma8x8_pred(chroma_mode, avail_top, avail_left, &ctop, &cleft, ccorner);
4801            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
4802                let mut predb = [0i32; 16];
4803                for dy in 0..4 {
4804                    for dx in 0..4 {
4805                        predb[dy * 4 + dx] = pred8[(by * 4 + dy) * 8 + (bx * 4 + dx)] as i32;
4806                    }
4807                }
4808                let mut deq = self.dequant(&c_q_blocks[c][by * 2 + bx], qpc, 1 + c);
4809                deq[0] = c_recon_dc[c][by * 2 + bx];
4810                let s = reconstruct_4x4(&deq, &predb);
4811                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
4812                store(plane, self.ccw, cx + bx * 4, cy + by * 4, &s);
4813            }
4814        }
4815        Ok(())
4816    }
4817
4818    /// Applies the in-loop deblocking filter to the reconstructed frame, with
4819    /// the slice's `FilterOffsetA`/`FilterOffsetB` (each = the coded `*_div2`
4820    /// value × 2).
4821    /// Per-frame per-MB dump for conformance bisection, keyed on `RH264_DUMP_MB`.
4822    /// Prints one char per macroblock: `i` = intra, otherwise the List-0 reference
4823    /// index of the MB's top-left 4x4 block. Directly comparable with ffmpeg's
4824    /// `-debug mb_type` map, which is the only per-MB ground truth we can get out
4825    /// of the reference decoder.
4826    fn dump_mb_map(&self) {
4827        if std::env::var_os("RH264_DUMP_MB").is_none() {
4828            return;
4829        }
4830        let w4 = self.mb_w * 4;
4831        let mut hist = [0usize; 4];
4832        eprintln!("--- frame poc {} ---", self.cur_poc);
4833        for mb_y in 0..self.mb_h {
4834            let mut row = String::new();
4835            for mb_x in 0..self.mb_w {
4836                let b = (mb_y * 4) * w4 + mb_x * 4;
4837                let r = self.ref_idx_y[b];
4838                if r < 0 {
4839                    row.push('i');
4840                } else {
4841                    if (r as usize) < 4 {
4842                        hist[r as usize] += 1;
4843                    }
4844                    row.push((b'0' + (r as u8).min(9)) as char);
4845                }
4846            }
4847            eprintln!("{row}");
4848        }
4849        eprintln!(
4850            "ref histogram: {hist:?}   num_ref_active={} refs.len()={}   OUT-OF-RANGE={}",
4851            self.num_ref_active,
4852            self.refs.len(),
4853            hist.iter().skip(self.refs.len()).sum::<usize>()
4854        );
4855        let list: Vec<String> = self
4856            .refs
4857            .iter()
4858            .enumerate()
4859            .map(|(i, f)| {
4860                // A synthesized frame_num-gap frame is uniform grey with w4 == 0;
4861                // flag it, because it silently displaces real pictures in the list.
4862                let synth = if f.w4 == 0 { " SYNTH-GREY" } else { "" };
4863                format!("[{i}] poc={} fn={}{synth}", f.poc, f.frame_num)
4864            })
4865            .collect();
4866        eprintln!("  RefPicList0: {}", list.join("  "));
4867    }
4868
4869    pub fn deblock(&mut self, offset_a: i32, offset_b: i32) {
4870        self.edc_flush(); // backstop: no pixel job may survive to filtering
4871        self.dump_mb_map();
4872        // ROW MODE: finish any rows not derived during decode (mid-row slice
4873        // ends, error paths) FIRST, while `self` is still mutably borrowable.
4874        if rowdb_on() {
4875            while self.bs_rows < self.mb_h {
4876                let r = self.bs_rows;
4877                self.derive_bs_row(r);
4878                self.bs_rows += 1;
4879            }
4880        }
4881        // Deblock boundary strength uses the *transform block's* coded status. For
4882        // an 8×8-transform macroblock the unit is the whole 8×8, so every 4×4 cell
4883        // shares the 8×8's coefficient presence (OR of its four sub-block counts)
4884        // — distinct from the per-sub-block `nnz_y` used for the CAVLC nC context.
4885        // Only differs from `nnz_y` when some MB uses the 8×8 transform (High
4886        // profile). On Baseline (no 8×8) it's identical — skip the clone + rewrite.
4887        let nnz_db_storage;
4888        let nnz_db: &[u8] = if self.mb_t8x8.iter().any(|&t| t) {
4889            let mut n = self.nnz_y.clone();
4890            let w4 = self.mb_w * 4;
4891            for mb_y in 0..self.mb_h {
4892                for mb_x in 0..self.mb_w {
4893                    if !self.mb_t8x8[mb_y * self.mb_w + mb_x] {
4894                        continue;
4895                    }
4896                    for b8 in 0..4 {
4897                        let (bx, by) = (mb_x * 4 + (b8 % 2) * 2, mb_y * 4 + (b8 / 2) * 2);
4898                        let any = (0..2).any(|sy| (0..2).any(|sx| self.nnz_y[(by + sy) * w4 + (bx + sx)] > 0));
4899                        for sy in 0..2 {
4900                            for sx in 0..2 {
4901                                n[(by + sy) * w4 + (bx + sx)] = u8::from(any);
4902                            }
4903                        }
4904                    }
4905                }
4906            }
4907            nnz_db_storage = n;
4908            &nnz_db_storage
4909        } else {
4910            &self.nnz_y
4911        };
4912        // Map per-block reference indices to a stable picture identity (POC) so
4913        // the boundary-strength comparison recognises the same picture across lists.
4914        // ≤16-entry ref→POC maps; the frame-wide pre-mapped Vec shims this
4915        // replaces cost 230-460 KB + 57,600 mapped elements PER FRAME (WHYS
4916        // Part 15 item 2) — `pack_frame` now maps per block via `poc0`/`poc1`.
4917        let poc0: Vec<i32> = self.refs.iter().map(|f| f.poc).collect();
4918        let poc1: Vec<i32> = self.refs1.iter().map(|f| f.poc).collect();
4919        let mut info = rusty_h264_common::deblock::BlockInfo {
4920            inter: &self.inter_y,
4921            nnz: nnz_db,
4922            mv: &self.mv_y,
4923            ref_id: &self.ref_idx_y,
4924            mv1: &self.mv1,
4925            ref_id1: if poc1.is_empty() { &[] } else { &self.ref_idx1 },
4926            w4: self.mb_w * 4,
4927            t8x8: &self.mb_t8x8,
4928            bs: &[],
4929            poc0: &poc0,
4930            poc1: &poc1,
4931            kind: &self.mb_kind,
4932        };
4933        // ROW MODE (R2): rows were derived during decode; the remainder was
4934        // finished above (before `info` borrowed the grids). Fallback: the
4935        // Part 16/17 picture-end precompute; `RS_H264_BS_PRE=0` further falls
4936        // back to the pack-then-derive-in-loop pipeline.
4937        let bs_store;
4938        if rowdb_on() {
4939            bs_store = std::mem::take(&mut self.bs_frame);
4940            info.bs = &bs_store;
4941        } else if !std::env::var_os("RS_H264_BS_PRE").is_some_and(|v| v == "0") {
4942            let mut buf = Vec::new();
4943            rusty_h264_common::deblock::precompute_bs_frame(&info, self.mb_w, self.mb_h, &mut buf);
4944            bs_store = buf;
4945            info.bs = &bs_store;
4946        } else {
4947            bs_store = Vec::new();
4948        }
4949        let first_row = if rowdb_on() { self.flt_rows } else { 0 };
4950        rusty_h264_common::deblock::filter_frame_rows(
4951            &mut self.rec_y,
4952            &mut self.rec_u,
4953            &mut self.rec_v,
4954            self.mb_w,
4955            self.mb_h,
4956            first_row..self.mb_h,
4957            &self.mb_qp,
4958            self.chroma_qp_offset,
4959            offset_a,
4960            offset_b,
4961            &info,
4962        );
4963        drop(info);
4964        if rowdb_on() {
4965            self.bs_frame = bs_store;
4966        }
4967    }
4968
4969    /// Crops the reconstructed coded-size planes to the display window.
4970    /// `into_frame`, additionally handing the per-picture grids back for reuse by
4971    /// the next picture. See `GridPool` for why this is worth doing.
4972    pub fn into_frame_recycle(mut self, crop_r: usize, crop_b: usize) -> (YuvFrame, GridPool) {
4973        let [c0, c1] = std::mem::take(&mut self.nnz_c);
4974        let pool = GridPool {
4975            bits_per_mb: self.bits_per_mb,
4976            mb_qp: std::mem::take(&mut self.mb_qp),
4977            bs_frame: std::mem::take(&mut self.bs_frame),
4978            pk_prev: std::mem::take(&mut self.pk_prev),
4979            pk_cur: std::mem::take(&mut self.pk_cur),
4980            nnz_dbr: std::mem::take(&mut self.nnz_dbr),
4981            bak_y: std::mem::take(&mut self.bak_y),
4982            bak_u: std::mem::take(&mut self.bak_u),
4983            bak_v: std::mem::take(&mut self.bak_v),
4984            nnz_y: std::mem::take(&mut self.nnz_y),
4985            nnz_c0: c0,
4986            nnz_c1: c1,
4987            modes_y: std::mem::take(&mut self.modes_y),
4988            coded_y: std::mem::take(&mut self.coded_y),
4989            mv_y: std::mem::take(&mut self.mv_y),
4990            inter_y: std::mem::take(&mut self.inter_y),
4991            ref_idx_y: std::mem::take(&mut self.ref_idx_y),
4992            mv1: std::mem::take(&mut self.mv1),
4993            ref_idx1: std::mem::take(&mut self.ref_idx1),
4994            mb_t8x8: std::mem::take(&mut self.mb_t8x8),
4995            mb_kind: std::mem::take(&mut self.mb_kind),
4996        };
4997        (self.into_frame(crop_r, crop_b), pool)
4998    }
4999
5000    pub fn into_frame(self, crop_r: usize, crop_b: usize) -> YuvFrame {
5001        // No cropping (the common case): the reconstruction planes ARE the output —
5002        // move them out instead of allocating + copying three full planes per frame.
5003        if crop_r == 0 && crop_b == 0 {
5004            return YuvFrame {
5005                width: self.cw,
5006                height: self.ch,
5007                y: self.rec_y,
5008                u: self.rec_u,
5009                v: self.rec_v,
5010            };
5011        }
5012        let dw = self.cw - 2 * crop_r;
5013        let dh = self.ch - 2 * crop_b;
5014        let mut y = vec![0u8; dw * dh];
5015        for row in 0..dh {
5016            y[row * dw..row * dw + dw].copy_from_slice(&self.rec_y[row * self.cw..row * self.cw + dw]);
5017        }
5018        let (cdw, cdh) = (dw / 2, dh / 2);
5019        let mut u = vec![0u8; cdw * cdh];
5020        let mut v = vec![0u8; cdw * cdh];
5021        for row in 0..cdh {
5022            u[row * cdw..row * cdw + cdw]
5023                .copy_from_slice(&self.rec_u[row * self.ccw..row * self.ccw + cdw]);
5024            v[row * cdw..row * cdw + cdw]
5025                .copy_from_slice(&self.rec_v[row * self.ccw..row * self.ccw + cdw]);
5026        }
5027        let _ = self.cch;
5028        YuvFrame {
5029            width: dw,
5030            height: dh,
5031            y,
5032            u,
5033            v,
5034        }
5035    }
5036}
5037
5038/// Reads `ref_idx_l0` as `te(v)` with range `num_ref_active - 1`: a single flag
5039/// when exactly two references are active (cMax == 1), else `ue(v)`.
5040// ---- CABAC binarization engine helpers (openh264 cabac_decoder.cpp) ----
5041
5042/// Unary bin (`DecodeUnaryBinCabac`): bin0 at `ctx`; if 1, count bins at `ctx+off`
5043/// (including the terminating 0) until a 0.
5044fn cabac_unary(cab: &mut crate::cabac::Cabac, ctx: usize, off: usize) -> u32 {
5045    if cab.decode_decision(ctx) == 0 {
5046        return 0;
5047    }
5048    let mut sym = 0;
5049    loop {
5050        let bin = cab.decode_decision(ctx + off);
5051        sym += 1;
5052        // Cap the unary run: no valid H.264 element coded through this helper
5053        // (mb_qp_delta) exceeds a few dozen bins, but on malformed / buffer-exhausted
5054        // input the arithmetic engine keeps yielding 1s (it zero-fills past the end),
5055        // which would loop forever. 512 is far beyond any legal value.
5056        if bin == 0 || sym >= 512 {
5057            break;
5058        }
5059    }
5060    sym
5061}
5062
5063/// k-th order Exp-Golomb in bypass (`DecodeExpBypassCabac`).
5064fn cabac_exp_bypass(cab: &mut crate::cabac::Cabac, mut count: i32) -> u32 {
5065    let mut sym = 0u32;
5066    loop {
5067        let c = cab.decode_bypass();
5068        if c == 1 {
5069            sym += 1 << count;
5070            count += 1;
5071        }
5072        if c == 0 || count == 16 {
5073            break;
5074        }
5075    }
5076    let mut sym2 = 0u32;
5077    while count > 0 {
5078        count -= 1;
5079        if cab.decode_bypass() != 0 {
5080            sym2 |= 1 << count;
5081        }
5082    }
5083    sym + sym2
5084}
5085
5086/// UEG0 coeff-level suffix (`DecodeUEGLevelCabac`): TU prefix at `ctx` (≤13) then an
5087/// EG0 bypass suffix.
5088fn cabac_ueg_level(cab: &mut crate::cabac::Cabac, ctx: usize) -> u32 {
5089    if cab.decode_decision(ctx) == 0 {
5090        return 0;
5091    }
5092    let mut code = 0u32;
5093    let mut count = 1;
5094    let mut tmp;
5095    loop {
5096        tmp = cab.decode_decision(ctx);
5097        code += 1;
5098        count += 1;
5099        if tmp == 0 || count == 13 {
5100            break;
5101        }
5102    }
5103    if tmp != 0 {
5104        code += cabac_exp_bypass(cab, 0) + 1;
5105    }
5106    code
5107}
5108
5109/// `mb_qp_delta` CABAC (`ParseDeltaQpCabac`): ctxIdxOffset 60, ctxInc = (prev delta ≠ 0).
5110fn parse_mb_qp_delta_cabac(cab: &mut crate::cabac::Cabac, last_delta_qp: &mut i32) -> i32 {
5111    const O: usize = 60;
5112    let ctx_inc = (*last_delta_qp != 0) as usize;
5113    let mut qp_delta = 0;
5114    if cab.decode_decision(O + ctx_inc) != 0 {
5115        let code = cabac_unary(cab, O + 2, 1) + 1;
5116        qp_delta = ((code + 1) >> 1) as i32;
5117        if code & 1 == 0 {
5118            qp_delta = -qp_delta;
5119        }
5120    }
5121    *last_delta_qp = qp_delta;
5122    qp_delta
5123}
5124
5125/// z-order block → padded (8-stride) nzc-cache index (openh264 g_kCacheNzcScanIdx):
5126/// 16 luma, 4 Cb, 4 Cr. Top neighbour = cache[idx-8], left = cache[idx-1].
5127const NZC_CACHE: [usize; 24] = [
5128    9, 10, 17, 18, 11, 12, 19, 20, 25, 26, 33, 34, 27, 28, 35, 36, // luma
5129    14, 15, 22, 23, // Cb
5130    38, 39, 46, 47, // Cr
5131];
5132
5133// g_kBlockCat2CtxOffset* + maxPos/maxC2, indexed by CABAC res-property (1..10; 0 unused).
5134const RES_MAXPOS: [i32; 11] = [0, 15, 14, 15, 3, 14, 63, 3, 3, 14, 14];
5135const RES_MAXC2: [i32; 11] = [0, 4, 4, 4, 3, 4, 4, 3, 3, 4, 4];
5136const RES_CBF: [usize; 11] = [0, 0, 4, 8, 12, 16, 0, 12, 12, 16, 16];
5137const RES_MAP: [usize; 11] = [0, 0, 15, 29, 44, 47, 0, 44, 44, 47, 47];
5138// Index 6 (luma 8×8) = 199 so that 227+199 = 426 and 232+199 = 431 — the spec's
5139// coeff_abs_level_minus1 base for ctxBlockCat 5 and its >1-bin sub-block.
5140const RES_ONE: [usize; 11] = [0, 0, 10, 20, 30, 39, 199, 30, 30, 39, 39];
5141// res-property values (post GetMbResProperty, CABAC): the ctx-table index.
5142const RP_I16_DC: usize = 1;
5143const RP_I16_AC: usize = 2;
5144const RP_LUMA_4X4: usize = 3;
5145const RP_CHROMA_DC: usize = 7; // U (V=8, same offsets)
5146const RP_CHROMA_AC: usize = 9; // U (V=10, same offsets)
5147/// Luma 8×8 (ctxBlockCat 5). Its RES_MAP/RES_CBF entries stay 0: cat 5 does NOT
5148/// share the `105 + off` / `166 + off` context bases the 4×4 categories use — it
5149/// has its own absolute bases (402 sig, 417 last) and its own per-position
5150/// ctxIdxInc maps below. RES_ONE[6] = 199 IS used, because 227 + 199 = 426 and
5151/// 232 + 199 = 431 reproduce the spec's coeff_abs_level_minus1 base exactly, so
5152/// the level loop needs no special case at all.
5153const RP_LUMA_8X8: usize = 6;
5154
5155// SIG8X8 / LAST8X8 moved to `rusty_h264_common::cabac_tables` (R6-1) so the encoder's
5156// ctxBlockCat 5 writer shares the exact spec data this reader is validated against.
5157use rusty_h264_common::cabac_tables::{LAST8X8, SIG8X8};
5158
5159/// One residual block (openh264 `ParseResidualBlockCabac`), generic over the 5 CABAC
5160/// block categories. `rp` selects the context offsets. DC categories (I16 luma DC,
5161/// chroma DC) take the cbf context from the per-MB `cbf_dc` bitmask + neighbour MB DC
5162/// cbf; AC categories from the padded nzc cache. Returns totalCoeffNum.
5163#[allow(clippy::too_many_arguments)]
5164fn parse_residual_cabac(
5165    cab: &mut crate::cabac::Cabac,
5166    nzc: &mut [u8; 48],
5167    cbf_dc: &mut u16,
5168    iz: usize,
5169    rp: usize,
5170    is_intra: bool,
5171    ndc: (Option<u16>, Option<u16>), // (top MB cbf_dc, left MB cbf_dc); None = unavailable
5172    out: &mut [i32],                 // scan-order coefficients written here (len ≥ maxPos+1)
5173) -> u32 {
5174    // The CABAC residual parse IS the decoder's entropy stage on Main-profile
5175    // streams — it was invisible (a ~47% residue) until this scope named it.
5176    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Entropy);
5177    // ---- coded_block_flag ----
5178    // ctxBlockCat 5 is the ONLY category with no coded_block_flag: its presence is
5179    // inferred from CodedBlockPatternLuma, so parsing one here would desync.
5180    let is8 = rp == RP_LUMA_8X8;
5181    let is_dc = rp == RP_I16_DC || rp == RP_CHROMA_DC || rp == RP_CHROMA_DC + 1;
5182    let (mut na, mut nb) = (is_intra as u8, is_intra as u8);
5183    let scan = NZC_CACHE[iz.min(23)];
5184    if is_dc {
5185        if let Some(t) = ndc.0 {
5186            nb = ((t >> rp) & 1) as u8;
5187        }
5188        if let Some(l) = ndc.1 {
5189            na = ((l >> rp) & 1) as u8;
5190        }
5191    } else {
5192        if nzc[scan - 8] != 0xff {
5193            nb = (nzc[scan - 8] != 0) as u8;
5194        }
5195        if nzc[scan - 1] != 0xff {
5196            na = (nzc[scan - 1] != 0) as u8;
5197        }
5198    }
5199    if !is8 {
5200        let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntCbf);
5201        let cbf = cab.decode_decision(85 + RES_CBF[rp] + (na + (nb << 1)) as usize);
5202        if cbf == 0 {
5203            if !is_dc {
5204                nzc[scan] = 0;
5205            }
5206            return 0;
5207        }
5208        if is_dc {
5209            *cbf_dc |= 1 << rp;
5210        }
5211    }
5212    // ---- significance map ----
5213    let maxpos = RES_MAXPOS[rp] as usize;
5214    // cat 5 uses its own absolute bases; the 4×4 categories share 105/166 + offset.
5215    let (map, last) = if is8 { (402, 417) } else { (105 + RES_MAP[rp], 166 + RES_MAP[rp]) };
5216    // SPARSE significance map: record each significant POSITION in `pos[..n]`
5217    // instead of marking a dense 64-entry array. Three costs disappear — the
5218    // 256-byte `sig` zeroing per call, the level loop's data-dependent
5219    // `sig[i] != 0` re-scan of every position (a branch mispredict per
5220    // transition on typical 2-4-coeff blocks), and the final dense copy into
5221    // `out`. Bin ORDER is unchanged: levels were decoded at descending
5222    // significant positions, which is exactly `pos[..n]` reversed.
5223    //
5224    // CONTRACT with the callers (all 10 sites): `out` is freshly zeroed, so
5225    // writing only the significant entries leaves the same contents the dense
5226    // copy produced. A reused non-zero `out` would be a correctness bug.
5227    let mut pos = [0u8; 64];
5228    let mut n = 0usize;
5229    let mut last_hit = false;
5230    let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntSig);
5231    for i in 0..maxpos {
5232        // 4×4: ctxIdxInc IS the scan position. 8×8: it comes from the folded maps.
5233        let (mi, li) = if is8 { (SIG8X8[i] as usize, LAST8X8[i] as usize) } else { (i, i) };
5234        if cab.decode_decision(map + mi) != 0 {
5235            pos[n] = i as u8;
5236            n += 1;
5237            if cab.decode_decision(last + li) != 0 {
5238                last_hit = true;
5239                break;
5240            }
5241        }
5242    }
5243    if !last_hit {
5244        pos[n] = maxpos as u8;
5245        n += 1;
5246    }
5247    let coeff_num = n as u32;
5248    // ---- levels ----
5249    let one = 227 + RES_ONE[rp];
5250    let abs = 232 + RES_ONE[rp];
5251    let maxc2 = RES_MAXC2[rp];
5252    let (mut c1, mut c2) = (1i32, 0i32);
5253    drop(_sg);
5254    let _lg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntLvl);
5255    for k in (0..n).rev() {
5256        let mut level = 1 + cab.decode_decision(one + c1 as usize) as i32;
5257        if level == 2 {
5258            level += cabac_ueg_level(cab, abs + c2 as usize) as i32;
5259            c2 = (c2 + 1).min(maxc2);
5260            c1 = 0;
5261        } else if c1 != 0 {
5262            c1 = (c1 + 1).min(4);
5263        }
5264        if cab.decode_bypass() != 0 {
5265            level = -level;
5266        }
5267        out[pos[k] as usize] = level;
5268    }
5269    if is8 {
5270        // One 8×8 covers four consecutive z-order 4×4 cells. Every later
5271        // coded_block_flag ctxIdxInc reads this cache, so all four must carry the
5272        // count — writing only `scan` would corrupt the NEXT macroblock's contexts.
5273        for k in 0..4 {
5274            nzc[NZC_CACHE[(iz + k).min(23)]] = coeff_num as u8;
5275        }
5276    } else if !is_dc {
5277        nzc[scan] = coeff_num as u8;
5278    }
5279    coeff_num
5280}
5281
5282
5283// ============================================================================
5284// Entropy-decouple E2: the OWNED pixel context that crosses the thread
5285// boundary (docs/entropy-decouple-plan.md). The worker owns the planes, the
5286// backup rows, the DPB Arcs and its own qp/t8/bs grids (fed by Row messages);
5287// the parse thread keeps every syntax grid. The methods below are ports of
5288// the FrameDecoder pixel halves — grid writes removed (parse commits its own
5289// grids), motion carried in the job instead of re-gathered.
5290// ============================================================================
5291
5292/// Messages from the parse thread to the pixel worker.
5293enum EdcMsg {
5294    Job(EdcJob),
5295    /// A ROW's worth of pixel jobs in one message (D10).
5296    ///
5297    /// The seam sent ONE message per macroblock: 208k sends per 60-frame pass
5298    /// against 2,596 rows. The overhead is per-JOB (channel lock, park/unpark)
5299    /// while the prize is proportional to pixel WORK, so the per-job send was
5300    /// dividing the payoff by ~80 for nothing. Batching per row cuts the
5301    /// synchronisation events by that factor and moves not one byte of pixel
5302    /// work off the worker.
5303    ///
5304    /// ORDER IS THE CORRECTNESS CONDITION: the worker must see a row's jobs
5305    /// before that row's `Row` filter message, so the batch is flushed at every
5306    /// row boundary, before `NeedCtx`, and at slice end.
5307    Batch(Vec<EdcJob>),
5308    /// A macroblock row finished parsing: install its qp/t8/bs and filter it.
5309    Row {
5310        r: usize,
5311        bs: Vec<rusty_h264_common::deblock::MbBs>,
5312        qp: Vec<u8>,
5313        t8: Vec<bool>,
5314    },
5315    /// An intra macroblock needs the planes on the parse thread: send the
5316    /// context over and wait for it to come back.
5317    NeedCtx,
5318}
5319
5320pub(crate) struct PixelCtx {
5321    rec_y: Vec<u8>,
5322    rec_u: Vec<u8>,
5323    rec_v: Vec<u8>,
5324    bak_y: Vec<u8>,
5325    bak_u: Vec<u8>,
5326    bak_v: Vec<u8>,
5327    refs: Vec<crate::Ref>,
5328    refs1: Vec<crate::Ref>,
5329    weights: Option<WeightTable>,
5330    scaling: Option<[[i32; 16]; 6]>,
5331    scaling8: Option<[[i32; 64]; 2]>,
5332    cw: usize,
5333    ccw: usize,
5334    mb_w: usize,
5335    mb_h: usize,
5336    chroma_qp_offset: i32,
5337    flt_rows: usize,
5338    db_ena: bool,
5339    db_oa: i32,
5340    db_ob: i32,
5341    cur_qp: u8,
5342    qp_grid: Vec<u8>,
5343    t8_grid: Vec<bool>,
5344    bs_store: Vec<rusty_h264_common::deblock::MbBs>,
5345}
5346
5347impl PixelCtx {
5348    fn chroma_qp_for(&self, qp: u8) -> u8 {
5349        rusty_h264_common::predict::chroma_qp(
5350            ((qp as i32 + self.chroma_qp_offset).clamp(0, 51)) as u8,
5351        )
5352    }
5353
5354    fn dequant(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
5355        match &self.scaling {
5356            Some(sc) => dequantize_weighted(levels, qp, &sc[list]),
5357            None => dequantize(levels, qp),
5358        }
5359    }
5360
5361    fn dequant_dc4(&self, level: i32, qp: u8, list: usize) -> i32 {
5362        rusty_h264_common::transform::dequantize_dc4(
5363            level,
5364            qp,
5365            self.scaling.as_ref().map(|sc| sc[list][0]),
5366        )
5367    }
5368
5369    fn inv_quant8(&self, raster: &[i32; 64], qp: u8, list: usize) -> [i32; 64] {
5370        match &self.scaling8 {
5371            Some(sc) => inverse_quant_8x8(raster, qp, &sc[list]),
5372            None => inverse_quant_8x8(raster, qp, &[16i32; 64]),
5373        }
5374    }
5375
5376    fn dequant_chroma_dc(&self, levels: &[i32; 4], qp: u8, list: usize) -> [i32; 4] {
5377        match &self.scaling {
5378            Some(sc) => inverse_quant_chroma_dc_weighted(levels, qp, sc[list][0]),
5379            None => inverse_quant_chroma_dc(levels, qp),
5380        }
5381    }
5382
5383    fn weight_partition(
5384        &self,
5385        pred_y: &mut [u8; 256],
5386        c_pred: &mut [[u8; 64]; 2],
5387        list: usize,
5388        refi: usize,
5389        rx: usize,
5390        ry: usize,
5391        rw: usize,
5392        rh: usize,
5393    ) {
5394        let Some(wt) = &self.weights else { return };
5395        for dy in 0..rh {
5396            for dx in 0..rw {
5397                let i = (ry + dy) * 16 + (rx + dx);
5398                pred_y[i] = wt.apply_luma(pred_y[i], list, refi);
5399            }
5400        }
5401        let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
5402        for cc in 0..2 {
5403            for dy in 0..crh {
5404                for dx in 0..crw {
5405                    let i = (cry + dy) * 8 + (crx + dx);
5406                    c_pred[cc][i] = wt.apply_chroma(c_pred[cc][i], list, refi, cc);
5407                }
5408            }
5409        }
5410    }
5411
5412        fn recon_p_inter(&mut self, j: &PInterJob) {
5413        let mbw = self.mb_w;
5414        // `add_inter_residual` (and anything under it) reads `self.cur_qp`,
5415        // which at FLUSH time belongs to a later macroblock — replay must
5416        // restore this MB's qp. The x264 corpus (near-constant QP) could not
5417        // see this; the encoder's delta-QP roundtrip stream caught it.
5418        let saved_qp = self.cur_qp;
5419        self.cur_qp = j.qp;
5420                    // ---- Recon: motion-comp (per 4×4 luma / co-located 2×2 chroma using the
5421                    // committed grid MV — the 6-tap/bilinear filter is per-output-pixel, so
5422                    // per-block MC is bit-identical to per-partition MC) + residual add via the
5423                    // SAME reconstruct_4x4 as intra, with the MC output as the prediction.
5424                    let qp = j.qp;
5425                    let qpc = self.chroma_qp_for(qp);
5426                    let (w4r, w2r) = (mbw * 4, mbw * 2);
5427                    let mut pred_y = [0u8; 256];
5428                    let mut c_pred = [[0u8; 64]; 2];
5429                    {
5430                        // MC-CALL COALESCING (side-by-side descent, dec target #2): the old
5431                        // loop paid 16 mc_luma(4×4) + 32 mc_chroma(2×2) per MB regardless of
5432                        // partitioning — 48 calls even for a single-MV 16×16 MB, and the
5433                        // per-call glue around 2.4M calls was ~40% of decoding real-world
5434                        // (x264) streams. The 6-tap/bilinear filters are per-output-pixel,
5435                        // so merging blocks with equal (mv, ref) into one wider MC call is
5436                        // BIT-IDENTICAL; the rect ladder mirrors the partition shapes.
5437                        let _ms = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMcStage);
5438                        let (rh16, cch) = (self.mb_h * 16, self.mb_h * 8);
5439                        // E2: the worker owns no syntax grids — the job carries the
5440                        // committed per-block motion (filled at parse time).
5441                        let gmv = j.gmv;
5442                        let mut gref = [0usize; 16];
5443                        for k in 0..16 {
5444                            gref[k] = (j.gref[k] as usize).min(self.refs.len() - 1);
5445                        }
5446                        // All blocks of the rect (in 4×4-block units) match its top-left?
5447                        let rect_eq = |x4: usize, y4: usize, w4: usize, h4: usize| -> bool {
5448                            let t = y4 * 4 + x4;
5449                            (0..h4).all(|dy| {
5450                                (0..w4).all(|dx| {
5451                                    let b = (y4 + dy) * 4 + (x4 + dx);
5452                                    gmv[b] == gmv[t] && gref[b] == gref[t]
5453                                })
5454                            })
5455                        };
5456                        let refs = &self.refs;
5457                        let (cw, ccw) = (self.cw, self.ccw);
5458                        let mut mc_rect = |x4: usize,
5459                                           y4: usize,
5460                                           w4: usize,
5461                                           h4: usize,
5462                                           pred_y: &mut [u8; 256],
5463                                           c_pred: &mut [[u8; 64]; 2]| {
5464                            let b = y4 * 4 + x4;
5465                            let (mv, reference) = (gmv[b], &refs[gref[b]]);
5466                            let (w, h) = (w4 * 4, h4 * 4);
5467                            // A FULL-WIDTH rect (w == 16, so x4 == 0) occupies contiguous
5468                            // whole rows of `pred_y` — the MC output layout and the
5469                            // destination layout coincide, so MC writes the prediction
5470                            // buffer DIRECTLY. The staging copy exists only for narrow
5471                            // rects, whose rows really are strided in `pred_y`. This is
5472                            // the diagnosis's "stage-boundary materialization" tax paid
5473                            // by the dominant 16×16/16×8 shapes: 256 B of `t` zeroing
5474                            // plus a 256 B copy per rect, for nothing.
5475                            if w == 16 {
5476                                rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &reference.py, reference.lstride(), crate::LPAD, cw, rh16, j.mbx * 16, j.mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut pred_y[y4 * 64..y4 * 64 + w * h]));
5477                            } else {
5478                                let mut t = [0u8; 256];
5479                                rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &reference.py, reference.lstride(), crate::LPAD, cw, rh16, j.mbx * 16 + x4 * 4, j.mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut t[..w * h]));
5480                                let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
5481                                for dy in 0..h {
5482                                    pred_y[(y4 * 4 + dy) * 16 + x4 * 4..][..w]
5483                                        .copy_from_slice(&t[dy * w..dy * w + w]);
5484                                }
5485                            }
5486                            let (cw4, ch4) = (w4 * 2, h4 * 2);
5487                            for cc in 0..2 {
5488                                let rc = if cc == 0 { &reference.pu } else { &reference.pv };
5489                                // Same full-width coincidence for chroma: cw4 == 8 rows
5490                                // are contiguous in the 8-wide `c_pred` plane.
5491                                if cw4 == 8 {
5492                                    mc_chroma_padded(rc, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut c_pred[cc][y4 * 16..y4 * 16 + cw4 * ch4]);
5493                                    continue;
5494                                }
5495                                let mut tc = [0u8; 64];
5496                                mc_chroma_padded(rc, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8 + x4 * 2, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut tc[..cw4 * ch4]);
5497                                let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
5498                                for dy in 0..ch4 {
5499                                    c_pred[cc][(y4 * 2 + dy) * 8 + x4 * 2..][..cw4]
5500                                        .copy_from_slice(&tc[dy * cw4..dy * cw4 + cw4]);
5501                                }
5502                            }
5503                        };
5504                        if rect_eq(0, 0, 4, 4) {
5505                            mc_rect(0, 0, 4, 4, &mut pred_y, &mut c_pred);
5506                        } else if rect_eq(0, 0, 4, 2) && rect_eq(0, 2, 4, 2) {
5507                            mc_rect(0, 0, 4, 2, &mut pred_y, &mut c_pred);
5508                            mc_rect(0, 2, 4, 2, &mut pred_y, &mut c_pred);
5509                        } else if rect_eq(0, 0, 2, 4) && rect_eq(2, 0, 2, 4) {
5510                            mc_rect(0, 0, 2, 4, &mut pred_y, &mut c_pred);
5511                            mc_rect(2, 0, 2, 4, &mut pred_y, &mut c_pred);
5512                        } else {
5513                            for q in 0..4usize {
5514                                let (qx, qy) = ((q % 2) * 2, (q / 2) * 2);
5515                                if rect_eq(qx, qy, 2, 2) {
5516                                    mc_rect(qx, qy, 2, 2, &mut pred_y, &mut c_pred);
5517                                } else if rect_eq(qx, qy, 2, 1) && rect_eq(qx, qy + 1, 2, 1) {
5518                                    mc_rect(qx, qy, 2, 1, &mut pred_y, &mut c_pred);
5519                                    mc_rect(qx, qy + 1, 2, 1, &mut pred_y, &mut c_pred);
5520                                } else if rect_eq(qx, qy, 1, 2) && rect_eq(qx + 1, qy, 1, 2) {
5521                                    mc_rect(qx, qy, 1, 2, &mut pred_y, &mut c_pred);
5522                                    mc_rect(qx + 1, qy, 1, 2, &mut pred_y, &mut c_pred);
5523                                } else {
5524                                    for j in 0..4usize {
5525                                        mc_rect(qx + (j % 2), qy + (j / 2), 1, 1, &mut pred_y, &mut c_pred);
5526                                    }
5527                                }
5528                            }
5529                        }
5530                        // EXPLICIT WEIGHTED PREDICTION (spec 8.4.2.3). The CAVLC inter
5531                        // path weights each partition after MC; the MC-call-coalescing
5532                        // rewrite of this CABAC path lost it, and nothing caught that
5533                        // because the effect is invisible unless a stream actually
5534                        // carries non-default weights. x264's `weightp` DUPLICATES a
5535                        // reference and distinguishes the copy ONLY by its weights, so
5536                        // every macroblock picking the weighted index decoded unweighted
5537                        // -- a silent, accumulating luma drift.
5538                        //
5539                        // Applied per 4x4 block rather than per partition: the weight
5540                        // depends solely on the block's reference index, so the two are
5541                        // equivalent, and `gref` already holds it for every block
5542                        // regardless of which rect ladder rung ran.
5543                        if self.weights.is_some() {
5544                            for by in 0..4usize {
5545                                for bx in 0..4usize {
5546                                    let refi = gref[by * 4 + bx];
5547                                    self.weight_partition(
5548                                        &mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4,
5549                                    );
5550                                }
5551                            }
5552                        }
5553                    }
5554                    // Residual add — the SAME helper the B path uses (this inline
5555                    // copy was a duplicate; deduped when the zero-block fast path
5556                    // landed so both paths share it).
5557                    self.add_inter_residual(j.mbx, j.mby, &pred_y, &c_pred, &j.luma_scan, if j.t8 { Some(&j.luma8) } else { None }, &j.cdc, &j.cac, j.cbp_chroma, &j.nnzs);
5558    }
5559
5560    fn recon_p_skip(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32)) {
5561        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
5562
5563        let mut pred = [0u8; 256];
5564        let rf0 = &self.refs[0];
5565        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);
5566        if let Some(wt) = &self.weights {
5567            for p in pred.iter_mut() {
5568                *p = wt.apply_luma(*p, 0, 0);
5569            }
5570        }
5571        {
5572            let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
5573            for dy in 0..16 {
5574                let d = (mb_y * 16 + dy) * self.cw + mb_x * 16;
5575                self.rec_y[d..d + 16].copy_from_slice(&pred[dy * 16..dy * 16 + 16]);
5576            }
5577        }
5578        for c in 0..2 {
5579            let mut pc = [0u8; 64];
5580            let rf0 = &self.refs[0];
5581            let rc = if c == 0 { &rf0.pu } else { &rf0.pv };
5582            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);
5583            if let Some(wt) = &self.weights {
5584                for p in pc.iter_mut() {
5585                    *p = wt.apply_chroma(*p, 0, 0, c);
5586                }
5587            }
5588            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
5589            for dy in 0..8 {
5590                let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
5591                plane[d..d + 8].copy_from_slice(&pc[dy * 8..dy * 8 + 8]);
5592            }
5593        }
5594    }
5595
5596    fn add_inter_residual(
5597        &mut self,
5598        mb_x: usize,
5599        mb_y: usize,
5600        pred_y: &[u8; 256],
5601        c_pred: &[[u8; 64]; 2],
5602        luma_scan: &[[i32; 16]; 16],
5603        // `Some` when the macroblock carries transform_size_8x8_flag: four 8x8
5604        // blocks in 8x8 scan order, replacing the sixteen 4x4 luma blocks.
5605        luma8: Option<&[[i32; 64]; 4]>,
5606        cdc: &[[i32; 4]; 2],
5607        cac: &[[[i32; 16]; 4]; 2],
5608        cbp_chroma: u32,
5609        // Parsed totalCoeff per block, indexed exactly as the parse's `iz`:
5610        // [0..16] luma 4x4 z-order (for t8, the 8x8 count sits at `id8*4`),
5611        // [16..24] chroma AC as `16 + c*4 + id4`. The parser already counted
5612        // every significant coefficient; re-deriving the counts here scanned
5613        // 16-64 array elements per block (~400 loads/MB) for information the
5614        // caller was holding — the diagnosis's stage-boundary re-derivation tax.
5615        nnzs: &[u8; 24],
5616    ) {
5617        let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecResidAdd);
5618        let qp = self.cur_qp;
5619        let qpc = self.chroma_qp_for(qp);
5620        let (w4r, w2r) = (self.mb_w * 4, self.mb_w * 2);
5621        if let Some(l8) = luma8 {
5622            // INTER 8x8 luma: same primitives the I_8x8 and CAVLC paths use.
5623            for b8 in 0..4usize {
5624                let (b8x, b8y) = (b8 % 2, b8 / 2);
5625                // Summed, not slot 0: with per-4x4 counts in the CAVLC case, slot 0
5626                // is only the first sub-block and can be 0 while the block is coded.
5627                let nnz: u32 = (0..4).map(|k| nnzs[b8 * 4 + k] as u32).sum();
5628                let res8 = if nnz == 0 {
5629                    [0i32; 64]
5630                } else {
5631                    let raster = un_scan_8x8(&l8[b8]);
5632                    // list 1 = INTER 8x8 luma scaling list (0 is the intra one).
5633                    self.inv_quant8(&raster, qp, 1)
5634                };
5635                // The 4x4 inter path marks coded_y per block; the 8x8 branch must too,
5636                // or a later intra macroblock's neighbour availability is wrong.
5637                let predb: [i32; 64] =
5638                    std::array::from_fn(|i| pred_y[(b8y * 8 + i / 8) * 16 + (b8x * 8 + i % 8)] as i32);
5639                let recon = add_residual_8x8(&res8, &predb);
5640                let (px, py) = (mb_x * 16 + b8x * 8, mb_y * 16 + b8y * 8);
5641                for dy in 0..8 {
5642                    for dx in 0..8 {
5643                        self.rec_y[(py + dy) * self.cw + (px + dx)] = recon[dy * 8 + dx];
5644                    }
5645                }
5646            }
5647        }
5648        for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
5649            if luma8.is_some() {
5650                break;
5651            }
5652            let nnz = nnzs[blk];
5653            let cw = self.cw;
5654            let p_off = (lby * 4) * 16 + lbx * 4;
5655            let r_off = (mb_y * 4 + lby) * 4 * cw + (mb_x * 4 + lbx) * 4;
5656            if nnz == 0 {
5657                // Zero residual → recon == prediction EXACTLY (the integer IDCT is
5658                // linear so zeros map to zeros, and pred is already 0..=255) — copy
5659                // the pred rows straight into the plane. On real (sparse-cbp)
5660                // streams this is MOST of the 4×4 blocks.
5661                for r in 0..4 {
5662                    self.rec_y[r_off + r * cw..r_off + r * cw + 4]
5663                        .copy_from_slice(&pred_y[p_off + r * 16..p_off + r * 16 + 4]);
5664                }
5665                continue;
5666            }
5667            // DC-ONLY: the sole significant coefficient is scan position 0 (the
5668            // zig-zag starts at DC, and un_scan keeps it at raster 0), so the
5669            // whole dequant + IDCT collapses to one multiply and a flat add.
5670            if nnz == 1 && luma_scan[blk][0] != 0 {
5671                let f = self.dequant_dc4(luma_scan[blk][0], qp, 3);
5672                reconstruct_4x4_dc_into((f + 32) >> 6, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
5673            } else {
5674                // Fused un-scan + dequant over ONLY the significant coefficients,
5675                // then IDCT + add + clip straight into the plane — no `qb`, no
5676                // `deq`-from-dense, no `predb` gather, no `s`, no `store` call.
5677                //
5678                // HYBRID: the scatter walks scan positions with a data-dependent
5679                // branch per slot, which beats the branchless dense 16-multiply
5680                // loop only while the block is SPARSE. The DC/zero fast paths
5681                // already removed the sparsest blocks, so the population here
5682                // skews denser — above ~6 coefficients the dense loop wins.
5683                let deq = if nnz <= 6 {
5684                    dequant_scatter_4x4(&luma_scan[blk], nnz, 0, qp, self.scaling.as_ref().map(|sc| &sc[3]))
5685                } else {
5686                    self.dequant(&un_scan_4x4_dcac(&luma_scan[blk]), qp, 3)
5687                };
5688                reconstruct_4x4_into(&deq, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
5689            }
5690        }
5691        let mut c_dc = [[0i32; 4]; 2];
5692        if cbp_chroma != 0 {
5693            for c in 0..2 {
5694                c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 4 + c);
5695            }
5696        }
5697        let ccw = self.ccw;
5698        for c in 0..2 {
5699            for &(bx, by) in &CHROMA_4X4_SCAN_XY {
5700                let mut ac_nz = false;
5701                if cbp_chroma == 2 {
5702                    let n = nnzs[16 + c * 4 + by * 2 + bx];
5703                    ac_nz = n != 0;
5704                }
5705                let dc = c_dc[c][by * 2 + bx];
5706                let p_off = (by * 4) * 8 + bx * 4;
5707                let r_off = (mb_y * 2 + by) * 4 * ccw + (mb_x * 2 + bx) * 4;
5708                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
5709                if dc == 0 && !ac_nz {
5710                    // Zero residual (no AC, zero DC) → recon == prediction exactly.
5711                    for r in 0..4 {
5712                        plane[r_off + r * ccw..r_off + r * ccw + 4]
5713                            .copy_from_slice(&c_pred[c][p_off + r * 8..p_off + r * 8 + 4]);
5714                    }
5715                    continue;
5716                }
5717                // DC-ONLY (no coded AC — covers every cbp_chroma==1 block and the
5718                // AC-empty blocks of cbp_chroma==2): the chroma DC arrives ALREADY
5719                // dequantized, so the residual is `(dc + 32) >> 6` flat.
5720                if !ac_nz {
5721                    reconstruct_4x4_dc_into((dc + 32) >> 6, &c_pred[c], p_off, 8, plane, r_off, ccw);
5722                    continue;
5723                }
5724                // AC-only scan: index i is overall scan position i+1 (ac_shift=1).
5725                // Same sparse/dense hybrid as luma.
5726                let n = nnzs[16 + c * 4 + by * 2 + bx];
5727                let mut deq = if n <= 6 {
5728                    dequant_scatter_4x4(&cac[c][by * 2 + bx], n, 1, qpc, self.scaling.as_ref().map(|sc| &sc[4 + c]))
5729                } else {
5730                    let mut ac = [0i32; 16];
5731                    un_scan_4x4_ac_into(&cac[c][by * 2 + bx], &mut ac);
5732                    // Free-fn dequant: `self.dequant` borrows all of `self`, which
5733                    // conflicts with the live `plane` (&mut self.rec_u/v) borrow.
5734                    match &self.scaling {
5735                        Some(sc) => dequantize_weighted(&ac, qpc, &sc[4 + c]),
5736                        None => dequantize(&ac, qpc),
5737                    }
5738                };
5739                deq[0] = dc;
5740                reconstruct_4x4_into(&deq, &c_pred[c], p_off, 8, plane, r_off, ccw);
5741            }
5742        }
5743    }
5744
5745    fn filter_row(&mut self, r: usize) {
5746        // The precomputed consumer path reads ONLY `bs` + `t8x8` (+ the qp grid
5747        // passed as a parameter) — verified when the path landed (WHYS Part 15).
5748        let info = rusty_h264_common::deblock::BlockInfo {
5749            inter: &[],
5750            nnz: &[],
5751            mv: &[],
5752            ref_id: &[],
5753            mv1: &[],
5754            ref_id1: &[],
5755            w4: self.mb_w * 4,
5756            t8x8: &self.t8_grid,
5757            bs: &self.bs_store,
5758            poc0: &[],
5759            poc1: &[],
5760            kind: &[],
5761        };
5762        rusty_h264_common::deblock::filter_frame_rows(
5763            &mut self.rec_y,
5764            &mut self.rec_u,
5765            &mut self.rec_v,
5766            self.mb_w,
5767            self.mb_h,
5768            r..r + 1,
5769            &self.qp_grid,
5770            self.chroma_qp_offset,
5771            self.db_oa,
5772            self.db_ob,
5773            &info,
5774        );
5775    }
5776
5777    fn save_bak(&mut self, r: usize) {
5778        let y0 = (r * 16 + 15) * self.cw;
5779        self.bak_y.copy_from_slice(&self.rec_y[y0..y0 + self.cw]);
5780        let c0 = (r * 8 + 7) * self.ccw;
5781        self.bak_u.copy_from_slice(&self.rec_u[c0..c0 + self.ccw]);
5782        self.bak_v.copy_from_slice(&self.rec_v[c0..c0 + self.ccw]);
5783    }
5784}
5785
5786/// The pixel worker: replays jobs in parse order, filters rows as their
5787/// messages arrive, and hands the whole context to the parse thread (and
5788/// back) around intra macroblocks. Returns the context at slice end.
5789/// E2 SEAM COUNTERS (D7). Deterministic — one run is the verdict, no pinning.
5790/// `RS_H264_EDC_STATS=1` prints at decode end. Counts, not clocks: the question
5791/// "does one intra macroblock drain the pipeline" is a COUNT question.
5792pub(crate) mod edcstat {
5793    use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
5794    pub static NEEDCTX: AtomicU64 = AtomicU64::new(0);
5795    pub static JOBS: AtomicU64 = AtomicU64::new(0);
5796    pub static ROWS: AtomicU64 = AtomicU64::new(0);
5797    pub static ROWBYTES: AtomicU64 = AtomicU64::new(0);
5798    pub static MBS: AtomicU64 = AtomicU64::new(0);
5799    pub static J_INTER: AtomicU64 = AtomicU64::new(0);
5800    pub static DOUBLED: AtomicU64 = AtomicU64::new(0);
5801    pub static J_NORES_SENT: AtomicU64 = AtomicU64::new(0);
5802    pub static BATCHES: AtomicU64 = AtomicU64::new(0);
5803    pub static DISPATCH_ON: AtomicU64 = AtomicU64::new(0);
5804    pub static DISPATCH_SEEN: AtomicU64 = AtomicU64::new(0);
5805    pub static J_INTER_NORES: AtomicU64 = AtomicU64::new(0);
5806    #[inline]
5807    pub fn bump(c: &AtomicU64, n: u64) {
5808        if on() {
5809            c.fetch_add(n, Relaxed);
5810        }
5811    }
5812    pub fn on() -> bool {
5813        static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5814        *V.get_or_init(|| std::env::var_os("RS_H264_EDC_STATS").is_some())
5815    }
5816    pub fn report() {
5817        if !on() {
5818            return;
5819        }
5820        eprintln!(
5821            "EDCDISPATCH threaded_slices={} eligible_slices={}",
5822            DISPATCH_ON.load(Relaxed), DISPATCH_SEEN.load(Relaxed)
5823        );
5824        eprintln!(
5825            "EDCSIZE EdcMsg={} EdcJob={} PInterJob={} BJob={}",
5826            std::mem::size_of::<super::EdcMsg>(),
5827            std::mem::size_of::<super::EdcJob>(),
5828            std::mem::size_of::<super::PInterJob>(),
5829            std::mem::size_of::<super::BJob>(),
5830        );
5831        let (n, j, r, b, m) = (
5832            NEEDCTX.load(Relaxed), JOBS.load(Relaxed), ROWS.load(Relaxed),
5833            ROWBYTES.load(Relaxed), MBS.load(Relaxed),
5834        );
5835        eprintln!(
5836            "EDCSTAT needctx={n} jobs={j} rows={r} rowbytes={b} mbs={m} batches={} jobs_per_batch={:.1} needctx_per_1k_mb={:.1} jobs_per_needctx={:.1}",
5837            BATCHES.load(Relaxed),
5838            j as f64 / BATCHES.load(Relaxed).max(1) as f64,
5839            1000.0 * n as f64 / m.max(1) as f64,
5840            j as f64 / n.max(1) as f64
5841        );
5842        let (ji, jn) = (J_INTER.load(Relaxed), J_INTER_NORES.load(Relaxed));
5843        eprintln!(
5844            "EDCMIX doubled={} nores_sent={} inter={ji} inter_no_residual={jn} ({:.1}% of inter) wasted_bytes={:.1} MB of {:.1} MB total inter payload",
5845            DOUBLED.load(Relaxed),
5846            J_NORES_SENT.load(Relaxed),
5847            100.0 * jn as f64 / ji.max(1) as f64,
5848            (jn * 2784) as f64 / 1.048576e6,
5849            (ji * 2784) as f64 / 1.048576e6,
5850        );
5851    }
5852}
5853
5854fn edc_worker(
5855    mut ctx: PixelCtx,
5856    rx: std::sync::mpsc::Receiver<EdcMsg>,
5857    ctx_tx: std::sync::mpsc::Sender<PixelCtx>,
5858    back_rx: std::sync::mpsc::Receiver<PixelCtx>,
5859) -> PixelCtx {
5860    while let Ok(msg) = rx.recv() {
5861        match msg {
5862            EdcMsg::Batch(jobs) => {
5863                for j in jobs {
5864                    match j {
5865                        EdcJob::Skip { mbx, mby, mv } => ctx.recon_p_skip(mbx, mby, mv),
5866                        EdcJob::Inter(j) => ctx.recon_p_inter(&j),
5867                        EdcJob::InterNoRes(j) => ctx.recon_p_inter(&j.to_full()),
5868                        EdcJob::B(j) => ctx.recon_b(&j),
5869                        EdcJob::BSkip { mbx, mby, regions } => ctx.recon_b_skip(mbx, mby, &regions),
5870                    }
5871                }
5872            }
5873            EdcMsg::Job(EdcJob::Skip { mbx, mby, mv }) => ctx.recon_p_skip(mbx, mby, mv),
5874            EdcMsg::Job(EdcJob::Inter(j)) => ctx.recon_p_inter(&j),
5875            EdcMsg::Job(EdcJob::InterNoRes(j)) => ctx.recon_p_inter(&j.to_full()),
5876            EdcMsg::Job(EdcJob::B(j)) => ctx.recon_b(&j),
5877            EdcMsg::Job(EdcJob::BSkip { mbx, mby, regions }) => ctx.recon_b_skip(mbx, mby, &regions),
5878            EdcMsg::Row { r, bs, qp, t8 } => {
5879                let (w, base) = (ctx.mb_w, r * ctx.mb_w);
5880                ctx.bs_store[base..base + w].copy_from_slice(&bs);
5881                ctx.qp_grid[base..base + w].copy_from_slice(&qp);
5882                ctx.t8_grid[base..base + w].copy_from_slice(&t8);
5883                if ctx.db_ena {
5884                    ctx.save_bak(r);
5885                    ctx.filter_row(r);
5886                    ctx.flt_rows = r + 1;
5887                }
5888            }
5889            EdcMsg::NeedCtx => {
5890                ctx_tx.send(ctx).expect("parse thread alive");
5891                ctx = back_rx.recv().expect("ctx returned after intra");
5892            }
5893        }
5894    }
5895    ctx
5896}
5897
5898
5899/// One motion-compensation region of a B macroblock, recorded at parse time
5900/// (E3). Weights are the RESOLVED implicit pair — computing them needs the
5901/// ref lists' POCs, which are parse-side state.
5902pub(crate) struct BRegion {
5903    px: usize,
5904    py: usize,
5905    rw: usize,
5906    rh: usize,
5907    refi0: i32,
5908    refi1: i32,
5909    mv0: (i32, i32),
5910    mv1: (i32, i32),
5911    w: Option<(i32, i32)>,
5912}
5913
5914/// A B macroblock's deferred pixel work: replay the regions into fresh
5915/// prediction buffers, then either copy them out (skip/direct, no residual)
5916/// or run the residual add.
5917pub(crate) struct BJob {
5918    mbx: usize,
5919    mby: usize,
5920    t8: bool,
5921    qp: u8,
5922    cbp_chroma: u32,
5923    skip: bool,
5924    regions: Vec<BRegion>,
5925    luma_scan: [[i32; 16]; 16],
5926    luma8: [[i32; 64]; 4],
5927    cdc: [[i32; 4]; 2],
5928    cac: [[[i32; 16]; 4]; 2],
5929    nnzs: [u8; 24],
5930}
5931
5932impl PixelCtx {
5933    fn b_mc(
5934        &self,
5935        mb_x: usize,
5936        mb_y: usize,
5937        px: usize,
5938        py: usize,
5939        rw: usize,
5940        rh: usize,
5941        refi0: i32,
5942        mv0: (i32, i32),
5943        refi1: i32,
5944        mv1: (i32, i32),
5945        pred_y: &mut [u8; 256],
5946        c_pred: &mut [[u8; 64]; 2],
5947        wparam: Option<(i32, i32)>,
5948    ) {
5949        let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBMc);
5950        // Malformed-stream armor, mirroring the P path: now that B slices actually
5951        // PARSE ref_idx (they used to be hardcoded to 0), a mutated stream can hand
5952        // us an index past the end of either list. Clamp rather than panic — the
5953        // crate is `forbid(unsafe_code)` and fuzz-gated to never panic, and a
5954        // wrong picture on garbage input carries no conformance duty.
5955        let refi0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
5956        let refi1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
5957        if (refi0 >= 0 && self.refs.is_empty()) || (refi1 >= 0 && self.refs1.is_empty()) {
5958            return;
5959        }
5960        let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
5961        // E3: implicit weights are PARSE-side (they read the ref lists' POCs);
5962        // the region carries the resolved pair.
5963        let weights = wparam;
5964        // Bi-prediction blend: the weights decision is LOOP-INVARIANT, so every
5965        // blend site below matches on `weights` ONCE and runs a branch-free
5966        // pixel loop — the unweighted `(p+q+1)>>1` average then autovectorizes
5967        // (the per-pixel closure this replaces hid the invariant behind a
5968        // capture, and its chroma form was a &dyn call PER PIXEL).
5969        // FULL-WIDTH regions (px == 0, rw == 16 — every 16×16/16×8 partition and
5970        // most direct regions) occupy contiguous rows of `pred_y`, so MC writes
5971        // the destination DIRECTLY: uni-pred needs no staging at all, bi-pred
5972        // stages only the second list and blends in place. The staging arrays
5973        // (512 B zeroed per call before this) now exist only on the branches
5974        // that read them. Same fusion as the P path's mc_rect (WHYS Part 8).
5975        let full = px == 0 && rw == 16;
5976        let _gl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBLuma);
5977        // One scratch borrow for the whole region — both bi-pred passes included.
5978        // The closure yields whether the arm already ran the chroma half (the
5979        // bi-pred full-width arm does, to keep its staging alive) — a plain
5980        // `return` inside would exit the CLOSURE only and chroma would run twice.
5981        let chroma_done = rusty_h264_common::inter::with_mc_scratch(|scr| match (refi0 >= 0, refi1 >= 0, full) {
5982            (true, false, true) => {
5983                let rf = &self.refs[refi0 as usize];
5984                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
5985                false
5986            }
5987            (false, true, true) => {
5988                let rf = &self.refs1[refi1 as usize];
5989                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
5990                false
5991            }
5992            (true, true, true) => {
5993                let rf = &self.refs[refi0 as usize];
5994                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv0.0, mv0.1, &mut pred_y[py * 16..py * 16 + rw * rh]);
5995                let mut b = [0u8; 256];
5996                let rf = &self.refs1[refi1 as usize];
5997                rusty_h264_common::inter::mc_luma_padded_pre(scr, &rf.py, rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut b[..rw * rh]);
5998                drop(_gl);
5999                let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
6000                // SLICE-then-zip: proving the bounds ONCE lets rustc emit the whole
6001                // 256-byte average as 8 straight-line vpavgb ops (verified in
6002                // isolation, x86-64-v3); the indexed form kept a per-iteration
6003                // bounds check and a loop. A hand AVX2 kernel is refuted — the
6004                // compiler already emits the ideal instruction.
6005                let dst = &mut pred_y[py * 16..py * 16 + rw * rh];
6006                match weights {
6007                    None => {
6008                        for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
6009                            *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
6010                        }
6011                    }
6012                    Some((w0, w1)) => {
6013                        for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
6014                            *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
6015                        }
6016                    }
6017                }
6018                let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
6019                self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
6020                true
6021            }
6022            _ => {
6023                // Narrow region — rows are strided in `pred_y`; stage and copy.
6024                let (mut a, mut b) = ([0u8; 256], [0u8; 256]);
6025                if refi0 >= 0 {
6026                    let rf = &self.refs[refi0 as usize];
6027                    rusty_h264_common::inter::mc_luma_padded_pre(scr, &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[..rw * rh]);
6028                }
6029                if refi1 >= 0 {
6030                    let rf = &self.refs1[refi1 as usize];
6031                    rusty_h264_common::inter::mc_luma_padded_pre(scr, &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[..rw * rh]);
6032                }
6033                drop(_gl);
6034                let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
6035                match (refi0 >= 0, refi1 >= 0) {
6036                    (true, true) => {
6037                        for dy in 0..rh {
6038                            let (ar, br) = (&a[dy * rw..dy * rw + rw], &b[dy * rw..dy * rw + rw]);
6039                            let base = (py + dy) * 16 + px;
6040                            let dst = &mut pred_y[base..base + rw];
6041                            match weights {
6042                                None => {
6043                                    for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
6044                                        *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
6045                                    }
6046                                }
6047                                Some((w0, w1)) => {
6048                                    for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
6049                                        *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
6050                                    }
6051                                }
6052                            }
6053                        }
6054                    }
6055                    (true, false) => {
6056                        for dy in 0..rh {
6057                            let d = (py + dy) * 16 + px;
6058                            pred_y[d..d + rw].copy_from_slice(&a[dy * rw..dy * rw + rw]);
6059                        }
6060                    }
6061                    _ => {
6062                        for dy in 0..rh {
6063                            let d = (py + dy) * 16 + px;
6064                            pred_y[d..d + rw].copy_from_slice(&b[dy * rw..dy * rw + rw]);
6065                        }
6066                    }
6067                }
6068                false
6069            }
6070        });
6071        if chroma_done {
6072            return;
6073        }
6074        let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
6075        self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
6076    }
6077
6078    fn b_mc_chroma(
6079        &self,
6080        mb_x: usize,
6081        mb_y: usize,
6082        px: usize,
6083        py: usize,
6084        rw: usize,
6085        rh: usize,
6086        refi0: i32,
6087        mv0: (i32, i32),
6088        refi1: i32,
6089        mv1: (i32, i32),
6090        c_pred: &mut [[u8; 64]; 2],
6091        weights: Option<(i32, i32)>,
6092        cch: usize,
6093    ) {
6094        let (crx, cry, crw, crh) = (px / 2, py / 2, rw / 2, rh / 2);
6095        let full = crx == 0 && crw == 8;
6096        for c in 0..2 {
6097            match (refi0 >= 0, refi1 >= 0, full) {
6098                (true, false, true) => {
6099                    let rf = &self.refs[refi0 as usize];
6100                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
6101                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
6102                }
6103                (false, true, true) => {
6104                    let rf = &self.refs1[refi1 as usize];
6105                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
6106                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
6107                }
6108                (true, true, true) => {
6109                    let rf = &self.refs[refi0 as usize];
6110                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
6111                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut c_pred[c][cry * 8..cry * 8 + crw * crh]);
6112                    let mut cb = [0u8; 64];
6113                    let rf = &self.refs1[refi1 as usize];
6114                    let pl = if c == 0 { &rf.pu } else { &rf.pv };
6115                    mc_chroma_padded(pl, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut cb[..crw * crh]);
6116                    let dst = &mut c_pred[c][cry * 8..cry * 8 + crw * crh];
6117                    match weights {
6118                        None => {
6119                            for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
6120                                *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
6121                            }
6122                        }
6123                        Some((w0, w1)) => {
6124                            for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
6125                                *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
6126                            }
6127                        }
6128                    }
6129                }
6130                _ => {
6131                    let (mut ca, mut cb) = ([0u8; 64], [0u8; 64]);
6132                    if refi0 >= 0 {
6133                        let rf = &self.refs[refi0 as usize];
6134                        let pl = if c == 0 { &rf.pu } else { &rf.pv };
6135                        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[..crw * crh]);
6136                    }
6137                    if refi1 >= 0 {
6138                        let rf = &self.refs1[refi1 as usize];
6139                        let pl = if c == 0 { &rf.pu } else { &rf.pv };
6140                        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[..crw * crh]);
6141                    }
6142                    match (refi0 >= 0, refi1 >= 0) {
6143                        (true, true) => {
6144                            for dy in 0..crh {
6145                                let (pr, qr) = (&ca[dy * crw..dy * crw + crw], &cb[dy * crw..dy * crw + crw]);
6146                                let base = (cry + dy) * 8 + crx;
6147                                let dst = &mut c_pred[c][base..base + crw];
6148                                match weights {
6149                                    None => {
6150                                        for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
6151                                            *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
6152                                        }
6153                                    }
6154                                    Some((w0, w1)) => {
6155                                        for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
6156                                            *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
6157                                        }
6158                                    }
6159                                }
6160                            }
6161                        }
6162                        (true, false) => {
6163                            for dy in 0..crh {
6164                                let d = (cry + dy) * 8 + crx;
6165                                c_pred[c][d..d + crw].copy_from_slice(&ca[dy * crw..dy * crw + crw]);
6166                            }
6167                        }
6168                        _ => {
6169                            for dy in 0..crh {
6170                                let d = (cry + dy) * 8 + crx;
6171                                c_pred[c][d..d + crw].copy_from_slice(&cb[dy * crw..dy * crw + crw]);
6172                            }
6173                        }
6174                    }
6175                }
6176            }
6177        }
6178    }
6179
6180    /// B_Skip replay: regions into fresh prediction buffers, then the plane
6181    /// copy — no residual, no coefficient arrays.
6182    fn recon_b_skip(&mut self, mbx: usize, mby: usize, regions: &[BRegion]) {
6183        let mut pred_y = [0u8; 256];
6184        let mut c_pred = [[0u8; 64]; 2];
6185        for r in regions {
6186            self.b_mc(mbx, mby, r.px, r.py, r.rw, r.rh, r.refi0, r.mv0, r.refi1, r.mv1, &mut pred_y, &mut c_pred, r.w);
6187        }
6188        for dy in 0..16 {
6189            let d = (mby * 16 + dy) * self.cw + mbx * 16;
6190            self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
6191        }
6192        for c in 0..2 {
6193            let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
6194            for dy in 0..8 {
6195                let d = (mby * 8 + dy) * self.ccw + mbx * 8;
6196                plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
6197            }
6198        }
6199    }
6200
6201    /// Replays one B macroblock's regions + residual (the worker half of the
6202    /// E3 seam). Mirrors the inline order exactly: MC regions in parse order
6203    /// into the prediction buffers, then the residual add (or the skip copy).
6204    fn recon_b(&mut self, j: &BJob) {
6205        let mut pred_y = [0u8; 256];
6206        let mut c_pred = [[0u8; 64]; 2];
6207        for r in &j.regions {
6208            self.b_mc(j.mbx, j.mby, r.px, r.py, r.rw, r.rh, r.refi0, r.mv0, r.refi1, r.mv1, &mut pred_y, &mut c_pred, r.w);
6209        }
6210        if j.skip {
6211            for dy in 0..16 {
6212                let d = (j.mby * 16 + dy) * self.cw + j.mbx * 16;
6213                self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
6214            }
6215            for c in 0..2 {
6216                let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
6217                for dy in 0..8 {
6218                    let d = (j.mby * 8 + dy) * self.ccw + j.mbx * 8;
6219                    plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
6220                }
6221            }
6222        } else {
6223            self.cur_qp = j.qp;
6224            self.add_inter_residual(j.mbx, j.mby, &pred_y, &c_pred, &j.luma_scan, if j.t8 { Some(&j.luma8) } else { None }, &j.cdc, &j.cac, j.cbp_chroma, &j.nnzs);
6225        }
6226    }
6227}
6228
6229/// One deferred pixel-reconstruction job (entropy-decouple E1 seam).
6230enum EdcJob {
6231    Skip { mbx: usize, mby: usize, mv: (i32, i32) },
6232    Inter(Box<PInterJob>),
6233    B(Box<BJob>),
6234    /// B_Skip / no-residual direct: regions only. The full `BJob` carried
6235    /// 2.6 KB of ZEROED coefficient arrays for ~60% of B macroblocks — the
6236    /// wall-time regression's main CPU tax (B-heavy MT arm measured +45% CPU).
6237    BSkip { mbx: usize, mby: usize, regions: Vec<BRegion> },
6238    /// P inter with `cbp == 0` — the P-side twin of `BSkip` (D9).
6239    InterNoRes(Box<PInterNoResJob>),
6240}
6241
6242/// A P inter macroblock with NO residual (`cbp == 0`) — motion only.
6243///
6244/// D9. `PInterJob` is 2,784 bytes and **93% of that is coefficient arrays**
6245/// (`luma_scan` 1024 + `luma8` 1024 + `cac` 512 + `cdc` 32 = 2,592). When
6246/// `cbp == 0` every one of them is ZERO, and the seam was heap-allocating,
6247/// filling, channel-passing and freeing all 2,592 bytes of nothing —
6248/// 12.8-37.5% of inter jobs on the x264 corpus, 15.8-44.1 MB per 60-frame pass.
6249///
6250/// This is the same pathology `EdcJob::BSkip` was introduced to fix on the B
6251/// side ("2.6 KB of ZEROED coefficient arrays for ~60% of B macroblocks — the
6252/// wall-time regression's main CPU tax"). It was never ported to P.
6253///
6254/// The worker rebuilds the full job with zeroed coefficients and calls the SAME
6255/// `recon_p_inter`, so this cannot diverge from the full path: there is no
6256/// second reconstruction implementation, only a cheaper way to ship the inputs.
6257struct PInterNoResJob {
6258    mbx: usize,
6259    mby: usize,
6260    t8: bool,
6261    qp: u8,
6262    gmv: [(i32, i32); 16],
6263    gref: [u8; 16],
6264}
6265
6266impl PInterNoResJob {
6267    /// Rebuild the full job. `cbp == 0` means every coefficient array is zero
6268    /// and `cbp_chroma`/`nnzs` are zero, which is exactly what this fills in.
6269    #[inline]
6270    fn to_full(&self) -> PInterJob {
6271        PInterJob {
6272            mbx: self.mbx,
6273            mby: self.mby,
6274            t8: self.t8,
6275            qp: self.qp,
6276            cbp_chroma: 0,
6277            gmv: self.gmv,
6278            gref: self.gref,
6279            luma_scan: [[0i32; 16]; 16],
6280            luma8: [[0i32; 64]; 4],
6281            cdc: [[0i32; 4]; 2],
6282            cac: [[[0i32; 16]; 4]; 2],
6283            nnzs: [0u8; 24],
6284        }
6285    }
6286}
6287
6288/// The compact inputs of one CABAC P inter macroblock's reconstruction.
6289struct PInterJob {
6290    mbx: usize,
6291    mby: usize,
6292    t8: bool,
6293    qp: u8,
6294    cbp_chroma: u32,
6295    /// The committed per-block motion, copied at parse time so the worker
6296    /// never reads the parse thread's grids (E2). Ref indices clamped by the
6297    /// consumer, kept u8 (spec max 15).
6298    gmv: [(i32, i32); 16],
6299    gref: [u8; 16],
6300    luma_scan: [[i32; 16]; 16],
6301    luma8: [[i32; 64]; 4],
6302    cdc: [[i32; 4]; 2],
6303    cac: [[[i32; 16]; 4]; 2],
6304    nnzs: [u8; 24],
6305}
6306
6307/// Entropy-decouple master knob — DEFAULT ON since 2026-08-05 (`RS_H264_EDC=0`
6308/// opts out). E1 was expected to be cost-neutral scaffolding for the E2
6309/// thread; it BANKED on its own: 13/15 pairs, z=2.84, median +4.0% (pooled
6310/// 19/24, z=2.86). Mechanism: LOOP FISSION — batching a row's parsing and
6311/// then a row's reconstruction keeps each large code path's I-cache and
6312/// branch state hot, instead of alternating two giant bodies per macroblock.
6313fn edc_on() -> bool {
6314    use std::sync::atomic::{AtomicU8, Ordering};
6315    static ON: AtomicU8 = AtomicU8::new(0);
6316    match ON.load(Ordering::Relaxed) {
6317        0 => {
6318            let v = !std::env::var_os("RS_H264_EDC").is_some_and(|v| v == "0");
6319            ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
6320            v
6321        }
6322        n => n == 1,
6323    }
6324}
6325
6326/// E2/E3 worker-thread knob — DEFAULT ON since 2026-08-05 (`RS_H264_EDC_MT=0`
6327/// opts out). Banked at wall-time 9/11 z=2.11, median +23.4% on P content
6328/// (measured on 2 cores — overlap is invisible to single-core CPU time).
6329/// D8: run every pixel reconstruction TWICE (the second pass is discarded work,
6330/// not discarded output -- recon is idempotent, so the bytes are unchanged).
6331/// `t(double) - t(single)` IS the pixel half's cost, which is the parallel
6332/// fraction the E2 seam can address. Byte-identity is the proof the ablation
6333/// did not change the program (unlike removing the stage, which cascades).
6334/// D9 compact no-residual P inter jobs. `RS_H264_NORES=0` restores the old
6335/// always-full-payload path for A/B (the arm must PIN the value, never inherit
6336/// a default -- an "off" arm that only omits an override measures
6337/// default-vs-default and prints all zeros).
6338/// D10 row batching — **DEFAULT ON. A DELIBERATE THROUGHPUT-OVER-LATENCY TRADE.**
6339///
6340/// Ships a row's pixel jobs in one message instead of one per macroblock:
6341/// 207,949 sends -> 3,086 (**67-70x fewer**), and **~3% less CPU**.
6342///
6343/// ⚠ IT COSTS 2-4% WALL on a single stream — 0.963x / 0.980x, **11/11 pairs on
6344/// two clips**, A/B'd against itself at a fixed queue bound. That is measured,
6345/// reproducible, and ACCEPTED, not an oversight. Do not "fix" it by flipping
6346/// the default; read this first.
6347///
6348/// WHY IT COSTS WALL: batching trades pipelining for synchronisation.
6349/// Per-macroblock sends let the worker start on job 1 immediately; a row batch
6350/// makes it idle until ~70 macroblocks are parsed, then hands it a burst.
6351///
6352/// WHY IT IS STILL THE RIGHT DEFAULT: wall time here is SINGLE-STREAM LATENCY;
6353/// CPU is THROUGHPUT. A host decoding many streams concurrently is CPU-bound,
6354/// not latency-bound, so 3% less CPU is ~3% more capacity while the 2-4% wall
6355/// cost falls on a dimension that is not the constraint. The 67x drop in
6356/// channel operations also removes a park/unpark storm that scales with the
6357/// number of runnable threads — it gets better, not worse, as the box fills.
6358///
6359/// `RS_H264_BATCH=0` restores per-macroblock sends for the latency-sensitive
6360/// single-stream case (playback, seek preview, anything where first-frame time
6361/// dominates).
6362fn batch_on() -> bool {
6363    static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6364    *V.get_or_init(|| !std::env::var_os("RS_H264_BATCH").is_some_and(|v| v == "0"))
6365}
6366
6367fn nores_on() -> bool {
6368    static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6369    *V.get_or_init(|| !std::env::var_os("RS_H264_NORES").is_some_and(|v| v == "0"))
6370}
6371
6372fn double_recon() -> bool {
6373    static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6374    *V.get_or_init(|| std::env::var_os("RS_H264_DOUBLE_RECON").is_some())
6375}
6376
6377fn edc_bound() -> usize {
6378    static V: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6379    *V.get_or_init(|| {
6380        std::env::var("RS_H264_EDC_BOUND")
6381            .ok()
6382            .and_then(|v| v.parse().ok())
6383            .unwrap_or(256)
6384    })
6385}
6386
6387/// D12 — E2 THREADING DISPATCH. Fires on `720p-or-smaller AND bits/MB > 38.4`.
6388///
6389/// The seam threads unconditionally before this, and that shipped a REGRESSION:
6390/// 8-10% slower wall on main profile for 38-56% more CPU. It cannot pay in
6391/// general — the pixel half is only ~15.6% of decode, so Amdahl caps two
6392/// threads at 1.085x — but it DOES win on some streams, so the answer is a
6393/// dispatch, not abandonment.
6394///
6395/// Fitted with `bench/examples/gate_optimizer.rs` over **28 interleaved
6396/// configurations** (12 clips x core counts 4-8, `bench/pinmtx.ps1`):
6397/// **net +29.40 of +30.70 perfect**, train +25.10 AND holdout +4.30, worst
6398/// fired class **+2.94**, precision 0.80. It forgoes +0.40 of wins to avoid
6399/// **169.90** of losses. Calibration: depth-2 **2/300** rules passed (0.67%),
6400/// so the separation carries information.
6401///
6402/// Per clause, both load-bearing (dropping either: -20.50 / -50.60, 6-7 big
6403/// losers):
6404/// * `bits/MB > 38.4` — coefficient density is the runtime proxy for PIXEL
6405///   SHARE: more coefficients means more residual work on the far side of the
6406///   seam. Threshold sits in an open gap (highest excluded 35.4, lowest firing
6407///   41.4). Note this is the OPPOSITE direction to an earlier hand-fitted
6408///   `bits < 65`, which was fitted to one clip and falsified.
6409/// * frame <= 720p — every 1080p configuration measured loses, including a
6410///   low-density one, so density alone is not sufficient.
6411/// * `cabac` — ADDED 2026-08-07 after the CAVLC arm made those units
6412///   measurable. bits/MB DOES NOT TRANSFER ACROSS ENTROPY CODERS: CAVLC needs
6413///   more bits for the SAME coefficients, so its density (62-65) reads deep
6414///   inside the firing region while its pixel work is unchanged. Without this
6415///   clause the rule routed CAVLC into threading, where it measured 1.29-1.49x
6416///   SLOWER — net -52.30, worst class -40.85. With it, +29.40 and worst class
6417///   +2.94. `gate_optimizer` could not find this: the rule needs THREE clauses
6418///   and the search is depth-2 (both depth-2 pairs fail, -20.50 / -50.60).
6419///
6420/// The estimate comes from ALREADY-DECODED slices, so the first slice of a
6421/// stream runs INLINE (the safe arm) until a measurement exists. Both arms are
6422/// byte-identical, so the choice can never affect output.
6423/// `RS_H264_EDC_MT=0` forces inline, `=1` forces threaded (skips the gate).
6424fn edc_dispatch(mb_w: usize, mb_h: usize, bits_per_mb: f64, cabac: bool) -> bool {
6425    const BITS_MIN: f64 = 38.4;
6426    const MAX_MBS: usize = 5000; // 720p = 3600, 1080p = 8160
6427    // The `cabac` clause is NOT cosmetic — see the header note. Without it this
6428    // rule scores net -52.30 with worst class -40.85 once CAVLC units are in the
6429    // corpus, because CAVLC's bits/MB is inflated by a less efficient entropy
6430    // coder rather than by more pixel work.
6431    cabac && bits_per_mb > BITS_MIN && mb_w * mb_h <= MAX_MBS
6432}
6433
6434fn edc_mt() -> Option<bool> {
6435    static V: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
6436    *V.get_or_init(|| match std::env::var("RS_H264_EDC_MT").ok().as_deref() {
6437        Some("0") => Some(false),
6438        Some("1") => Some(true),
6439        _ => None,
6440    })
6441}
6442
6443
6444
6445/// Row-interleaved deblocking master knob: `RS_H264_ROWDB=0` opts out,
6446/// restoring the picture-end pipeline (WHYS Part 17) as the A/B comparator.
6447fn rowdb_on() -> bool {
6448    use std::sync::atomic::{AtomicU8, Ordering};
6449    static ON: AtomicU8 = AtomicU8::new(0);
6450    match ON.load(Ordering::Relaxed) {
6451        0 => {
6452            let v = !std::env::var_os("RS_H264_ROWDB").is_some_and(|v| v == "0");
6453            ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
6454            v
6455        }
6456        n => n == 1,
6457    }
6458}
6459
6460/// 4×4-block (z-order) → 30-entry (6-stride) mv/ref/mvd cache index (openh264
6461/// g_kCache30ScanIdx). Top neighbour = cache[idx-6], left = cache[idx-1].
6462const CACHE30: [usize; 16] = [7, 8, 13, 14, 9, 10, 15, 16, 19, 20, 25, 26, 21, 22, 27, 28];
6463
6464/// z-order 4×4-block → raster index (openh264 g_kuiScan4). Per-MB mvd/ref state is
6465/// stored raster-indexed (matching how neighbour blocks 3/7/11/15 and 12..15 are read).
6466const G_SCAN4: [usize; 16] = [0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15];
6467
6468/// P `sub_mb_type` CABAC (openh264 `ParseSubMBTypeCabac`, ctx 21). 0=8×8, 1=8×4, 2=4×8, 3=4×4.
6469fn parse_sub_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
6470    const S: usize = 21;
6471    if cab.decode_decision(S) != 0 {
6472        return 0;
6473    }
6474    if cab.decode_decision(S + 1) != 0 {
6475        3 - cab.decode_decision(S + 2)
6476    } else {
6477        1
6478    }
6479}
6480
6481/// Intra `mb_type` sub-parse for P/B slices (openh264 `DecodeCabacIntraMbType`, `base`=32
6482/// for B). Returns 0 = I_4x4, 1..=24 = I_16x16, 25 = I_PCM (in the intra numbering).
6483fn parse_intra_mb_type_cabac(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
6484    if cab.decode_decision(base) == 0 {
6485        return 0; // I_4x4
6486    }
6487    if cab.decode_terminate() {
6488        return 25; // I_PCM
6489    }
6490    let mut t = 1 + 12 * cab.decode_decision(base + 1) as u32; // cbp_luma != 0
6491    if cab.decode_decision(base + 2) != 0 {
6492        t += 4 + 4 * cab.decode_decision(base + 2) as u32;
6493    }
6494    t += 2 * cab.decode_decision(base + 3) as u32;
6495    t += cab.decode_decision(base + 3) as u32;
6496    t
6497}
6498
6499/// B `mb_type` CABAC (openh264 `ParseMBTypeBSliceCabac`, ctx base 27). `ctx_inc` = (left
6500/// avail & !direct) + (top avail & !direct). Returns 0 = B_Direct_16x16, 1..=21 = the
6501/// L0/L1/Bi 16×16/16×8/8×16 shapes, 22 = B_8x8, 23.. = intra (mb_type − 23).
6502/// Test-only alias so the ENCODER crate can gate `cb_mb_type_b` against this
6503/// parser directly — they are exact inverses, so a round-trip is a complete gate.
6504#[doc(hidden)]
6505pub fn parse_mb_type_b(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
6506    parse_mb_type_b_cabac(cab, ctx_inc)
6507}
6508
6509fn parse_mb_type_b_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
6510    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
6511    const B: usize = 27;
6512    if cab.decode_decision(B + ctx_inc) == 0 {
6513        return 0; // B_Direct_16x16
6514    }
6515    if cab.decode_decision(B + 3) == 0 {
6516        return 1 + cab.decode_decision(B + 5) as u32; // 16×16 L0 / L1
6517    }
6518    let mut m = (cab.decode_decision(B + 4) as u32) << 3;
6519    m |= (cab.decode_decision(B + 5) as u32) << 2;
6520    m |= (cab.decode_decision(B + 5) as u32) << 1;
6521    m |= cab.decode_decision(B + 5) as u32;
6522    if m < 8 {
6523        return m + 3;
6524    }
6525    if m == 13 {
6526        return parse_intra_mb_type_cabac(cab, 32) + 23;
6527    }
6528    if m == 14 {
6529        return 11; // B_Bi_8x16
6530    }
6531    if m == 15 {
6532        return 22; // B_8x8
6533    }
6534    m = (m << 1) | cab.decode_decision(B + 5) as u32;
6535    m - 4
6536}
6537
6538/// B `sub_mb_type` CABAC (openh264 `ParseBSubMBTypeCabac`, ctx base 36). Returns 0..=12
6539/// per spec Table 7-18 (0 = B_Direct_8x8, 1 = B_L0_8x8, …, 12 = B_Bi_4x4).
6540fn parse_sub_mb_type_b_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
6541    const B: usize = 36;
6542    if cab.decode_decision(B) == 0 {
6543        return 0; // B_Direct_8x8
6544    }
6545    if cab.decode_decision(B + 1) == 0 {
6546        return 1 + cab.decode_decision(B + 3) as u32; // B_L0_8x8 / B_L1_8x8
6547    }
6548    let mut st = 3u32;
6549    if cab.decode_decision(B + 2) != 0 {
6550        if cab.decode_decision(B + 3) != 0 {
6551            return 11 + cab.decode_decision(B + 3) as u32; // B_L1_4x4 / B_Bi_4x4
6552        }
6553        st += 4;
6554    }
6555    st += 2 * cab.decode_decision(B + 3) as u32;
6556    st += cab.decode_decision(B + 3) as u32;
6557    st
6558}
6559
6560/// Parse one motion partition's `mvd` (x,y) and splat it into the 30-entry cache + the
6561/// per-MB raster mvd/ref state. `part_idx` = the partition's top-left z-order block (for
6562/// the ctxInc neighbour lookup); `zblocks` = every z-order 4×4 block the partition covers.
6563fn parse_mvd_partition(
6564    cab: &mut crate::cabac::Cabac,
6565    part_idx: usize,
6566    zblocks: &[usize],
6567    mvdc: &mut [[i16; 2]; 30],
6568    refc: &mut [i8; 30],
6569    mmvd: &mut [[i16; 2]; 16],
6570    mref: &mut [i8; 16],
6571    ref_idx: i8,
6572) -> (i32, i32) {
6573    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
6574    let s = CACHE30[part_idx];
6575    let ctx = |comp: usize| -> usize {
6576        let mut a = 0i32;
6577        if refc[s - 6] >= 0 {
6578            a += mvdc[s - 6][comp].unsigned_abs() as i32;
6579        }
6580        if refc[s - 1] >= 0 {
6581            a += mvdc[s - 1][comp].unsigned_abs() as i32;
6582        }
6583        if a >= 3 {
6584            1 + (a > 32) as usize
6585        } else {
6586            0
6587        }
6588    };
6589    let (cx, cy) = (ctx(0), ctx(1));
6590    let mvx = parse_mvd_cabac(cab, 0, cx);
6591    let mvy = parse_mvd_cabac(cab, 1, cy);
6592    for &zb in zblocks {
6593        mvdc[CACHE30[zb]] = [mvx, mvy];
6594        refc[CACHE30[zb]] = ref_idx;
6595        mmvd[G_SCAN4[zb]] = [mvx, mvy];
6596        mref[G_SCAN4[zb]] = ref_idx;
6597    }
6598    (mvx as i32, mvy as i32)
6599}
6600
6601/// `ref_idx_l0` (P) CABAC — mirror of the encoder `cb_ref_idx`. Unary, ctxIdxOffset
6602/// 54: binIdx 0 → `ctx0` (condTermFlagA + 2·condTermFlagB), binIdx 1 → 4, binIdx ≥2 → 5.
6603fn parse_ref_idx_cabac(cab: &mut crate::cabac::Cabac, ctx0: usize) -> i8 {
6604    const B: usize = 54;
6605    let mut r = 0i8;
6606    let mut bin_idx = 0u32;
6607    // Cap the unary length: valid ref_idx ≤ 15 (16 refs max); the cap keeps a corrupt
6608    // stream from looping unboundedly. The MC clamps the index, so an over-range value
6609    // is decoded as garbage (never a panic) — the robustness contract, not correctness.
6610    while bin_idx < 32 {
6611        let ctx = match bin_idx {
6612            0 => ctx0,
6613            1 => 4,
6614            _ => 5,
6615        };
6616        if cab.decode_decision(B + ctx) == 0 {
6617            break;
6618        }
6619        r += 1;
6620        bin_idx += 1;
6621    }
6622    r
6623}
6624
6625/// UEG3 mvd suffix (openh264 `DecodeUEGMvCabac`): TU prefix at `base + {0,1,2,3,3,..}`
6626/// (≤7), then EG3 bypass.
6627fn decode_ueg_mv(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
6628    const P2C: [usize; 8] = [0, 1, 2, 3, 3, 3, 3, 3];
6629    if cab.decode_decision(base) == 0 {
6630        return 0;
6631    }
6632    let mut code = 0u32;
6633    let mut count = 1usize;
6634    let mut tmp;
6635    loop {
6636        tmp = cab.decode_decision(base + P2C[count]);
6637        code += 1;
6638        count += 1;
6639        if tmp == 0 || count == 8 {
6640            break;
6641        }
6642    }
6643    if tmp != 0 {
6644        code += cabac_exp_bypass(cab, 3) + 1;
6645    }
6646    code
6647}
6648
6649/// One `mvd` component (openh264 `ParseMvdInfoCabac`). `ctx_inc` (0/1/2) from the
6650/// neighbour |mvd| sum. ctxIdxOffset 40 (x) / 47 (y).
6651fn parse_mvd_cabac(cab: &mut crate::cabac::Cabac, comp: usize, ctx_inc: usize) -> i16 {
6652    let base = 40 + comp * 7; // NEW_CTX_OFFSET_MVD + comp*CTX_NUM_MVD
6653    if cab.decode_decision(base + ctx_inc) == 0 {
6654        return 0;
6655    }
6656    let mag = (decode_ueg_mv(cab, base + 3) + 1) as i16;
6657    if cab.decode_bypass() != 0 {
6658        -mag
6659    } else {
6660        mag
6661    }
6662}
6663
6664/// `mb_skip_flag` CABAC (openh264 `ParseSkipFlagCabac`). `ctx_inc` = base 11 (P) or 24
6665/// (B) + (left avail & not-skip) + (top avail & not-skip). Returns true if skipped.
6666fn parse_mb_skip_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> bool {
6667    cab.decode_decision(ctx_inc) != 0
6668}
6669
6670/// P-slice `mb_type` CABAC (openh264 `ParseMBTypePSliceCabac`). Returns 0..3 = inter
6671/// (P_L0_16x16 / P_16x8 / P_8x16 / P_8x8), 5 = I_4x4, 6..29 = I_16x16, 30 = I_PCM.
6672fn parse_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
6673    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
6674    const S: usize = 11; // NEW_CTX_OFFSET_SKIP; P mb_type contexts hang off it
6675    if cab.decode_decision(S + 3) == 0 {
6676        // inter
6677        return if cab.decode_decision(S + 4) != 0 {
6678            if cab.decode_decision(S + 6) != 0 { 1 } else { 2 }
6679        } else if cab.decode_decision(S + 5) != 0 {
6680            3
6681        } else {
6682            0
6683        };
6684    }
6685    // intra (prefix bit was 1)
6686    if cab.decode_decision(S + 6) == 0 {
6687        return 5; // I_4x4
6688    }
6689    if cab.decode_terminate() {
6690        return 30; // I_PCM
6691    }
6692    let mut t = 6 + cab.decode_decision(S + 7) * 12;
6693    if cab.decode_decision(S + 8) != 0 {
6694        t += 4;
6695        if cab.decode_decision(S + 8) != 0 {
6696            t += 4;
6697        }
6698    }
6699    t += cab.decode_decision(S + 9) << 1;
6700    t += cab.decode_decision(S + 9);
6701    t
6702}
6703
6704/// I-slice `mb_type` CABAC parse (spec §9.3.2.5 / openh264 `ParseMBTypeISliceCabac`).
6705/// `ctx_inc` = (left MB is I_16x16/non-intra) + (top MB is …), i.e. 0..2; the corner
6706/// MB has no neighbours so `ctx_inc = 0`. Returns the raw mb_type: 0 = I_NxN (I_4x4/
6707/// I_8x8), 1..24 = I_16x16 (pred-mode/cbp packed), 25 = I_PCM.
6708fn parse_mb_type_i_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
6709    const O: usize = 3; // ctxIdxOffset for I-slice mb_type
6710    if cab.decode_decision(O + ctx_inc) == 0 {
6711        return 0; // I_NxN
6712    }
6713    if cab.decode_terminate() {
6714        return 25; // I_PCM
6715    }
6716    let mut t = 1 + cab.decode_decision(O + 3) * 12; // CBP luma: 0 or 12
6717    if cab.decode_decision(O + 4) != 0 {
6718        t += 4; // CBP chroma 1 or 2
6719        if cab.decode_decision(O + 5) != 0 {
6720            t += 4;
6721        }
6722    }
6723    t += cab.decode_decision(O + 6) << 1; // I_16x16 pred mode (2 bins)
6724    t += cab.decode_decision(O + 7);
6725    t
6726}
6727
6728/// One `Intra_4x4` (or `8x8`) pred-mode CABAC parse (openh264 `ParseIntraPredModeLuma
6729/// Cabac`): `prev_intra4x4_pred_mode_flag` (ctx 68) then, if 0, `rem_intra4x4_pred_mode`
6730/// (3 bins at ctx 69). Returns `-1` for "use predicted mode", else the 0..7 remainder.
6731fn parse_intra4x4_pred_mode_cabac(cab: &mut crate::cabac::Cabac) -> i32 {
6732    const IPR: usize = 68;
6733    if cab.decode_decision(IPR) == 1 {
6734        return -1; // prev_intra4x4_pred_mode_flag = 1
6735    }
6736    let mut m = cab.decode_decision(IPR + 1) as i32;
6737    m |= (cab.decode_decision(IPR + 1) as i32) << 1;
6738    m |= (cab.decode_decision(IPR + 1) as i32) << 2;
6739    m
6740}
6741
6742/// `intra_chroma_pred_mode` CABAC parse (openh264 `ParseIntraPredModeChromaCabac`):
6743/// TU(cMax=3) — bin0 at ctx `64 + ctx_inc` (ctx_inc from neighbour chroma modes, 0 for
6744/// the corner MB), the rest at ctx 67. Returns the mode 0..3.
6745fn parse_intra_chroma_pred_mode_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
6746    const CIPR: usize = 64;
6747    if cab.decode_decision(CIPR + ctx_inc) == 0 {
6748        return 0;
6749    }
6750    if cab.decode_decision(CIPR + 3) == 0 {
6751        return 1;
6752    }
6753    if cab.decode_decision(CIPR + 3) == 0 {
6754        return 2;
6755    }
6756    3
6757}
6758
6759/// `coded_block_pattern` CABAC parse (openh264 `ParseCbpInfoCabac`), corner-MB variant
6760/// (top/left neighbours unavailable → their terms are 0). ctxIdxOffset 73 (luma) with 4
6761/// z-order 8×8 bins whose ctxInc uses the EARLIER-decoded bits within this MB, then
6762/// chroma bits at 77/81. Returns cbp: bits 0-3 = luma 8×8, bits 4-5 = chroma pattern.
6763fn parse_cbp_cabac(cab: &mut crate::cabac::Cabac, top: Option<u8>, left: Option<u8>) -> u32 {
6764    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
6765    const CBP: usize = 73;
6766    let t = |m: u32| top.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
6767    let l = |m: u32| left.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
6768    let nb = |x: u32| (x == 0) as u32; // earlier 8×8 bin within this MB was NOT coded
6769    // Luma, 4 8×8 blocks in z-order. Top uses cbp bits 2/3, left uses 1/3.
6770    let b0 = cab.decode_decision(CBP + (l(1 << 1) + (t(1 << 2) << 1)) as usize);
6771    let b1 = cab.decode_decision(CBP + (nb(b0) + (t(1 << 3) << 1)) as usize);
6772    let b2 = cab.decode_decision(CBP + (l(1 << 3) + (nb(b0) << 1)) as usize);
6773    let b3 = cab.decode_decision(CBP + (nb(b2) + (nb(b1) << 1)) as usize);
6774    let mut cbp = b0 | (b1 << 1) | (b2 << 2) | (b3 << 3);
6775    // Chroma (4:2:0). ctxInc from neighbour chroma cbp (>>4).
6776    let ct = top.map_or(0u32, |c| ((c >> 4) != 0) as u32);
6777    let cl = left.map_or(0u32, |c| ((c >> 4) != 0) as u32);
6778    if cab.decode_decision(CBP + 4 + (cl + (ct << 1)) as usize) != 0 {
6779        let ct2 = top.map_or(0u32, |c| ((c >> 4) == 2) as u32);
6780        let cl2 = left.map_or(0u32, |c| ((c >> 4) == 2) as u32);
6781        let c1 = cab.decode_decision(CBP + 8 + (cl2 + (ct2 << 1)) as usize);
6782        cbp |= 1 << (4 + c1);
6783    }
6784    cbp
6785}
6786
6787fn read_ref_idx(r: &mut BitReader, num_ref_active: usize) -> Result<i32, OutOfData> {
6788    if num_ref_active == 2 {
6789        Ok(if r.read_bit()? { 0 } else { 1 }) // te(v): value = !bit
6790    } else {
6791        Ok(r.read_ue()? as i32)
6792    }
6793}
6794
6795/// B-partition prediction direction.
6796#[derive(Clone, Copy, PartialEq)]
6797enum BPred {
6798    L0,
6799    L1,
6800    Bi,
6801}
6802impl BPred {
6803    /// Whether this direction uses reference list `list` (0 or 1).
6804    fn uses(self, list: usize) -> bool {
6805        matches!(
6806            (self, list),
6807            (BPred::L0, 0) | (BPred::L1, 1) | (BPred::Bi, 0) | (BPred::Bi, 1)
6808        )
6809    }
6810}
6811
6812const B16X16: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 16)];
6813const B16X8: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 8), (0, 8, 16, 8)];
6814const B8X16: &[(usize, usize, usize, usize)] = &[(0, 0, 8, 16), (8, 0, 8, 16)];
6815
6816/// A partition region `(x, y, w, h)` in samples.
6817type Region = (usize, usize, usize, usize);
6818
6819/// B `mb_type` 1..=21 → (partition layout, MV-prediction mode 0/1/2 for 16×16/
6820/// 16×8/8×16, per-partition prediction direction) (spec Table 7-14).
6821/// Test-only view of [`b_inter_layout`] for the ENCODER crate: `(mvmode, p0, p1)`
6822/// with pred coded 1 = L0, 2 = L1, 3 = Bi — the encoder's `b_part_mb_type` is the
6823/// exact inverse, so a round-trip over 4..=21 gates the two tables against drift.
6824pub fn b_inter_shape(mb_type: u32) -> (u8, u8, u8) {
6825    let (_, mvmode, preds) = b_inter_layout(mb_type);
6826    let code = |p: BPred| match (p.uses(0), p.uses(1)) {
6827        (true, true) => 3,
6828        (true, false) => 1,
6829        _ => 2,
6830    };
6831    (mvmode, code(preds[0]), code(preds[1]))
6832}
6833
6834fn b_inter_layout(mb_type: u32) -> (&'static [Region], u8, [BPred; 2]) {
6835    use BPred::*;
6836    match mb_type {
6837        1 => (B16X16, 0, [L0, L0]),
6838        2 => (B16X16, 0, [L1, L1]),
6839        3 => (B16X16, 0, [Bi, Bi]),
6840        4 => (B16X8, 1, [L0, L0]),
6841        5 => (B8X16, 2, [L0, L0]),
6842        6 => (B16X8, 1, [L1, L1]),
6843        7 => (B8X16, 2, [L1, L1]),
6844        8 => (B16X8, 1, [L0, L1]),
6845        9 => (B8X16, 2, [L0, L1]),
6846        10 => (B16X8, 1, [L1, L0]),
6847        11 => (B8X16, 2, [L1, L0]),
6848        12 => (B16X8, 1, [L0, Bi]),
6849        13 => (B8X16, 2, [L0, Bi]),
6850        14 => (B16X8, 1, [L1, Bi]),
6851        15 => (B8X16, 2, [L1, Bi]),
6852        16 => (B16X8, 1, [Bi, L0]),
6853        17 => (B8X16, 2, [Bi, L0]),
6854        18 => (B16X8, 1, [Bi, L1]),
6855        19 => (B8X16, 2, [Bi, L1]),
6856        20 => (B16X8, 1, [Bi, Bi]),
6857        _ => (B8X16, 2, [Bi, Bi]), // 21
6858    }
6859}
6860
6861/// Whether a B `sub_mb_type` (1..=12) uses reference list `list`.
6862fn b_sub_uses(st: u32, list: usize) -> bool {
6863    let pred = match st {
6864        1 | 4 | 5 | 10 => 0,  // L0
6865        2 | 6 | 7 | 11 => 1,  // L1
6866        _ => 2,               // Bi (3, 8, 9, 12)
6867    };
6868    (list == 0 && pred != 1) || (list == 1 && pred != 0)
6869}
6870
6871/// Sub-partition shapes within an 8×8 for a B `sub_mb_type` (1..=12).
6872fn b_sub_parts(st: u32) -> &'static [(usize, usize, usize, usize)] {
6873    match st {
6874        1..=3 => &[(0, 0, 8, 8)],
6875        4 | 6 | 8 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
6876        5 | 7 | 9 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
6877        _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)], // 10/11/12
6878    }
6879}
6880
6881/// Sub-macroblock partition layout `(x, y, w, h)` in samples within an 8×8, for
6882/// a P-slice `sub_mb_type` (0 = 8×8, 1 = 8×4, 2 = 4×8, 3 = 4×4).
6883fn sub_mb_partitions(sub_type: u32) -> &'static [(usize, usize, usize, usize)] {
6884    match sub_type {
6885        0 => &[(0, 0, 8, 8)],
6886        1 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
6887        2 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
6888        _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)],
6889    }
6890}
6891
6892/// Copy a contiguous `w`x`h` block into a strided destination at `(x0, y0)`.
6893///
6894/// The width is SPECIALISED. Written as a per-pixel loop bounded by a runtime `w`,
6895/// this lowers to a bounds-checked store per pixel — and where it is a row copy of
6896/// runtime length, to a variable-length `memcpy` CALL per row. Both are the same
6897/// codegen trap the ENCODER fixed long ago ("H-17"); the decoder's copy of it was
6898/// never fixed, and it costs the most on exactly the streams a real encoder emits,
6899/// because x264's sub-16x16 partitions call it far more often than our own
6900/// 16x16-dominated bitstreams ever did. Byte-identical to the scalar form.
6901#[inline]
6902fn restride(dst: &mut [u8], dst_stride: usize, x0: usize, y0: usize, src: &[u8], w: usize, h: usize) {
6903    macro_rules! rows {
6904        ($n:expr) => {{
6905            for dy in 0..h {
6906                dst[(y0 + dy) * dst_stride + x0..][..$n].copy_from_slice(&src[dy * $n..][..$n]);
6907            }
6908        }};
6909    }
6910    match w {
6911        16 => rows!(16),
6912        8 => rows!(8),
6913        4 => rows!(4),
6914        2 => rows!(2),
6915        _ => {
6916            for dy in 0..h {
6917                dst[(y0 + dy) * dst_stride + x0..][..w].copy_from_slice(&src[dy * w..][..w]);
6918            }
6919        }
6920    }
6921}
6922
6923fn store(plane: &mut [u8], stride: usize, x0: usize, y0: usize, s: &[u8; 16]) {
6924    let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Scatter);
6925    for dy in 0..4 {
6926        for dx in 0..4 {
6927            plane[(y0 + dy) * stride + (x0 + dx)] = s[dy * 4 + dx];
6928        }
6929    }
6930}
6931
6932/// Un-scans an 8×8 block from frame zig-zag scan order to raster (spec Table 8-12).
6933fn un_scan_8x8(scan: &[i32; 64]) -> [i32; 64] {
6934    const ZZ8: [usize; 64] = [
6935        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,
6936        20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
6937        58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
6938    ];
6939    let mut out = [0i32; 64];
6940    for k in 0..64 {
6941        out[ZZ8[k]] = scan[k];
6942    }
6943    out
6944}
6945
6946#[cfg(test)]
6947mod tests {
6948    use super::*;
6949
6950    fn fd(qp: u8, offset: i32) -> FrameDecoder {
6951        FrameDecoder::new(1, 1, qp, offset, Vec::new(), 1, false, false, true)
6952    }
6953
6954    #[test]
6955    fn mb_qp_delta_accumulates_mod_52() {
6956        let mut d = fd(26, 0);
6957        assert_eq!(d.cur_qp, 26, "QPy starts at the slice QP");
6958        d.step_qp(4);
6959        assert_eq!(d.cur_qp, 30); // 26 + 4
6960        d.step_qp(-10);
6961        assert_eq!(d.cur_qp, 20); // carries from the previous MB, not the slice
6962        // Wrap-around: (20 + 40 + 52) % 52 = 112 % 52 = 8.
6963        d.step_qp(40);
6964        assert_eq!(d.cur_qp, 8);
6965        // Negative wrap: (8 - 20 + 52) % 52 = 40.
6966        d.step_qp(-20);
6967        assert_eq!(d.cur_qp, 40);
6968    }
6969
6970    #[test]
6971    fn chroma_qp_index_offset_applied_and_clamped() {
6972        // Offset 0 reproduces the bare luma->chroma table (QP30 -> 29).
6973        assert_eq!(fd(0, 0).chroma_qp_for(30), 29);
6974        // Positive offset shifts the table lookup (QP30 + 2 -> table[2] = 31).
6975        assert_eq!(fd(0, 2).chroma_qp_for(30), 31);
6976        // The qPi index is clamped into 0..=51 before the lookup.
6977        assert_eq!(fd(0, -12).chroma_qp_for(5), chroma_qp(0));
6978        assert_eq!(fd(0, 99).chroma_qp_for(40), chroma_qp(51));
6979    }
6980}