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_with, read_cbp_inter, read_cbp_intra, un_scan_4x4_ac_into,
11 un_scan_4x4_dcac, vlc_tables,
12};
13use rusty_h264_common::inter::{
14 inter_partitions, mc_chroma_padded, mc_luma_padded, predict_mv, predict_partition_mv,
15 MvNeighbor,
16};
17use rusty_h264_common::predict::{
18 add_residual_8x8, chroma8x8_pred, chroma_qp, intra4x4_pred, intra8x8_pred, luma16x16_pred,
19 reconstruct_4x4_dc_into, reconstruct_4x4_into, I16Mode,
20 CHROMA_4X4_SCAN_XY, LUMA_4X4_SCAN_XY,
21};
22use rusty_h264_common::transform::{
23 dequant_scatter_4x4, dequantize, dequantize_weighted, inverse_quant_8x8,
24 inverse_quant_chroma_dc,
25 inverse_quant_chroma_dc_weighted, inverse_quant_luma_dc, inverse_quant_luma_dc_weighted,
26};
27use rusty_h264_common::{BitReader, YuvFrame};
28
29/// One frame's motion field, in 4x4-block raster (`mb_w*4` wide).
30///
31/// Captured from any conformant stream this decoder parses — including x264's —
32/// so a harness can compare motion fields between encoders without depending on
33/// external MV-export tooling.
34pub struct MvField {
35 pub mb_w: usize,
36 pub mb_h: usize,
37 pub mv: Vec<(i32, i32)>,
38 pub ref_idx: Vec<i32>,
39 pub inter: Vec<bool>,
40}
41
42/// Frames captured in decode order when `RFF_MV_DUMP=1`. Diagnostic only.
43pub static MV_DUMP: std::sync::Mutex<Vec<MvField>> = std::sync::Mutex::new(Vec::new());
44
45pub fn mv_dump_on() -> bool {
46 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
47 *ON.get_or_init(|| std::env::var("RFF_MV_DUMP").map_or(false, |v| v != "0"))
48}
49
50/// Copy filtered MB rows `[prev..mb_rows)` from coded-size planes into a
51/// Frame-MT progress slot's live padded planes, then bump `ready_rows`.
52/// Coarser publish: only advance the watermark every 2 MB rows (and always
53/// on the last row) to cut lock/notify traffic.
54fn publish_filtered_rows_to_slot(
55 slot: &crate::RefFrame,
56 rec_y: &[u8],
57 rec_u: &[u8],
58 rec_v: &[u8],
59 cw: usize,
60 ccw: usize,
61 ch: usize,
62 mb_rows: usize,
63) {
64 if mb_rows == 0 || slot.frozen.get().is_some() {
65 return;
66 }
67 if !crate::frame_mt::row_publish_on() {
68 return;
69 }
70 let mb_h = (ch + 15) / 16;
71 // Batch: defer watermark until even MB rows (or picture end).
72 if mb_rows < mb_h && (mb_rows & 1) != 0 {
73 return;
74 }
75 let Some(live) = &slot.live else {
76 slot.publish_ready_rows(mb_rows * 16);
77 return;
78 };
79 let cch = ch / 2;
80 let prev = slot.ready_rows.load(std::sync::atomic::Ordering::Acquire) / 16;
81 if mb_rows <= prev {
82 return;
83 }
84 let mut py = live.py.write().unwrap();
85 let mut pu = live.pu.write().unwrap();
86 let mut pv = live.pv.write().unwrap();
87 let ls = cw + 2 * crate::LPAD;
88 let cs = ccw + 2 * crate::CPAD;
89 for mr in prev..mb_rows {
90 for dy in 0..16 {
91 let y = mr * 16 + dy;
92 if y >= ch {
93 break;
94 }
95 let src = &rec_y[y * cw..(y + 1) * cw];
96 let dst_y = y + crate::LPAD;
97 // ONE row segment covering pad+picture+pad, then `split_at_mut` and
98 // two fills — the edge reads and the 2*LPAD writes were separate
99 // whole-plane indexes. Same shape as `expand_plane`.
100 let seg = &mut py[dst_y * ls..][..2 * crate::LPAD + cw];
101 let (lpad, rest) = seg.split_at_mut(crate::LPAD);
102 let (mid, rpad) = rest.split_at_mut(cw);
103 mid.copy_from_slice(src);
104 let (Some(&left), Some(&right)) = (mid.first(), mid.last()) else {
105 continue;
106 };
107 lpad.fill(left);
108 rpad[..crate::LPAD].fill(right);
109 }
110 for dy in 0..8 {
111 let cy = mr * 8 + dy;
112 if cy >= cch {
113 break;
114 }
115 for (rec, plane) in [(rec_u, &mut *pu), (rec_v, &mut *pv)] {
116 let src = &rec[cy * ccw..(cy + 1) * ccw];
117 let dst_y = cy + crate::CPAD;
118 let seg = &mut plane[dst_y * cs..][..2 * crate::CPAD + ccw];
119 let (lpad, rest) = seg.split_at_mut(crate::CPAD);
120 let (mid, rpad) = rest.split_at_mut(ccw);
121 mid.copy_from_slice(src);
122 let (Some(&left), Some(&right)) = (mid.first(), mid.last()) else {
123 continue;
124 };
125 lpad.fill(left);
126 rpad[..crate::CPAD].fill(right);
127 }
128 }
129 }
130 if prev == 0 {
131 // Copy the whole first picture ROW upward instead of walking columns:
132 // `split_at_mut` at the pad boundary proves both sides at once.
133 let (pad, rest) = py.split_at_mut(crate::LPAD * ls);
134 if let Some(first) = rest.get(..ls) {
135 for row in pad.chunks_mut(ls) {
136 row.copy_from_slice(first);
137 }
138 }
139 for plane in [&mut *pu, &mut *pv] {
140 let (pad, rest) = plane.split_at_mut(crate::CPAD * cs);
141 if let Some(first) = rest.get(..cs) {
142 for row in pad.chunks_mut(cs) {
143 row.copy_from_slice(first);
144 }
145 }
146 }
147 }
148 slot.publish_ready_rows(mb_rows * 16);
149}
150
151/// Reconstructed coded-size planes plus CAVLC `nnz` context grids.
152pub struct FrameDecoder {
153 mb_w: usize,
154 mb_h: usize,
155 /// Slice QP (`SliceQPy`) — the deblock filter's frame-level QP.
156 qp: u8,
157 /// Running luma QP (`QPy`), carried across macroblocks and stepped by each
158 /// `mb_qp_delta` (spec §7.4.5). Equals `qp` on constant-QP streams.
159 cur_qp: u8,
160 /// `chroma_qp_index_offset` from the active PPS (§8.5.8).
161 chroma_qp_offset: i32,
162 cw: usize,
163 ch: usize,
164 ccw: usize,
165 cch: usize,
166 rec_y: Vec<u8>,
167 rec_u: Vec<u8>,
168 rec_v: Vec<u8>,
169 /// Per-macroblock luma QP (`QPy`), for per-edge deblock strength.
170 mb_qp: Vec<u8>,
171 /// First macroblock address of the slice currently being decoded. Neighbors
172 /// with a lower address belong to an earlier slice and are "not available"
173 /// for prediction (spec §8.3/§8.4). Slices are contiguous raster ranges (we
174 /// reject FMO/slice-groups), so address ≥ this ⇔ same slice.
175 slice_first_mb: usize,
176 nnz_y: Vec<u8>,
177 nnz_c: [Vec<u8>; 2],
178 modes_y: Vec<u8>,
179 coded_y: Vec<bool>,
180 /// Per-4×4-block List-0 motion (mv + ref index, `-1` = no L0). For P slices
181 /// this is the only motion; B slices add the List-1 grids below.
182 mv_y: Vec<(i32, i32)>,
183 inter_y: Vec<bool>,
184 ref_idx_y: Vec<i32>,
185 /// Per-4×4-block List-1 motion for B slices (`ref_idx1 = -1` = no L1).
186 mv1: Vec<(i32, i32)>,
187 ref_idx1: Vec<i32>,
188 /// `RefPicList1` and B-slice flags (unused outside B slices).
189 refs1: Vec<crate::Ref>,
190 num_ref_active1: usize,
191 is_b: bool,
192 /// True if the stream's profile permits B-slices (`profile_idc != 66`). When
193 /// false (Baseline / Constrained Baseline), `as_reference_pooled` skips the per-block
194 /// motion (mv/ref_idx/ref_poc) that only B temporal/spatial direct ever reads.
195 b_possible: bool,
196 direct_spatial: bool,
197 nnz_l_cache: [u8; 25],
198 nnz_c_cache: [[u8; 9]; 2],
199 /// Decoded-picture buffer (most-recent first); empty in I-slices. `ref_idx`
200 /// indexes into this list.
201 refs: Vec<crate::Ref>,
202 /// `num_ref_idx_l0_active` for the current slice — drives whether `ref_idx`
203 /// is coded (active > 1) and its te(v)/ue(v) form, independently of how many
204 /// reference pictures actually exist (spec §7.4.5.1, §9.1).
205 num_ref_active: usize,
206 /// `constrained_intra_pred_flag`: when set, intra prediction may only use
207 /// samples from intra-coded neighbors (inter neighbors are "not available").
208 constrained_intra: bool,
209 /// High-profile 4×4 scaling matrices in **raster** order, indexed by
210 /// `[Y-intra, Cb-intra, Cr-intra, Y-inter, Cb-inter, Cr-inter]`. `None` = flat.
211 scaling: Option<[[i32; 16]; 6]>,
212 /// High-profile 8×8 luma scaling matrices in raster order `[Y-intra, Y-inter]`
213 /// (4:2:0 has only these two). `None` = flat.
214 scaling8: Option<[[i32; 64]; 2]>,
215 /// `transform_8x8_mode_flag` from the PPS: enables `transform_size_8x8_flag`.
216 transform_8x8_mode: bool,
217 /// SPS `qpprime_y_zero_transform_bypass_flag` — refusal gate in `step_qp`.
218 transform_bypass: bool,
219 /// GATE 1 router counters (big-oppy-decoder §2): skip MBs and
220 /// residual-coded MBs this picture. Two integer adds per MB, no atomics.
221 route_skip_mbs: u32,
222 route_coded_mbs: u32,
223 /// Per-slice `(first_mb, idc == 2)` in decode order — drives the
224 /// `disable_deblocking_filter_idc == 2` cross-slice-edge suppression.
225 slice_bounds: Vec<(usize, bool)>,
226 /// The CURRENT slice's `disable_deblocking_filter_idc == 2`, latched by
227 /// `set_deblock_params` and recorded per slice by the decode loops.
228 cur_idc2: bool,
229 /// Per-macroblock `transform_size_8x8_flag` (for deblocking: internal 4×4
230 /// luma edges of 8×8-transform MBs are not filtered).
231 mb_t8x8: Vec<bool>,
232 // ---- Row-interleaved deblocking state (docs/row-interleave-plan.md) ----
233 /// Per-MB boundary strengths, filled row-by-row as decode completes rows.
234 bs_frame: Vec<rusty_h264_common::deblock::MbBs>,
235 /// Rows whose bS is derived (watermark).
236 bs_rows: usize,
237 /// Rows already deblock-FILTERED (watermark; R3).
238 flt_rows: usize,
239 /// Two-row rolling window of packed records (prev = row r-1, cur = row r).
240 pk_prev: Vec<rusty_h264_common::deblock::MbPack>,
241 pk_cur: Vec<rusty_h264_common::deblock::MbPack>,
242 /// Transform-block coded mask (nnz with the 8x8 OR applied), filled per row.
243 nnz_dbr: Vec<u8>,
244 /// Unfiltered bottom rows of the last-filtered MB row (intra reads these).
245 bak_y: Vec<u8>,
246 bak_u: Vec<u8>,
247 bak_v: Vec<u8>,
248 /// Entropy-decouple: deferred pixel jobs + the per-slice activation flag
249 /// (CABAC slices only — the CAVLC loop has no flush hooks).
250 edc_jobs: Vec<EdcJob>,
251 edc_active: bool,
252 // ---- E2: the worker-thread plumbing (all None outside a threaded slice) ----
253 edc_tx: Option<std::sync::mpsc::SyncSender<EdcMsg>>,
254 edc_ctx_rx: Option<std::sync::mpsc::Receiver<PixelCtx>>,
255 edc_back_tx: Option<std::sync::mpsc::Sender<PixelCtx>>,
256 /// While the parse thread holds the pixel context for an intra macroblock
257 /// (planes moved into `self`), the rest of the context parks here.
258 edc_parked: Option<PixelCtx>,
259 /// E3: while parsing a B macroblock in threaded mode, its MC regions
260 /// accumulate here instead of executing (the pixel side is the worker's).
261 edc_regions: Option<Vec<BRegion>>,
262 /// Measurement only: previous full-MB single-rect direct (x, y, refi0,
263 /// refi1, m0, m1) for run-adjacency counting. No decode behavior.
264 bsk_last: Option<(usize, usize, i32, i32, (i32, i32), (i32, i32))>,
265 /// D10: jobs accumulated for the current row, sent as ONE message.
266 edc_batch: Vec<EdcJob>,
267 /// D12: bits/MB carried in from previous slices (0 = not yet known).
268 bits_per_mb: f64,
269 /// Current slice's deblock parameters (set per slice by the caller).
270 db_ena: bool,
271 db_oa: i32,
272 db_ob: i32,
273 /// Per-macroblock deblock derivation CLASS (`MB_KIND_*`), so the loop filter
274 /// can skip the 24-block neighbourhood gather on macroblocks whose strengths
275 /// are determined by syntax alone. Starts UNSET; anything left UNSET simply
276 /// takes the blind path, so a missed producer site costs speed, not
277 /// correctness. Only classes that are uniform BY SYNTAX are written — notably
278 /// NOT `B_Skip`/`B_Direct`, whose direct-derived motion varies per 4×4.
279 mb_kind: Vec<u8>,
280 /// Explicit weighted-prediction tables, when active for this slice.
281 weights: Option<WeightTable>,
282 /// `frame_mt::row_progress_on()` cached at construction (see
283 /// wait_refs_for_mb).
284 row_wait: bool,
285 /// Colocated-picture fast-probe cache (set_b_context): true when refs1[0]
286 /// is frozen (1T), short-term, with motion grids — col_zero then skips
287 /// the Option + live + long_term + w4 branch chain per probe.
288 col_ok: bool,
289 col_w4: usize,
290 /// Measurement knobs cached at construction — these were OnceLock derefs
291 /// on EVERY skip macroblock (no_bskipfast per B skip, no_runmv per P
292 /// skip); one field test now.
293 k_no_bskipfast: bool,
294 k_no_runmv: bool,
295 /// rowdb_on()/rowhook_eager() cached at construction — these were atomic
296 /// loads inside row_hook_at on EVERY macroblock.
297 k_rowdb: bool,
298 k_roweager: bool,
299 /// MEASUREMENT KNOB, cached. `kind_loads()` is a `OnceLock` deref and was
300 /// evaluated as a MATCH GUARD on every Skip/InterUniform macroblock in
301 /// `derive_bs_row` — the dominant class. The census (DBSDERIVE kindarm=0 on
302 /// 6/6 corpus streams) shows the arm it guards never fires by default, so
303 /// every one of those derefs was pure tax. Same treatment `k_rowdb` /
304 /// `k_roweager` already had.
305 k_kindloads: bool,
306 /// True once ANY macroblock in this picture used the 8x8 transform. The
307 /// `nnz_dbr` row copy + the per-row t8 scan in `derive_bs_row` exist ONLY
308 /// to apply the 8x8 coded-status OR; with no t8 macroblock `nnz_dbr` is a
309 /// byte-for-byte copy of `nnz_y`, so both are skipped and the derivation
310 /// reads `nnz_y` directly. Census: t8mb=0 on every cavlc/main stream.
311 any_t8: bool,
312 /// True once any slice in this picture carries
313 /// `disable_deblocking_filter_idc == 2`. Replaces a per-ROW
314 /// `slice_bounds.iter().any(..)` scan (census: idc2row=0 on the corpus).
315 any_idc2: bool,
316 /// Raster address whose P_Skip MV is FORCED (0,0): the previous
317 /// decode_p_skip was at addr-1 and committed (0,0), so this MB's left
318 /// neighbor either fires §8.4.1.1's zero-MV rule (in-slice: ref 0,
319 /// (0,0)) or is unavailable (out-of-slice — same rule). Interior MBs of
320 /// a skip run skip the 3-neighbor gather entirely.
321 skip_zero_next: usize,
322 /// Pending B_Skip grid+recon span: (mb row, x0, n, kind) of CONSECUTIVE
323 /// fast B_Skips with IDENTICAL committed values, deferred and range-
324 /// filled/band-reconned at flush. Flushed before ANY grid or pixel
325 /// reader runs — see span_flush callers.
326 bzspan: Option<(usize, usize, usize, BzKind)>,
327 /// Pending P_Skip (0,0) grid-commit span — the P mirror of `bzspan`
328 /// (values: ref 0 list-0, mv (0,0), inter, coded, DC mode, kind SKIP).
329 /// The bool records whether the RECON is deferred too (1T, identity/no
330 /// weights) — decided at push time, not re-derived at flush (begin_slice
331 /// may have changed the fields in between).
332 pzspan: Option<(usize, usize, usize, bool)>,
333 /// Per-picture bitmap: MB decoded as a ref0/(0,0)-both-lists zero-bi fast
334 /// B_Skip. Never cleared mid-picture — false only ever means "derive
335 /// normally" — so no MB path carries a clearing duty.
336 bzero: Vec<bool>,
337 /// Pooled CABAC slice scratch — refilled (not reallocated) at slice entry;
338 /// carried across pictures via `GridPool`. Taken out of `self` at
339 /// `decode_slice_cabac_inner` entry and put back at its normal exit (an
340 /// error exit simply forfeits the pooled allocation).
341 sc_cat: Vec<u8>,
342 sc_cbp: Vec<u8>,
343 sc_cmode: Vec<i32>,
344 sc_nzc: Vec<[u8; 24]>,
345 sc_cbfdc: Vec<u16>,
346 sc_skip: Vec<bool>,
347 sc_ref: Vec<[i8; 16]>,
348 sc_mvd: Vec<[[i16; 2]; 16]>,
349 sc_ref1: Vec<[i8; 16]>,
350 sc_mvd1: Vec<[[i16; 2]; 16]>,
351 sc_direct: Vec<bool>,
352 /// Lazily cached `implicit_weights(0, 0)` — slice-constant (POCs of
353 /// refs[0]/refs1[0] and cur don't change within a slice). Reset in
354 /// set_b_context.
355 iw00: Option<Option<(i32, i32)>>,
356 /// Cached `weights.list_identity(0)` — all list-0 refs identity; gates the
357 /// whole weight_partition pass for P inter (one choke point, all sites).
358 weights_l0id: bool,
359 /// Cached `weights.ref0_identity()` — identity tables (the x264 weightp=2
360 /// common case) make every ref-0 weighting pass an exact no-op, and the
361 /// skip fast paths check this per MB, so it is computed once per slice.
362 weights_id0: bool,
363 /// Current picture's `PicOrderCnt` (for temporal direct + implicit weighting).
364 cur_poc: i32,
365 /// `weighted_bipred_idc` (0 = none/average, 1 = explicit, 2 = implicit).
366 weighted_bipred_idc: u8,
367 /// `direct_8x8_inference_flag` (B direct co-located sub-block selection).
368 direct_8x8_inference: bool,
369 /// Frame-MT Phase B: shared progress Arc for the picture being decoded.
370 progress: Option<crate::Ref>,
371 /// Slice-stable ref→POC tables for bS (≤16 entries). `derive_bs_row` used
372 /// to `collect()` these every row — same values for the whole slice.
373 ref_poc0: Vec<i32>,
374 ref_poc1: Vec<i32>,
375}
376
377/// Explicit weighted-prediction tables (spec §7.4.3.2 / §8.4.2.3.2). Per
378/// reference list, per ref index: a luma `(weight, offset)` and two chroma
379/// `(weight, offset)` (Cb, Cr). `log2` denominators are shared.
380#[derive(Clone, Default)]
381pub struct WeightTable {
382 pub luma_log2_denom: i32,
383 pub chroma_log2_denom: i32,
384 /// `[list][ref_idx] = (weight, offset)`.
385 pub luma: [Vec<(i32, i32)>; 2],
386 /// `[list][ref_idx][cb=0/cr=1] = (weight, offset)`.
387 pub chroma: [Vec<[(i32, i32); 2]>; 2],
388}
389
390impl WeightTable {
391 /// True when list-0 ref-0 carries identity weights (w == 1<<denom, offset 0)
392 /// for luma and both chroma planes. x264's weightp=2 puts a pred_weight_table
393 /// in EVERY P slice, but outside fades the entries are identity — and the
394 /// identity weight is an EXACT no-op ((s*2^d + 2^(d-1))>>d + 0 == s), so a
395 /// P_Skip recon may bypass the weighting pass byte-identically.
396 fn ref0_identity(&self) -> bool {
397 self.luma[0].first().is_some_and(|&(w, o)| w == 1 << self.luma_log2_denom && o == 0)
398 && self.chroma[0].first().is_some_and(|c| {
399 c.iter().all(|&(w, o)| w == 1 << self.chroma_log2_denom && o == 0)
400 })
401 }
402
403 /// True when EVERY entry of `list` (all refs, luma + both chroma) is the
404 /// identity weight — the x264 weightp=2 shape outside fades. Identity is
405 /// an exact per-pixel no-op (see ref0_identity), so the whole per-ref
406 /// weighting pass may be skipped for this list.
407 fn list_identity(&self, list: usize) -> bool {
408 self.luma[list].iter().all(|&(w, o)| w == 1 << self.luma_log2_denom && o == 0)
409 && self.chroma[list].iter().all(|c| {
410 c.iter().all(|&(w, o)| w == 1 << self.chroma_log2_denom && o == 0)
411 })
412 }
413
414 /// Applies a single-list (uni-prediction) luma weight (spec §8.4.2.3.2).
415 /// Resolves a list/ref slot's (weight, offset) ONCE. `list` is 0..1 and the
416 /// fallback is the IDENTITY weight, which is what an absent entry means.
417 #[inline]
418 fn luma_wo(&self, list: usize, refi: usize) -> (i32, i32) {
419 self.luma[list & 1].get(refi).copied().unwrap_or((1 << self.luma_log2_denom, 0))
420 }
421
422 /// Ditto for chroma component `cc`.
423 #[inline]
424 fn chroma_wo(&self, list: usize, refi: usize, cc: usize) -> (i32, i32) {
425 self.chroma[list & 1]
426 .get(refi)
427 .map(|c| c[cc & 1])
428 .unwrap_or((1 << self.chroma_log2_denom, 0))
429 }
430
431 /// [`Self::apply_luma`] with the weight already resolved — the form the
432 /// per-sample loops use, so the lookup is not repeated 256 times.
433 #[inline]
434 fn apply_luma_wo(&self, sample: u8, w: i32, o: i32) -> u8 {
435 let lwd = self.luma_log2_denom;
436 let v = if lwd >= 1 {
437 ((sample as i32 * w + (1 << (lwd - 1))) >> lwd) + o
438 } else {
439 sample as i32 * w + o
440 };
441 v.clamp(0, 255) as u8
442 }
443
444 /// [`Self::apply_chroma`] with the weight already resolved.
445 #[inline]
446 fn apply_chroma_wo(&self, sample: u8, w: i32, o: i32) -> u8 {
447 let cwd = self.chroma_log2_denom;
448 let v = if cwd >= 1 {
449 ((sample as i32 * w + (1 << (cwd - 1))) >> cwd) + o
450 } else {
451 sample as i32 * w + o
452 };
453 v.clamp(0, 255) as u8
454 }
455
456 fn apply_luma(&self, sample: u8, list: usize, refi: usize) -> u8 {
457 let (w, o) = self.luma_wo(list, refi);
458 let lwd = self.luma_log2_denom;
459 let v = if lwd >= 1 {
460 ((sample as i32 * w + (1 << (lwd - 1))) >> lwd) + o
461 } else {
462 sample as i32 * w + o
463 };
464 v.clamp(0, 255) as u8
465 }
466
467 /// Applies a single-list (uni-prediction) chroma weight for component `cc`.
468 fn apply_chroma(&self, sample: u8, list: usize, refi: usize, cc: usize) -> u8 {
469 let (w, o) = self.chroma_wo(list, refi, cc);
470 let cwd = self.chroma_log2_denom;
471 let v = if cwd >= 1 {
472 ((sample as i32 * w + (1 << (cwd - 1))) >> cwd) + o
473 } else {
474 sample as i32 * w + o
475 };
476 v.clamp(0, 255) as u8
477 }
478}
479
480/// Why a macroblock could not be decoded.
481#[derive(Debug, Clone, PartialEq, Eq)]
482pub enum MbError {
483 Truncated,
484 Unsupported(&'static str),
485}
486
487impl From<OutOfData> for MbError {
488 fn from(_: OutOfData) -> Self {
489 MbError::Truncated
490 }
491}
492
493/// Recycled per-picture scratch grids.
494///
495/// `FrameDecoder::new` used to allocate ~1.65 MB of frame-wide grids for EVERY
496/// coded picture and drop them when the picture finished. The sampled profiler
497/// prices that (stage `dec-setup`) at 6.7% of decode — larger than dequant,
498/// reconstruct and intra prediction combined, and none of it is codec work.
499///
500/// Two costs are being paid, and the allocation is the bigger one. A ~460 KB
501/// `Vec` goes straight to the OS, so every page is a fresh zero page and the
502/// decoder takes a soft page fault on FIRST TOUCH of each 4 KB — a cost charged
503/// to whatever per-macroblock stage happens to touch it first, not to the
504/// allocation. Handing the same buffers back keeps the pages mapped and warm.
505///
506/// The initialising fill is NOT skipped: these grids are read as neighbour
507/// context (`modes_y` must read 2/DC, `ref_idx_y` must read -1) before every
508/// block that writes them, so a stale value from the previous picture is a
509/// correctness bug, not a performance trade. `clear()` + `resize()` keeps the
510/// fill and drops only the allocation.
511///
512/// The reconstruction planes are deliberately NOT pooled: `into_frame` MOVES
513/// them out as the caller's output frame, so there is nothing to hand back.
514#[derive(Default)]
515pub struct GridPool {
516 /// D12: running bits-per-macroblock of decoded slices, the E2 dispatch's
517 /// density signal. Lives here because `GridPool` is the only state that
518 /// survives a picture (`FrameDecoder` is rebuilt per picture).
519 bits_per_mb: f64,
520 mb_qp: Vec<u8>,
521 bs_frame: Vec<rusty_h264_common::deblock::MbBs>,
522 pk_prev: Vec<rusty_h264_common::deblock::MbPack>,
523 pk_cur: Vec<rusty_h264_common::deblock::MbPack>,
524 nnz_dbr: Vec<u8>,
525 bak_y: Vec<u8>,
526 bak_u: Vec<u8>,
527 bak_v: Vec<u8>,
528 nnz_y: Vec<u8>,
529 nnz_c0: Vec<u8>,
530 nnz_c1: Vec<u8>,
531 modes_y: Vec<u8>,
532 coded_y: Vec<bool>,
533 mv_y: Vec<(i32, i32)>,
534 inter_y: Vec<bool>,
535 ref_idx_y: Vec<i32>,
536 mv1: Vec<(i32, i32)>,
537 ref_idx1: Vec<i32>,
538 mb_t8x8: Vec<bool>,
539 mb_kind: Vec<u8>,
540 bzero: Vec<bool>,
541 // CABAC slice scratch (D13 follow-through): these were fresh `vec![..]`
542 // allocations at EVERY slice entry — ~407 KB per P slice, ~700 KB per B
543 // slice at 720p, the same fresh-page class GridPool exists to kill.
544 sc_cat: Vec<u8>,
545 sc_cbp: Vec<u8>,
546 sc_cmode: Vec<i32>,
547 sc_nzc: Vec<[u8; 24]>,
548 sc_cbfdc: Vec<u16>,
549 sc_skip: Vec<bool>,
550 sc_ref: Vec<[i8; 16]>,
551 sc_mvd: Vec<[[i16; 2]; 16]>,
552 sc_ref1: Vec<[i8; 16]>,
553 sc_mvd1: Vec<[[i16; 2]; 16]>,
554 sc_direct: Vec<bool>,
555 // D24 (inline-execution.md 11.10): the ref-POC mirrors were the only
556 // per-picture Vecs NOT recycled - a fresh alloc + collect per picture each.
557 ref_poc0: Vec<i32>,
558 ref_poc1: Vec<i32>,
559}
560
561
562/// Reuse `v`'s allocation for `n` copies of `val`. Identical OBSERVABLE result to
563/// `vec![val; n]`; differs only in that it reuses the existing allocation when the
564/// capacity already suffices.
565#[inline]
566fn refill<T: Clone>(mut v: Vec<T>, n: usize, val: T) -> Vec<T> {
567 v.clear();
568 v.resize(n, val);
569 v
570}
571
572impl FrameDecoder {
573 /// Non-pooled ctor — tests only; production always enters via `with_pool`
574 /// (`GridPool` recycling, see lib.rs).
575 #[cfg(test)]
576 pub fn new(
577 mb_w: usize,
578 mb_h: usize,
579 qp: u8,
580 chroma_qp_offset: i32,
581 refs: Vec<crate::Ref>,
582 num_ref_active: usize,
583 constrained_intra: bool,
584 transform_8x8_mode: bool,
585 b_possible: bool,
586 ) -> Self {
587 Self::with_pool(
588 mb_w,
589 mb_h,
590 qp,
591 chroma_qp_offset,
592 refs,
593 num_ref_active,
594 constrained_intra,
595 transform_8x8_mode,
596 b_possible,
597 GridPool::default(),
598 )
599 }
600
601 /// As `new`, but reusing a previous picture's grid allocations. See `GridPool`.
602 #[allow(clippy::too_many_arguments)]
603 pub fn with_pool(
604 mb_w: usize,
605 mb_h: usize,
606 qp: u8,
607 chroma_qp_offset: i32,
608 refs: Vec<crate::Ref>,
609 num_ref_active: usize,
610 constrained_intra: bool,
611 transform_8x8_mode: bool,
612 b_possible: bool,
613 pool: GridPool,
614 ) -> Self {
615 let (cw, ch) = (mb_w * 16, mb_h * 16);
616 let (ccw, cch) = (cw / 2, ch / 2);
617 let bits_per_mb = pool.bits_per_mb;
618 let ref_poc0 = {
619 let mut v = pool.ref_poc0;
620 v.clear();
621 v.extend(refs.iter().map(|f| f.pic_poc()));
622 v
623 };
624 Self {
625 mb_w,
626 mb_h,
627 qp,
628 cur_qp: qp,
629 chroma_qp_offset,
630 cw,
631 ch,
632 ccw,
633 cch,
634 rec_y: vec![0; cw * ch],
635 rec_u: vec![0; ccw * cch],
636 rec_v: vec![0; ccw * cch],
637 mb_qp: refill(pool.mb_qp, mb_w * mb_h, qp),
638 slice_first_mb: 0,
639 nnz_y: refill(pool.nnz_y, (mb_w * 4) * (mb_h * 4), 0),
640 nnz_c: [
641 refill(pool.nnz_c0, (mb_w * 2) * (mb_h * 2), 0),
642 refill(pool.nnz_c1, (mb_w * 2) * (mb_h * 2), 0),
643 ],
644 modes_y: refill(pool.modes_y, (mb_w * 4) * (mb_h * 4), 2),
645 coded_y: refill(pool.coded_y, (mb_w * 4) * (mb_h * 4), false),
646 mv_y: refill(pool.mv_y, (mb_w * 4) * (mb_h * 4), (0, 0)),
647 inter_y: refill(pool.inter_y, (mb_w * 4) * (mb_h * 4), false),
648 ref_idx_y: refill(pool.ref_idx_y, (mb_w * 4) * (mb_h * 4), -1),
649 mv1: refill(pool.mv1, (mb_w * 4) * (mb_h * 4), (0, 0)),
650 ref_idx1: refill(pool.ref_idx1, (mb_w * 4) * (mb_h * 4), -1),
651 refs1: Vec::new(),
652 num_ref_active1: 0,
653 is_b: false,
654 b_possible,
655 direct_spatial: true,
656 nnz_l_cache: [0x80; 25],
657 nnz_c_cache: [[0x80; 9]; 2],
658 refs,
659 num_ref_active,
660 constrained_intra,
661 scaling: None,
662 scaling8: None,
663 transform_8x8_mode,
664 transform_bypass: false,
665 route_skip_mbs: 0,
666 route_coded_mbs: 0,
667 slice_bounds: Vec::new(),
668 cur_idc2: false,
669 mb_t8x8: refill(pool.mb_t8x8, mb_w * mb_h, false),
670 bs_frame: refill(pool.bs_frame, mb_w * mb_h, Default::default()),
671 bs_rows: 0,
672 flt_rows: 0,
673 pk_prev: {
674 let mut v = pool.pk_prev;
675 v.clear();
676 v
677 },
678 pk_cur: {
679 let mut v = pool.pk_cur;
680 v.clear();
681 v
682 },
683 nnz_dbr: refill(pool.nnz_dbr, (mb_w * 4) * (mb_h * 4), 0),
684 bak_y: refill(pool.bak_y, cw, 0),
685 bak_u: refill(pool.bak_u, ccw, 0),
686 bak_v: refill(pool.bak_v, ccw, 0),
687 edc_jobs: Vec::new(),
688 edc_active: false,
689 edc_tx: None,
690 edc_ctx_rx: None,
691 edc_back_tx: None,
692 edc_parked: None,
693 edc_regions: None,
694 bsk_last: None,
695 edc_batch: Vec::new(),
696 bits_per_mb,
697 db_ena: false,
698 db_oa: 0,
699 db_ob: 0,
700 mb_kind: refill(
701 pool.mb_kind,
702 mb_w * mb_h,
703 rusty_h264_common::deblock::MB_KIND_UNSET,
704 ),
705 weights: None,
706 row_wait: crate::frame_mt::row_progress_on(),
707 col_ok: false,
708 col_w4: 0,
709 k_no_bskipfast: no_bskipfast(),
710 k_no_runmv: no_runmv(),
711 k_rowdb: rowdb_on(),
712 k_roweager: rowhook_eager(),
713 k_kindloads: kind_loads(),
714 any_t8: false,
715 any_idc2: false,
716 skip_zero_next: usize::MAX,
717 bzero: refill(pool.bzero, mb_w * mb_h, false),
718 sc_cat: pool.sc_cat,
719 sc_cbp: pool.sc_cbp,
720 sc_cmode: pool.sc_cmode,
721 sc_nzc: pool.sc_nzc,
722 sc_cbfdc: pool.sc_cbfdc,
723 sc_skip: pool.sc_skip,
724 sc_ref: pool.sc_ref,
725 sc_mvd: pool.sc_mvd,
726 sc_ref1: pool.sc_ref1,
727 sc_mvd1: pool.sc_mvd1,
728 sc_direct: pool.sc_direct,
729 bzspan: None,
730 pzspan: None,
731 iw00: None,
732 weights_id0: false,
733 weights_l0id: false,
734 cur_poc: 0,
735 weighted_bipred_idc: 0,
736 direct_8x8_inference: false,
737 progress: None,
738 ref_poc0,
739 ref_poc1: {
740 let mut v = pool.ref_poc1;
741 v.clear();
742 v
743 },
744 }
745 }
746
747 fn refresh_ref_pocs(&mut self) {
748 self.ref_poc0.clear();
749 self.ref_poc0.extend(self.refs.iter().map(|f| f.pic_poc()));
750 self.ref_poc1.clear();
751 self.ref_poc1.extend(self.refs1.iter().map(|f| f.pic_poc()));
752 }
753
754 /// Frame-MT Phase B: attach the shared progress Arc for row watermarks.
755 pub fn set_progress_slot(&mut self, slot: crate::Ref) {
756 self.progress = Some(slot);
757 }
758
759 /// Wait until every L0/L1 ref has enough ready luma rows for MB row `mb_y`
760 /// (pad conservatively for MV overshoot).
761 #[inline(always)]
762 fn wait_refs_for_mb(&self, mb_y: usize) {
763 // Cached at construction: in 1T (row-progress off, the default) this
764 // is one field test instead of a fn call + OnceLock read per MB.
765 if !self.row_wait {
766 return;
767 }
768 let need = crate::RefFrame::rows_needed_for_mb(mb_y, self.ch);
769 crate::RefFrame::set_mc_row_need(mb_y, self.ch);
770 for r in self.refs.iter().chain(self.refs1.iter()) {
771 // Only pay the wait when the ref may still be in-flight (Phase B).
772 if r.live.is_some() && r.frozen.get().is_none() {
773 r.wait_ready_rows(need);
774 }
775 }
776 }
777
778 fn publish_progress(&mut self) {
779 let Some(slot) = &self.progress else {
780 return;
781 };
782 // Under EDC the worker publishes (PixelCtx::publish_progress_rows).
783 if self.edc_tx.is_some() {
784 return;
785 }
786 // Cached field, not the knob function: this runs per ROW.
787 if !self.k_rowdb || !self.db_ena || self.flt_rows == 0 {
788 return;
789 }
790 publish_filtered_rows_to_slot(
791 slot,
792 &self.rec_y,
793 &self.rec_u,
794 &self.rec_v,
795 self.cw,
796 self.ccw,
797 self.ch,
798 self.flt_rows,
799 );
800 }
801
802 /// Sets the explicit weighted-prediction tables for this slice.
803 pub fn set_weights(&mut self, weights: WeightTable) {
804 self.weights_id0 = weights.ref0_identity();
805 self.weights_l0id = weights.list_identity(0);
806 self.weights = Some(weights);
807 }
808
809 /// Applies explicit uni-prediction weighting to a motion-compensated partition
810 /// (luma `pred_y` region + the two chroma planes), if weighting is active.
811 /// `list` is the reference list and `refi` the partition's reference index.
812 fn weight_partition(
813 &self,
814 pred_y: &mut [u8; 256],
815 c_pred: &mut [[u8; 64]; 2],
816 list: usize,
817 refi: usize,
818 rx: usize,
819 ry: usize,
820 rw: usize,
821 rh: usize,
822 ) {
823 let Some(wt) = &self.weights else { return };
824 // Identity list-0 table (x264 weightp=2 outside fades): the whole pass
825 // is an exact per-pixel no-op — skip it. RS_H264_NO_SKIPFP restores it
826 // for paired A/B (same knob family as the P_Skip weight skip).
827 if list == 0 && self.weights_l0id && !no_skipfp() {
828 edcstat::bump(&edcstat::WP_SKIPPED, 1);
829 return;
830 }
831 // REFUTED, reverted: row-slicing these loops measured +28% instructions
832 // on `weight_partition` (the extents are runtime values, so the slice
833 // bounds cost more than the per-sample checks they replaced). The real
834 // win for this function was hoisting the identity test to its CALLERS,
835 // which stands.
836 // RESOLVE ONCE, APPLY MANY. `apply_luma` re-read `self.luma[list][refi]`
837 // — an array index, a Vec deref and an element index, two of them bounds
838 // checked — on EVERY sample, up to 256 luma plus 128 chroma per call.
839 // The weight is a property of the partition, not of the pixel. (The loop
840 // SHAPE is untouched: row-slicing it is refuted above.)
841 // MASKED, not row-sliced. `pred_y` is a fixed `[u8; 256]` and `c_pred` a
842 // `[[u8; 64]; 2]`, so `& 255` / `& 63` are no-ops that prove the index
843 // outright — where SLICING these loops cost +28% (the extents are runtime
844 // values, so the slice bounds outweigh the checks). Same lesson as
845 // `luma_centre`: the refutation was of one SHAPE, not of the goal.
846 let (lw, lo) = wt.luma_wo(list, refi);
847 for dy in 0..rh {
848 for dx in 0..rw {
849 let i = ((ry + dy) * 16 + (rx + dx)) & 255;
850 pred_y[i] = wt.apply_luma_wo(pred_y[i], lw, lo);
851 }
852 }
853 let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
854 for cc in 0..2 {
855 let (cw, co) = wt.chroma_wo(list, refi, cc);
856 let plane = &mut c_pred[cc & 1];
857 for dy in 0..crh {
858 for dx in 0..crw {
859 let i = ((cry + dy) * 8 + (crx + dx)) & 63;
860 plane[i] = wt.apply_chroma_wo(plane[i], cw, co);
861 }
862 }
863 }
864 }
865
866 /// Sets the High-profile scaling matrices (raster order: six 4×4 lists, two
867 /// 8×8 luma lists). The caller un-zig-zags the SPS lists. Flat is the default.
868 /// GATE 1 router counters: (skip MBs, residual-coded MBs) this picture.
869 pub fn route_counters(&self) -> (u32, u32) {
870 (self.route_skip_mbs, self.route_coded_mbs)
871 }
872
873 /// SPS `qpprime_y_zero_transform_bypass_flag` — see [`Self::step_qp`].
874 pub fn set_transform_bypass(&mut self, on: bool) {
875 self.transform_bypass = on;
876 }
877
878 pub fn set_scaling(&mut self, scaling: [[i32; 16]; 6], scaling8: [[i32; 64]; 2]) {
879 self.scaling = Some(scaling);
880 self.scaling8 = Some(scaling8);
881 }
882
883 /// Dequantizes a 4×4 AC block with scaling list `list` (flat if none active).
884 fn dequant(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
885 match &self.scaling {
886 Some(s) => dequantize_weighted(levels, qp, &s[list]),
887 None => dequantize(levels, qp),
888 }
889 }
890
891 /// Single-coefficient twin of `dequant` for position 0 (DC-only fast path).
892 fn dequant_dc4(&self, level: i32, qp: u8, list: usize) -> i32 {
893 rusty_h264_common::transform::dequantize_dc4(
894 level,
895 qp,
896 self.scaling.as_ref().map(|s| s[list][0]),
897 )
898 }
899
900 /// Inverse-quantizes the I_16x16 luma DC with scaling list `list`'s DC weight.
901 fn dequant_luma_dc(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
902 match &self.scaling {
903 Some(s) => inverse_quant_luma_dc_weighted(levels, qp, s[list][0]),
904 None => inverse_quant_luma_dc(levels, qp),
905 }
906 }
907
908 /// Inverse-quantizes a chroma DC block with scaling list `list`'s DC weight.
909 fn dequant_chroma_dc(&self, levels: &[i32; 4], qp: u8, list: usize) -> [i32; 4] {
910 match &self.scaling {
911 Some(s) => inverse_quant_chroma_dc_weighted(levels, qp, s[list][0]),
912 None => inverse_quant_chroma_dc(levels, qp),
913 }
914 }
915
916 /// Sets the B-slice context for the slice about to be decoded: `RefPicList1`,
917 /// its active count, and the direct-mode flag.
918 #[allow(clippy::too_many_arguments)]
919 pub fn set_b_context(
920 &mut self,
921 refs1: Vec<crate::Ref>,
922 num_ref_active1: usize,
923 direct_spatial: bool,
924 cur_poc: i32,
925 weighted_bipred_idc: u8,
926 direct_8x8_inference: bool,
927 ) {
928 self.is_b = true;
929 self.refs1 = refs1;
930 self.num_ref_active1 = num_ref_active1;
931 self.direct_spatial = direct_spatial;
932 self.cur_poc = cur_poc;
933 self.weighted_bipred_idc = weighted_bipred_idc;
934 self.direct_8x8_inference = direct_8x8_inference;
935 self.iw00 = None; // slice-scoped cache: lists/POC may have changed
936 // Colocated fast-probe cache (see col_zero / col_zero_fast).
937 self.col_ok = self.refs1.first().is_some_and(|c| {
938 c.live.is_none() && !c.long_term && c.w4 != 0
939 });
940 self.col_w4 = self.refs1.first().map_or(0, |c| c.w4);
941 self.refresh_ref_pocs();
942 }
943
944 /// Cached `implicit_weights(0, 0)` — constant within a slice.
945 fn iw00(&mut self) -> Option<(i32, i32)> {
946 // Read the discriminant ONCE. The `is_none()` + `unwrap()` pair tested it
947 // twice and carried an `unwrap_failed` path for a value this very function
948 // had just written. `get_or_insert_with` cannot be used here: the closure
949 // would need `&mut self` while the `Option` is already borrowed.
950 match self.iw00 {
951 Some(v) => v,
952 None => {
953 let v = if self.refs.is_empty() || self.refs1.is_empty() {
954 None
955 } else {
956 self.implicit_weights(0, 0)
957 };
958 self.iw00 = Some(v);
959 v
960 }
961 }
962 }
963
964 /// Steps the running luma QP by a `mb_qp_delta` (spec §7.4.5, 8-bit depth):
965 /// `QPy = (QPy_prev + delta + 52) % 52`.
966 /// Steps QPy by `mb_qp_delta` (spec §7.4.5 modulo). Called exactly for
967 /// residual-coded macroblocks (mb_qp_delta presence ⇔ cbp != 0 or I_16x16),
968 /// which makes it the one chokepoint for the transform-bypass refusal:
969 /// with `qpprime_y_zero_transform_bypass_flag` set and QP'Y == 0 the
970 /// residual is LOSSLESS-bypassed (no transform, no quant — and DPCM intra
971 /// forms), which this decoder does not implement. Refusing here is loud
972 /// and exact: all-PCM lossless streams (no mb_qp_delta) still decode.
973 fn step_qp(&mut self, delta: i32) -> Result<(), MbError> {
974 // Router counter: step_qp fires exactly once per residual-coded MB.
975 self.route_coded_mbs += 1;
976 self.cur_qp = (self.cur_qp as i32 + delta + 52).rem_euclid(52) as u8;
977 if self.transform_bypass && self.cur_qp == 0 {
978 return Err(MbError::Unsupported("transform-bypass (lossless) macroblock"));
979 }
980 Ok(())
981 }
982
983 /// Maps a luma QP to its chroma QP, applying `chroma_qp_index_offset`
984 /// (spec §8.5.8): `QPc = qpc_table(Clip3(0, 51, QPy + offset))`.
985 fn chroma_qp_for(&self, qp_y: u8) -> u8 {
986 let qpi = (qp_y as i32 + self.chroma_qp_offset).clamp(0, 51) as u8;
987 chroma_qp(qpi)
988 }
989
990 /// Resets per-slice state before decoding a continuation slice of the same
991 /// picture: the running QP (each slice carries its own `slice_qp`) and the
992 /// reference list (each slice may reorder it).
993 pub fn begin_slice(&mut self, slice_qp: u8, refs: Vec<crate::Ref>, num_ref_active: usize) {
994 self.cur_qp = slice_qp;
995 self.qp = slice_qp;
996 self.refs = refs;
997 self.num_ref_active = num_ref_active;
998 self.weights = None; // re-set per slice if a pred_weight_table is present
999 self.weights_id0 = false;
1000 self.weights_l0id = false;
1001 self.refresh_ref_pocs();
1002 }
1003
1004 /// Whether the neighbor macroblock at `(nbx, nby)` is in the slice currently
1005 /// being decoded (address ≥ the slice's first MB). For single-slice pictures
1006 /// `slice_first_mb == 0`, so this is always true and prediction is unchanged.
1007 #[inline]
1008 fn nbr_in_slice(&self, nbx: usize, nby: usize) -> bool {
1009 nby * self.mb_w + nbx >= self.slice_first_mb
1010 }
1011
1012 /// Whether the neighbor 4×4 block at `(nbx, nby)` may contribute to intra
1013 /// prediction. With `constrained_intra_pred`, an inter-coded neighbor is
1014 /// treated as unavailable (spec §8.3.1.2.{1,2}); otherwise always usable.
1015 #[inline]
1016 fn intra_nbr_ok(&self, nbx: usize, nby: usize) -> bool {
1017 // Fallible read: this is inlined at eleven sites across the intra paths,
1018 // and `constrained_intra` short-circuits it on nearly every stream, so
1019 // the check was pure tax on a value that is usually never loaded.
1020 // `unwrap_or(true)` is the conservative direction — an out-of-range
1021 // neighbour reads as inter, i.e. UNAVAILABLE, which is what the
1022 // constrained-intra rule means by a neighbour it cannot use.
1023 !self.constrained_intra
1024 || !self.inter_y.get(nby * (self.mb_w * 4) + nbx).copied().unwrap_or(true)
1025 }
1026
1027 fn mv_neighbors(&self, mb_x: usize, mb_y: usize) -> [MvNeighbor; 3] {
1028 let w4 = self.mb_w * 4;
1029 let get = |avail: bool, bx: isize, by: isize| {
1030 if avail {
1031 let idx = by as usize * w4 + bx as usize;
1032 match (self.mv_y.get(idx), self.ref_idx_y.get(idx)) {
1033 (Some(&m), Some(&r)) => MvNeighbor { available: true, mv: m, ref_idx: r },
1034 _ => MvNeighbor::NONE,
1035 }
1036 } else {
1037 MvNeighbor::NONE
1038 }
1039 };
1040 let (bx, by) = (mb_x as isize * 4, mb_y as isize * 4);
1041 let a = get(mb_x > 0 && self.nbr_in_slice(mb_x - 1, mb_y), bx - 1, by);
1042 let b = get(mb_y > 0 && self.nbr_in_slice(mb_x, mb_y - 1), bx, by - 1);
1043 let c = if mb_y > 0 && mb_x + 1 < self.mb_w && self.nbr_in_slice(mb_x + 1, mb_y - 1) {
1044 get(true, bx + 4, by - 1)
1045 } else {
1046 get(mb_x > 0 && mb_y > 0 && self.nbr_in_slice(mb_x - 1, mb_y - 1), bx - 1, by - 1)
1047 };
1048 [a, b, c]
1049 }
1050
1051 fn mv_neighbors_block(&self, pbx: isize, pby: isize, pwb: isize) -> [MvNeighbor; 3] {
1052 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Neighbors);
1053 self.mv_neighbors_block_grid(pbx, pby, pwb, 0)
1054 }
1055
1056 fn skip_mv(&self, mb_x: usize, mb_y: usize) -> (i32, i32) {
1057 let [a, b, c] = self.mv_neighbors(mb_x, mb_y);
1058 if !a.available
1059 || !b.available
1060 || (a.ref_idx == 0 && a.mv == (0, 0))
1061 || (b.ref_idx == 0 && b.mv == (0, 0))
1062 {
1063 (0, 0)
1064 } else {
1065 predict_mv(a, b, c, 0)
1066 }
1067 }
1068
1069 fn set_mb_mv(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32), inter: bool, refi: i32) {
1070 // ROW FILLS. This wrote all sixteen cells of three grids one indexed
1071 // store at a time - FORTY-EIGHT separate bounds checks per call - and
1072 // re-evaluated `if inter` inside the inner loop. Each macroblock row is
1073 // four CONTIGUOUS cells, so twelve `fill`s cover it.
1074 let w4 = self.mb_w * 4;
1075 let r = if inter { refi } else { -1 };
1076 for dy in 0..4 {
1077 let a = (mb_y * 4 + dy) * w4 + mb_x * 4;
1078 self.mv_y[a..a + 4].fill(mv);
1079 self.inter_y[a..a + 4].fill(inter);
1080 self.ref_idx_y[a..a + 4].fill(r);
1081 }
1082 }
1083
1084 /// Commit one inter partition's motion into the 4×4 grid (ref 0, 1-ref P).
1085 /// `(rx,ry,rw,rh)` are MB-relative luma pixels; committing before the next
1086 /// partition's prediction is what lets a later partition predict from it.
1087 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) {
1088 // Row-range fills instead of the per-4x4 scatter: partitions are
1089 // rectangles, so each grid row is one contiguous run (same values).
1090 let w4 = self.mb_w * 4;
1091 let (bx0, bw) = (mb_x * 4 + rx / 4, rw / 4);
1092 for by in ry / 4..ry / 4 + rh / 4 {
1093 let a = (mb_y * 4 + by) * w4 + bx0;
1094 self.mv_y[a..a + bw].fill(mv);
1095 self.inter_y[a..a + bw].fill(true);
1096 self.ref_idx_y[a..a + bw].fill(refi as i32);
1097 self.coded_y[a..a + bw].fill(true);
1098 }
1099 }
1100
1101 /// Per-slice deblock parameters, needed DURING decode by the row-interleave
1102 /// path. `ena` is already resolved against `RFF_ABL_DEBLOCK` by the caller.
1103 pub fn set_deblock_params(&mut self, ena: bool, oa: i32, ob: i32, idc2: bool) {
1104 // Latch: the FIRST disabling slice turns row filtering off for the rest
1105 // of the picture (see `row_hook`); rows already filtered stay counted
1106 // in `flt_rows` and the picture-end tail handles the remainder.
1107 self.db_ena = ena && (self.flt_rows == 0 || self.db_ena);
1108 self.db_oa = oa;
1109 self.db_ob = ob;
1110 self.cur_idc2 = idc2;
1111 }
1112
1113 /// Slice index owning `addr` (slices are raster-contiguous, bounds ascending).
1114 fn slice_of(&self, addr: usize) -> usize {
1115 match self.slice_bounds.binary_search_by(|&(f, _)| f.cmp(&addr)) {
1116 Ok(i) => i,
1117 Err(i) => i - 1,
1118 }
1119 }
1120
1121 /// Derives bS for macroblock row `r` from the just-decoded (hot) grids into
1122 /// `bs_frame`, maintaining the two-row rolling record window (R2 of
1123 /// docs/row-interleave-plan.md).
1124 fn derive_bs_row(&mut self, r: usize) {
1125 // bS derivation reads the motion/nnz grids — flush deferred spans.
1126 self.span_flush();
1127 // Same stage label the in-filter derivation used, so profiles keep
1128 // pricing bS derivation wherever it lives.
1129 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DebDerive);
1130 use rusty_h264_common::deblock::{
1131 derive_mb_kind, derive_mb_records, pack_mb, BlockInfo, MbBs, MbKind,
1132 };
1133 let (mb_w, w4) = (self.mb_w, self.mb_w * 4);
1134 edcstat::bump(&edcstat::DBS_ROWS, 1);
1135 // Transform-block coded mask for this row: raw nnz, then the 8x8 OR for
1136 // t8 macroblocks (spec §8.7: the 8x8 transform's coded status is per 8x8).
1137 //
1138 // T8 GATE. `nnz_dbr` exists ONLY to carry that OR. With no 8x8
1139 // macroblock anywhere in the picture it is a byte-for-byte copy of
1140 // `nnz_y`, so both the 4-row copy and the per-row scan below are pure
1141 // work: the derivation reads `nnz_y` directly instead. Census
1142 // (DBSDERIVE) measured t8mb=0 on EVERY cavlc and main stream while
1143 // nnz_rowcopy ran 10800-16320 sub-rows per decode.
1144 if self.any_t8 {
1145 for br in r * 4..r * 4 + 4 {
1146 let a = br * w4;
1147 self.nnz_dbr[a..a + w4].copy_from_slice(&self.nnz_y[a..a + w4]);
1148 }
1149 edcstat::bump(&edcstat::DBS_NNZ_ROWCOPY, 4);
1150 // WHOLE-MACROBLOCK ROW SLICES. The 2x2-of-2x2 nest did sixteen
1151 // bounds-checked reads and sixteen bounds-checked writes per t8
1152 // macroblock; the same cells are four contiguous runs of four, so
1153 // four slice reads and four slice writes cover them. The OR is over
1154 // bytes, so `a | b | c | d > 0` is exactly the old per-cell `> 0`.
1155 let t8_row_pre = &self.mb_t8x8[r * mb_w..][..mb_w];
1156 for mb_x in 0..mb_w {
1157 if !t8_row_pre[mb_x] {
1158 continue;
1159 }
1160 edcstat::bump(&edcstat::DBS_T8MB, 1);
1161 let base = r * 4 * w4 + mb_x * 4;
1162 let mut src = [[0u8; 4]; 4];
1163 for (k, row) in src.iter_mut().enumerate() {
1164 row.copy_from_slice(&self.nnz_y[base + k * w4..][..4]);
1165 }
1166 let mut out = [[0u8; 4]; 4];
1167 for b8 in 0..4usize {
1168 let (cx, cy) = ((b8 % 2) * 2, (b8 / 2) * 2);
1169 let any = (src[cy][cx] | src[cy][cx + 1] | src[cy + 1][cx] | src[cy + 1][cx + 1])
1170 > 0;
1171 for sy in 0..2 {
1172 for sx in 0..2 {
1173 out[cy + sy][cx + sx] = any as u8;
1174 }
1175 }
1176 }
1177 for (k, row) in out.iter().enumerate() {
1178 self.nnz_dbr[base + k * w4..][..4].copy_from_slice(row);
1179 }
1180 }
1181 }
1182 let info = BlockInfo {
1183 inter: &self.inter_y,
1184 nnz: if self.any_t8 { &self.nnz_dbr } else { &self.nnz_y },
1185 mv: &self.mv_y,
1186 ref_id: &self.ref_idx_y,
1187 mv1: &self.mv1,
1188 ref_id1: if self.ref_poc1.is_empty() { &[] } else { &self.ref_idx1 },
1189 w4,
1190 t8x8: &self.mb_t8x8,
1191 bs: &[],
1192 poc0: &self.ref_poc0,
1193 poc1: &self.ref_poc1,
1194 kind: &self.mb_kind,
1195 };
1196 let has1 = !info.ref_id1.is_empty();
1197 std::mem::swap(&mut self.pk_prev, &mut self.pk_cur);
1198 self.pk_cur.clear();
1199 let kl = self.k_kindloads;
1200 // Census gate resolved ONCE per row: `edcstat::on()` is a relaxed atomic
1201 // load and LLVM will not CSE atomic loads, so a per-MB bump costs a load
1202 // each. Same rule as hoisting an A/B arm selector out of the loop under
1203 // test (codec-measurement 15).
1204 let stats = edcstat::on();
1205 // ROW SLICES for the three per-macroblock grids. Each was indexed
1206 // `r * mb_w + mb_x` against a whole-FRAME Vec, which the compiler
1207 // cannot prove in range; sliced to THIS row, `[mb_x]` is provably
1208 // `< mb_w`. These are disjoint FIELDS of `self`, so the `&mut` on
1209 // `bs_frame` coexists with the `&` borrows `info` holds.
1210 let row0 = r * mb_w;
1211 let bs_row = &mut self.bs_frame[row0..][..mb_w];
1212 let t8_row = &self.mb_t8x8[row0..][..mb_w];
1213 let kind_row: &[u8] = if self.mb_kind.is_empty() {
1214 &[]
1215 } else {
1216 &self.mb_kind[row0..][..mb_w]
1217 };
1218 for mb_x in 0..mb_w {
1219 // Always pack: UNSET / Inter neighbours in this row and the next
1220 // read left/top MbPack. Kind stores MbBs directly (no i32 hop).
1221 self.pk_cur.push(pack_mb(&info, has1, mb_x, r));
1222 let slot = r * mb_w + mb_x;
1223 if stats {
1224 edcstat::bump(&edcstat::DBS_MB, 1);
1225 }
1226 // KIND ECONOMICS ON THE PACKED PATH: derive_mb_kind(Skip) does 9
1227 // fresh strided Blk::loads per MB, but pack_mb already ran for this
1228 // MB (the line above) — the packed `_` arm derives from those
1229 // records with no further gathers. A B_Skip kind experiment
1230 // measured kind-arm 1.8% SLOWER (z=-2.69) with +1.07M loads, so
1231 // Skip/InterUniform route to the packed arm; Intra keeps the kind
1232 // arm (pure constants, no loads). `RS_H264_KIND_LOADS=1` restores
1233 // the old routing for paired A/B.
1234 match kind_row.get(mb_x).copied().and_then(MbKind::from_u8) {
1235 Some(MbKind::Intra) => {
1236 if stats {
1237 edcstat::bump(&edcstat::DBS_INTRA, 1);
1238 }
1239 bs_row[mb_x] = derive_mb_kind(&info, mb_x, r, MbKind::Intra);
1240 }
1241 Some(k @ (MbKind::Skip | MbKind::InterUniform)) if kl => {
1242 if stats {
1243 edcstat::bump(&edcstat::DBS_KINDARM, 1);
1244 }
1245 bs_row[mb_x] = derive_mb_kind(&info, mb_x, r, k);
1246 }
1247 _ => {
1248 let Some(cur) = self.pk_cur.get(mb_x) else { continue };
1249 let left = if mb_x > 0 { self.pk_cur.get(mb_x - 1) } else { None };
1250 let top = if r > 0 { self.pk_prev.get(mb_x) } else { None };
1251 let mb_t8 = t8_row[mb_x];
1252 let (mut bv, mut bh) = ([[0i32; 4]; 4], [[0i32; 4]; 4]);
1253 let flat = derive_mb_records(cur, left, top, mb_t8, &mut bv, &mut bh);
1254 if stats {
1255 edcstat::bump(&edcstat::DBS_PACKED, 1);
1256 }
1257 // MEASUREMENT ONLY: sizes the `kind_loads()` match guard that
1258 // used to be a OnceLock deref here (now `k_kindloads`).
1259 if stats
1260 && matches!(
1261 self.mb_kind.get(slot).copied().and_then(MbKind::from_u8),
1262 Some(MbKind::Skip | MbKind::InterUniform)
1263 )
1264 {
1265 edcstat::bump(&edcstat::DBS_KINDGUARD, 1);
1266 }
1267 // FLAT-AWARE NARROWING. `derive_mb_records` returns early on a
1268 // flat inter macroblock having written ONLY edge 0 of each
1269 // orientation; edges 1..4 are still the caller's zero-init, so
1270 // widening all 32 entries copies 24 known zeros onto 24 known
1271 // zeros - after a `MbBs::default()` that zeroed them a third
1272 // time. Census DBSDERIVE flat: 96.8% screen_text, 90.2%
1273 // FourPeople, 82.1% akiyo - the dominant class.
1274 let m = if flat {
1275 if stats {
1276 edcstat::bump(&edcstat::DBS_FLAT, 1);
1277 }
1278 debug_assert!(bv[1..] == [[0i32; 4]; 3] && bh[1..] == [[0i32; 4]; 3]);
1279 let mut m = MbBs::default();
1280 m.v[0] = std::array::from_fn(|sg| bv[0][sg] as u8);
1281 m.h[0] = std::array::from_fn(|sg| bh[0][sg] as u8);
1282 m
1283 } else {
1284 // Written once, not zeroed by `default()` and then written.
1285 MbBs {
1286 v: std::array::from_fn(|e| std::array::from_fn(|sg| bv[e][sg] as u8)),
1287 h: std::array::from_fn(|e| std::array::from_fn(|sg| bh[e][sg] as u8)),
1288 }
1289 };
1290 // MEASUREMENT ONLY: `on()` first, so the 32-byte compare
1291 // never runs in a shipped decode (it is not otherwise needed
1292 // here — the consumer in filter_frame_rows makes it).
1293 if stats && m.v == [[0u8; 4]; 4] && m.h == [[0u8; 4]; 4] {
1294 edcstat::bump(&edcstat::DBS_ALLZERO, 1);
1295 }
1296 bs_row[mb_x] = m;
1297 }
1298 }
1299 }
1300 // disable_deblocking_filter_idc == 2 (spec §7.4.3): a slice may forbid
1301 // filtering ITS macroblocks' edges against OTHER slices. bS = 0 on the
1302 // crossing MB edges kills exactly those filters; interior edges keep
1303 // their derived strengths. Guarded so single-slice / idc 0-1 pictures
1304 // pay one branch per row.
1305 if self.any_idc2 && self.slice_bounds.len() > 1 {
1306 edcstat::bump(&edcstat::DBS_IDC2ROW, 1);
1307 for mb_x in 0..mb_w {
1308 let slot = r * mb_w + mb_x;
1309 let si = self.slice_of(slot);
1310 if !self.slice_bounds.get(si).is_some_and(|b| b.1) {
1311 continue;
1312 }
1313 if mb_x > 0 && self.slice_of(slot - 1) != si {
1314 if let Some(b) = self.bs_frame.get_mut(slot) {
1315 b.v[0] = [0; 4];
1316 }
1317 }
1318 if r > 0 && self.slice_of(slot - mb_w) != si {
1319 if let Some(b) = self.bs_frame.get_mut(slot) {
1320 b.h[0] = [0; 4];
1321 }
1322 }
1323 }
1324 }
1325 }
1326
1327 /// Decode-loop hook: called at each MB-loop head with the NEXT address to be
1328 /// decoded; derives AND FILTERS (R3) every fully-decoded row. Filtering a
1329 /// row here preserves the spec's raster per-MB filter order exactly (every
1330 /// MB the row's edges touch is already decoded; bottom-adjacent edges
1331 /// belong to the NEXT row's MBs, which filter later).
1332 ///
1333 /// Fast path: mid-row heads (`done <= bs_rows`) do no row work — return
1334 /// before the profiler scope (this hook is entered once per MB; scoping
1335 /// every call was measuring the timer, not the filter).
1336 /// row_hook with the caller's carried row — avoids the per-MB division.
1337 #[inline(always)]
1338 fn row_hook_at(&mut self, addr: usize, mby: usize) {
1339 if self.k_rowdb {
1340 // ~44/45 of calls are mid-row: one compare, no atomic loads.
1341 if !self.k_roweager && mby <= self.bs_rows {
1342 return;
1343 }
1344 }
1345 self.row_hook(addr, mby);
1346 }
1347
1348 /// `mby` is the row `addr` sits in. It is a PARAMETER rather than
1349 /// `addr / self.mb_w` because both callers already carry it: the slice loops
1350 /// track `(mbx, mby)` with a compare-and-wrap precisely so the per-macroblock
1351 /// div+mod never runs, and this hook -- entered PER MACROBLOCK on the
1352 /// non-rowdb arm -- was reconstructing it with an integer divide anyway.
1353 fn row_hook(&mut self, addr: usize, mby: usize) {
1354 debug_assert_eq!(mby, addr / self.mb_w, "row_hook: mby must be addr's row");
1355 // `RS_H264_ROWHOOK_EAGER=1` restores per-MB profiler scoping (A/B oracle).
1356 // Read from the CACHED fields: `row_hook_at` already gates on
1357 // `self.k_roweager` / `self.k_rowdb`, and re-reading the knob functions
1358 // here paid a OnceLock deref plus a relaxed atomic load on every entry -
1359 // and on the non-rowdb arm this function is entered per MACROBLOCK.
1360 let eager = self.k_roweager;
1361 if !self.k_rowdb {
1362 edcstat::bump(&edcstat::MBS, 1);
1363 let _rh = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecRowHook);
1364 self.edc_flush();
1365 let done = mby;
1366 if done > self.bs_rows {
1367 self.bs_rows = done;
1368 self.publish_progress();
1369 }
1370 return;
1371 }
1372 let done = mby;
1373 // ~44/45 of calls are mid-row: no derive/filter/handoff yet.
1374 if !eager && done <= self.bs_rows {
1375 return;
1376 }
1377 edcstat::bump(&edcstat::MBS, 1);
1378 let _rh = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecRowHook);
1379 if done <= self.bs_rows {
1380 return;
1381 }
1382 if self.edc_tx.is_some() {
1383 // E2: derivation stays here (it reads the syntax grids); filtering
1384 // is the worker's, fed the row's bs/qp/t8 snapshot. `flt_rows`
1385 // advances on the worker and comes home with the context.
1386 self.edc_giveback();
1387 while self.bs_rows < done {
1388 let r = self.bs_rows;
1389 self.derive_bs_row(r);
1390 self.bs_rows += 1;
1391 let base = r * self.mb_w;
1392 // ORDER: this row's pixel jobs must reach the worker BEFORE the
1393 // filter message for the same row.
1394 self.edc_flush_batch();
1395 edcstat::bump(&edcstat::ROWS, 1);
1396 edcstat::bump(
1397 &edcstat::ROWBYTES,
1398 (self.mb_w
1399 * (std::mem::size_of::<rusty_h264_common::deblock::MbBs>() + 2))
1400 as u64,
1401 );
1402 let msg = EdcMsg::Row {
1403 r,
1404 bs: self.bs_frame[base..base + self.mb_w].to_vec(),
1405 qp: self.mb_qp[base..base + self.mb_w].to_vec(),
1406 t8: self.mb_t8x8[base..base + self.mb_w].to_vec(),
1407 };
1408 self.edc_tx.as_ref().unwrap().send(msg).expect("worker alive");
1409 }
1410 return;
1411 }
1412 self.edc_flush();
1413 while self.bs_rows < done {
1414 let r = self.bs_rows;
1415 self.derive_bs_row(r);
1416 self.bs_rows += 1;
1417 // Row filtering requires deblock enabled on EVERY slice so far
1418 // (`db_ena` latches false once any slice disables it): a mixed
1419 // picture falls back to the picture-end tail so "latest slice
1420 // wins" semantics are preserved.
1421 if self.db_ena {
1422 self.save_bak(r);
1423 self.filter_row(r);
1424 self.flt_rows = r + 1;
1425 }
1426 self.publish_progress();
1427 }
1428 }
1429
1430 /// Saves the UNFILTERED bottom pixel rows of MB row `r` before filtering
1431 /// modifies them: the next row's intra prediction must read pre-deblock
1432 /// samples (spec §8.3), and filtering touches the bottom three rows while
1433 /// intra reads exactly the bottom ONE (+ the corner) — so one backup row
1434 /// per plane suffices, overwritten per row.
1435 fn save_bak(&mut self, r: usize) {
1436 let y0 = (r * 16 + 15) * self.cw;
1437 self.bak_y.copy_from_slice(&self.rec_y[y0..y0 + self.cw]);
1438 let c0 = (r * 8 + 7) * self.ccw;
1439 self.bak_u.copy_from_slice(&self.rec_u[c0..c0 + self.ccw]);
1440 self.bak_v.copy_from_slice(&self.rec_v[c0..c0 + self.ccw]);
1441 }
1442
1443 /// Filters one MB row against the stored strengths, using the CURRENT
1444 /// slice's alpha/beta offsets (single-offset streams — the whole corpus —
1445 /// are bit-identical to the picture-end call; the plan's risk register
1446 /// documents the multi-offset divergence).
1447 fn filter_row(&mut self, r: usize) {
1448 let info = rusty_h264_common::deblock::BlockInfo {
1449 inter: &self.inter_y,
1450 // Same source `derive_bs_row` used (see the T8 GATE there). This
1451 // path passes a non-empty `bs`, so it takes the PRECOMPUTED arm and
1452 // never reads `nnz` at all; kept in step so the two cannot diverge.
1453 nnz: if self.any_t8 { &self.nnz_dbr } else { &self.nnz_y },
1454 mv: &self.mv_y,
1455 ref_id: &self.ref_idx_y,
1456 mv1: &self.mv1,
1457 ref_id1: &self.ref_idx1,
1458 w4: self.mb_w * 4,
1459 t8x8: &self.mb_t8x8,
1460 bs: &self.bs_frame,
1461 poc0: &[],
1462 poc1: &[],
1463 kind: &self.mb_kind,
1464 };
1465 rusty_h264_common::deblock::filter_frame_rows(
1466 &mut self.rec_y,
1467 &mut self.rec_u,
1468 &mut self.rec_v,
1469 self.mb_w,
1470 self.mb_h,
1471 r..r + 1,
1472 &self.mb_qp,
1473 self.chroma_qp_offset,
1474 self.db_oa,
1475 self.db_ob,
1476 &info,
1477 );
1478 }
1479
1480 /// Top-neighbour LUMA pixel for intra prediction: reads the unfiltered
1481 /// backup row when the row above has already been deblock-filtered by the
1482 /// row-interleave (flt_rows gates it; 0 when the interleave is off, so
1483 /// this compiles to the plain read on the fallback path).
1484 #[inline]
1485 fn top_y_px(&self, py: usize, x: usize) -> u8 {
1486 // 128 is the spec's value for an unavailable sample (1 << (BitDepth - 1),
1487 // §8.3.1.2), so the fallback is the one the prediction rules already use
1488 // — and unreachable anyway, since callers gate on availability first.
1489 if py % 16 == 0 && self.flt_rows * 16 >= py {
1490 self.bak_y.get(x).copied().unwrap_or(128)
1491 } else {
1492 self.rec_y.get((py - 1) * self.cw + x).copied().unwrap_or(128)
1493 }
1494 }
1495
1496 /// Slice form of [`Self::top_y_px`] for the contiguous 16-wide I16 gather.
1497 #[inline]
1498 fn top_y_row(&self, py: usize, x: usize, n: usize) -> &[u8] {
1499 if py % 16 == 0 && self.flt_rows * 16 >= py {
1500 &self.bak_y[x..x + n]
1501 } else {
1502 &self.rec_y[(py - 1) * self.cw + x..][..n]
1503 }
1504 }
1505
1506 /// Top-neighbour CHROMA pixel (plane `c`: 0 = U, 1 = V).
1507 #[inline]
1508 fn top_c_px(&self, c: usize, cy: usize, x: usize) -> u8 {
1509 // 128 = the spec's unavailable sample, as in `top_y_px`.
1510 if cy % 8 == 0 && self.flt_rows * 8 >= cy {
1511 let bak = if c == 0 { &self.bak_u } else { &self.bak_v };
1512 bak.get(x).copied().unwrap_or(128)
1513 } else {
1514 let rec = if c == 0 { &self.rec_u } else { &self.rec_v };
1515 rec.get((cy - 1) * self.ccw + x).copied().unwrap_or(128)
1516 }
1517 }
1518
1519 /// Slice form of [`Self::top_c_px`] for the 8-wide chroma gather.
1520 #[inline]
1521 fn top_c_row(&self, c: usize, cy: usize, x: usize, n: usize) -> &[u8] {
1522 if cy % 8 == 0 && self.flt_rows * 8 >= cy {
1523 if c == 0 { &self.bak_u[x..x + n] } else { &self.bak_v[x..x + n] }
1524 } else {
1525 let rec = if c == 0 { &self.rec_u } else { &self.rec_v };
1526 &rec[(cy - 1) * self.ccw + x..][..n]
1527 }
1528 }
1529
1530 /// Snapshots the (deblocked) reconstruction as a reference picture, drawing
1531 /// its padded-plane allocations from `pool` (recycled
1532 /// planes of evicted DPB frames — see `Decoder::reclaim_retired`). ~1.9 MB of
1533 /// fresh allocation per reference picture otherwise (`dpb-clone` stage, 3-4%
1534 /// of decode, mostly first-touch page faults).
1535 pub fn as_reference_pooled(&self, pool: &mut Vec<Vec<u8>>) -> crate::RefFrame {
1536 // MV CAPTURE (`RFF_MV_DUMP=1`) — lets a harness read the motion field any
1537 // conformant H.264 stream carries, including x264's, using this decoder as
1538 // the parser. Diagnostic only; inert unless the env var is set.
1539 if mv_dump_on() {
1540 MV_DUMP.lock().unwrap().push(MvField {
1541 mb_w: self.mb_w,
1542 mb_h: self.mb_h,
1543 mv: self.mv_y.clone(),
1544 ref_idx: self.ref_idx_y.clone(),
1545 inter: self.inter_y.clone(),
1546 });
1547 }
1548
1549 // The per-block motion (mv/ref_idx/ref_poc) is read ONLY by B temporal/spatial
1550 // direct (`col.mv/ref_idx/ref_poc`, guarded on `w4 != 0` + `idx < len`). On
1551 // Baseline/Constrained-Baseline streams (no B) it's pure waste — skip the two
1552 // grid clones + the per-block ref_poc resolve/alloc. `w4 = 0` makes the B
1553 // readers no-op even on malformed input.
1554 let (mv, ref_idx, mv1, ref_idx1, ref_poc, w4) = if self.b_possible {
1555 (
1556 self.mv_y.clone(),
1557 self.ref_idx_y.clone(),
1558 self.mv1.clone(),
1559 self.ref_idx1.clone(),
1560 // Resolve each block's List-0 ref index to the referenced picture's
1561 // POC, so temporal direct can map it into the current list.
1562 // Via a tiny per-ref LUT: the per-block bounds + Option chain +
1563 // Ref pointer chase (57k blocks at 720p, once per reference
1564 // frame) becomes one table index. Identical output: LUT slots
1565 // past refs.len() hold MIN, exactly what .get() returned.
1566 {
1567 let mut poc_lut = [i32::MIN; 32];
1568 for (i, f) in self.refs.iter().take(32).enumerate() {
1569 poc_lut[i & 31] = f.pic_poc();
1570 }
1571 self.ref_idx_y
1572 .iter()
1573 .map(|&r| if (0..32).contains(&r) { poc_lut[r as usize] } else { i32::MIN })
1574 .collect()
1575 },
1576 self.mb_w * 4,
1577 )
1578 } else {
1579 (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), 0)
1580 };
1581 // Pop an exact-size recycled buffer per plane; a miss falls back to a
1582 // fresh allocation inside `pad_plane_into`.
1583 let mut take = |len: usize| -> Vec<u8> {
1584 match pool.iter().position(|v| v.len() == len) {
1585 Some(i) => pool.swap_remove(i),
1586 None => Vec::new(),
1587 }
1588 };
1589 let (lpw, lph) = (self.cw + 2 * crate::LPAD, self.ch + 2 * crate::LPAD);
1590 let (cpw, cph) = (self.ccw + 2 * crate::CPAD, self.ch / 2 + 2 * crate::CPAD);
1591 crate::RefFrame {
1592 // Pad once here (ExpandPicture) instead of extracting a clamped tile
1593 // on every MC call — same copy class as the old plane clone.
1594 py: rusty_h264_common::inter::pad_plane_into(take(lpw * lph), &self.rec_y, self.cw, self.ch, crate::LPAD),
1595 pu: rusty_h264_common::inter::pad_plane_into(take(cpw * cph), &self.rec_u, self.ccw, self.ch / 2, crate::CPAD),
1596 pv: rusty_h264_common::inter::pad_plane_into(take(cpw * cph), &self.rec_v, self.ccw, self.ch / 2, crate::CPAD),
1597 cw: self.cw,
1598 ch: self.ch,
1599 ready_rows: std::sync::atomic::AtomicUsize::new(0),
1600 live: None,
1601 frozen: std::sync::OnceLock::new(),
1602 frame_num: 0, // set by the caller (decode_slice knows frame_num)
1603 poc: 0, // set by the caller
1604 mv,
1605 ref_idx,
1606 mv1,
1607 ref_idx1,
1608 ref_poc,
1609 w4,
1610 long_term: false,
1611 long_term_idx: 0,
1612 }
1613 }
1614
1615 fn nnz_cache_load(&mut self, mb_x: usize, mb_y: usize) {
1616 let w4 = self.mb_w * 4;
1617 let top_unavail = mb_y == 0 || !self.nbr_in_slice(mb_x, mb_y - 1);
1618 let left_unavail = mb_x == 0 || !self.nbr_in_slice(mb_x - 1, mb_y);
1619 // The four TOP neighbours are one contiguous run of the nnz grid; only
1620 // the left column is strided. Hoisting the uniform `top_unavail` test out
1621 // of the loop turns four checked grid loads into one slice copy.
1622 if top_unavail {
1623 self.nnz_l_cache[1..5].fill(0x80);
1624 } else {
1625 let src = &self.nnz_y[(mb_y * 4 - 1) * w4 + mb_x * 4..][..4];
1626 self.nnz_l_cache[1..5].copy_from_slice(src);
1627 }
1628 for lby in 0..4 {
1629 self.nnz_l_cache[(lby + 1) * 5] =
1630 if left_unavail {
1631 0x80
1632 } else {
1633 self.nnz_y.get((mb_y * 4 + lby) * w4 + (mb_x * 4 - 1)).copied().unwrap_or(0)
1634 };
1635 }
1636 }
1637 #[inline]
1638 fn nc_pred(&self, lbx: usize, lby: usize) -> i32 {
1639 // MASKED, and it pays twice over: this helper and `nnz_cache_set` are
1640 // inlined into the intra path, the inter path AND both slice loops, so
1641 // the two unprovable indexes were replicated at every call site. The
1642 // cache is a 5x5 grid in a `[u8; 25]` and every caller passes a 4x4 block
1643 // coordinate (`LUMA_4X4_SCAN_XY`, `b8*2 + s`, or a literal), so `& 3` is
1644 // a no-op that puts the worst case at 4 * 5 + 4 = 24.
1645 let (lbx, lby) = (lbx & 3, lby & 3);
1646 let left = self.nnz_l_cache[(lby + 1) * 5 + lbx] as i32;
1647 let top = self.nnz_l_cache[lby * 5 + (lbx + 1)] as i32;
1648 let r = left + top;
1649 if r < 0x80 { (r + 1) >> 1 } else { r & 0x7f }
1650 }
1651 #[inline]
1652 fn nnz_cache_set(&mut self, lbx: usize, lby: usize, total: u8) {
1653 self.nnz_l_cache[((lby & 3) + 1) * 5 + ((lbx & 3) + 1)] = total;
1654 }
1655 fn chroma_cache_load(&mut self, mb_x: usize, mb_y: usize) {
1656 let w2 = self.mb_w * 2;
1657 let top_unavail = mb_y == 0 || !self.nbr_in_slice(mb_x, mb_y - 1);
1658 let left_unavail = mb_x == 0 || !self.nbr_in_slice(mb_x - 1, mb_y);
1659 for c in 0..2 {
1660 if top_unavail {
1661 self.nnz_c_cache[c][1..3].fill(0x80);
1662 } else {
1663 let src = &self.nnz_c[c][(mb_y * 2 - 1) * w2 + mb_x * 2..][..2];
1664 self.nnz_c_cache[c][1..3].copy_from_slice(src);
1665 }
1666 for by in 0..2 {
1667 self.nnz_c_cache[c][(by + 1) * 3] =
1668 if left_unavail {
1669 0x80
1670 } else {
1671 self.nnz_c[c & 1].get((mb_y * 2 + by) * w2 + (mb_x * 2 - 1)).copied().unwrap_or(0)
1672 };
1673 }
1674 }
1675 }
1676 #[inline]
1677 fn chroma_nc_pred(&self, c: usize, bx: usize, by: usize) -> i32 {
1678 // Same shape as `nc_pred`, one size down: a 3x3 grid in `[u8; 9]`, two
1679 // planes, and every caller passes 0..1 for all three coordinates.
1680 let (c, bx, by) = (c & 1, bx & 1, by & 1);
1681 let left = self.nnz_c_cache[c][(by + 1) * 3 + bx] as i32;
1682 let top = self.nnz_c_cache[c][by * 3 + (bx + 1)] as i32;
1683 let r = left + top;
1684 if r < 0x80 { (r + 1) >> 1 } else { r & 0x7f }
1685 }
1686 #[inline]
1687 fn chroma_nnz_cache_set(&mut self, c: usize, bx: usize, by: usize, total: u8) {
1688 self.nnz_c_cache[c & 1][((by & 1) + 1) * 3 + ((bx & 1) + 1)] = total;
1689 }
1690
1691 /// Decodes one slice's macroblocks (raster order) starting at `first_mb`,
1692 /// until `more_rbsp_data()` is exhausted or the picture is full. Returns the
1693 /// next macroblock address (= total when the picture is complete). In a
1694 /// P-slice each macroblock is preceded by `mb_skip_run`.
1695 /// CABAC slice-data decode (docs/cabac-decode-plan.md), brought up brick by brick
1696 /// against the instrumented openh264 oracle. Phase 1: verify engine init; the
1697 /// syntax layer (Phase 2+) is WIP.
1698 #[allow(clippy::too_many_arguments)]
1699 pub fn decode_slice_data_cabac(
1700 &mut self,
1701 rbsp: &[u8],
1702 start_byte: usize,
1703 slice_qp: u8,
1704 cabac_init_idc: u32,
1705 is_i: bool,
1706 is_p: bool,
1707 first_mb: usize,
1708 ) -> Result<usize, MbError> {
1709 // E2: overlap parse (this thread) with pixel reconstruction (a scoped
1710 // worker owning the planes) for P slices. I slices and B slices keep
1711 // the inline path (their pixel coupling is per-MB); the ownership
1712 // ping-pong around intra-in-P macroblocks is `edc_intra_sync`.
1713 let eligible = edc_on() && rowdb_on() && !is_i && (is_p || self.is_b);
1714 let threaded = eligible && edc_spawn_worker(self.mb_w, self.mb_h, self.bits_per_mb, true);
1715 edcstat::bump(&edcstat::DISPATCH_ON, threaded as u64);
1716 edcstat::bump(&edcstat::DISPATCH_SEEN, eligible as u64);
1717 if !threaded {
1718 let r = self.decode_slice_cabac_inner(rbsp, start_byte, slice_qp, cabac_init_idc, is_i, is_p, first_mb);
1719 self.note_slice_density(rbsp.len().saturating_sub(start_byte), first_mb, &r);
1720 return r;
1721 }
1722 let ctx = self.edc_take_ctx();
1723 // D7 PROBE: is the CPU overhead PAYLOAD (alloc/copy per job) or
1724 // SYNCHRONISATION (blocking on a full queue, park/unpark)? The bound
1725 // separates them: raising it removes send-blocking without changing a
1726 // single byte copied. `RS_H264_EDC_BOUND` sweeps it.
1727 let (tx, rx) = std::sync::mpsc::sync_channel::<EdcMsg>(edc_bound());
1728 let (ctx_tx, ctx_rx) = std::sync::mpsc::channel::<PixelCtx>();
1729 let (back_tx, back_rx) = std::sync::mpsc::channel::<PixelCtx>();
1730 let (res, ctx, panicked) = std::thread::scope(|sc| {
1731 let h = sc.spawn(move || edc_worker(ctx, rx, ctx_tx, back_rx));
1732 self.edc_tx = Some(tx);
1733 self.edc_ctx_rx = Some(ctx_rx);
1734 self.edc_back_tx = Some(back_tx);
1735 // UNWIND SAFETY (found by the fuzzer as a HANG, not a failure): a
1736 // panic inside the parse loop would skip the cleanup below — but
1737 // the sender lives in `self`, which outlives the unwind, so the
1738 // channel would never close, the worker would never exit, and the
1739 // scope's join would block forever, converting a diagnosable panic
1740 // into a silent deadlock under `catch_unwind` harnesses. Catch,
1741 // clean up, join, restore the planes, THEN resume the panic.
1742 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1743 self.decode_slice_cabac_inner(rbsp, start_byte, slice_qp, cabac_init_idc, is_i, is_p, first_mb)
1744 }));
1745 self.edc_flush_batch(); // ORDER: no job may outlive the channel
1746 self.edc_giveback(); // if an intra macroblock left us holding
1747 self.edc_tx = None; // closes the channel -> worker drains + returns
1748 self.edc_ctx_rx = None;
1749 self.edc_back_tx = None;
1750 match (r, h.join()) {
1751 (Ok(res), Ok(ctx)) => (res, Some(ctx), None),
1752 (Err(p), Ok(ctx)) => (Err(MbError::Truncated), Some(ctx), Some(p)),
1753 (Ok(_), Err(p)) | (Err(_), Err(p)) => (Err(MbError::Truncated), None, Some(p)),
1754 }
1755 });
1756 if let Some(ctx) = ctx {
1757 self.edc_restore_ctx(ctx);
1758 }
1759 if let Some(p) = panicked {
1760 std::panic::resume_unwind(p);
1761 }
1762 self.note_slice_density(rbsp.len().saturating_sub(start_byte), first_mb, &res);
1763 res
1764 }
1765
1766 /// Feed the D12 dispatch its density signal from a slice just decoded.
1767 /// Exponentially smoothed so one atypical slice cannot flip the arm, and
1768 /// only ever read on the NEXT slice — the current one is already committed.
1769 fn note_slice_density(&mut self, bytes: usize, first_mb: usize, r: &Result<usize, MbError>) {
1770 let Ok(end) = r else { return };
1771 let mbs = end.saturating_sub(first_mb);
1772 if mbs == 0 {
1773 return;
1774 }
1775 let bpm = (bytes * 8) as f64 / mbs as f64;
1776 self.bits_per_mb = if self.bits_per_mb == 0.0 {
1777 bpm
1778 } else {
1779 0.75 * self.bits_per_mb + 0.25 * bpm
1780 };
1781 }
1782
1783 fn decode_slice_cabac_inner(
1784 &mut self,
1785 rbsp: &[u8],
1786 start_byte: usize,
1787 slice_qp: u8,
1788 cabac_init_idc: u32,
1789 is_i: bool,
1790 is_p: bool,
1791 first_mb: usize,
1792 ) -> Result<usize, MbError> {
1793 self.edc_active = edc_on();
1794 let mut cab = crate::cabac::Cabac::new(rbsp, start_byte, slice_qp as i32, cabac_init_idc, is_i);
1795 let (range, _offset) = cab.dbg_state();
1796 let trace = std::env::var_os("RH_CABAC_TRACE").is_some();
1797 debug_assert_eq!(range, 510, "CABAC init range must be 510");
1798
1799 let mbw = self.mb_w;
1800 let total = self.mb_w * self.mb_h;
1801 // Per-MB neighbour state (single-slice assumption: avail == in-bounds).
1802 // SCOPED: zero-initialised allocations sized by MB count, once per slice.
1803 // D13: B-only grids (List-1 ref/mvd + direct flags) are ~292 KB at 720p and
1804 // are ONLY written on the B branch below — allocating them on every P/I
1805 // slice was the same fresh-page class GridPool fixed for the frame grids.
1806 // `RS_H264_FAT_SLICE=1` restores the always-alloc path for A/B.
1807 let _alloc_g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecSliceAlloc);
1808 // POOLED (was fresh `vec![..]` per slice): refill reuses the pooled
1809 // allocation with contents identical to a fresh vec, so the body below
1810 // is untouched. Taken out of `self` here; put back at the normal exit.
1811 let mut cat = refill(std::mem::take(&mut self.sc_cat), total, 255u8); // 0=I4x4, 2=I16, 255=unavailable
1812 let mut mb_cbp = refill(std::mem::take(&mut self.sc_cbp), total, 0u8);
1813 let mut cmode = refill(std::mem::take(&mut self.sc_cmode), total, -1i32); // chroma pred mode
1814 let mut mb_nzc = refill(std::mem::take(&mut self.sc_nzc), total, [0u8; 24]); // 16 luma raster + 8 chroma
1815 let mut cbf_dc = refill(std::mem::take(&mut self.sc_cbfdc), total, 0u16);
1816 let mut mb_skip = refill(std::mem::take(&mut self.sc_skip), total, false);
1817 let mut mb_ref = refill(std::mem::take(&mut self.sc_ref), total, [-1i8; 16]); // per-4×4-block List-0 ref (-1 = intra)
1818 let mut mb_mvd = refill(std::mem::take(&mut self.sc_mvd), total, [[0i16; 2]; 16]); // per-block mvd (for mvd ctxInc)
1819 // D13: B-only grids (~292 KB @720p) only on B slices (or FAT_SLICE A/B).
1820 let want_b_grids = self.is_b || fat_slice_on();
1821 let mut mb_ref1 = if want_b_grids {
1822 refill(std::mem::take(&mut self.sc_ref1), total, [-1i8; 16])
1823 } else {
1824 Vec::new()
1825 };
1826 let mut mb_mvd1 = if want_b_grids {
1827 refill(std::mem::take(&mut self.sc_mvd1), total, [[0i16; 2]; 16])
1828 } else {
1829 Vec::new()
1830 };
1831 let mut mb_direct = if want_b_grids {
1832 refill(std::mem::take(&mut self.sc_direct), total, false)
1833 } else {
1834 Vec::new()
1835 };
1836 drop(_alloc_g);
1837 // Multi-slice availability (spec §6.4.x): a macroblock before this
1838 // slice's first_mb is NOT available — for pixel-domain prediction
1839 // (`nbr_in_slice`, like the CAVLC twin sets) AND for every CABAC ctx
1840 // neighbour below. The per-slice ctx arrays' defaults are not the
1841 // "unavailable" value (cat 255 reads as available-I16, mb_cbp 0 as
1842 // all-zero-cbp, mb_t8x8 is a frame grid…), so the `left`/`top`
1843 // options themselves are gated on slice membership — one gate that
1844 // every downstream ctxIdxInc read inherits. Confirmed against ffmpeg
1845 // on an x264 `--slices 4` stream, which desynced without this.
1846 self.slice_first_mb = first_mb;
1847 self.slice_bounds.push((first_mb, self.cur_idc2));
1848 self.any_idc2 |= self.cur_idc2;
1849 let mut last_delta_qp = 0i32;
1850 let mut addr = first_mb;
1851
1852 let _mbloop_g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbLoop);
1853 // A new slice's first MBs may read grids a previous slice's deferred
1854 // spans still owe (P-after-B in one picture included).
1855 self.span_flush();
1856 // `mbw` is `pic_width_in_mbs_minus1 + 1` from the SPS. It cannot be 0 in
1857 // a conformant stream, but it is a runtime value, so this div+rem pair
1858 // carried a divide-by-zero panic reachable from a MALFORMED header --
1859 // on the untrusted-input path, at slice entry. `.max(1)` retires both
1860 // checks and cannot change a conformant decode.
1861 let mbw_nz = mbw.max(1);
1862 let (mut mbx, mut mby) = (addr % mbw_nz, addr / mbw_nz);
1863 // Slice-invariant B properties, formerly re-derived per macroblock.
1864 if self.is_b {
1865 self.edc_flush(); // drain anything a preceding P slice queued
1866 }
1867 let b_records = self.edc_tx.is_some();
1868 let b_refs_ok = !self.refs.is_empty() && !self.refs1.is_empty();
1869 // Loop-invariant: the lowest `addr` whose top neighbour is in this slice.
1870 let top_lim = first_mb + mbw;
1871 // Reused across macroblocks: MC overwrites every byte it reads back (all
1872 // B layouts cover the full macroblock), so the per-MB 384-byte zero-init
1873 // these used to pay was dead. Byte-identity is the guard.
1874 let mut pred_y = [0u8; 256];
1875 let mut c_pred = [[0u8; 64]; 2];
1876 // The loop wraps mbx at entry; bias so the first iteration is exact.
1877 // BOUND the entropy-coded loop (fuzzer-surfaced): a mutated stream can
1878 // never produce decode_terminate and the engine zero-fills forever. The
1879 // bound needs checking ONCE at entry — every in-loop path that advances
1880 // `addr` already breaks on `addr >= total` before continuing.
1881 if addr >= total {
1882 return Err(MbError::Truncated);
1883 }
1884 // BIND THE LENGTH FOR THE LOOP. Every grid below is `refill(.., total, ..)`
1885 // so its length is EXACTLY `total`, and the loop is entered only with
1886 // `addr < total` (checked above, re-asserted each turn) — but nothing
1887 // related the two, so every `[addr]` carried a check. Twenty-eight of
1888 // those became `get_mut` in the previous pass and cost ~2% on CABAC
1889 // content (crowd_run-main 1.021x, z=+2.11): a branch per write on the
1890 // decoder's hottest loop. Reborrowing at the literal `total` makes
1891 // `addr < total` and `addr < len` the SAME fact — no check, no branch.
1892 // The borrows end with this block, before the grids go back to the pool.
1893 // `mb_direct`/`mb_ref1`/`mb_mvd1` are deliberately NOT bound here: they
1894 // are `Vec::new()` on non-B slices, so slicing them to `total` would be
1895 // the very panic this campaign is removing.
1896 {
1897 let cat = &mut cat[..total];
1898 let mb_cbp = &mut mb_cbp[..total];
1899 let cmode = &mut cmode[..total];
1900 let mb_nzc = &mut mb_nzc[..total];
1901 let cbf_dc = &mut cbf_dc[..total];
1902 let mb_skip = &mut mb_skip[..total];
1903 let mb_ref = &mut mb_ref[..total];
1904 let mb_mvd = &mut mb_mvd[..total];
1905 loop {
1906 // A REAL check, not `debug_assert` — which compiles OUT in release,
1907 // so nothing carried `addr < total` across the loop's back-edge and
1908 // the reborrows above folded nothing on their own. This cannot fire
1909 // (every path that advances `addr` already breaks on `addr >= total`,
1910 // and entry is guarded above), but stating it once per macroblock
1911 // replaces FIFTEEN per-write branches with one that never taken.
1912 if addr >= total {
1913 break;
1914 }
1915 // Carried coordinates: one compare-and-wrap replaces the per-MB
1916 // div+mod pair (and row_hook's own division).
1917 if mbx == mbw {
1918 mbx = 0;
1919 mby += 1;
1920 }
1921 self.row_hook_at(addr, mby);
1922 self.wait_refs_for_mb(mby);
1923 let left = (mbx > 0 && addr > first_mb).then(|| addr - 1);
1924 let top = (mby > 0 && addr >= top_lim).then(|| addr - mbw);
1925
1926 // Brick 3.1/3.2: P-slice mb_skip_flag, then mb_type (P mb_type is neighbour-
1927 // independent; intra sub-types map to the I dispatch below).
1928 let mb_type;
1929 if is_p {
1930 // Direct bool arithmetic — no Option chain on the hot path.
1931 let sctx = 11
1932 + (left.is_some() && !mb_skip.get(addr - 1).copied().unwrap_or(true)) as usize
1933 + (top.is_some()
1934 && !mb_skip.get(addr.wrapping_sub(mbw)).copied().unwrap_or(true))
1935 as usize;
1936 if parse_mb_skip_cabac(&mut cab, sctx) {
1937 if let Some(p) = mb_skip.get_mut(addr) {
1938 *p = true;
1939 }
1940 last_delta_qp = 0; // skip codes no mb_qp_delta → delta ctxInc resets
1941 // P_Skip recon reuses the entropy-free CAVLC primitive verbatim: it
1942 // takes no bit-reader (skip has no coded syntax past the flag), just
1943 // predicts the skip MV, motion-compensates, and commits the grid.
1944 self.decode_p_skip(mbx, mby)?;
1945 if let Some(p) = self.mb_qp.get_mut(addr) {
1946 *p = self.cur_qp;
1947 } // skip inherits QPy
1948 let eos = cab.decode_terminate();
1949 addr += 1;
1950 mbx += 1;
1951 if eos || addr >= total {
1952 break;
1953 }
1954 continue;
1955 }
1956 let mbt = parse_mb_type_p_cabac(&mut cab);
1957 // NON-SKIP P MB: the inter arms below predict MVs from the
1958 // grids (mv_neighbors_block) and the intra arm gathers — flush
1959 // the deferred P span (B span cannot be pending in a P slice,
1960 // but span_flush is one Option check each).
1961 self.span_flush();
1962 if mbt <= 3 {
1963 let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbP);
1964 // noSubMbPartSizeLessThan8x8Flag (spec 7.3.5): P_8x8 permits the
1965 // 8x8 transform only when every sub-partition is itself 8x8.
1966 let mut allow8 = true;
1967 // Inter MB (Bricks 3.3/3.4/3.5). 1-ref stream → ref_idx not coded (ref=0).
1968 // Build the 30-entry mvd/ref neighbour cache (openh264 WelsFillCacheInterCabac).
1969 let mut mvdc = [[0i16; 2]; 30];
1970 let mut refc = [-1i8; 30];
1971 // Index the neighbour's RECORD once, then read within it.
1972 // `mb_ref[l][bi]` is two bounds checks - one on the Vec, one
1973 // on the array - repeated for each of the four entries and
1974 // again for `mb_mvd`, i.e. sixteen checks per macroblock for
1975 // four neighbours. Binding the record hoists the Vec check
1976 // out; the inner index is a literal and folds.
1977 if let Some(l) = left {
1978 let (Some(lr), Some(lm)) = (mb_ref.get(l), mb_mvd.get(l)) else {
1979 return Err(MbError::Truncated);
1980 };
1981 for (ci, bi) in [(6usize, 3usize), (12, 7), (18, 11), (24, 15)] {
1982 refc[ci] = lr[bi];
1983 mvdc[ci] = lm[bi];
1984 }
1985 }
1986 if let Some(t) = top {
1987 let (Some(tr), Some(tm)) = (mb_ref.get(t), mb_mvd.get(t)) else {
1988 return Err(MbError::Truncated);
1989 };
1990 for (ci, bi) in [(1usize, 12usize), (2, 13), (3, 14), (4, 15)] {
1991 refc[ci] = tr[bi];
1992 mvdc[ci] = tm[bi];
1993 }
1994 }
1995 if mbx > 0 && mby > 0 {
1996 let a = addr - mbw - 1;
1997 if let (Some(r), Some(m)) = (mb_ref.get(a), mb_mvd.get(a)) {
1998 (refc[0], mvdc[0]) = (r[15], m[15]);
1999 }
2000 }
2001 if mby > 0 && mbx + 1 < mbw {
2002 let a = addr - mbw + 1;
2003 if let (Some(r), Some(m)) = (mb_ref.get(a), mb_mvd.get(a)) {
2004 (refc[5], mvdc[5]) = (r[12], m[12]);
2005 }
2006 }
2007 let mut mmvd = [[0i16; 2]; 16];
2008 let mut mref = [0i8; 16];
2009 // mb_pred (spec 7.3.5.1): all ref_idx_l0 FIRST (only when >1 active
2010 // ref), then all mvd + ref-aware predict + commit. `refidx!` parses one
2011 // partition's ref_idx (ctxIdxOffset 54, ctx from neighbour refc) and
2012 // seeds refc so a later partition's ref/mvd context sees it — mirror
2013 // of the encoder's two-phase emit_mb_cabac_p_inter.
2014 macro_rules! refidx {
2015 ($pi:expr, $zb:expr) => {{
2016 if self.num_ref_active > 1 {
2017 let s = CACHE30[$pi & 15].clamp(6, 29);
2018 let c0 = (refc[s - 1] > 0) as usize + 2 * (refc[s - 6] > 0) as usize;
2019 let r = parse_ref_idx_cabac(&mut cab, c0);
2020 for &zb in $zb.iter() {
2021 refc[CACHE30[zb & 15]] = r;
2022 }
2023 r
2024 } else {
2025 0i8
2026 }
2027 }};
2028 }
2029 macro_rules! part {
2030 ($pi:expr, $zb:expr, $pred:expr, $rx:expr, $ry:expr, $rw:expr, $rh:expr, $refi:expr) => {{
2031 let (mvx, mvy) = parse_mvd_partition(&mut cab, $pi, $zb, &mut mvdc, &mut refc, &mut mmvd, &mut mref, $refi);
2032 let [na, nb, nc] = self.mv_neighbors_block(
2033 (mbx * 4 + $rx / 4) as isize,
2034 (mby * 4 + $ry / 4) as isize,
2035 ($rw / 4) as isize,
2036 );
2037 let pmv = $pred(na, nb, nc);
2038 self.commit_inter_grid(mbx, mby, $rx, $ry, $rw, $rh, (pmv.0 + mvx, pmv.1 + mvy), $refi);
2039 }};
2040 }
2041 match mbt {
2042 0 => {
2043 // Sibling of CAVLC P_16x16: one (ref, mv) for all 16
2044 // blocks → every internal edge is strength 0 (§8.7.2.1).
2045 // Without this, CABAC Main/High P_16x16 stayed UNSET and
2046 // paid the blind 24-block bS gather.
2047 if let Some(k) = self.mb_kind.get_mut(mby * self.mb_w + mbx) {
2048 *k =
2049 rusty_h264_common::deblock::MB_KIND_INTER_UNIFORM;
2050 }
2051 let r0 = refidx!(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
2052 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);
2053 }
2054 1 => {
2055 let r0 = refidx!(0, &[0, 1, 2, 3, 4, 5, 6, 7]);
2056 let r1 = refidx!(8, &[8, 9, 10, 11, 12, 13, 14, 15]);
2057 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);
2058 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);
2059 }
2060 2 => {
2061 let r0 = refidx!(0, &[0, 1, 2, 3, 8, 9, 10, 11]);
2062 let r1 = refidx!(4, &[4, 5, 6, 7, 12, 13, 14, 15]);
2063 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);
2064 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);
2065 }
2066 _ => {
2067 // P_8x8: 4 sub_mb_types, then 4 ref_idx (one per 8×8), then mvd.
2068 let mut subt = [0u32; 4];
2069 for st in &mut subt {
2070 *st = parse_sub_mb_type_p_cabac(&mut cab);
2071 }
2072 allow8 = subt.iter().all(|&t| t == 0);
2073 let mut pr = [0i8; 4];
2074 for (i, r) in pr.iter_mut().enumerate() {
2075 let b = i * 4;
2076 *r = refidx!(b, &[b, b + 1, b + 2, b + 3]);
2077 }
2078 for i in 0..4usize {
2079 let b = i * 4;
2080 let (ox, oy) = ((i % 2) * 8, (i / 2) * 8); // 8×8 pixel origin in MB
2081 let ri = pr[i];
2082 match subt[i] {
2083 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),
2084 1 => {
2085 part!(b, &[b, b + 1], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 8, 4, ri);
2086 part!(b + 2, &[b + 2, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy + 4, 8, 4, ri);
2087 }
2088 2 => {
2089 part!(b, &[b, b + 2], |a, b, c| predict_mv(a, b, c, ri as i32), ox, oy, 4, 8, ri);
2090 part!(b + 1, &[b + 1, b + 3], |a, b, c| predict_mv(a, b, c, ri as i32), ox + 4, oy, 4, 8, ri);
2091 }
2092 _ => {
2093 for j in 0..4usize {
2094 let (sx, sy) = ((j % 2) * 4, (j / 2) * 4);
2095 part!(b + j, &[b + j], |a, b, c| predict_mv(a, b, c, ri as i32), ox + sx, oy + sy, 4, 4, ri);
2096 }
2097 }
2098 }
2099 }
2100 }
2101 }
2102 if let Some(p) = mb_ref.get_mut(addr) {
2103 *p = mref;
2104 }
2105 if let Some(p) = mb_mvd.get_mut(addr) {
2106 *p = mmvd;
2107 }
2108
2109 // Inter cbp + residual (is_intra = false → cbf default nA=nB=0).
2110 let cbp = parse_cbp_cabac(&mut cab, top.and_then(|a| mb_cbp.get(a).copied()), left.and_then(|a| mb_cbp.get(a).copied()));
2111 if let Some(p) = mb_cbp.get_mut(addr) {
2112 *p = cbp as u8;
2113 }
2114 // H-49: an INTER macroblock carries transform_size_8x8_flag AFTER cbp
2115 // (spec 7.3.5), present only when CodedBlockPatternLuma > 0 and
2116 // noSubMbPartSizeLessThan8x8Flag. Same context as the intra read.
2117 let t8 = self.transform_8x8_mode && (cbp & 15) != 0 && allow8 && {
2118 let a = left.map_or(0, |x| self.mb_t8x8.get(x).is_some_and(|&f| f) as usize);
2119 let b = top.map_or(0, |x| self.mb_t8x8.get(x).is_some_and(|&f| f) as usize);
2120 cab.decode_decision(399 + a + b) != 0
2121 };
2122 if let Some(p) = self.mb_t8x8.get_mut(addr) {
2123 *p = t8;
2124 }
2125 self.any_t8 |= t8;
2126 // D9c: cbp==0 never parses residuals — skip the 2.5 KB coeff
2127 // zero-init + PInterJob entirely when NORES is on (default).
2128 // Current-MB nzc slots stay unset under cbp==0 and export as 0
2129 // (same as the 0xff→0 scrub below), so mb_nzc = [0;24] is exact.
2130 if cbp == 0 && nores_on() {
2131 last_delta_qp = 0;
2132 if let Some(p) = self.mb_qp.get_mut(addr) {
2133 *p = self.cur_qp;
2134 }
2135 if let Some(p) = cbf_dc.get_mut(addr) {
2136 *p = 0;
2137 }
2138 if let Some(p) = mb_nzc.get_mut(addr) {
2139 *p = [0u8; 24];
2140 }
2141 if self.refs.is_empty() {
2142 return Err(MbError::Unsupported("inter without reference"));
2143 }
2144 let (mut jgmv, mut jgref) = ([(0i32, 0i32); 16], [0u8; 16]);
2145 {
2146 let w4r = self.mb_w * 4;
2147 for by in 0..4usize {
2148 // Row-contiguous — see the coded-inter gather.
2149 let row = (mby * 4 + by) * w4r + mbx * 4;
2150 jgmv[by * 4..by * 4 + 4]
2151 .copy_from_slice(&self.mv_y[row..row + 4]);
2152 // Row-slice the ref grid too - it was the only
2153 // one still indexed per block.
2154 let ridx = &self.ref_idx_y[row..row + 4];
2155 for bx in 0..4usize {
2156 jgref[by * 4 + bx] = ridx[bx].clamp(0, 15) as u8;
2157 }
2158 }
2159 }
2160 let pj = PInterNoResJob {
2161 mbx,
2162 mby,
2163 t8,
2164 gmv: jgmv,
2165 gref: jgref,
2166 };
2167 if self.edc_tx.is_some() {
2168 self.edc_giveback();
2169 self.edc_commit_nnz(mbx, mby, t8, &[0u8; 24], 0);
2170 if edcstat::on() {
2171 edcstat::bump(&edcstat::J_INTER, 1);
2172 edcstat::bump(&edcstat::J_INTER_NORES, 1);
2173 }
2174 edcstat::bump(&edcstat::J_NORES_SENT, 1);
2175 self.edc_send_job(EdcJob::InterNoRes(Box::new(pj)));
2176 } else if self.edc_active {
2177 edcstat::bump(&edcstat::J_NORES_SENT, 1);
2178 self.edc_jobs.push(EdcJob::InterNoRes(Box::new(pj)));
2179 } else {
2180 self.recon_p_inter_nores(&pj);
2181 if double_recon() {
2182 self.recon_p_inter_nores(&pj);
2183 }
2184 }
2185 let eos = cab.decode_terminate();
2186 addr += 1;
2187 mbx += 1;
2188 if eos || addr >= total {
2189 break;
2190 }
2191 continue;
2192 }
2193 // Zeroed ONLY when the 8x8 transform is in play — this was a
2194 // 1KB memset per coded inter MB on non-t8 tiers.
2195 let mut luma8: Option<[[i32; 64]; 4]> = None;
2196 let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
2197 let mut nzc = [0xffu8; 48];
2198 if let Some(t) = top {
2199 let tnz = mb_nzc.get(t).unwrap_or(&ZERO_NZC);
2200 nzc[1..5].copy_from_slice(&tnz[12..16]);
2201 (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
2202 (nzc[6], nzc[7], nzc[30], nzc[31]) = (tnz[20], tnz[21], tnz[22], tnz[23]);
2203 }
2204 if let Some(l) = left {
2205 let lnz = mb_nzc.get(l).unwrap_or(&ZERO_NZC);
2206 (nzc[8], nzc[16], nzc[24], nzc[32]) = (lnz[3], lnz[7], lnz[11], lnz[15]);
2207 (nzc[13], nzc[21], nzc[37], nzc[45]) = (lnz[17], lnz[21], lnz[19], lnz[23]);
2208 }
2209 let mut cbfdc = 0u16;
2210 let mut nnzs = [0u8; 24]; // parsed totalCoeff per block (see add_inter_residual)
2211 // Materialised ONLY when the parse actually writes them: a
2212 // t8 macroblock never touches luma_scan (1 KB), and
2213 // cbp_chroma < 2 never touches cac (512 B).
2214 let mut luma_scan: Option<[[i32; 16]; 16]> = None; // per z-order 4×4 block
2215 let mut cdc = [[0i32; 4]; 2]; // chroma DC per plane (scan order)
2216 let mut cac: Option<[[[i32; 16]; 4]; 2]> = None; // chroma AC per plane
2217 // A cbp==0 MB codes no mb_qp_delta → the next MB's delta ctxInc sees 0.
2218 if cbp == 0 {
2219 last_delta_qp = 0;
2220 }
2221 if cbp != 0 {
2222 let ndc = (top.and_then(|a| cbf_dc.get(a).copied()), left.and_then(|a| cbf_dc.get(a).copied()));
2223 let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
2224 self.step_qp(qpd)?;
2225 for id8 in 0..4usize {
2226 if cbp_luma & (1 << id8) != 0 {
2227 if t8 {
2228 // All four slots carry the 8x8 total: cat 5 has no per-4x4
2229 // counts, and the recon helper now reads one slot
2230 // per 4x4 cell.
2231 let n8 = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, false, ndc, &mut luma8.get_or_insert_with(|| [[0i32; 64]; 4])[id8]) as u8;
2232 for k in 0..4 {
2233 nnzs[id8 * 4 + k] = n8;
2234 }
2235 } else {
2236 let ls = luma_scan.get_or_insert_with(|| [[0i32; 16]; 16]);
2237 for id4 in 0..4usize {
2238 let iz = id8 * 4 + id4;
2239 nnzs[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, false, ndc, &mut ls[iz]) as u8;
2240 }
2241 }
2242 } else {
2243 for k in 0..4 {
2244 nzc[NZC_CACHE[(id8 * 4 + k).min(23)].min(47)] = 0;
2245 }
2246 }
2247 }
2248 if cbp_chroma >= 1 {
2249 for i in 0..2usize {
2250 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, false, ndc, &mut cdc[i]);
2251 }
2252 }
2253 if cbp_chroma == 2 {
2254 let cacm = cac.get_or_insert_with(|| [[[0i32; 16]; 4]; 2]);
2255 for i in 0..2usize {
2256 for id4 in 0..4usize {
2257 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 cacm[i][id4]) as u8;
2258 }
2259 }
2260 }
2261 }
2262 if let Some(p) = self.mb_qp.get_mut(addr) {
2263 *p = self.cur_qp;
2264 }
2265 if let Some(p) = cbf_dc.get_mut(addr) {
2266 *p = cbfdc;
2267 }
2268 let _sc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecStateCache);
2269 let mut mn = [0u8; 24];
2270 for k in 0..4 {
2271 mn[k] = nzc[9 + k];
2272 mn[4 + k] = nzc[17 + k];
2273 mn[8 + k] = nzc[25 + k];
2274 mn[12 + k] = nzc[33 + k];
2275 }
2276 (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
2277 (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
2278 // A block whose residual was skipped (cbp bit clear / no chroma AC)
2279 // has 0 coeffs, not "unavailable" — export 0 so an intra neighbour's
2280 // CBF ctxInc reads 0 (not the 0xff sentinel → is_intra default).
2281 for v in mn.iter_mut() {
2282 if *v == 0xff {
2283 *v = 0;
2284 }
2285 }
2286 if let Some(p) = mb_nzc.get_mut(addr) {
2287 *p = mn;
2288 }
2289 drop(_sc);
2290
2291 if self.refs.is_empty() {
2292 return Err(MbError::Unsupported("inter without reference"));
2293 }
2294 // NOTE (measured, do not "optimise" this away): EDC is
2295 // DEFAULT ON (`edc_on()`, opt out with RS_H264_EDC=0), so
2296 // even single-threaded this job is built and deferred —
2297 // that batching is the E1 loop-fission win, not overhead.
2298 // A "skip the job in 1T" bypass added here could never fire
2299 // and was removed.
2300 let (mut jgmv, mut jgref) = ([(0i32, 0i32); 16], [0u8; 16]);
2301 {
2302 let w4r = self.mb_w * 4;
2303 for by in 0..4usize {
2304 // Row-contiguous: one slice copy for the MVs
2305 // instead of four bounds-checked strided loads.
2306 let row = (mby * 4 + by) * w4r + mbx * 4;
2307 jgmv[by * 4..by * 4 + 4].copy_from_slice(&self.mv_y[row..row + 4]);
2308 // Row-slice the ref grid too - it was the only one
2309 // still indexed per block.
2310 let ridx = &self.ref_idx_y[row..row + 4];
2311 for bx in 0..4usize {
2312 jgref[by * 4 + bx] = ridx[bx].clamp(0, 15) as u8;
2313 }
2314 }
2315 }
2316 // D9c: when `cbp == 0`, never materialise the 2.5 KB coeff
2317 // arrays into a `PInterJob` — ship `InterNoRes` (or call
2318 // `recon_p_inter_nores` inline). `RS_H264_NORES=0` keeps the
2319 // old full-job path for A/B.
2320 let nores = cbp == 0 && nores_on();
2321 if nores {
2322 let pj = PInterNoResJob {
2323 mbx,
2324 mby,
2325 t8,
2326 gmv: jgmv,
2327 gref: jgref,
2328 };
2329 if self.edc_tx.is_some() {
2330 self.edc_giveback();
2331 self.edc_commit_nnz(mbx, mby, t8, &[0u8; 24], 0);
2332 if edcstat::on() {
2333 edcstat::bump(&edcstat::J_INTER, 1);
2334 edcstat::bump(&edcstat::J_INTER_NORES, 1);
2335 }
2336 edcstat::bump(&edcstat::J_NORES_SENT, 1);
2337 self.edc_send_job(EdcJob::InterNoRes(Box::new(pj)));
2338 } else if self.edc_active {
2339 edcstat::bump(&edcstat::J_NORES_SENT, 1);
2340 self.edc_jobs.push(EdcJob::InterNoRes(Box::new(pj)));
2341 } else {
2342 self.recon_p_inter_nores(&pj);
2343 if double_recon() {
2344 self.recon_p_inter_nores(&pj);
2345 }
2346 }
2347 let eos = cab.decode_terminate();
2348 addr += 1;
2349 mbx += 1;
2350 if eos || addr >= total {
2351 break;
2352 }
2353 continue;
2354 }
2355 let job = PInterJob {
2356 mbx,
2357 mby,
2358 qp: self.cur_qp,
2359 cbp_chroma,
2360 gmv: jgmv,
2361 gref: jgref,
2362 luma_scan,
2363 luma8,
2364 cdc,
2365 cac,
2366 nnzs,
2367 };
2368 if self.edc_tx.is_some() {
2369 self.edc_giveback();
2370 self.edc_commit_nnz(mbx, mby, t8, &nnzs, cbp_chroma);
2371 if edcstat::on() {
2372 edcstat::bump(&edcstat::J_INTER, 1);
2373 }
2374 self.edc_send_job(EdcJob::Inter(Box::new(job)));
2375 } else if self.edc_active {
2376 self.edc_jobs.push(EdcJob::Inter(Box::new(job)));
2377 } else {
2378 self.recon_p_inter(&job);
2379 if double_recon() {
2380 self.recon_p_inter(&job);
2381 }
2382 }
2383
2384 let eos = cab.decode_terminate();
2385 addr += 1;
2386 mbx += 1;
2387 if eos || addr >= total {
2388 break;
2389 }
2390 continue;
2391 }
2392 mb_type = mbt - 5; // 5→0 (I_4x4), 6..29→1..24 (I_16x16)
2393 } else if self.is_b {
2394 // NOTE: the per-macroblock edc_flush() that used to sit here is
2395 // hoisted to slice entry (see below) — nothing inside a B slice
2396 // pushes to edc_jobs.
2397 if b_records {
2398 // E3: this B macroblock's MC regions record instead of executing.
2399 self.edc_regions = Some(Vec::with_capacity(8));
2400 }
2401 let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbB);
2402 // noSubMbPartSizeLessThan8x8Flag for B: direct MBs qualify only under
2403 // direct_8x8_inference_flag; B_8x8 needs every sub-partition 8x8.
2404 let mut allow8 = true;
2405 // B-slice: mb_skip_flag (ctx 24 + neighbour-not-skip), then B mb_type.
2406 let (hl, ht) = (left.is_some(), top.is_some());
2407 let sctx = 24
2408 + (hl && !mb_skip.get(addr - 1).copied().unwrap_or(true)) as usize
2409 + (ht && !mb_skip.get(addr.wrapping_sub(mbw)).copied().unwrap_or(true)) as usize;
2410 if parse_mb_skip_cabac(&mut cab, sctx) {
2411 if let (Some(sk), Some(di)) = (mb_skip.get_mut(addr), mb_direct.get_mut(addr)) {
2412 (*sk, *di) = (true, true);
2413 }
2414 last_delta_qp = 0; // skip codes no mb_qp_delta → delta ctxInc resets
2415 // B_Skip recon reuses the entropy-free CAVLC primitive (spatial/temporal
2416 // direct with no residual), which also commits the motion grid.
2417 // Hot prefix inline: forced run continuations skip the call.
2418 if !self.b_skip_hot(mbx, mby) {
2419 self.decode_b_skip(mbx, mby)?;
2420 }
2421 if let Some(p) = self.mb_qp.get_mut(addr) {
2422 *p = self.cur_qp;
2423 }
2424 // Skip/direct blocks contribute mvd 0 to a later MB's mvd ctxInc; the
2425 // ref stays in-list so |mvd|=0 is summed (same result either way).
2426 // mb_ref/mb_ref1 stay at their -1 init: every reader is
2427 // either a `> 0` context test (-1 and 0 both false) or the
2428 // mvd-sum's `>= 0` gate — and a skip's mvd is (0,0), so
2429 // exclusion (-1) and inclusion-of-zero (0) give the same
2430 // sum. The 64-byte per-skip zero-fill was pure waste.
2431 let eos = cab.decode_terminate();
2432 addr += 1;
2433 mbx += 1;
2434 if eos || addr >= total {
2435 break;
2436 }
2437 continue;
2438 }
2439 // NON-SKIP B MB: every arm below reads the grids INLINE
2440 // (H-48: CABAC never routes through decode_b_mb) — flush
2441 // the deferred spans. Caught by the tempete ARM-DIFF.
2442 self.span_flush();
2443 let bci = (hl && !mb_direct.get(addr - 1).copied().unwrap_or(true)) as usize
2444 + (ht && !mb_direct.get(addr.wrapping_sub(mbw)).copied().unwrap_or(true)) as usize;
2445 let bmt = { let _s = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BTypeParse); parse_mb_type_b_cabac(&mut cab, bci) };
2446 if bmt < 23 {
2447 // ---- B inter: parse motion (mvd L0/L1; ref not coded on this 1-ref
2448 // stream) + residual. Recon (b_mc/direct) deferred to B.3. ----
2449 let mut mvdc0 = [[0i16; 2]; 30];
2450 let mut refc0 = [-1i8; 30];
2451 let mut mvdc1 = [[0i16; 2]; 30];
2452 let mut refc1 = [-1i8; 30];
2453 // WelsFillCacheInterCabac, per list (L0 = mb_ref/mb_mvd, L1 = mb_ref1/mb_mvd1).
2454 // The corner addresses and their guards are macroblock
2455 // properties, resolved ONCE here rather than rebuilt inside
2456 // each list expansion; each neighbour grid row is borrowed
2457 // once instead of re-indexed per entry.
2458 let tl = (mbx > 0 && mby > 0).then(|| addr - mbw - 1);
2459 let tr = (mby > 0 && mbx + 1 < mbw).then(|| addr - mbw + 1);
2460 macro_rules! fill {
2461 ($mrf:expr, $mmv:expr, $rc:expr, $mc:expr) => {{
2462 // `.get` PER PARALLEL GRID. The ref and mvd grids are
2463 // separate Vecs indexed by the same macroblock address,
2464 // so `$mrf[l]` proved nothing about `$mmv[l]` and each
2465 // of the four neighbour slots carried two panic paths —
2466 // doubled again because this macro expands once per
2467 // list. A `None` here degrades to exactly what the
2468 // cache already means by "neighbour unavailable" (the
2469 // `-1` ref it is initialised to), so the fallible form
2470 // is the honest one as well as the cheap one.
2471 if let Some(l) = left {
2472 if let (Some(rr), Some(mm)) = ($mrf.get(l), $mmv.get(l)) {
2473 for (ci, bi) in [(6usize, 3usize), (12, 7), (18, 11), (24, 15)] {
2474 $rc[ci] = rr[bi];
2475 $mc[ci] = mm[bi];
2476 }
2477 }
2478 }
2479 if let Some(t) = top {
2480 if let (Some(rr), Some(mm)) = ($mrf.get(t), $mmv.get(t)) {
2481 for (ci, bi) in [(1usize, 12usize), (2, 13), (3, 14), (4, 15)] {
2482 $rc[ci] = rr[bi];
2483 $mc[ci] = mm[bi];
2484 }
2485 }
2486 }
2487 if let Some(a) = tl {
2488 if let (Some(rr), Some(mm)) = ($mrf.get(a), $mmv.get(a)) {
2489 ($rc[0], $mc[0]) = (rr[15], mm[15]);
2490 }
2491 }
2492 if let Some(a) = tr {
2493 if let (Some(rr), Some(mm)) = ($mrf.get(a), $mmv.get(a)) {
2494 ($rc[5], $mc[5]) = (rr[12], mm[12]);
2495 }
2496 }
2497 }};
2498 }
2499 {
2500 let _s = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BFillCache);
2501 fill!(mb_ref, mb_mvd, refc0, mvdc0);
2502 fill!(mb_ref1, mb_mvd1, refc1, mvdc1);
2503 }
2504 let mut _smv = Some(rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BMvdParse));
2505 let mut mmvd0 = [[0i16; 2]; 16];
2506 let mut mref0 = [-1i8; 16];
2507 let mut mmvd1 = [[0i16; 2]; 16];
2508 let mut mref1 = [-1i8; 16];
2509 if !b_refs_ok {
2510 return Err(MbError::Unsupported("B without references"));
2511 }
2512 // Recon (mirrors CAVLC decode_b_mb / decode_b_8x8): predict each list's
2513 // MV off the committed grid + the CABAC-parsed mvd, commit, MC (bi-pred
2514 // blend), then add the residual. Prediction reads mmvd0/mmvd1 (the mvd
2515 // per raster block, splatted during the parse above).
2516 // pred_y / c_pred are slice-scope scratch (see the loop head).
2517 // Recording is a per-macroblock property — ask once, not per
2518 // partition, so the 1T path calls b_mc straight through.
2519 let rec_mode = self.edc_regions.is_some();
2520
2521 if bmt == 0 {
2522 // B_Direct_16x16: no coded motion. A direct block contributes mvd 0
2523 // to a later MB's mvd ctxInc with its ref in-list (|0| summed).
2524 if let Some(p) = mb_direct.get_mut(addr) {
2525 *p = true;
2526 }
2527 allow8 = self.direct_8x8_inference;
2528 (mref0, mref1) = ([0i8; 16], [0i8; 16]);
2529 // FORCED derivation via the zero-bi bitmap (same triple
2530 // as b_skip_hot): a direct-16 whose left/top/topright are
2531 // recorded ref0/(0,0) committers derives (0,0)/(0,0) bi —
2532 // skip the gather + derivation, run the region half
2533 // directly. Its own commit is zero-bi too, so it EXTENDS
2534 // forcing chains through coded direct MBs.
2535 // `mbw` is already carried by the loop, and the row-above
2536 // address is one subtraction, not three.
2537 let up = addr.wrapping_sub(mbw);
2538 let forced = !b_records
2539 && mbx > 0
2540 && mby > 0
2541 && mbx + 1 < mbw
2542 && up >= self.slice_first_mb
2543 && match self.bzero.get(..=addr) {
2544 // A slice ENDING at `addr` proves all three
2545 // neighbour reads at once — the guards above put
2546 // every one of them below `addr`. Same shape as
2547 // `b_skip_hot`.
2548 Some(bz) => {
2549 bz.get(addr - 1).copied().unwrap_or(false)
2550 && bz.get(up).copied().unwrap_or(false)
2551 && bz.get(up + 1).copied().unwrap_or(false)
2552 }
2553 None => false,
2554 };
2555 if forced {
2556 self.b_direct_region(mbx, mby, 0, 0, 16, 16, &mut pred_y, &mut c_pred, (0, 0, (0, 0), (0, 0), false));
2557 if let Some(b) = self.bzero.get_mut(addr) {
2558 *b = true;
2559 }
2560 } else {
2561 self.decode_b_direct(mbx, mby, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
2562 }
2563 } else if bmt == 22 {
2564 // B_8x8: 4 sub_mb_types, (ref not coded on 1-ref), then mvd
2565 // list-major → sub-MB → sub-partition (openh264 order).
2566 let mut subt = [0u32; 4];
2567 for s in &mut subt {
2568 *s = parse_sub_mb_type_b_cabac(&mut cab);
2569 }
2570 let d8i = self.direct_8x8_inference;
2571 allow8 = subt.iter().all(|&t| if t == 0 { d8i } else { (1..=3).contains(&t) });
2572 // uses(list) depends only on the sub_mb_type — resolve the
2573 // whole 4x2 table once here; the ref_idx, mvd and recon
2574 // loops below all read it instead of re-asking.
2575 let su = [
2576 [b_sub_uses(subt[0], 0), b_sub_uses(subt[0], 1)],
2577 [b_sub_uses(subt[1], 0), b_sub_uses(subt[1], 1)],
2578 [b_sub_uses(subt[2], 0), b_sub_uses(subt[2], 1)],
2579 [b_sub_uses(subt[3], 0), b_sub_uses(subt[3], 1)],
2580 ];
2581 // A direct sub-partition contributes mvd 0 / ref in-list to the
2582 // ctxInc — both the per-MB export and the within-MB 30-cache that a
2583 // later (non-direct) sub in this MB reads.
2584 for i in 0..4usize {
2585 if subt[i] == 0 {
2586 let b = i * 4;
2587 for zb in b..b + 4 {
2588 // each table read once, not twice
2589 let (g, c) = (G_SCAN4[zb & 15], CACHE30[zb & 15]);
2590 (mref0[g], mref1[g]) = (0, 0);
2591 (refc0[c], refc1[c]) = (0, 0);
2592 }
2593 }
2594 }
2595 // ref_idx_l0 for all four 8x8s, then ref_idx_l1, then the mvds
2596 // (spec 7.3.5.2 sub_mb_pred). ONE ref per 8x8 -- never per
2597 // sub-partition -- and B_Direct_8x8 codes none.
2598 let mut sref = [[0i8; 2]; 4]; // [sub-MB][list]
2599 for list in 0..2usize {
2600 // Single-reference lists code no ref_idx at all.
2601 if (if list == 0 { self.num_ref_active } else { self.num_ref_active1 }) <= 1 {
2602 continue;
2603 }
2604 let rc = if list == 0 { &mut refc0 } else { &mut refc1 };
2605 for i in 0..4usize {
2606 let st = subt[i];
2607 if st == 0 || !su[i][list] {
2608 continue;
2609 }
2610 let b = i * 4;
2611 let s = CACHE30[b & 15].clamp(6, 29);
2612 let c0 = (rc[s - 1] > 0) as usize + 2 * (rc[s - 6] > 0) as usize;
2613 let r = parse_ref_idx_cabac(&mut cab, c0);
2614 for &zb in &[b, b + 1, b + 2, b + 3] {
2615 rc[CACHE30[zb]] = r;
2616 }
2617 sref[i][list] = r;
2618 }
2619 }
2620 for list in 0..2usize {
2621 let (mmv, mrf, mc, rc) = if list == 0 {
2622 (&mut mmvd0, &mut mref0, &mut mvdc0, &mut refc0)
2623 } else {
2624 (&mut mmvd1, &mut mref1, &mut mvdc1, &mut refc1)
2625 };
2626 for i in 0..4usize {
2627 let st = subt[i];
2628 if st == 0 || !su[i][list] {
2629 continue;
2630 }
2631 let b = i * 4;
2632 for &(sx, sy, sw, sh) in b_sub_parts(st) {
2633 // Shapes are 8x8 / 8x4 / 4x8 / 4x4, so the block
2634 // list is closed-form: `step` is 1 when the part
2635 // spans both columns, 2 when it spans both rows.
2636 let (w4b, h4b) = (sw / 4, sh / 4);
2637 let base = b + (sy / 4) * 2 + sx / 4;
2638 let step = if w4b == 2 { 1 } else { 2 };
2639 let zb = [base, base + step, base + 2, base + 3];
2640 let n = w4b * h4b;
2641 parse_mvd_partition(&mut cab, zb[0], &zb[..n], mc, rc, mmv, mrf, sref[i][list]);
2642 }
2643 }
2644 }
2645 // Recon each 8×8: direct sub → decode_b_direct; else per sub-part
2646 // predict (median) + commit + MC.
2647 // Spatial-direct A/B/C are MB-level — walk once if any sub is
2648 // direct. `dmemo=0` rewalks every direct 8×8 (A/B oracle).
2649 // Derivation hoist: A/B/C AND the rid/median result are
2650 // MB-level — derive once, run only the per-8x8 region half
2651 // (czg differs per sub) for every direct sub.
2652 let hoisted = if self.direct_spatial
2653 && direct_memo_on()
2654 && subt.iter().any(|&t| t == 0)
2655 {
2656 let (n0, n1) = self.b_direct_nbrs(mbx, mby);
2657 Some(Self::b_direct_refs_mvs(&n0, &n1))
2658 } else {
2659 None
2660 };
2661 for (p, &st) in subt.iter().enumerate() {
2662 let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
2663 if st == 0 {
2664 match hoisted {
2665 Some(derived) => self.b_direct_region(
2666 mbx, mby, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred, derived,
2667 ),
2668 None => self.decode_b_direct(
2669 mbx, mby, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred,
2670 ),
2671 }
2672 continue;
2673 }
2674 // From the table built at the top of this arm.
2675 let (u0, u1) = (su[p & 3][0], su[p & 3][1]);
2676 for &(sx, sy, sw, sh) in b_sub_parts(st) {
2677 let (px, py) = (b8x + sx, b8y + sy);
2678 let mut mv = [(0i32, 0i32); 2];
2679 let (bx4, by4) = ((mbx * 4 + px / 4) as isize, (mby * 4 + py / 4) as isize);
2680 let didx = (py / 4) * 4 + px / 4;
2681 // A BI sub-part gathers both lists at ONE position, so the
2682 // availability work (bounds + coded + slice) is shared —
2683 // the fusion the 16x16 layout path already had.
2684 if u0 && u1 {
2685 let (n0, n1) = self.mv_neighbors_both(bx4, by4, (sw / 4) as isize);
2686 for (list, nb) in [(0usize, &n0), (1, &n1)] {
2687 let d = (if list == 0 { &mmvd0 } else { &mmvd1 })[didx & 15];
2688 let pmv = predict_mv(nb[0], nb[1], nb[2], sref[p & 3][list & 1] as i32);
2689 mv[list & 1] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
2690 }
2691 } else {
2692 for list in 0..2usize {
2693 if if list == 0 { u0 } else { u1 } {
2694 let d = (if list == 0 { &mmvd0 } else { &mmvd1 })[didx & 15];
2695 let nb = self.mv_neighbors_list(bx4, by4, (sw / 4) as isize, list);
2696 let pmv = predict_mv(nb[0], nb[1], nb[2], sref[p & 3][list & 1] as i32);
2697 mv[list & 1] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
2698 }
2699 }
2700 }
2701 let refi0 = if u0 { sref[p & 3][0] as i32 } else { -1 };
2702 let refi1 = if u1 { sref[p & 3][1] as i32 } else { -1 };
2703 self.b_set_motion(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1]);
2704 if rec_mode {
2705 self.b_mc_or_record(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
2706 } else {
2707 self.b_mc(mbx, mby, px, py, sw, sh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
2708 }
2709 }
2710 }
2711 } else {
2712 let (layout, mvmode, preds) = b_inter_layout(bmt);
2713 let parts: &[(usize, &[usize])] = match mvmode {
2714 0 => &[(0, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])],
2715 1 => &[(0, &[0, 1, 2, 3, 4, 5, 6, 7]), (8, &[8, 9, 10, 11, 12, 13, 14, 15])],
2716 _ => &[(0, &[0, 1, 2, 3, 8, 9, 10, 11]), (4, &[4, 5, 6, 7, 12, 13, 14, 15])],
2717 };
2718 // ref_idx_l0 for EVERY partition, then ref_idx_l1, then the mvds
2719 // (spec 7.3.5.1 macroblock_prediction). This was missing entirely
2720 // -- the B path assumed a single reference -- so any B slice with
2721 // more than one active reference in either list desynced the
2722 // arithmetic decoder at the first partition that codes a ref_idx,
2723 // and the slice ended early at a phantom end_of_slice_flag.
2724 let mut pref = [[0i8; 2]; 2]; // [partition][list]
2725 for list in 0..2usize {
2726 let active = if list == 0 { self.num_ref_active } else { self.num_ref_active1 };
2727 if active <= 1 {
2728 continue;
2729 }
2730 let rc = if list == 0 { &mut refc0 } else { &mut refc1 };
2731 for (p, &(pidx, zb)) in parts.iter().enumerate() {
2732 if !preds[p & 1].uses(list) {
2733 continue;
2734 }
2735 let s = CACHE30[pidx & 15].clamp(6, 29);
2736 let c0 = (rc[s - 1] > 0) as usize + 2 * (rc[s - 6] > 0) as usize;
2737 let r = parse_ref_idx_cabac(&mut cab, c0);
2738 // Seed the cache so a later partition's ref/mvd ctxInc sees it.
2739 for &zbi in zb.iter() {
2740 rc[CACHE30[zbi & 15]] = r;
2741 }
2742 pref[p & 1][list & 1] = r;
2743 }
2744 }
2745 // mvd parse order: list-major, partition-minor (openh264
2746 // ParseInterBMotionInfoCabac); the ctxInc reads the same-list cache.
2747 for list in 0..2usize {
2748 let (mmv, mrf, mc, rc) = if list == 0 {
2749 (&mut mmvd0, &mut mref0, &mut mvdc0, &mut refc0)
2750 } else {
2751 (&mut mmvd1, &mut mref1, &mut mvdc1, &mut refc1)
2752 };
2753 for (p, &(pidx, zb)) in parts.iter().enumerate() {
2754 if preds[p & 1].uses(list) {
2755 parse_mvd_partition(&mut cab, pidx, zb, mc, rc, mmv, mrf, pref[p & 1][list & 1]);
2756 }
2757 }
2758 }
2759 // Per-partition recon: predict each list's MV, commit, MC.
2760 let (mbb_x, mbb_y) = (mbx * 4, mby * 4);
2761 for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
2762 let mut mv = [(0i32, 0i32); 2];
2763 // uses(list) is a property of the layout — resolve both
2764 // once instead of five times per partition, and build
2765 // the neighbour coordinates once instead of per list.
2766 let (u0, u1) = (preds[p & 1].uses(0), preds[p & 1].uses(1));
2767 let (nx, ny) = ((mbb_x + rx / 4) as isize, (mbb_y + ry / 4) as isize);
2768 let (nw, didx) = ((rw / 4) as isize, (ry / 4) * 4 + rx / 4);
2769 // Bi partitions gather both lists at ONE position —
2770 // fuse the availability work (same trick as
2771 // b_direct_nbrs); uni partitions keep the single
2772 // gather.
2773 if u0 && u1 {
2774 let (n0, n1) = self.mv_neighbors_both(nx, ny, nw);
2775 for (list, n) in [(0usize, &n0), (1, &n1)] {
2776 let d = (if list == 0 { &mmvd0 } else { &mmvd1 })[didx & 15];
2777 let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], pref[p & 1][list & 1] as i32);
2778 mv[list & 1] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
2779 }
2780 } else {
2781 for list in 0..2usize {
2782 if if list == 0 { u0 } else { u1 } {
2783 let d = (if list == 0 { &mmvd0 } else { &mmvd1 })[didx & 15];
2784 let n = self.mv_neighbors_list(nx, ny, nw, list);
2785 let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], pref[p & 1][list & 1] as i32);
2786 mv[list & 1] = (pmv.0 + d[0] as i32, pmv.1 + d[1] as i32);
2787 }
2788 }
2789 }
2790 let refi0 = if u0 { pref[p & 1][0] as i32 } else { -1 };
2791 let refi1 = if u1 { pref[p & 1][1] as i32 } else { -1 };
2792 self.b_set_motion(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1]);
2793 // Proper spec bi-prediction (average of L0+L1). NOTE: the CAVLC
2794 // decode_b_mb replicates an openh264 bug here for a Bi 16×8/8×16
2795 // partition; our pixel gate is ffmpeg (spec-correct), so we do NOT.
2796 if rec_mode {
2797 self.b_mc_or_record(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
2798 } else {
2799 self.b_mc(mbx, mby, rx, ry, rw, rh, refi0, mv[0], refi1, mv[1], &mut pred_y, &mut c_pred);
2800 }
2801 }
2802 }
2803 // Paired stores: one index expression per grid pair.
2804 // Four per-macroblock grids at one address; each is its own
2805 // Vec, so each assignment carried its own check.
2806 if let (Some(r0), Some(d0)) = (mb_ref.get_mut(addr), mb_mvd.get_mut(addr)) {
2807 (*r0, *d0) = (mref0, mmvd0);
2808 }
2809 if let (Some(r1), Some(d1)) = (mb_ref1.get_mut(addr), mb_mvd1.get_mut(addr)) {
2810 (*r1, *d1) = (mref1, mmvd1);
2811 }
2812 _smv = None; // close b:mvd-parse; the residual half follows
2813 let _sres = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BResid);
2814
2815 // Inter cbp + residual (identical to the P path).
2816 let cbp = parse_cbp_cabac(&mut cab, top.and_then(|a| mb_cbp.get(a).copied()), left.and_then(|a| mb_cbp.get(a).copied()));
2817 if let Some(p) = mb_cbp.get_mut(addr) {
2818 *p = cbp as u8;
2819 }
2820 // H-49: an INTER macroblock carries transform_size_8x8_flag AFTER cbp
2821 // (spec 7.3.5), present only when CodedBlockPatternLuma > 0 and
2822 // noSubMbPartSizeLessThan8x8Flag. Same context as the intra read.
2823 let t8 = self.transform_8x8_mode && (cbp & 15) != 0 && allow8 && {
2824 let a = left.map_or(0, |x| self.mb_t8x8.get(x).is_some_and(|&f| f) as usize);
2825 let b = top.map_or(0, |x| self.mb_t8x8.get(x).is_some_and(|&f| f) as usize);
2826 cab.decode_decision(399 + a + b) != 0
2827 };
2828 if let Some(p) = self.mb_t8x8.get_mut(addr) {
2829 *p = t8;
2830 }
2831 self.any_t8 |= t8;
2832 // D9c-B: coded B with cbp==0 never parses residuals — skip the
2833 // ~2.5 KB coeff zero-init + fat BJob when NORES is on (default).
2834 // MC already filled pred_y / edc_regions; recon == pred (same as
2835 // B_Skip / P InterNoRes). t8 is always false here ((cbp&15)==0).
2836 if cbp == 0 && nores_on() {
2837 last_delta_qp = 0;
2838 if let Some(p) = self.mb_qp.get_mut(addr) {
2839 *p = self.cur_qp;
2840 }
2841 if let Some(p) = cbf_dc.get_mut(addr) {
2842 *p = 0;
2843 }
2844 if let Some(p) = mb_nzc.get_mut(addr) {
2845 *p = [0u8; 24];
2846 }
2847 if let Some(regions) = self.edc_regions.take() {
2848 self.edc_giveback();
2849 self.edc_commit_nnz(mbx, mby, false, &[0u8; 24], 0);
2850 edcstat::bump(&edcstat::J_NORES_SENT, 1);
2851 self.edc_send_job(EdcJob::BSkip { mbx, mby, regions });
2852 } else {
2853 // Inline twin of decode_b_skip's plane copy (MC already done).
2854 for dy in 0..16 {
2855 let d = (mby * 16 + dy) * self.cw + mbx * 16;
2856 self.rec_y[d..d + 16]
2857 .copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
2858 }
2859 for c in 0..2 {
2860 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
2861 for dy in 0..8 {
2862 let d = (mby * 8 + dy) * self.ccw + mbx * 8;
2863 plane[d..d + 8]
2864 .copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
2865 }
2866 }
2867 let w4 = self.mb_w * 4;
2868 for dy in 0..4 {
2869 self.nnz_y[(mby * 4 + dy) * w4 + mbx * 4..][..4].fill(0);
2870 }
2871 }
2872 let eos = cab.decode_terminate();
2873 addr += 1;
2874 mbx += 1;
2875 if eos || addr >= total {
2876 break;
2877 }
2878 continue;
2879 }
2880 // Zeroed ONLY when the 8x8 transform is in play — this was a
2881 // 1KB memset per coded inter MB on non-t8 tiers.
2882 let mut luma8: Option<[[i32; 64]; 4]> = None;
2883 let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
2884 let mut nzc = [0xffu8; 48];
2885 if let Some(t) = top {
2886 let tnz = mb_nzc.get(t).unwrap_or(&ZERO_NZC);
2887 nzc[1..5].copy_from_slice(&tnz[12..16]);
2888 (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
2889 (nzc[6], nzc[7], nzc[30], nzc[31]) = (tnz[20], tnz[21], tnz[22], tnz[23]);
2890 }
2891 if let Some(l) = left {
2892 let lnz = mb_nzc.get(l).unwrap_or(&ZERO_NZC);
2893 (nzc[8], nzc[16], nzc[24], nzc[32]) = (lnz[3], lnz[7], lnz[11], lnz[15]);
2894 (nzc[13], nzc[21], nzc[37], nzc[45]) = (lnz[17], lnz[21], lnz[19], lnz[23]);
2895 }
2896 let mut cbfdc = 0u16;
2897 let mut nnzs = [0u8; 24]; // parsed totalCoeff per block
2898 // See the P arm: materialised only when actually written.
2899 let mut luma_scan: Option<[[i32; 16]; 16]> = None;
2900 let mut cdc = [[0i32; 4]; 2];
2901 let mut cac: Option<[[[i32; 16]; 4]; 2]> = None;
2902 if cbp == 0 {
2903 last_delta_qp = 0;
2904 }
2905 if cbp != 0 {
2906 let ndc = (top.and_then(|a| cbf_dc.get(a).copied()), left.and_then(|a| cbf_dc.get(a).copied()));
2907 let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
2908 self.step_qp(qpd)?;
2909 for id8 in 0..4usize {
2910 if cbp_luma & (1 << id8) != 0 {
2911 if t8 {
2912 // All four slots carry the 8x8 total: cat 5 has no per-4x4
2913 // counts, and the recon helper now reads one slot
2914 // per 4x4 cell.
2915 let n8 = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, false, ndc, &mut luma8.get_or_insert_with(|| [[0i32; 64]; 4])[id8]) as u8;
2916 for k in 0..4 {
2917 nnzs[id8 * 4 + k] = n8;
2918 }
2919 } else {
2920 let ls = luma_scan.get_or_insert_with(|| [[0i32; 16]; 16]);
2921 for id4 in 0..4usize {
2922 let iz = id8 * 4 + id4;
2923 nnzs[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, false, ndc, &mut ls[iz]) as u8;
2924 }
2925 }
2926 } else {
2927 for k in 0..4 {
2928 nzc[NZC_CACHE[(id8 * 4 + k).min(23)].min(47)] = 0;
2929 }
2930 }
2931 }
2932 if cbp_chroma >= 1 {
2933 for i in 0..2usize {
2934 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, false, ndc, &mut cdc[i]);
2935 }
2936 }
2937 if cbp_chroma == 2 {
2938 let cacm = cac.get_or_insert_with(|| [[[0i32; 16]; 4]; 2]);
2939 for i in 0..2usize {
2940 for id4 in 0..4usize {
2941 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 cacm[i][id4]) as u8;
2942 }
2943 }
2944 }
2945 }
2946 if let Some(p) = self.mb_qp.get_mut(addr) {
2947 *p = self.cur_qp;
2948 }
2949 if let Some(p) = cbf_dc.get_mut(addr) {
2950 *p = cbfdc;
2951 }
2952 let _sc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecStateCache);
2953 let mut mn = [0u8; 24];
2954 for k in 0..4 {
2955 mn[k] = nzc[9 + k];
2956 mn[4 + k] = nzc[17 + k];
2957 mn[8 + k] = nzc[25 + k];
2958 mn[12 + k] = nzc[33 + k];
2959 }
2960 (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
2961 (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
2962 // A block whose residual was skipped (cbp bit clear / no chroma AC)
2963 // has 0 coeffs, not "unavailable" — export 0 so an intra neighbour's
2964 // CBF ctxInc reads 0 (not the 0xff sentinel → is_intra default).
2965 for v in mn.iter_mut() {
2966 if *v == 0xff {
2967 *v = 0;
2968 }
2969 }
2970 if let Some(p) = mb_nzc.get_mut(addr) {
2971 *p = mn;
2972 }
2973 drop(_sc);
2974 if let Some(regions) = self.edc_regions.take() {
2975 self.edc_giveback();
2976 self.edc_commit_nnz(mbx, mby, t8, &nnzs, cbp_chroma);
2977 let job = BJob {
2978 mbx,
2979 mby,
2980 qp: self.cur_qp,
2981 cbp_chroma,
2982 skip: false,
2983 regions,
2984 luma_scan,
2985 luma8,
2986 cdc,
2987 cac,
2988 nnzs,
2989 };
2990 self.edc_send_job(EdcJob::B(Box::new(job)));
2991 } else {
2992 self.add_inter_residual(mbx, mby, &pred_y, &c_pred, luma_scan.as_ref(), luma8.as_ref(), &cdc, cac.as_ref(), cbp_chroma, &nnzs);
2993 }
2994
2995 let eos = cab.decode_terminate();
2996 addr += 1;
2997 mbx += 1;
2998 if eos || addr >= total {
2999 break;
3000 }
3001 continue;
3002 }
3003 mb_type = bmt - 23; // 23→0 (I_4x4), 24..=47→1..24 (I_16x16), 48→25 (PCM)
3004 } else {
3005 let li = left.map_or(0, |a| cat.get(a).is_some_and(|&c| c >= 2) as usize);
3006 let ti = top.map_or(0, |a| cat.get(a).is_some_and(|&c| c >= 2) as usize);
3007 mb_type = parse_mb_type_i_cabac(&mut cab, li + ti);
3008 }
3009 // H-48: the CABAC intra path is INLINED in this loop, not routed through
3010 // `decode_intra_mb` (which only the CAVLC readers call) — wiring the scope
3011 // there reported ZERO calls against 480,510 intra-pred calls. All three
3012 // intra entries (I-slice, P-slice mb_type>3, B-slice bmt>=23) converge
3013 // here, so this is the one point that sees every intra MB.
3014 self.edc_intra_sync(); // intra reconstruction reads neighbour PIXELS
3015 // Intra prediction ALSO reads the coded_y/modes_y/inter_y grids a
3016 // deferred span still owes (this arm is inline — it never passes
3017 // decode_b_mb's flush). Caught by the tempete ARM-DIFF.
3018 self.span_flush();
3019 let _gi = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbI);
3020 // Intra bS is a constant pattern (4 on MB edges, 3 internal) — written
3021 // here because CABAC inlines I recon and never calls decode_intra_mb.
3022 if let Some(p) = self.mb_kind.get_mut(mby * self.mb_w + mbx) {
3023 *p = rusty_h264_common::deblock::MB_KIND_INTRA;
3024 }
3025 if mb_type == 25 {
3026 // ---- I_PCM (spec §7.3.5): 384 raw byte-aligned sample bytes inside
3027 // the CABAC stream. The PCM marker was a terminate bin, so the engine
3028 // has stopped; DecodeFlush + pcm_alignment_zero_bit put the samples
3029 // at `pcm_start_byte()`, and the engine re-initialises after them
3030 // with its CONTEXTS KEPT (§9.3.1). All three slice types (I, P via
3031 // mbt 30, B via bmt 48) reach here as mb_type 25.
3032 let pcm = cab.pcm_start_byte();
3033 let end = pcm + 384;
3034 if end > rbsp.len() {
3035 return Err(MbError::Truncated);
3036 }
3037 let mut pr = BitReader::new(&rbsp[pcm..end]);
3038 self.decode_ipcm(&mut pr, mbx, mby)?;
3039 cab.reinit_at(end);
3040 // Neighbour context (§7.4.5 + §9.3.3.1.1.x inferences): intra, QPy
3041 // unchanged (no mb_qp_delta — its ctxInc resets), CodedBlockPattern
3042 // luma/chroma inferred 15/2, every coded_block_flag (incl. DC) 1,
3043 // nnz 16, chroma pred mode 0.
3044 if let Some(p) = self.mb_qp.get_mut(addr) {
3045 *p = self.cur_qp;
3046 }
3047 if let Some(p) = cat.get_mut(addr) {
3048 *p = 25;
3049 }
3050 if let Some(p) = cmode.get_mut(addr) {
3051 *p = 0;
3052 }
3053 if let Some(p) = mb_cbp.get_mut(addr) {
3054 *p = 0x2f;
3055 }
3056 if let Some(p) = cbf_dc.get_mut(addr) {
3057 *p = (1 << RP_I16_DC) | (1 << RP_CHROMA_DC) | (1 << (RP_CHROMA_DC + 1));
3058 }
3059 if let Some(p) = mb_nzc.get_mut(addr) {
3060 *p = [16u8; 24];
3061 }
3062 last_delta_qp = 0;
3063 let eos = cab.decode_terminate();
3064 addr += 1;
3065 mbx += 1;
3066 if eos || addr >= total {
3067 break;
3068 }
3069 continue;
3070 }
3071 // chroma-pred-mode ctxInc from neighbour chroma modes (1..=3).
3072 let cci = left.map_or(0, |a| cmode.get(a).is_some_and(|c| (1..=3).contains(c)) as usize)
3073 + top.map_or(0, |a| cmode.get(a).is_some_and(|c| (1..=3).contains(c)) as usize);
3074
3075 if mb_type != 0 {
3076 // ---- I_16x16 (mb_type 1..=24): pred mode & cbp DERIVED from mb_type;
3077 // luma DC always coded. Syntax order: intra_chroma_pred_mode, mb_qp_delta,
3078 // luma DC (Hadamard), luma AC (if cbp_luma), chroma DC/AC. Mirrors the CAVLC
3079 // decode_i16, driven by the CABAC residual. ----
3080 let mt = mb_type - 1;
3081 let pred_mode = I16Mode::from_id(mt % 4);
3082 let cbp_chroma = (mt % 12) / 4;
3083 let cbp_luma_15 = mt / 12 == 1;
3084 let chroma_mode = parse_intra_chroma_pred_mode_cabac(&mut cab, cci) as u8;
3085 if let Some(p) = cmode.get_mut(addr) {
3086 *p = chroma_mode as i32;
3087 }
3088 if let Some(p) = cat.get_mut(addr) {
3089 *p = 2;
3090 }
3091 if let Some(p) = mb_cbp.get_mut(addr) {
3092 *p = ((cbp_chroma as u8) << 4) | if cbp_luma_15 { 15 } else { 0 };
3093 }
3094 let w4 = self.mb_w * 4;
3095
3096 let mut nzc = [0xffu8; 48];
3097 if let Some(t) = top {
3098 let tn = mb_nzc.get(t).unwrap_or(&ZERO_NZC);
3099 nzc[1..5].copy_from_slice(&tn[12..16]);
3100 (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
3101 (nzc[6], nzc[7]) = (tn[20], tn[21]);
3102 (nzc[30], nzc[31]) = (tn[22], tn[23]);
3103 }
3104 if let Some(l) = left {
3105 let ln = mb_nzc.get(l).unwrap_or(&ZERO_NZC);
3106 (nzc[8], nzc[16], nzc[24], nzc[32]) = (ln[3], ln[7], ln[11], ln[15]);
3107 (nzc[13], nzc[21], nzc[37], nzc[45]) = (ln[17], ln[21], ln[19], ln[23]);
3108 }
3109
3110 let ndc = (top.and_then(|a| cbf_dc.get(a).copied()), left.and_then(|a| cbf_dc.get(a).copied()));
3111 let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
3112 self.step_qp(qpd)?;
3113 let qp = self.cur_qp;
3114 let mut cbfdc = 0u16;
3115
3116 // Luma DC (iz=0, category I16_LUMA_DC, 16 coeffs) → Hadamard dequant.
3117 let mut dc_scan = [0i32; 16];
3118 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 0, RP_I16_DC, true, ndc, &mut dc_scan);
3119 let recon_dc = self.dequant_luma_dc(&un_scan_4x4_dcac(&dc_scan), qp, 0);
3120
3121 // Luma AC (iz 0..15, category I16_LUMA_AC, 15 coeffs) when cbp_luma set.
3122 // Materialised only when AC is actually coded — a DC-only
3123 // I_16x16 zeroed 1 KB of stack for nothing.
3124 let mut q_blocks: Option<[[i32; 16]; 16]> = None;
3125 for (iz, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
3126 let total = if cbp_luma_15 {
3127 let mut ac = [0i32; 16];
3128 let t = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_I16_AC, true, ndc, &mut ac);
3129 un_scan_4x4_ac_into(&ac, &mut q_blocks.get_or_insert_with(|| [[0i32; 16]; 16])[(lby & 3) * 4 + (lbx & 3)]);
3130 t as u8
3131 } else {
3132 nzc[NZC_CACHE[iz.min(23)].min(47)] = 0;
3133 0
3134 };
3135 if let Some(p) = self.nnz_y.get_mut((mby * 4 + lby) * w4 + (mbx * 4 + lbx)) {
3136 *p = total;
3137 }
3138 }
3139
3140 let mut cdc = [[0i32; 4]; 2];
3141 let mut cac: Option<[[[i32; 16]; 4]; 2]> = None;
3142 if cbp_chroma >= 1 {
3143 for i in 0..2usize {
3144 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, true, ndc, &mut cdc[i]);
3145 }
3146 }
3147 if cbp_chroma == 2 {
3148 let cacm = cac.get_or_insert_with(|| [[[0i32; 16]; 4]; 2]);
3149 for i in 0..2usize {
3150 for id4 in 0..4usize {
3151 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, true, ndc, &mut cacm[i][id4]);
3152 }
3153 }
3154 }
3155
3156 // Luma recon: 16×16 intra prediction, then per-4×4 (dequant AC + injected DC).
3157 let top_ok = mby > 0 && self.nbr_in_slice(mbx, mby - 1) && self.intra_nbr_ok(mbx * 4, mby * 4 - 1);
3158 let left_ok = mbx > 0 && self.nbr_in_slice(mbx - 1, mby) && self.intra_nbr_ok(mbx * 4 - 1, mby * 4);
3159 self.recon_i16_luma(mbx, mby, pred_mode, top_ok, left_ok, q_blocks.as_ref(), &recon_dc, qp);
3160 self.recon_chroma_cabac(mbx, mby, chroma_mode, &cdc, cac.as_ref(), cbp_chroma, top_ok, left_ok);
3161
3162 if let Some(p) = self.mb_qp.get_mut(addr) {
3163 *p = self.cur_qp;
3164 }
3165 if let Some(p) = cbf_dc.get_mut(addr) {
3166 *p = cbfdc;
3167 }
3168 let mut mn = [0u8; 24];
3169 for k in 0..4 {
3170 mn[k] = nzc[9 + k];
3171 mn[4 + k] = nzc[17 + k];
3172 mn[8 + k] = nzc[25 + k];
3173 mn[12 + k] = nzc[33 + k];
3174 }
3175 (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
3176 (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
3177 for v in mn.iter_mut() {
3178 if *v == 0xff {
3179 *v = 0;
3180 }
3181 }
3182 if let Some(p) = mb_nzc.get_mut(addr) {
3183 *p = mn;
3184 }
3185
3186 let eos = cab.decode_terminate();
3187 addr += 1;
3188 mbx += 1;
3189 if eos || addr >= total {
3190 break;
3191 }
3192 continue;
3193 }
3194 if let Some(p) = cat.get_mut(addr) {
3195 *p = 0;
3196 }
3197 let w4 = self.mb_w * 4;
3198 // H-49: transform_size_8x8_flag. For I_NxN it precedes the intra pred
3199 // modes (spec §7.3.5); ctxIdx = 399 + condTermFlagA + condTermFlagB,
3200 // each 1 when that neighbour MB carries the flag. Omitting this read is
3201 // what desynced every High-profile stream.
3202 let t8 = self.transform_8x8_mode && {
3203 let a = left.map_or(0, |x| self.mb_t8x8.get(x).is_some_and(|&f| f) as usize);
3204 let b = top.map_or(0, |x| self.mb_t8x8.get(x).is_some_and(|&f| f) as usize);
3205 cab.decode_decision(399 + a + b) != 0
3206 };
3207 if let Some(p) = self.mb_t8x8.get_mut(addr) {
3208 *p = t8;
3209 }
3210 self.any_t8 |= t8;
3211 // Hoisted ABOVE the mode loop (it was computed after it) so the
3212 // per-block mode prediction can use it - see `predict_i4_mode_fast`.
3213 let top_ok = mby > 0
3214 && self.nbr_in_slice(mbx, mby - 1)
3215 && self.intra_nbr_ok(mbx * 4, mby * 4 - 1);
3216 let left_ok = mbx > 0
3217 && self.nbr_in_slice(mbx - 1, mby)
3218 && self.intra_nbr_ok(mbx * 4 - 1, mby * 4);
3219 // Brick 2.4 + recon: derive & store each intra mode (prev-flag → the
3220 // neighbour-predicted mode, else rem), exactly as the CAVLC path.
3221 let mut modes = [2u8; 16]; // raster [lby*4+lbx]
3222 let mut modes8 = [2u8; 4]; // one per 8×8 when t8
3223 if t8 {
3224 // One mode per 8×8, broadcast to its four 4×4 cells so neighbour
3225 // mode prediction keeps working unchanged.
3226 for b8 in 0..4usize {
3227 let (b8x, b8y) = (b8 % 2, b8 / 2);
3228 let (bx, by) = (mbx * 4 + b8x * 2, mby * 4 + b8y * 2);
3229 let predicted =
3230 self.predict_i4_mode_fast(bx, by, b8x * 2, b8y * 2, top_ok, left_ok);
3231 let rr = parse_intra4x4_pred_mode_cabac(&mut cab);
3232 let actual = if rr < 0 {
3233 predicted
3234 } else {
3235 let rem = rr as u8;
3236 if rem < predicted { rem } else { rem + 1 }
3237 };
3238 modes8[b8] = actual;
3239 for dy in 0..2 {
3240 for dx in 0..2 {
3241 if let Some(p) = self.modes_y.get_mut((by + dy) * w4 + (bx + dx)) {
3242 *p = actual;
3243 }
3244 modes[(b8y * 2 + dy) * 4 + (b8x * 2 + dx)] = actual;
3245 }
3246 }
3247 }
3248 } else {
3249 for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
3250 let (bx, by) = (mbx * 4 + lbx, mby * 4 + lby);
3251 let predicted = self.predict_i4_mode_fast(bx, by, lbx, lby, top_ok, left_ok);
3252 let rr = parse_intra4x4_pred_mode_cabac(&mut cab);
3253 let actual = if rr < 0 {
3254 predicted
3255 } else {
3256 let rem = rr as u8;
3257 if rem < predicted { rem } else { rem + 1 }
3258 };
3259 if let Some(m) = self.modes_y.get_mut(by * w4 + bx) {
3260 *m = actual;
3261 }
3262 modes[(lby & 3) * 4 + (lbx & 3)] = actual;
3263 }
3264 }
3265 let chroma_mode = parse_intra_chroma_pred_mode_cabac(&mut cab, cci) as u8;
3266 if let Some(p) = cmode.get_mut(addr) {
3267 *p = chroma_mode as i32;
3268 }
3269 let cbp = parse_cbp_cabac(&mut cab, top.and_then(|a| mb_cbp.get(a).copied()), left.and_then(|a| mb_cbp.get(a).copied()));
3270 if let Some(p) = mb_cbp.get_mut(addr) {
3271 *p = cbp as u8;
3272 }
3273 let (cbp_luma, cbp_chroma) = (cbp & 15, cbp >> 4);
3274
3275 // Build the padded nzc cache from neighbours (openh264 WelsFillCacheNonZeroCount).
3276 let mut nzc = [0xffu8; 48];
3277 if let Some(t) = top {
3278 let tn = mb_nzc.get(t).unwrap_or(&ZERO_NZC);
3279 nzc[1..5].copy_from_slice(&tn[12..16]);
3280 (nzc[0], nzc[5], nzc[29]) = (0, 0, 0);
3281 (nzc[6], nzc[7]) = (tn[20], tn[21]);
3282 (nzc[30], nzc[31]) = (tn[22], tn[23]);
3283 }
3284 if let Some(l) = left {
3285 let ln = mb_nzc.get(l).unwrap_or(&ZERO_NZC);
3286 (nzc[8], nzc[16], nzc[24], nzc[32]) = (ln[3], ln[7], ln[11], ln[15]);
3287 (nzc[13], nzc[21], nzc[37], nzc[45]) = (ln[17], ln[21], ln[19], ln[23]);
3288 }
3289
3290 // Bricks 2.6 + 2.7: mb_qp_delta + residual (I_4x4 luma 4×4 + chroma DC/AC),
3291 // storing scan-order coefficients for recon.
3292 let mut cbfdc = 0u16;
3293 // Both materialised only when the parse writes them: an I_8x8
3294 // macroblock never touches luma_scan (it carries luma8), and
3295 // cbp_chroma < 2 never touches cac.
3296 let mut luma_scan: Option<[[i32; 16]; 16]> = None; // per z-order 4×4 block
3297 let mut luma8: Option<[[i32; 64]; 4]> = None; // allocated only under t8
3298 let mut cdc = [[0i32; 4]; 2]; // chroma DC per plane
3299 let mut cac: Option<[[[i32; 16]; 4]; 2]> = None; // chroma AC per plane, per 4×4 block
3300 let mut i4n = [0u8; 16]; // parse-side per-block coeff counts (I_4x4)
3301 if cbp == 0 {
3302 last_delta_qp = 0;
3303 }
3304 if cbp != 0 {
3305 let ndc = (top.and_then(|a| cbf_dc.get(a).copied()), left.and_then(|a| cbf_dc.get(a).copied()));
3306 let qpd = parse_mb_qp_delta_cabac(&mut cab, &mut last_delta_qp);
3307 self.step_qp(qpd)?;
3308 for id8 in 0..4usize {
3309 if cbp_luma & (1 << id8) != 0 {
3310 if t8 {
3311 // ctxBlockCat 5: ONE 64-coefficient block per 8×8, and no
3312 // coded_block_flag — presence comes from cbp_luma alone.
3313 let n = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, id8 * 4, RP_LUMA_8X8, true, ndc, &mut luma8.get_or_insert_with(|| [[0i32; 64]; 4])[id8]);
3314 let (b8x, b8y) = (id8 % 2, id8 / 2);
3315 for sy in 0..2 {
3316 for sx in 0..2 {
3317 if let Some(p) = self.nnz_y.get_mut((mby * 4 + b8y * 2 + sy) * w4 + (mbx * 4 + b8x * 2 + sx)) {
3318 *p = n as u8;
3319 }
3320 }
3321 }
3322 } else {
3323 for id4 in 0..4usize {
3324 let iz = id8 * 4 + id4;
3325 // Capture the parse's own count — the recon loop
3326 // used to re-scan all 16 coefficients per block.
3327 i4n[iz] = parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, iz, RP_LUMA_4X4, true, ndc, &mut luma_scan.get_or_insert_with(|| [[0i32; 16]; 16])[iz]) as u8;
3328 }
3329 }
3330 } else {
3331 for k in 0..4 {
3332 nzc[NZC_CACHE[(id8 * 4 + k).min(23)].min(47)] = 0;
3333 }
3334 if t8 {
3335 let (b8x, b8y) = (id8 % 2, id8 / 2);
3336 for sy in 0..2 {
3337 for sx in 0..2 {
3338 if let Some(p) = self.nnz_y.get_mut((mby * 4 + b8y * 2 + sy) * w4 + (mbx * 4 + b8x * 2 + sx)) {
3339 *p = 0;
3340 }
3341 }
3342 }
3343 }
3344 }
3345 }
3346 if cbp_chroma >= 1 {
3347 for i in 0..2usize {
3348 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4, RP_CHROMA_DC + i, true, ndc, &mut cdc[i]);
3349 }
3350 }
3351 if cbp_chroma == 2 {
3352 for i in 0..2usize {
3353 for id4 in 0..4usize {
3354 parse_residual_cabac(&mut cab, &mut nzc, &mut cbfdc, 16 + i * 4 + id4, RP_CHROMA_AC + i, true, ndc, &mut cac.get_or_insert_with(|| [[[0i32; 16]; 4]; 2])[i][id4]);
3355 }
3356 }
3357 }
3358 }
3359 if let Some(p) = self.mb_qp.get_mut(addr) {
3360 *p = self.cur_qp;
3361 }
3362 if let Some(p) = cbf_dc.get_mut(addr) {
3363 *p = cbfdc;
3364 }
3365 // Extract the MB's nzc (raster luma + chroma) for future neighbours.
3366 let mut mn = [0u8; 24];
3367 for k in 0..4 {
3368 mn[k] = nzc[9 + k];
3369 mn[4 + k] = nzc[17 + k];
3370 mn[8 + k] = nzc[25 + k];
3371 mn[12 + k] = nzc[33 + k];
3372 }
3373 (mn[16], mn[17], mn[20], mn[21]) = (nzc[14], nzc[15], nzc[22], nzc[23]);
3374 (mn[18], mn[19], mn[22], mn[23]) = (nzc[38], nzc[39], nzc[46], nzc[47]);
3375 for v in mn.iter_mut() {
3376 if *v == 0xff {
3377 *v = 0;
3378 }
3379 }
3380 if let Some(p) = mb_nzc.get_mut(addr) {
3381 *p = mn;
3382 }
3383
3384 // ---- Brick 4.3a: recon (I_4x4 luma + chroma) via the CAVLC-proven primitives.
3385 let qp = self.cur_qp;
3386
3387 if t8 {
3388 // I_8x8 recon, reusing the CAVLC-proven primitives verbatim
3389 // (un_scan_8x8 / inv_quant8 / gather_i8 / intra8x8_pred /
3390 // add_residual_8x8). Only the ENTROPY half differed.
3391 for b8 in 0..4usize {
3392 let (b8x, b8y) = (b8 % 2, b8 / 2);
3393 let (bx, by) = (mbx * 4 + b8x * 2, mby * 4 + b8y * 2);
3394 let coded = cbp_luma & (1 << b8) != 0;
3395 let avail_top = b8y > 0 || top_ok;
3396 let avail_left = b8x > 0 || left_ok;
3397 self.recon_i8_block(bx, by, modes8[b8], avail_top, avail_left, luma8.as_ref().and_then(|l| coded.then(|| &l[b8])), qp);
3398 for sy in 0..2 {
3399 // Row fill: the 2x2 cell block is two contiguous PAIRS.
3400 self.coded_y[(by + sy) * w4 + bx..][..2].fill(true);
3401 }
3402 }
3403 }
3404 // The `t8` guard was INSIDE the loop (breaking on the first
3405 // iteration) and the `luma_scan` Option was re-resolved on EVERY one
3406 // of the sixteen blocks. Both are macroblock-invariant.
3407 if !t8 {
3408 let scans = luma_scan.as_ref().unwrap_or(&ZERO_LUMA_SCAN);
3409 for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
3410 let (bx, by) = (mbx * 4 + lbx, mby * 4 + lby);
3411 let at = lby > 0 || top_ok;
3412 let al = lbx > 0 || left_ok;
3413 let nnz = i4n[blk & 15];
3414 if let Some(p) = self.nnz_y.get_mut(by * w4 + bx) {
3415 *p = nnz;
3416 }
3417 self.recon_i4_block(
3418 bx, by, modes[(lby & 3) * 4 + (lbx & 3)], at, al, &scans[blk & 15], nnz, qp,
3419 );
3420 }
3421 }
3422 self.recon_chroma_cabac(mbx, mby, chroma_mode, &cdc, cac.as_ref(), cbp_chroma, top_ok, left_ok);
3423
3424 // Brick 2.1: end_of_slice_flag.
3425 let eos = cab.decode_terminate();
3426 addr += 1;
3427 mbx += 1;
3428 if eos || addr >= total {
3429 break;
3430 }
3431 }
3432 }
3433 if trace {
3434 eprintln!("# CABAC decoded {} MBs (of {total})", addr - first_mb);
3435 }
3436 self.edc_flush(); // slice end: no job crosses a slice boundary
3437 // Return the pooled slice scratch (an error exit forfeits it — the
3438 // next slice then refills fresh allocations, correctness unchanged).
3439 self.sc_cat = cat;
3440 self.sc_cbp = mb_cbp;
3441 self.sc_cmode = cmode;
3442 self.sc_nzc = mb_nzc;
3443 self.sc_cbfdc = cbf_dc;
3444 self.sc_skip = mb_skip;
3445 self.sc_ref = mb_ref;
3446 self.sc_mvd = mb_mvd;
3447 if want_b_grids {
3448 self.sc_ref1 = mb_ref1;
3449 self.sc_mvd1 = mb_mvd1;
3450 self.sc_direct = mb_direct;
3451 }
3452 Ok(addr)
3453 }
3454
3455 /// CABAC chroma recon (mirrors `decode_chroma`'s reconstruction, driven by the
3456 /// CABAC-parsed DC/AC coefficients). `cdc[c]` = 2×2 DC (scan order); `cac[c][blk]`
3457 /// = 15 AC per 4×4 block (scan order).
3458 #[allow(clippy::too_many_arguments)]
3459 /// Add a CABAC-parsed inter residual to an already-built motion-comp prediction
3460 /// (`pred_y`/`c_pred`), writing the reconstruction. Shared by the P and B inter
3461 /// paths — same `reconstruct_4x4` as intra, MC output as the prediction, inter
3462 /// scaling lists (luma 3 / chroma 4+c). `luma_scan[z]`/`cdc`/`cac` are the
3463 /// scan-order coefficients; uncoded blocks are zero so recon == prediction.
3464 #[allow(clippy::too_many_arguments)]
3465 fn add_inter_residual(
3466 &mut self,
3467 mb_x: usize,
3468 mb_y: usize,
3469 pred_y: &[u8; 256],
3470 c_pred: &[[u8; 64]; 2],
3471 luma_scan: Option<&[[i32; 16]; 16]>,
3472 // `Some` when the macroblock carries transform_size_8x8_flag: four 8x8
3473 // blocks in 8x8 scan order, replacing the sixteen 4x4 luma blocks.
3474 luma8: Option<&[[i32; 64]; 4]>,
3475 cdc: &[[i32; 4]; 2],
3476 cac: Option<&[[[i32; 16]; 4]; 2]>,
3477 cbp_chroma: u32,
3478 // Parsed totalCoeff per block, indexed exactly as the parse's `iz`:
3479 // [0..16] luma 4x4 z-order (for t8, the 8x8 count sits at `id8*4`),
3480 // [16..24] chroma AC as `16 + c*4 + id4`. The parser already counted
3481 // every significant coefficient; re-deriving the counts here scanned
3482 // 16-64 array elements per block (~400 loads/MB) for information the
3483 // caller was holding — the diagnosis's stage-boundary re-derivation tax.
3484 nnzs: &[u8; 24],
3485 ) {
3486 // A `None` field means the parse wrote nothing there; every read below
3487 // is guarded by a zero-count test, so the shared zero plane is
3488 // read-equivalent to the per-macroblock zeroed stack array it replaces.
3489 let luma_scan = luma_scan.unwrap_or(&ZERO_LUMA_SCAN);
3490 let cac = cac.unwrap_or(&ZERO_CAC);
3491 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecResidAdd);
3492 let qp = self.cur_qp;
3493 let qpc = self.chroma_qp_for(qp);
3494 let (w4r, w2r) = (self.mb_w * 4, self.mb_w * 2);
3495 if let Some(l8) = luma8 {
3496 // INTER 8x8 luma: same primitives the I_8x8 and CAVLC paths use.
3497 for b8 in 0..4usize {
3498 let (b8x, b8y) = (b8 % 2, b8 / 2);
3499 // PER-CELL, not one aggregate broadcast over all four cells. CAVLC
3500 // codes an 8x8 block as four 4x4 sub-blocks and its nC predictor
3501 // reads these per-4x4 counts from `nnz_y`, so the broadcast
3502 // corrupted the NEXT macroblock's nC and desynced the parse -- which
3503 // is why CAVLC 8x8 streams ffmpeg accepts would not decode here. The
3504 // worker copy of this function never wrote `nnz_y` at all, so the
3505 // threaded path was unaffected and hid the defect. CABAC has no
3506 // per-4x4 counts, so its callers put the 8x8 total in all four slots.
3507 let nnz: u32 = (0..4).map(|k| nnzs[b8 * 4 + k] as u32).sum();
3508 // Two row slices, not four whole-grid indexed stores. The pair
3509 // written per row is contiguous and `nnzs` is a fixed 24-entry
3510 // array, so both sides become provable.
3511 for sy in 0..2 {
3512 let base = (mb_y * 4 + b8y * 2 + sy) * w4r + mb_x * 4 + b8x * 2;
3513 self.nnz_y[base..][..2].copy_from_slice(&nnzs[b8 * 4 + sy * 2..][..2]);
3514 }
3515 // The 4x4 inter path marks coded_y per block; the 8x8 branch must too,
3516 // or a later intra macroblock's neighbour availability is wrong.
3517 for sy in 0..2 {
3518 let base = (mb_y * 4 + b8y * 2 + sy) * w4r + mb_x * 4 + b8x * 2;
3519 self.coded_y[base..][..2].fill(true);
3520 }
3521 let (px, py) = (mb_x * 16 + b8x * 8, mb_y * 16 + b8y * 8);
3522 if nnz == 0 {
3523 // Zero residual: recon == pred — the 4x4 arm zero shortcut,
3524 // ported to the 8x8 branch.
3525 edcstat::bump(&edcstat::T8_ZERO, 1);
3526 for dy in 0..8 {
3527 let d = (py + dy) * self.cw + px;
3528 let po = (b8y * 8 + dy) * 16 + b8x * 8;
3529 self.rec_y[d..d + 8].copy_from_slice(&pred_y[po..po + 8]);
3530 }
3531 } else {
3532 let raster = un_scan_8x8(&l8[b8]);
3533 // list 1 = INTER 8x8 luma scaling list (0 is the intra one).
3534 let res8 = self.inv_quant8(&raster, qp, 1);
3535 let predb: [i32; 64] =
3536 std::array::from_fn(|i| pred_y[(b8y * 8 + i / 8) * 16 + (b8x * 8 + i % 8)] as i32);
3537 let recon = add_residual_8x8(&res8, &predb);
3538 // Eight row copies. This wrote SIXTY-FOUR individually
3539 // bounds-checked samples into the luma plane per coded 8x8
3540 // block — the same shape whose fix carried `recon_i8_block`.
3541 // Present in BOTH the main path and the EDC worker twin.
3542 for dy in 0..8 {
3543 let d = (py + dy) * self.cw + px;
3544 self.rec_y[d..][..8].copy_from_slice(&recon[dy * 8..][..8]);
3545 }
3546 }
3547 }
3548 }
3549 for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
3550 if luma8.is_some() {
3551 break;
3552 }
3553 let nnz = nnzs[blk];
3554 if let Some(p) = self.nnz_y.get_mut((mb_y * 4 + lby) * w4r + (mb_x * 4 + lbx)) {
3555 *p = nnz;
3556 }
3557 let cw = self.cw;
3558 let p_off = (lby * 4) * 16 + lbx * 4;
3559 let r_off = (mb_y * 4 + lby) * 4 * cw + (mb_x * 4 + lbx) * 4;
3560 if nnz == 0 {
3561 // Zero residual → recon == prediction EXACTLY (the integer IDCT is
3562 // linear so zeros map to zeros, and pred is already 0..=255) — copy
3563 // the pred rows straight into the plane. On real (sparse-cbp)
3564 // streams this is MOST of the 4×4 blocks.
3565 for r in 0..4 {
3566 self.rec_y[r_off + r * cw..r_off + r * cw + 4]
3567 .copy_from_slice(&pred_y[p_off + r * 16..p_off + r * 16 + 4]);
3568 }
3569 continue;
3570 }
3571 // DC-ONLY: the sole significant coefficient is scan position 0 (the
3572 // zig-zag starts at DC, and un_scan keeps it at raster 0), so the
3573 // whole dequant + IDCT collapses to one multiply and a flat add.
3574 if nnz == 1 && luma_scan[blk][0] != 0 {
3575 let f = self.dequant_dc4(luma_scan[blk][0], qp, 3);
3576 reconstruct_4x4_dc_into((f + 32) >> 6, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
3577 } else {
3578 // Fused un-scan + dequant over ONLY the significant coefficients,
3579 // then IDCT + add + clip straight into the plane — no `qb`, no
3580 // `deq`-from-dense, no `predb` gather, no `s`, no `store` call.
3581 //
3582 // HYBRID: the scatter walks scan positions with a data-dependent
3583 // branch per slot, which beats the branchless dense 16-multiply
3584 // loop only while the block is SPARSE. The DC/zero fast paths
3585 // already removed the sparsest blocks, so the population here
3586 // skews denser — above ~6 coefficients the dense loop wins.
3587 let deq = if nnz <= 6 {
3588 dequant_scatter_4x4(&luma_scan[blk], nnz, 0, qp, self.scaling.as_ref().map(|sc| &sc[3]))
3589 } else {
3590 self.dequant(&un_scan_4x4_dcac(&luma_scan[blk]), qp, 3)
3591 };
3592 reconstruct_4x4_into(&deq, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
3593 }
3594 }
3595 let mut c_dc = [[0i32; 4]; 2];
3596 if cbp_chroma != 0 {
3597 for c in 0..2 {
3598 c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 4 + c);
3599 }
3600 }
3601 let ccw = self.ccw;
3602 for c in 0..2 {
3603 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
3604 let mut ac_nz = false;
3605 if cbp_chroma == 2 {
3606 let n = nnzs[(16 + c * 4 + by * 2 + bx).min(23)];
3607 if let Some(p) = self.nnz_c[c & 1].get_mut((mb_y * 2 + by) * w2r + (mb_x * 2 + bx)) {
3608 *p = n;
3609 }
3610 ac_nz = n != 0;
3611 }
3612 let dc = c_dc[c & 1][(by * 2 + bx) & 3];
3613 let p_off = (by * 4) * 8 + bx * 4;
3614 let r_off = (mb_y * 2 + by) * 4 * ccw + (mb_x * 2 + bx) * 4;
3615 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3616 if dc == 0 && !ac_nz {
3617 // Zero residual (no AC, zero DC) → recon == prediction exactly.
3618 for r in 0..4 {
3619 plane[r_off + r * ccw..r_off + r * ccw + 4]
3620 .copy_from_slice(&c_pred[c][p_off + r * 8..p_off + r * 8 + 4]);
3621 }
3622 continue;
3623 }
3624 // DC-ONLY (no coded AC — covers every cbp_chroma==1 block and the
3625 // AC-empty blocks of cbp_chroma==2): the chroma DC arrives ALREADY
3626 // dequantized, so the residual is `(dc + 32) >> 6` flat.
3627 if !ac_nz {
3628 reconstruct_4x4_dc_into((dc + 32) >> 6, &c_pred[c], p_off, 8, plane, r_off, ccw);
3629 continue;
3630 }
3631 // AC-only scan: index i is overall scan position i+1 (ac_shift=1).
3632 // Same sparse/dense hybrid as luma.
3633 let n = nnzs[(16 + c * 4 + by * 2 + bx).min(23)];
3634 let mut deq = if n <= 6 {
3635 dequant_scatter_4x4(&cac[c & 1][(by * 2 + bx) & 3], n, 1, qpc, self.scaling.as_ref().map(|sc| &sc[4 + c]))
3636 } else {
3637 let mut ac = [0i32; 16];
3638 un_scan_4x4_ac_into(&cac[c & 1][(by * 2 + bx) & 3], &mut ac);
3639 // Free-fn dequant: `self.dequant` borrows all of `self`, which
3640 // conflicts with the live `plane` (&mut self.rec_u/v) borrow.
3641 match &self.scaling {
3642 Some(sc) => dequantize_weighted(&ac, qpc, &sc[4 + c]),
3643 None => dequantize(&ac, qpc),
3644 }
3645 };
3646 deq[0] = dc;
3647 reconstruct_4x4_into(&deq, &c_pred[c], p_off, 8, plane, r_off, ccw);
3648 }
3649 }
3650 }
3651
3652 // ── SHARED INTRA RECON PRIMITIVES ──────────────────────────────────
3653 // ONE implementation per block family, called by BOTH entropy arms.
3654 // The per-coder recon twins are where routing rot lived: the residual
3655 // ladder and the DC-only collapse each had to be found and ported
3656 // twin-by-twin. The parse halves stay per-coder (that is the real
3657 // difference between the coders); the pixel halves live here — the
3658 // same convergence D14 gave the inter path via add_inter_residual.
3659
3660 /// Intra 4x4 luma block: gather + predict + the residual ladder
3661 /// (zero / DC-only / sparse-scatter / dense), fused into the plane.
3662 /// `scan` = the block's coefficients in SCAN order; `nnz` its count
3663 /// (the caller has already committed nnz to its entropy-side state).
3664 /// Marks the block coded.
3665 #[allow(clippy::too_many_arguments)]
3666 fn recon_i4_block(&mut self, bx: usize, by: usize, mode: u8, at: bool, al: bool, scan: &[i32; 16], nnz: u8, qp: u8) {
3667 let (px, py) = (bx * 4, by * 4);
3668 let (t, l, corner) = self.gather_i4(px, py, at, al, bx, by);
3669 let pred = intra4x4_pred(mode, at, al, &t, &l, corner);
3670 let r_off = py * self.cw + px;
3671 let cw = self.cw;
3672 if nnz == 0 {
3673 edcstat::bump(&edcstat::I4_ZERO, 1);
3674 // ONE span for the 4x4 write window (rows `cw` apart), so the four
3675 // row copies share a single bounds check. This is the DOMINANT arm:
3676 // 63.4% of I_4x4 blocks are all-zero on all-intra content.
3677 let win = &mut self.rec_y[r_off..r_off + 3 * cw + 4];
3678 for r in 0..4 {
3679 win[r * cw..r * cw + 4].copy_from_slice(&pred[r * 4..r * 4 + 4]);
3680 }
3681 } else if nnz == 1 && scan[0] != 0 {
3682 edcstat::bump(&edcstat::I4_DC, 1);
3683 let f = self.dequant_dc4(scan[0], qp, 0);
3684 reconstruct_4x4_dc_into((f + 32) >> 6, &pred, 0, 4, &mut self.rec_y, r_off, cw);
3685 } else {
3686 let deq = if nnz <= 6 {
3687 edcstat::bump(&edcstat::I4_SPARSE, 1);
3688 dequant_scatter_4x4(scan, nnz, 0, qp, self.scaling.as_ref().map(|sc| &sc[0]))
3689 } else {
3690 edcstat::bump(&edcstat::I4_DENSE, 1);
3691 self.dequant(&un_scan_4x4_dcac(scan), qp, 0)
3692 };
3693 reconstruct_4x4_into(&deq, &pred, 0, 4, &mut self.rec_y, r_off, cw);
3694 }
3695 if let Some(c) = self.coded_y.get_mut(by * (self.mb_w * 4) + bx) {
3696 *c = true;
3697 }
3698 }
3699
3700 /// Intra 8x8 luma block: predict + zero-arm or un-scan/quant/add.
3701 /// `coeffs` = SCAN-order 8x8 coefficients, `None` when the cbp bit is
3702 /// unset. `coded_y` marking stays with the callers (their orders differ).
3703 fn recon_i8_block(&mut self, bx: usize, by: usize, mode: u8, avail_top: bool, avail_left: bool, coeffs: Option<&[i32; 64]>, qp: u8) {
3704 let (px, py) = (bx * 4, by * 4);
3705 let (t, l, corner, avail_corner) = self.gather_i8(px, py, avail_top, avail_left, bx, by);
3706 let pred = intra8x8_pred(mode, avail_top, avail_left, avail_corner, &t, &l, corner);
3707 match coeffs {
3708 None => {
3709 // Zero residual: recon == pred exactly.
3710 edcstat::bump(&edcstat::T8_ZERO, 1);
3711 let (cw, base) = (self.cw, py * self.cw + px);
3712 let win = &mut self.rec_y[base..base + 7 * cw + 8];
3713 for dy in 0..8 {
3714 win[dy * cw..dy * cw + 8].copy_from_slice(&pred[dy * 8..dy * 8 + 8]);
3715 }
3716 }
3717 Some(scan8) => {
3718 let raster = un_scan_8x8(scan8);
3719 let res8 = self.inv_quant8(&raster, qp, 0);
3720 // Written once rather than zeroed-then-filled.
3721 let predb: [i32; 64] = core::array::from_fn(|i| pred[i] as i32);
3722 let recon = add_residual_8x8(&res8, &predb);
3723 // ROW COPIES, NOT PER-PIXEL STORES. This wrote all 64 samples
3724 // one at a time, each separately bounds-checked, where the
3725 // destination is eight contiguous 8-byte runs `cw` apart.
3726 // REFUTED, do not retry: replacing this WINDOW + sub-slice with
3727 // eight direct `rec_y[d..][..8]` slices left the panic count
3728 // EXACTLY unchanged (12) and cost +3.0% instructions. Neither
3729 // form folds the check, but the window amortises the base
3730 // address computation across the eight rows. (Contrast
3731 // `gather_tile`, where a window cost +57.6% — a window pays only
3732 // when the rows it spans are written in one tight loop.)
3733 let (cw, base) = (self.cw, py * self.cw + px);
3734 let win = &mut self.rec_y[base..base + 7 * cw + 8];
3735 for dy in 0..8 {
3736 win[dy * cw..dy * cw + 8].copy_from_slice(&recon[dy * 8..dy * 8 + 8]);
3737 }
3738 }
3739 }
3740 }
3741
3742 /// I_16x16 luma: neighbor gather + prediction + the 16 4x4 AC blocks
3743 /// with the DC-only collapse. `q_blocks` = RASTER AC (slot 0 unused),
3744 /// `recon_dc` = the Hadamard-dequantized DC per block. Marks modes_y
3745 /// (I_16x16 predicts as DC for neighbors) + coded_y.
3746 #[allow(clippy::too_many_arguments)]
3747 fn recon_i16_luma(&mut self, mbx: usize, mby: usize, pred_mode: rusty_h264_common::predict::I16Mode, top_ok: bool, left_ok: bool, q_blocks: Option<&[[i32; 16]; 16]>, recon_dc: &[i32; 16], qp: u8) {
3748 // `None` on a DC-only I_16x16 macroblock (CodedBlockPatternLuma == 0):
3749 // no AC was parsed, so the shared zero plane is read-equivalent to the
3750 // 1 KB of stack this used to zero per macroblock.
3751 let q_blocks = q_blocks.unwrap_or(&ZERO_LUMA_SCAN);
3752 let w4 = self.mb_w * 4;
3753 let (lx, ly) = (mbx * 16, mby * 16);
3754 let mut t16 = [0u8; 16];
3755 let mut l16 = [0u8; 16];
3756 if top_ok {
3757 t16.copy_from_slice(self.top_y_row(ly, lx, 16));
3758 }
3759 if left_ok {
3760 // ONE check for the 16-sample column (see `gather_i4`).
3761 // `step_by` walk rather than a span + `col[i * cw]` (see `gather_i8`):
3762 // the span form's inner index stayed checked on all sixteen samples.
3763 let (cw, base) = (self.cw, ly * self.cw + lx - 1);
3764 for (s, &v) in l16.iter_mut().zip(self.rec_y[base..].iter().step_by(cw)) {
3765 *s = v;
3766 }
3767 }
3768 let corner = if top_ok && left_ok { self.top_y_px(ly, lx - 1) } else { 0 };
3769 let pred_l = luma16x16_pred(pred_mode, top_ok, left_ok, &t16, &l16, corner);
3770 for by in 0..4 {
3771 for bx in 0..4 {
3772 let p_off = (by * 4) * 16 + bx * 4;
3773 let r_off = (ly + by * 4) * self.cw + lx + bx * 4;
3774 if q_blocks[(by & 3) * 4 + (bx & 3)] == [0i32; 16] {
3775 // Zero AC: the residual is the Hadamard DC alone.
3776 edcstat::bump(&edcstat::I16_DCONLY, 1);
3777 reconstruct_4x4_dc_into((recon_dc[by * 4 + bx] + 32) >> 6, &pred_l, p_off, 16, &mut self.rec_y, r_off, self.cw);
3778 } else {
3779 let mut deq = self.dequant(&q_blocks[(by & 3) * 4 + (bx & 3)], qp, 0);
3780 deq[0] = recon_dc[by * 4 + bx];
3781 reconstruct_4x4_into(&deq, &pred_l, p_off, 16, &mut self.rec_y, r_off, self.cw);
3782 }
3783 }
3784 }
3785 // ROW FILLS. The mode/coded grids were written one 4x4 cell at a time
3786 // inside the recon loop; each macroblock row is four CONTIGUOUS cells,
3787 // so four fills replace sixteen indexed stores.
3788 for by in 0..4 {
3789 let r = (mby * 4 + by) * w4 + mbx * 4;
3790 self.modes_y[r..r + 4].fill(2);
3791 self.coded_y[r..r + 4].fill(true);
3792 }
3793 }
3794
3795 /// Chroma 8x8 recon, both planes: prediction + four 4x4 blocks per
3796 /// plane with the DC-only collapse. `qac` = RASTER AC per plane/block
3797 /// (all-zero when uncoded), `dc` = the 2x2-Hadamard-dequantized DC.
3798 #[allow(clippy::too_many_arguments)]
3799 fn recon_chroma_blocks(&mut self, mb_x: usize, mb_y: usize, chroma_mode: u8, avail_top: bool, avail_left: bool, qac: &[[[i32; 16]; 4]; 2], dc: &[[i32; 4]; 2], qpc: u8) {
3800 let (cx, cy) = (mb_x * 8, mb_y * 8);
3801 for c in 0..2 {
3802 let mut ctop = [0u8; 8];
3803 let mut cleft = [0u8; 8];
3804 let mut ccorner = 0u8;
3805 {
3806 let rec_c = if c == 0 { &self.rec_u } else { &self.rec_v };
3807 if avail_top {
3808 ctop.copy_from_slice(self.top_c_row(c, cy, cx, 8));
3809 }
3810 if avail_left {
3811 // Strided WALK, as in `gather_i4`/`gather_i8`.
3812 let cbase = cy * self.ccw + cx - 1;
3813 for (o, &v) in cleft.iter_mut().zip(rec_c[cbase..].iter().step_by(self.ccw)) {
3814 *o = v;
3815 }
3816 }
3817 if avail_top && avail_left {
3818 ccorner = self.top_c_px(c, cy, cx - 1);
3819 }
3820 }
3821 let pred8 = chroma8x8_pred(chroma_mode, avail_top, avail_left, &ctop, &cleft, ccorner);
3822 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
3823 let p_off = (by * 4) * 8 + bx * 4;
3824 let ccw = self.ccw;
3825 let r_off = (cy + by * 4) * ccw + cx + bx * 4;
3826 if qac[c & 1][(by * 2 + bx) & 3] == [0i32; 16] {
3827 // Zero AC (cbp_chroma <= 1, the common case): DC-alone flat add.
3828 edcstat::bump(&edcstat::I16_DCONLY, 1);
3829 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3830 reconstruct_4x4_dc_into((dc[c & 1][(by * 2 + bx) & 3] + 32) >> 6, &pred8, p_off, 8, plane, r_off, ccw);
3831 } else {
3832 let mut deq = self.dequant(&qac[c & 1][(by * 2 + bx) & 3], qpc, 1 + c);
3833 deq[0] = dc[c & 1][(by * 2 + bx) & 3];
3834 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
3835 reconstruct_4x4_into(&deq, &pred8, p_off, 8, plane, r_off, ccw);
3836 }
3837 }
3838 }
3839 }
3840
3841 fn recon_chroma_cabac(
3842 &mut self,
3843 mb_x: usize,
3844 mb_y: usize,
3845 chroma_mode: u8,
3846 cdc: &[[i32; 4]; 2],
3847 cac: Option<&[[[i32; 16]; 4]; 2]>,
3848 cbp_chroma: u32,
3849 avail_top: bool,
3850 avail_left: bool,
3851 ) {
3852 // `None` = no chroma AC was parsed; every read below is already gated
3853 // on cbp_chroma == 2, so the shared zero plane is read-equivalent.
3854 let cac = cac.unwrap_or(&ZERO_CAC);
3855 let qpc = self.chroma_qp_for(self.cur_qp);
3856 let mut c_dc = [[0i32; 4]; 2];
3857 if cbp_chroma != 0 {
3858 for c in 0..2 {
3859 c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 1 + c);
3860 }
3861 }
3862 // ENTROPY-SIDE half: un-scan the AC into raster + commit nnz_c; the
3863 // pixel half is the SHARED recon_chroma_blocks.
3864 let w2 = self.mb_w * 2;
3865 let mut qac = [[[0i32; 16]; 4]; 2];
3866 if cbp_chroma == 2 {
3867 for c in 0..2 {
3868 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
3869 let cnt = cac[c & 1][(by * 2 + bx) & 3].iter().filter(|&&v| v != 0).count() as u8;
3870 if let Some(p) = self.nnz_c[c & 1].get_mut((mb_y * 2 + by) * w2 + (mb_x * 2 + bx)) {
3871 *p = cnt;
3872 }
3873 // Zero-skip: empty AC leaves the fresh-zero raster block.
3874 if cnt != 0 {
3875 un_scan_4x4_ac_into(&cac[c & 1][(by * 2 + bx) & 3], &mut qac[c & 1][(by * 2 + bx) & 3]);
3876 }
3877 }
3878 }
3879 }
3880 self.recon_chroma_blocks(mb_x, mb_y, chroma_mode, avail_top, avail_left, &qac, &c_dc, qpc);
3881 }
3882
3883 /// D14 — the CAVLC E-seam (P3 item 5). Mirrors `decode_slice_data_cabac`:
3884 /// overlap parse (this thread) with pixel reconstruction (a scoped worker
3885 /// owning the planes). Now possible because the CAVLC inter recon was
3886 /// converged onto `add_inter_residual`, so both entropy coders emit the SAME
3887 /// `PInterJob` and share one worker recon.
3888 pub fn decode_slice_data(
3889 &mut self,
3890 r: &mut BitReader,
3891 is_p: bool,
3892 first_mb: usize,
3893 ) -> Result<usize, MbError> {
3894 // Same cross-slice guard as the CABAC loop head.
3895 self.span_flush();
3896 let eligible = edc_on() && rowdb_on() && (is_p || self.is_b);
3897 let threaded = eligible && edc_spawn_worker(self.mb_w, self.mb_h, self.bits_per_mb, false);
3898 edcstat::bump(&edcstat::DISPATCH_ON, threaded as u64);
3899 edcstat::bump(&edcstat::DISPATCH_SEEN, eligible as u64);
3900 if !threaded {
3901 return self.decode_slice_cavlc_inner(r, is_p, first_mb);
3902 }
3903 let ctx = self.edc_take_ctx();
3904 let (tx, rx) = std::sync::mpsc::sync_channel::<EdcMsg>(edc_bound());
3905 let (ctx_tx, ctx_rx) = std::sync::mpsc::channel::<PixelCtx>();
3906 let (back_tx, back_rx) = std::sync::mpsc::channel::<PixelCtx>();
3907 let (res, ctx, panicked) = std::thread::scope(|sc| {
3908 let h = sc.spawn(move || edc_worker(ctx, rx, ctx_tx, back_rx));
3909 self.edc_tx = Some(tx);
3910 self.edc_ctx_rx = Some(ctx_rx);
3911 self.edc_back_tx = Some(back_tx);
3912 // UNWIND SAFETY (same trap the CABAC wrapper documents): the sender
3913 // lives in `self`, which outlives an unwind, so a panic in the parse
3914 // loop would leave the channel open, the worker alive and the scope
3915 // join blocking forever — turning a diagnosable panic into a silent
3916 // deadlock. Catch, clean up, join, restore, THEN resume.
3917 let r2 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3918 self.decode_slice_cavlc_inner(r, is_p, first_mb)
3919 }));
3920 self.edc_flush_batch();
3921 self.edc_giveback();
3922 self.edc_tx = None;
3923 self.edc_ctx_rx = None;
3924 self.edc_back_tx = None;
3925 match (r2, h.join()) {
3926 (Ok(res), Ok(ctx)) => (res, Some(ctx), None),
3927 (Err(pn), Ok(ctx)) => (Err(MbError::Truncated), Some(ctx), Some(pn)),
3928 (Ok(_), Err(pn)) | (Err(_), Err(pn)) => (Err(MbError::Truncated), None, Some(pn)),
3929 }
3930 });
3931 if let Some(ctx) = ctx {
3932 self.edc_restore_ctx(ctx);
3933 }
3934 if let Some(pn) = panicked {
3935 std::panic::resume_unwind(pn);
3936 }
3937 res
3938 }
3939
3940 fn decode_slice_cavlc_inner(
3941 &mut self,
3942 r: &mut BitReader,
3943 is_p: bool,
3944 first_mb: usize,
3945 ) -> Result<usize, MbError> {
3946 let total = self.mb_w * self.mb_h;
3947 self.slice_first_mb = first_mb;
3948 self.slice_bounds.push((first_mb, self.cur_idc2));
3949 self.any_idc2 |= self.cur_idc2;
3950 self.edc_active = edc_on();
3951 let mut addr = first_mb;
3952 // Same malformed-header divide-by-zero as the sibling slice loop above.
3953 let mbw_nz = self.mb_w.max(1);
3954 let (mut mbx, mut mby) = (addr % mbw_nz, addr / mbw_nz);
3955 let mbw_c = self.mb_w;
3956 while addr < total {
3957 if mbx == mbw_c {
3958 mbx = 0;
3959 mby += 1;
3960 }
3961 self.row_hook_at(addr, mby);
3962 if is_p || self.is_b {
3963 let skip_run = {
3964 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
3965 r.read_ue()?
3966 } as usize;
3967 // A run past the picture end is a corrupt stream (ffmpeg errors
3968 // here too); a run TO the end is legal. Silently clamping used
3969 // to fill the remainder with skip MBs.
3970 if skip_run > total - addr {
3971 return Err(MbError::Truncated);
3972 }
3973 // The run length is KNOWN here (CAVLC codes it as one syntax
3974 // element). P runs: after the first skip commits (0,0), every
3975 // later run MB is FORCED (0,0) by the zero-MV rule — process
3976 // the remainder SEGMENT-WISE: one span extension + one mb_qp
3977 // fill per row segment instead of a per-MB call + span match +
3978 // store. Non-(0,0) runs and B runs keep the per-MB loop. The
3979 // per-MB `addr >= total` check is gone: the run was validated
3980 // against `total - addr` above.
3981 let mut remaining = skip_run;
3982 while remaining > 0 {
3983 debug_assert!(addr < total);
3984 if mbx == mbw_c {
3985 mbx = 0;
3986 mby += 1;
3987 }
3988 if self.is_b {
3989 if !self.b_skip_hot(mbx, mby) {
3990 self.decode_b_skip(mbx, mby)?;
3991 }
3992 if let Some(p) = self.mb_qp.get_mut(addr) {
3993 *p = self.cur_qp;
3994 } // skip inherits QPy
3995 addr += 1;
3996 mbx += 1;
3997 remaining -= 1;
3998 continue;
3999 }
4000 self.decode_p_skip(mbx, mby)?;
4001 if let Some(p) = self.mb_qp.get_mut(addr) {
4002 *p = self.cur_qp;
4003 }
4004 addr += 1;
4005 mbx += 1;
4006 remaining -= 1;
4007 // Segment-wise remainder: forced (0,0) continuation holds
4008 // exactly while skip_zero_next tracks addr (decode_p_skip
4009 // set it iff this skip committed (0,0)).
4010 // Worker mode (edc_tx) must route per-MB jobs through the
4011 // channel — the parse thread no longer owns the planes.
4012 while remaining > 0
4013 && self.skip_zero_next == addr
4014 && !self.k_no_runmv
4015 && self.edc_tx.is_none()
4016 {
4017 if mbx == mbw_c {
4018 mbx = 0;
4019 mby += 1;
4020 }
4021 // Row segment: run to row end or run end.
4022 let seg = remaining.min(mbw_c - mbx);
4023 let recon = self.edc_tx.is_none()
4024 && (self.weights.is_none() || self.weights_id0);
4025 for k in 0..seg {
4026 self.pz_push(mbx + k, mby, recon);
4027 }
4028 edcstat::bump(&edcstat::SKIPMV_FORCED, seg as u64);
4029 self.route_skip_mbs += seg as u32;
4030 self.mb_qp[addr..addr + seg].fill(self.cur_qp);
4031 if !recon {
4032 for k in 0..seg {
4033 self.edc_jobs.push(EdcJob::Skip { mbx: mbx + k, mby, mv: (0, 0) });
4034 }
4035 }
4036 addr += seg;
4037 mbx += seg;
4038 remaining -= seg;
4039 self.skip_zero_next = addr;
4040 }
4041 }
4042 if addr >= total {
4043 break;
4044 }
4045 // A trailing skip run with no following macroblock ends the slice.
4046 if skip_run > 0 && !r.more_rbsp_data() {
4047 break;
4048 }
4049 }
4050 // The skip run above may have crossed a row boundary within this
4051 // iteration — wrap before the non-skip MB decodes.
4052 if mbx == mbw_c {
4053 mbx = 0;
4054 mby += 1;
4055 }
4056 if self.is_b {
4057 // ORDER: B reconstructs inline (not seam-ready).
4058 self.edc_intra_sync();
4059 self.decode_b_mb(r, mbx, mby)?;
4060 } else {
4061 // Non-skip CAVLC MB: inter MV prediction + intra gathers
4062 // read the grids — flush deferred spans.
4063 self.span_flush();
4064 self.decode_mb(r, mbx, mby, is_p)?;
4065 }
4066 if let Some(p) = self.mb_qp.get_mut(addr) {
4067 *p = self.cur_qp;
4068 }
4069 addr += 1;
4070 mbx += 1;
4071 // CAVLC slice end: no more data after this macroblock.
4072 if !r.more_rbsp_data() {
4073 break;
4074 }
4075 }
4076 self.edc_flush(); // slice end: no job crosses a slice boundary
4077 Ok(addr)
4078 }
4079
4080 fn decode_mb(
4081 &mut self,
4082 r: &mut BitReader,
4083 mb_x: usize,
4084 mb_y: usize,
4085 is_p: bool,
4086 ) -> Result<(), MbError> {
4087 self.wait_refs_for_mb(mb_y);
4088 let mut mb_type = {
4089 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
4090 r.read_ue()?
4091 };
4092 if is_p {
4093 // In P-slices, mb_type 0/1/2 are inter (16×16, 16×8, 8×16),
4094 // 3 = P_8x8, 4 = P_8x8ref0 (ref_idx forced 0), 5+ intra.
4095 if mb_type <= 2 {
4096 return self.decode_inter(r, mb_x, mb_y, mb_type as u8);
4097 }
4098 if mb_type == 3 || mb_type == 4 {
4099 return self.decode_p8x8(r, mb_x, mb_y, mb_type == 4);
4100 }
4101 mb_type -= 5;
4102 }
4103 // ORDER: intra reconstruction reads neighbour PIXELS, so the worker
4104 // must have applied every deferred job before this point.
4105 self.edc_intra_sync();
4106 self.decode_intra_mb(r, mb_x, mb_y, mb_type)
4107 }
4108
4109 /// Decodes an intra macroblock given its intra `mb_type` (0 = I_4x4,
4110 /// 1..=24 = I_16x16, 25 = I_PCM) — shared by I-, P- and B-slice paths.
4111 fn decode_intra_mb(
4112 &mut self,
4113 r: &mut BitReader,
4114 mb_x: usize,
4115 mb_y: usize,
4116 mb_type: u32,
4117 ) -> Result<(), MbError> {
4118 // H-48: this scope was DECLARED and never wired, which is precisely why the
4119 // stage table left 19.8% unaccounted — 66,120 of 475,200 macroblocks on the
4120 // reference stream are I-type and had no scope at all.
4121 let _gi = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMbI);
4122 if let Some(p) = self.mb_kind.get_mut(mb_y * self.mb_w + mb_x) {
4123 *p = rusty_h264_common::deblock::MB_KIND_INTRA;
4124 }
4125 if mb_type == 0 {
4126 // I_NxN: transform_size_8x8_flag (when enabled) selects I_8x8 vs I_4x4.
4127 if self.transform_8x8_mode && r.read_bit()? {
4128 self.decode_i8x8(r, mb_x, mb_y)?;
4129 } else {
4130 self.decode_i4x4(r, mb_x, mb_y)?;
4131 }
4132 } else if (1..=24).contains(&mb_type) {
4133 self.decode_i16(r, mb_x, mb_y, mb_type - 1)?;
4134 } else if mb_type == 25 {
4135 // ORDER: I_PCM writes pixels directly.
4136 self.edc_intra_sync();
4137 self.decode_ipcm(r, mb_x, mb_y)?;
4138 } else {
4139 return Err(MbError::Unsupported("only I_4x4 / I_16x16 / I_PCM macroblocks"));
4140 }
4141 // Mark all luma blocks coded for the next macroblock's top-right.
4142 // Four contiguous cells per macroblock row - `fill`, not sixteen
4143 // separately bounds-checked indexed stores.
4144 let w4 = self.mb_w * 4;
4145 for lby in 0..4usize {
4146 let a = (mb_y * 4 + lby) * w4 + mb_x * 4;
4147 self.coded_y[a..a + 4].fill(true);
4148 }
4149 Ok(())
4150 }
4151
4152 /// Reconstructs an inter macroblock (`mode` 0 = P_L0_16x16, 1 = P_16x8,
4153 /// 2 = P_8x16): parse the per-partition motion vectors and residual,
4154 /// motion-compensate each partition, and add the residual.
4155 fn decode_inter(
4156 &mut self,
4157 r: &mut BitReader,
4158 mb_x: usize,
4159 mb_y: usize,
4160 mode: u8,
4161 ) -> Result<(), MbError> {
4162 if self.refs.is_empty() {
4163 return Err(MbError::Unsupported("inter without reference"));
4164 }
4165 // DEBLOCK CLASS: mode 0 is P_L0_16x16 — ONE partition, so all 16 blocks
4166 // share a reference and motion vector and no internal edge can reach
4167 // strength 1. Internal strengths then follow from coefficients alone, i.e.
4168 // 16 nnz bytes instead of a 24-block gather across 5-7 grids. Modes 1/2
4169 // (P_16x8 / P_8x16) have two partitions with independent motion and stay
4170 // UNSET (blind path).
4171 if mode == 0 {
4172 if let Some(k) = self.mb_kind.get_mut(mb_y * self.mb_w + mb_x) {
4173 *k = rusty_h264_common::deblock::MB_KIND_INTER_UNIFORM;
4174 }
4175 }
4176 // QP (qp/qpc) is bound after mb_qp_delta is read below.
4177 let w4 = self.mb_w * 4;
4178 let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
4179 let num_refs = self.refs.len();
4180 let layout = inter_partitions(mode);
4181
4182 // mb_pred order (spec 7.3.5.1): all ref_idx_l0 first (only when more than
4183 // one reference is active), then all mvd_l0.
4184 let nparts = layout.len();
4185 let mut ref_idxs = [0i32; 4];
4186 if self.num_ref_active > 1 {
4187 for ri in ref_idxs[..nparts].iter_mut() {
4188 *ri = read_ref_idx(r, self.num_ref_active)?;
4189 if *ri as usize >= num_refs {
4190 return Err(MbError::Truncated); // references a non-existent picture
4191 }
4192 }
4193 }
4194
4195 // Phase 1: per partition, ref-aware MV prediction + mvd, committing the
4196 // motion grid so a later partition predicts from an earlier one.
4197 let mut part_mv = [(0i32, (0i32, 0i32)); 4];
4198 {
4199 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::MvGrid);
4200 for (part, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
4201 let refi = ref_idxs[part];
4202 let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
4203 let [a, b, c] = self.mv_neighbors_block(pbx, pby, (rw / 4) as isize);
4204 let pmv = predict_partition_mv(mode, part, a, b, c, refi);
4205 let mvd_x = read_mvd(r)?;
4206 let mvd_y = read_mvd(r)?;
4207 let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
4208 part_mv[part] = (refi, mv);
4209 for by in ry / 4..ry / 4 + rh / 4 {
4210 for bx in rx / 4..rx / 4 + rw / 4 {
4211 let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
4212 if let (Some(m), Some(it), Some(rf), Some(cd)) = (
4213 self.mv_y.get_mut(idx),
4214 self.inter_y.get_mut(idx),
4215 self.ref_idx_y.get_mut(idx),
4216 self.coded_y.get_mut(idx),
4217 ) {
4218 (*m, *it, *rf, *cd) = (mv, true, refi, true);
4219 }
4220 }
4221 }
4222 }
4223 }
4224
4225 // Phase 2: motion-compensate each partition from its reference.
4226 let mut pred_y = [0u8; 256];
4227 let mut c_pred = [[0u8; 64]; 2];
4228 // D8-CAVLC: the double-stage ablation, extended to the CAVLC loop. The
4229 // CABAC measurement could not run here at all (`edc_active` is false on
4230 // this path, so nothing replays through `edc_flush` and `doubled` read
4231 // 0) — yet CAVLC is exactly the population P3 item 5 targets, and its
4232 // cheaper parse should make the PIXEL share larger. Doubling the whole
4233 // partition loop is idempotent: pass 2 overwrites `pred_y` with fresh MC
4234 // BEFORE `weight_partition` runs, so weighting cannot apply twice.
4235 // This doubles MC only (not the residual add inside `inter_finish`), so
4236 // it is a LOWER BOUND on the CAVLC pixel share.
4237 // D14 (CAVLC E-seam): when the seam is live the WORKER motion-compensates
4238 // from the committed MV grids, so skip MC here rather than computing a
4239 // prediction that would be discarded.
4240 let defer = self.edc_tx.is_some() || self.edc_active;
4241 let mc_passes = if defer { 0 } else if double_recon() { 2 } else { 1 };
4242 for _pass in 0..mc_passes {
4243 if _pass > 0 {
4244 edcstat::bump(&edcstat::DOUBLED, 1);
4245 }
4246 for (part, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
4247 let (refi, mv) = part_mv[part & 3];
4248 let Some(reference) = self.refs.get(refi as usize) else { continue };
4249 let mut tmp = [0u8; 256];
4250 mc_luma_padded(&*reference.luma_guard(reference.ch), reference.lstride(), crate::LPAD, self.cw, ch, mb_x * 16 + rx, mb_y * 16 + ry, rw, rh, mv.0, mv.1, &mut tmp);
4251 {
4252 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
4253 restride(&mut pred_y, 16, rx, ry, &tmp, rw, rh);
4254 }
4255 let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
4256 for cc in 0..2 {
4257 let rc = if cc == 0 { &*reference.chroma_guard(0, reference.ch) } else { &*reference.chroma_guard(1, reference.ch) };
4258 let mut tc = [0u8; 64];
4259 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);
4260 {
4261 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
4262 restride(&mut c_pred[cc], 8, crx, cry, &tc, crw, crh);
4263 }
4264 }
4265 self.weight_partition(&mut pred_y, &mut c_pred, 0, refi as usize, rx, ry, rw, rh);
4266 }
4267 }
4268
4269 // 16×16/16×8/8×16 partitions are all ≥ 8×8, so the 8×8 transform is allowed.
4270 self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true, defer)
4271 }
4272
4273 /// Shared inter tail: parse `coded_block_pattern` + `mb_qp_delta`, decode the
4274 /// luma/chroma residual, and add it to the already-built motion-compensated
4275 /// prediction. Used by both the 16×16/16×8/8×16 path and `P_8x8`.
4276 fn inter_finish(
4277 &mut self,
4278 r: &mut BitReader,
4279 mb_x: usize,
4280 mb_y: usize,
4281 pred_y: &[u8; 256],
4282 c_pred: &[[u8; 64]; 2],
4283 allow_8x8: bool,
4284 // D14: emit a worker job instead of reconstructing. Only the CAVLC
4285 // 16x16/16x8/8x16 path sets this; B and P_8x8 stay inline.
4286 defer: bool,
4287 ) -> Result<(), MbError> {
4288 let vt = vlc_tables();
4289 let w4 = self.mb_w * 4;
4290 let cbp = {
4291 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
4292 read_cbp_inter(r)?
4293 };
4294 let cbp_luma = cbp & 15;
4295 let cbp_chroma = cbp >> 4;
4296 // transform_size_8x8_flag follows cbp (before mb_qp_delta) when luma has
4297 // coefficients, the 8×8 transform is enabled, and every partition ≥ 8×8.
4298 let t8x8 = cbp_luma > 0 && self.transform_8x8_mode && allow_8x8 && r.read_bit()?;
4299 if t8x8 {
4300 if let Some(f) = self.mb_t8x8.get_mut(mb_y * self.mb_w + mb_x) {
4301 *f = true;
4302 }
4303 self.any_t8 = true;
4304 }
4305 if cbp != 0 {
4306 self.step_qp(r.read_se()?)?;
4307 }
4308 let (qp, _qpc) = (self.cur_qp, self.chroma_qp_for(self.cur_qp));
4309
4310 // ---- luma residual ----
4311 self.nnz_cache_load(mb_x, mb_y);
4312 let mut luma_scan = [[0i32; 16]; 16];
4313 let mut nnzs = [0u8; 24];
4314 let mut luma8: Option<[[i32; 64]; 4]> = None; // allocated only under t8x8
4315 // BUILD THE MACROBLOCK'S nnz RASTER ON THE STACK, COPY IT ROW-WISE ONCE.
4316 // Both arms below scattered sixteen individually bounds-checked stores
4317 // into the frame grid while the entropy parse ran. Nothing reads
4318 // `nnz_y` for THIS macroblock before the function returns - `nc_pred`
4319 // predicts from the separate `nnz_cache` - so the writes can be
4320 // deferred. The zero arm then costs NOTHING: the raster already holds
4321 // zeros.
4322 let mut nnz_raster = [0u8; 16];
4323 if t8x8 {
4324 for b8 in 0..4 {
4325 let (b8x, b8y) = (b8 % 2, b8 / 2);
4326 let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
4327 let _ = (bx, by);
4328 if cbp_luma & (1 << b8) != 0 {
4329 let mut scan8 = [0i32; 64];
4330 for sub in 0..4 {
4331 let (sx, sy) = (sub % 2, sub / 2);
4332 let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
4333 let nc = self.nc_pred(cx, cy);
4334 let (blk, total) = decode_residual_block_with(vt, r, 16, nc)?;
4335 self.nnz_cache_set(cx, cy, total);
4336 nnz_raster[cy * 4 + cx] = total;
4337 // The PER-SUB-BLOCK count the next macroblock's nC prediction
4338 // depends on -- summing these into one slot and letting the
4339 // recon helper broadcast it back is what broke CAVLC 8x8.
4340 nnzs[b8 * 4 + sub] = total;
4341 for k in 0..16 {
4342 scan8[4 * k + sub] = blk[k];
4343 }
4344 }
4345 // RAW: `add_inter_residual` applies un_scan_8x8 + inv_quant8
4346 // itself, exactly as it does for the CABAC path.
4347 luma8.get_or_insert_with(|| [[0i32; 64]; 4])[b8] = scan8;
4348 } else {
4349 for sub in 0..4 {
4350 let (sx, sy) = (sub % 2, sub / 2);
4351 self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
4352 // `nnz_raster` is already zero here - no store needed.
4353 }
4354 }
4355 }
4356 } else {
4357 for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
4358 let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
4359 let total = if cbp_luma & (1 << (blk / 4)) != 0 {
4360 let nc = self.nc_pred(lbx, lby);
4361 let (scan16, total) = decode_residual_block_with(vt, r, 16, nc)?;
4362 luma_scan[blk] = scan16; // RAW scan order, like CABAC
4363 total
4364 } else {
4365 0
4366 };
4367 self.nnz_cache_set(lbx, lby, total);
4368 let _ = (bx, by);
4369 nnz_raster[(lby & 3) * 4 + (lbx & 3)] = total;
4370 nnzs[blk] = total;
4371 }
4372 }
4373 // ONE contiguous copy per macroblock row.
4374 for by in 0..4usize {
4375 let a = (mb_y * 4 + by) * w4 + mb_x * 4;
4376 self.nnz_y[a..a + 4].copy_from_slice(&nnz_raster[by * 4..by * 4 + 4]);
4377 }
4378
4379 // ---- chroma residual ----
4380 let mut c_recon_dc = [[0i32; 4]; 2];
4381 if cbp_chroma != 0 {
4382 for slot in c_recon_dc.iter_mut() {
4383 let (dc, _) = decode_residual_block_with(vt, r, 4, -1)?;
4384 *slot = [dc[0], dc[1], dc[2], dc[3]]; // RAW; dequantised in the helper
4385 }
4386 }
4387 let mut c_q = [[[0i32; 16]; 4]; 2];
4388 if cbp_chroma == 2 {
4389 self.chroma_cache_load(mb_x, mb_y);
4390 let w2 = self.mb_w * 2;
4391 let mut cnnz = [[0u8; 4]; 2];
4392 for c in 0..2 {
4393 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
4394 let nc = self.chroma_nc_pred(c, bx, by);
4395 let (ac, total) = decode_residual_block_with(vt, r, 15, nc)?;
4396 self.chroma_nnz_cache_set(c, bx, by, total);
4397 cnnz[c][by * 2 + bx] = total;
4398 c_q[c][by * 2 + bx] = ac; // RAW scan order
4399 nnzs[(16 + c * 4 + by * 2 + bx).min(23)] = total;
4400 }
4401 for by in 0..2usize {
4402 let a = (mb_y * 2 + by) * w2 + mb_x * 2;
4403 self.nnz_c[c][a..a + 2].copy_from_slice(&cnnz[c][by * 2..by * 2 + 2]);
4404 }
4405 }
4406 }
4407
4408 // ---- reconstruction ----
4409 //
4410 // D13: this used to be a 109-line hand-rolled copy of the residual add.
4411 // It now calls the SAME `add_inter_residual` the CABAC path uses, which
4412 // is what makes the CAVLC E-seam possible at all: the two paths had
4413 // different residual representations (CAVLC pre-applied un_scan and
4414 // inv_quant at PARSE time; CABAC carries raw scan-order coefficients and
4415 // dequantises inside the helper), so no job could be shared. Carrying the
4416 // raw forms — which CAVLC already had in hand — converges them, deletes
4417 // a duplicate implementation, and lets a deferred job reuse the existing
4418 // worker recon instead of needing a second copy that could drift.
4419 if defer {
4420 // The residual representation now matches CABAC exactly (the
4421 // convergence commit), so the SAME `PInterJob` and the SAME worker
4422 // `recon_p_inter` serve both entropy coders — no second recon
4423 // implementation exists that could drift.
4424 let (mut gmv, mut gref) = ([(0i32, 0i32); 16], [0u8; 16]);
4425 let w4r = self.mb_w * 4;
4426 // ROW SLICES over BOTH grids. This was the single densest panic site
4427 // left in the decoder crate: sixteen iterations reading two PARALLEL
4428 // frame grids at the same index, and `mv_y`'s bound proved nothing
4429 // about `ref_idx_y`, so all thirty-two loads carried their own check.
4430 // Each macroblock row is four contiguous cells in both.
4431 for by in 0..4usize {
4432 let base = (mb_y * 4 + by) * w4r + mb_x * 4;
4433 let mvr = &self.mv_y[base..][..4];
4434 let rfr = &self.ref_idx_y[base..][..4];
4435 gmv[by * 4..][..4].copy_from_slice(mvr);
4436 for bx in 0..4usize {
4437 gref[by * 4 + bx] = rfr[bx].clamp(0, 15) as u8;
4438 }
4439 }
4440 // D9 applies here too: `cbp == 0` means all 2,592 coefficient bytes
4441 // of the 2,784-byte job are ZERO, so ship the 176-byte motion-only
4442 // form. Discovered on the CABAC path; it transfers for free because
4443 // the CAVLC arm now emits the SAME job type.
4444 let ej = if cbp == 0 && nores_on() {
4445 edcstat::bump(&edcstat::J_NORES_SENT, 1);
4446 EdcJob::InterNoRes(Box::new(PInterNoResJob {
4447 mbx: mb_x, mby: mb_y, t8: t8x8, gmv, gref,
4448 }))
4449 } else {
4450 EdcJob::Inter(Box::new(PInterJob {
4451 mbx: mb_x, mby: mb_y, qp,
4452 cbp_chroma, gmv, gref,
4453 luma_scan: Some(luma_scan), luma8, cdc: c_recon_dc, cac: Some(c_q), nnzs,
4454 }))
4455 };
4456 if self.edc_tx.is_some() {
4457 self.edc_giveback();
4458 self.edc_send_job(ej);
4459 } else {
4460 self.edc_jobs.push(ej);
4461 }
4462 } else {
4463 self.add_inter_residual(
4464 mb_x, mb_y, pred_y, c_pred, Some(&luma_scan),
4465 luma8.as_ref(),
4466 &c_recon_dc, Some(&c_q), cbp_chroma, &nnzs,
4467 );
4468 }
4469
4470 // MV grid + coded flags were set per partition; mark modes as DC.
4471 // Four contiguous cells per row - `fill`, not sixteen indexed stores.
4472 for lby in 0..4usize {
4473 let a = (mb_y * 4 + lby) * w4 + mb_x * 4;
4474 self.modes_y[a..a + 4].fill(2);
4475 }
4476 Ok(())
4477 }
4478
4479 // ---------------------------------------------------------------------
4480 // B-slice macroblock decoding
4481 // ---------------------------------------------------------------------
4482
4483 /// Per-list (`list` 0 or 1) MV-prediction neighbors for the block region at
4484 /// `(pbx, pby)` of width `pwb` blocks — the L0/L1 analogue of
4485 /// `mv_neighbors_block`.
4486 fn mv_neighbors_list(&self, pbx: isize, pby: isize, pwb: isize, list: usize) -> [MvNeighbor; 3] {
4487 self.mv_neighbors_block_grid(pbx, pby, pwb, list)
4488 }
4489
4490 /// Spatial-direct A/B/C at the MB origin — same for every 8×8 in the MB
4491 /// (spec §8.4.1.2.2). B_8x8 callers hoist this once; 16×16 skip/direct walk
4492 /// once inside `decode_b_direct`.
4493 #[inline]
4494 fn b_direct_nbrs(&self, mb_x: usize, mb_y: usize) -> ([MvNeighbor; 3], [MvNeighbor; 3]) {
4495 self.mv_neighbors_both((mb_x * 4) as isize, (mb_y * 4) as isize, 4)
4496 }
4497
4498 /// FUSED dual-list gather at arbitrary partition geometry: A/B/C positions
4499 /// and their availability (bounds + coded + slice) are list-independent —
4500 /// compute once, load both lists' grids from the same resolved index.
4501 fn mv_neighbors_both(&self, pbx: isize, pby: isize, pwb: isize) -> ([MvNeighbor; 3], [MvNeighbor; 3]) {
4502 let (w4, h4) = ((self.mb_w * 4) as isize, (self.mb_h * 4) as isize);
4503 let get2 = |bx: isize, by: isize| -> (MvNeighbor, MvNeighbor) {
4504 if bx < 0
4505 || by < 0
4506 || bx >= w4
4507 || by >= h4
4508 || !self.coded_y.get((by * w4 + bx) as usize).copied().unwrap_or(false)
4509 || !self.nbr_in_slice(bx as usize / 4, by as usize / 4)
4510 {
4511 (MvNeighbor::NONE, MvNeighbor::NONE)
4512 } else {
4513 // FOUR PARALLEL GRIDS at one index. The `coded_y` guard above
4514 // bounds none of them — they are separate Vecs — so each carried
4515 // its own panic path. `MvNeighbor::NONE` is the established
4516 // "no such neighbour" value, so the fallible form degrades into
4517 // the branch directly above it.
4518 let idx = (by * w4 + bx) as usize;
4519 match (self.mv_y.get(idx), self.ref_idx_y.get(idx), self.mv1.get(idx), self.ref_idx1.get(idx)) {
4520 (Some(&m0), Some(&r0), Some(&m1), Some(&r1)) => (
4521 MvNeighbor { available: true, mv: m0, ref_idx: r0 },
4522 MvNeighbor { available: true, mv: m1, ref_idx: r1 },
4523 ),
4524 _ => (MvNeighbor::NONE, MvNeighbor::NONE),
4525 }
4526 }
4527 };
4528 let (a0, a1) = get2(pbx - 1, pby);
4529 let (b0, b1) = get2(pbx, pby - 1);
4530 let (mut c0, mut c1) = get2(pbx + pwb, pby - 1);
4531 if !c0.available {
4532 // The C fallback is position-driven (topright unavailable ⇒
4533 // topleft), so both lists fall back together — same decision the
4534 // two per-list gathers made independently.
4535 let t = get2(pbx - 1, pby - 1);
4536 c0 = t.0;
4537 c1 = t.1;
4538 }
4539 ([a0, b0, c0], [a1, b1, c1])
4540 }
4541
4542 fn mv_neighbors_block_grid(&self, pbx: isize, pby: isize, pwb: isize, list: usize) -> [MvNeighbor; 3] {
4543 let (w4, h4) = ((self.mb_w * 4) as isize, (self.mb_h * 4) as isize);
4544 let (mvg, refg) = if list == 0 {
4545 (&self.mv_y, &self.ref_idx_y)
4546 } else {
4547 (&self.mv1, &self.ref_idx1)
4548 };
4549 let get = |bx: isize, by: isize| -> MvNeighbor {
4550 if bx < 0
4551 || by < 0
4552 || bx >= w4
4553 || by >= h4
4554 || !self.coded_y.get((by * w4 + bx) as usize).copied().unwrap_or(false)
4555 || !self.nbr_in_slice(bx as usize / 4, by as usize / 4)
4556 {
4557 MvNeighbor::NONE
4558 } else {
4559 let idx = (by * w4 + bx) as usize;
4560 match (mvg.get(idx), refg.get(idx)) {
4561 (Some(&m), Some(&r)) => MvNeighbor { available: true, mv: m, ref_idx: r },
4562 _ => MvNeighbor::NONE,
4563 }
4564 }
4565 };
4566 let a = get(pbx - 1, pby);
4567 let b = get(pbx, pby - 1);
4568 let mut c = get(pbx + pwb, pby - 1);
4569 if !c.available {
4570 c = get(pbx - 1, pby - 1);
4571 }
4572 [a, b, c]
4573 }
4574
4575 /// `colZeroFlag` for the 4×4 block at absolute block coords `(bx, by)`: true
4576 /// when `RefPicList1[0]` is a short-term picture whose co-located block uses
4577 /// reference 0 with a near-zero motion vector (spec §8.4.1.2.2).
4578 /// Co-located 4x4 block coords for the current block's `(bx4, by4)` within the
4579 /// macroblock, per spec 8.4.1.2.1. Under `direct_8x8_inference_flag` every 4x4
4580 /// in an 8x8 takes that 8x8's OUTER CORNER (`luma4x4BlkIdx = 5 * mbPartIdx`,
4581 /// i.e. (0,0) (3,0) (0,3) (3,3)); otherwise motion is genuinely per-4x4.
4582 ///
4583 /// 8.4.1.2.1 is SHARED by both direct modes, so spatial and temporal must map
4584 /// identically. They did not: temporal mapped the corner and spatial read the
4585 /// block's own coords, which is invisible while every 4x4 in the co-located 8x8
4586 /// carries the same motion -- true of every stream until sub-8x8 P partitions
4587 /// (x264 `--partitions p4x4`) make them differ. Hence one function.
4588 #[inline]
4589 fn col_block(&self, bx4: usize, by4: usize) -> (usize, usize) {
4590 if self.direct_8x8_inference {
4591 ((bx4 / 2) * 3, (by4 / 2) * 3)
4592 } else {
4593 (bx4, by4)
4594 }
4595 }
4596
4597 fn col_zero(&self, bx: usize, by: usize) -> bool {
4598 // Fast path (1T frozen colocated, the default): the invariant checks
4599 // were hoisted to set_b_context; two grid loads + the threshold test.
4600 if self.col_ok {
4601 let Some(col) = self.refs1.first() else { return false };
4602 let idx = by * self.col_w4 + bx;
4603 // `.get` on EVERY parallel array, not a length test on one of them.
4604 // The `idx >= ref_idx.len()` guard proved nothing about `mv`,
4605 // `ref_idx1` or `mv1` — separate Vecs with independent lengths — so
4606 // each of those reads carried its own panic path. They are built
4607 // together and always agree; the fallible form states that instead of
4608 // asserting it, and keeps the "no panic on malformed input" contract.
4609 let (cref, cmv) = match col.ref_idx.get(idx) {
4610 Some(&r0) if r0 >= 0 => match col.mv.get(idx) {
4611 Some(&m) => (r0, m),
4612 None => return false,
4613 },
4614 Some(_) => match (col.ref_idx1.get(idx), col.mv1.get(idx)) {
4615 (Some(&r1), Some(&m)) if r1 >= 0 => (r1, m),
4616 _ => return false,
4617 },
4618 None => return false,
4619 };
4620 return cref == 0 && cmv.0.abs() <= 1 && cmv.1.abs() <= 1;
4621 }
4622 let Some(col) = self.refs1.first() else { return false };
4623 if let Some(live) = col.live.as_ref() {
4624 col.wait_motion_ready();
4625 let meta = live.meta.read().unwrap();
4626 if meta.long_term || meta.w4 == 0 {
4627 return false;
4628 }
4629 let idx = by * meta.w4 + bx;
4630 // Same `.get`-per-array shape as the frozen-colocated path above.
4631 let (cref, cmv) = match meta.ref_idx.get(idx) {
4632 Some(&r0) if r0 >= 0 => match meta.mv.get(idx) {
4633 Some(&m) => (r0, m),
4634 None => return false,
4635 },
4636 Some(_) => match (meta.ref_idx1.get(idx), meta.mv1.get(idx)) {
4637 (Some(&r1), Some(&m)) if r1 >= 0 => (r1, m),
4638 _ => return false,
4639 },
4640 None => return false,
4641 };
4642 return cref == 0 && cmv.0.abs() <= 1 && cmv.1.abs() <= 1;
4643 }
4644 if col.long_term || col.w4 == 0 {
4645 return false;
4646 }
4647 let idx = by * col.w4 + bx;
4648 if idx >= col.ref_idx.len() {
4649 return false;
4650 }
4651 let (cref, cmv) = match col.ref_idx.get(idx) {
4652 Some(&r0) if r0 >= 0 => match col.mv.get(idx) {
4653 Some(&m) => (r0, m),
4654 None => return false,
4655 },
4656 Some(_) => match (col.ref_idx1.get(idx), col.mv1.get(idx)) {
4657 (Some(&r1), Some(&m)) if r1 >= 0 => (r1, m),
4658 _ => return false,
4659 },
4660 None => return false,
4661 };
4662 cref == 0 && cmv.0.abs() <= 1 && cmv.1.abs() <= 1
4663 }
4664
4665 /// Implicit bi-prediction weights `(w0, w1)` from POC distances (spec
4666 /// §8.4.2.3.2), or `None` for the plain average (idc≠2, uni-pred, or the
4667 /// equidistant / out-of-range fall-back to 32:32 which equals the average).
4668 fn implicit_weights(&self, refi0: i32, refi1: i32) -> Option<(i32, i32)> {
4669 if self.weighted_bipred_idc != 2 || refi0 < 0 || refi1 < 0 {
4670 return None;
4671 }
4672 let (Some(r0), Some(r1)) = (self.refs.get(refi0 as usize), self.refs1.get(refi1 as usize))
4673 else {
4674 return None;
4675 };
4676 let td = (r1.pic_poc() - r0.pic_poc()).clamp(-128, 127);
4677 let tb = (self.cur_poc - r0.pic_poc()).clamp(-128, 127);
4678 if td == 0 || r0.long_term || r1.long_term {
4679 return None; // 32:32 → identical to the average
4680 }
4681 let tx = tx_for_td(td);
4682 let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
4683 let w1 = dsf >> 2;
4684 if !(-64..=128).contains(&w1) {
4685 return None; // out of range → 32:32 average
4686 }
4687 Some((64 - w1, w1))
4688 }
4689
4690 /// Motion-compensates a region with the given per-list refs/MVs. Bi-prediction
4691 /// is the simple `(a+b+1)>>1` average, or POC-weighted when implicit weighting
4692 /// (idc 2) is active. Writes into `pred_y`/`c_pred`.
4693 #[allow(clippy::too_many_arguments)]
4694 fn b_mc(
4695 &self,
4696 mb_x: usize,
4697 mb_y: usize,
4698 px: usize,
4699 py: usize,
4700 rw: usize,
4701 rh: usize,
4702 refi0: i32,
4703 mv0: (i32, i32),
4704 refi1: i32,
4705 mv1: (i32, i32),
4706 pred_y: &mut [u8; 256],
4707 c_pred: &mut [[u8; 64]; 2],
4708 ) {
4709 let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBMc);
4710 // Malformed-stream armor, mirroring the P path: now that B slices actually
4711 // PARSE ref_idx (they used to be hardcoded to 0), a mutated stream can hand
4712 // us an index past the end of either list. Clamp rather than panic — the
4713 // crate is `forbid(unsafe_code)` and fuzz-gated to never panic, and a
4714 // wrong picture on garbage input carries no conformance duty.
4715 let refi0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
4716 let refi1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
4717 if (refi0 >= 0 && self.refs.is_empty()) || (refi1 >= 0 && self.refs1.is_empty()) {
4718 return;
4719 }
4720 let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
4721 let weights = {
4722 let _gw = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBWeights);
4723 self.implicit_weights(refi0, refi1)
4724 };
4725 // Bi-prediction blend: the weights decision is LOOP-INVARIANT, so every
4726 // blend site below matches on `weights` ONCE and runs a branch-free
4727 // pixel loop — the unweighted `(p+q+1)>>1` average then autovectorizes
4728 // (the per-pixel closure this replaces hid the invariant behind a
4729 // capture, and its chroma form was a &dyn call PER PIXEL).
4730 // FULL-WIDTH regions (px == 0, rw == 16 — every 16×16/16×8 partition and
4731 // most direct regions) occupy contiguous rows of `pred_y`, so MC writes
4732 // the destination DIRECTLY: uni-pred needs no staging at all, bi-pred
4733 // stages only the second list and blends in place. The staging arrays
4734 // (512 B zeroed per call before this) now exist only on the branches
4735 // that read them. Same fusion as the P path's mc_rect (WHYS Part 8).
4736 let full = px == 0 && rw == 16;
4737 if refi0 >= 0 && refi1 >= 0 && mv0.0 % 4 == 0 && mv0.1 % 4 == 0 && mv1.0 % 4 == 0 && mv1.1 % 4 == 0 {
4738 edcstat::bump(&edcstat::BMC_BI_FP, 1);
4739 }
4740 let _gl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBLuma);
4741 // One scratch borrow for the whole region — both bi-pred passes included.
4742 // The closure yields whether the arm already ran the chroma half (the
4743 // bi-pred full-width arm does, to keep its staging alive) — a plain
4744 // `return` inside would exit the CLOSURE only and chroma would run twice.
4745 let chroma_done = rusty_h264_common::inter::with_mc_scratch(|scr| match (refi0 >= 0, refi1 >= 0, full) {
4746 (true, false, true) => {
4747 let Some(rf) = self.refs.get(refi0 as usize) else { return false };
4748 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
4749 false
4750 }
4751 (false, true, true) => {
4752 let Some(rf) = self.refs1.get(refi1 as usize) else { return false };
4753 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
4754 false
4755 }
4756 (true, true, true) => {
4757 let Some(rf) = self.refs.get(refi0 as usize) else { return false };
4758 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
4759 let mut b = [0u8; 256];
4760 let Some(rf) = self.refs1.get(refi1 as usize) else { return false };
4761 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut b[..rw * rh]);
4762 drop(_gl);
4763 let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
4764 // SLICE-then-zip: proving the bounds ONCE lets rustc emit the whole
4765 // 256-byte average as 8 straight-line vpavgb ops (verified in
4766 // isolation, x86-64-v3); the indexed form kept a per-iteration
4767 // bounds check and a loop. A hand AVX2 kernel is refuted — the
4768 // compiler already emits the ideal instruction.
4769 let dst = &mut pred_y[py * 16..py * 16 + rw * rh];
4770 match weights {
4771 None => {
4772 for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
4773 *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
4774 }
4775 }
4776 Some((w0, w1)) => {
4777 for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
4778 *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
4779 }
4780 }
4781 }
4782 let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
4783 self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
4784 true
4785 }
4786 _ => {
4787 // Narrow region — rows are strided in `pred_y`; stage and copy.
4788 let (mut a, mut b) = ([0u8; 256], [0u8; 256]);
4789 if refi0 >= 0 {
4790 let Some(rf) = self.refs.get(refi0 as usize) else { return false };
4791 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
4792 }
4793 if refi1 >= 0 {
4794 let Some(rf) = self.refs1.get(refi1 as usize) else { return false };
4795 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
4796 }
4797 drop(_gl);
4798 let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
4799 match (refi0 >= 0, refi1 >= 0) {
4800 (true, true) => {
4801 for dy in 0..rh {
4802 let (ar, br) = (&a[dy * rw..dy * rw + rw], &b[dy * rw..dy * rw + rw]);
4803 let base = (py + dy) * 16 + px;
4804 let dst = &mut pred_y[base..base + rw];
4805 match weights {
4806 None => {
4807 for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
4808 *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
4809 }
4810 }
4811 Some((w0, w1)) => {
4812 for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
4813 *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
4814 }
4815 }
4816 }
4817 }
4818 }
4819 (true, false) => {
4820 for dy in 0..rh {
4821 let d = (py + dy) * 16 + px;
4822 pred_y[d..d + rw].copy_from_slice(&a[dy * rw..dy * rw + rw]);
4823 }
4824 }
4825 _ => {
4826 for dy in 0..rh {
4827 let d = (py + dy) * 16 + px;
4828 pred_y[d..d + rw].copy_from_slice(&b[dy * rw..dy * rw + rw]);
4829 }
4830 }
4831 }
4832 false
4833 }
4834 });
4835 if chroma_done {
4836 return;
4837 }
4838 let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
4839 self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
4840 }
4841
4842 /// Chroma half of `b_mc`, with the same full-width direct-write fusion
4843 /// (crw == 8 rows are contiguous in the 8-wide `c_pred` planes).
4844 #[allow(clippy::too_many_arguments)]
4845 /// Chroma half of `b_mc`. U and V share every piece of MC geometry, so
4846 /// each list is ONE `mc_chroma_padded_pair` call (setup + range check paid
4847 /// once, kernels unchanged) instead of two per-plane calls — the per-plane
4848 /// pixel math is byte-identical to the old per-plane flow.
4849 #[allow(clippy::too_many_arguments)]
4850 fn b_mc_chroma(
4851 &self,
4852 mb_x: usize,
4853 mb_y: usize,
4854 px: usize,
4855 py: usize,
4856 rw: usize,
4857 rh: usize,
4858 refi0: i32,
4859 mv0: (i32, i32),
4860 refi1: i32,
4861 mv1: (i32, i32),
4862 c_pred: &mut [[u8; 64]; 2],
4863 weights: Option<(i32, i32)>,
4864 cch: usize,
4865 ) {
4866 use rusty_h264_common::inter::mc_chroma_padded_pair;
4867 let (crx, cry, crw, crh) = (px / 2, py / 2, rw / 2, rh / 2);
4868 let full = crx == 0 && crw == 8;
4869 let n = crw * crh;
4870 // RESOLVE BOTH SLOTS ONCE, then let the match patterns bind them. Every
4871 // arm below re-indexed `refs`/`refs1` by a raw `refi` — six checked Vec
4872 // indexes across the four arms — although `a0`/`a1` already meant
4873 // exactly "this slot is present". Carrying `Option<&RefPic>` instead of
4874 // a bool makes "present" and "in range" the same fact.
4875 let rf0 = (refi0 >= 0).then(|| self.refs.get(refi0 as usize)).flatten();
4876 let rf1 = (refi1 >= 0).then(|| self.refs1.get(refi1 as usize)).flatten();
4877 let (a0, a1) = (rf0.is_some(), rf1.is_some());
4878 let [cu, cv] = c_pred;
4879 match (a0, a1, full) {
4880 (true, false, true) | (false, true, true) => {
4881 let (Some(rf), mv) = (if a0 { rf0 } else { rf1 }, if a0 { mv0 } else { mv1 }) else {
4882 return;
4883 };
4884 let (gu, gv) = (rf.chroma_guard(0, rf.ch), rf.chroma_guard(1, rf.ch));
4885 mc_chroma_padded_pair(&gu, &gv, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv.0, mv.1, &mut cu[cry * 8..cry * 8 + n], &mut cv[cry * 8..cry * 8 + n]);
4886 }
4887 (true, true, true) => {
4888 let (Some(rfa), Some(rfb)) = (rf0, rf1) else { return };
4889 {
4890 let rf = rfa;
4891 let (gu, gv) = (rf.chroma_guard(0, rf.ch), rf.chroma_guard(1, rf.ch));
4892 mc_chroma_padded_pair(&gu, &gv, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut cu[cry * 8..cry * 8 + n], &mut cv[cry * 8..cry * 8 + n]);
4893 }
4894 let (mut bu, mut bv) = ([0u8; 64], [0u8; 64]);
4895 {
4896 let rf = rfb;
4897 let (gu, gv) = (rf.chroma_guard(0, rf.ch), rf.chroma_guard(1, rf.ch));
4898 mc_chroma_padded_pair(&gu, &gv, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut bu[..n], &mut bv[..n]);
4899 }
4900 for (dst, stage) in [(&mut *cu, &bu), (&mut *cv, &bv)] {
4901 let d = &mut dst[cry * 8..cry * 8 + n];
4902 match weights {
4903 None => {
4904 for (o, q) in d.iter_mut().zip(&stage[..n]) {
4905 *o = ((*o as u16 + *q as u16 + 1) >> 1) as u8;
4906 }
4907 }
4908 Some((w0, w1)) => {
4909 for (o, q) in d.iter_mut().zip(&stage[..n]) {
4910 *o = ((*o as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
4911 }
4912 }
4913 }
4914 }
4915 }
4916 _ => {
4917 // Narrow region — rows are strided in the 8-wide pred planes;
4918 // stage per list (paired), then copy/blend strided per plane.
4919 let (mut au, mut av, mut bu, mut bv) = ([0u8; 64], [0u8; 64], [0u8; 64], [0u8; 64]);
4920 if let Some(rf) = rf0 {
4921 let (gu, gv) = (rf.chroma_guard(0, rf.ch), rf.chroma_guard(1, rf.ch));
4922 mc_chroma_padded_pair(&gu, &gv, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8 + crx, mb_y * 8 + cry, crw, crh, mv0.0, mv0.1, &mut au[..n], &mut av[..n]);
4923 }
4924 if let Some(rf) = rf1 {
4925 let (gu, gv) = (rf.chroma_guard(0, rf.ch), rf.chroma_guard(1, rf.ch));
4926 mc_chroma_padded_pair(&gu, &gv, rf.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8 + crx, mb_y * 8 + cry, crw, crh, mv1.0, mv1.1, &mut bu[..n], &mut bv[..n]);
4927 }
4928 for (dst, sa, sb) in [(&mut *cu, &au, &bu), (&mut *cv, &av, &bv)] {
4929 for dy in 0..crh {
4930 let base = (cry + dy) * 8 + crx;
4931 let d = &mut dst[base..base + crw];
4932 match (a0, a1) {
4933 (true, true) => {
4934 let (pr, qr) = (&sa[dy * crw..dy * crw + crw], &sb[dy * crw..dy * crw + crw]);
4935 match weights {
4936 None => {
4937 for ((o, pp), q) in d.iter_mut().zip(pr).zip(qr) {
4938 *o = ((*pp as u16 + *q as u16 + 1) >> 1) as u8;
4939 }
4940 }
4941 Some((w0, w1)) => {
4942 for ((o, pp), q) in d.iter_mut().zip(pr).zip(qr) {
4943 *o = ((*pp as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
4944 }
4945 }
4946 }
4947 }
4948 (true, false) => d.copy_from_slice(&sa[dy * crw..dy * crw + crw]),
4949 _ => d.copy_from_slice(&sb[dy * crw..dy * crw + crw]),
4950 }
4951 }
4952 }
4953 }
4954 }
4955 }
4956
4957 /// Commits a region's per-list motion to the 4×4 grids (and marks coded).
4958 #[allow(clippy::too_many_arguments)]
4959 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)) {
4960 let _gs = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBSet);
4961 let w4 = self.mb_w * 4;
4962 let mv0w = if refi0 >= 0 { mv0 } else { (0, 0) };
4963 let mv1w = if refi1 >= 0 { mv1 } else { (0, 0) };
4964 let by0 = mb_y * 4 + py / 4;
4965 let bx0 = mb_x * 4 + px / 4;
4966 let (bw, bh) = (rw / 4, rh / 4);
4967 // Contiguous per-row slices: fill instead of per-4x4 stores (same values).
4968 // SEVEN parallel grids, seven separate range checks — they are disjoint
4969 // fields of `self`, so one `get_mut` apiece resolves them all together
4970 // and a short row simply writes nothing rather than panicking.
4971 for by in by0..by0 + bh {
4972 let row = by * w4 + bx0;
4973 let end = row + bw;
4974 let (Some(r0), Some(m0), Some(r1), Some(m1)) = (
4975 self.ref_idx_y.get_mut(row..end),
4976 self.mv_y.get_mut(row..end),
4977 self.ref_idx1.get_mut(row..end),
4978 self.mv1.get_mut(row..end),
4979 ) else {
4980 continue;
4981 };
4982 r0.fill(refi0);
4983 m0.fill(mv0w);
4984 r1.fill(refi1);
4985 m1.fill(mv1w);
4986 let (Some(it), Some(cd), Some(md)) = (
4987 self.inter_y.get_mut(row..end),
4988 self.coded_y.get_mut(row..end),
4989 self.modes_y.get_mut(row..end),
4990 ) else {
4991 continue;
4992 };
4993 it.fill(true);
4994 cd.fill(true);
4995 md.fill(2);
4996 }
4997 }
4998
4999 /// Spatial direct prediction for a region (whole MB or an 8×8): derives the
5000 /// per-list reference indices and base MVs, then motion-compensates each 4×4
5001 /// sub-block (applying `colZeroFlag`) and commits the motion (spec §8.4.1.2.2).
5002 #[allow(clippy::too_many_arguments)]
5003 /// Splits a `w`×`h` block region (4×4-block units) into the fewest rectangles
5004 /// whose contents are `uniform`, preferring partition-shaped cuts (whole →
5005 /// horizontal halves → vertical halves → quadrants). Emits at most w·h rects
5006 /// (the all-different worst case degenerates to per-block, i.e. the old loop).
5007 fn coalesce_region<U: Fn(usize, usize, usize, usize) -> bool, E: FnMut(usize, usize, usize, usize)>(
5008 x: usize,
5009 y: usize,
5010 w: usize,
5011 h: usize,
5012 uniform: &U,
5013 emit: &mut E,
5014 ) {
5015 if uniform(x, y, w, h) {
5016 emit(x, y, w, h);
5017 return;
5018 }
5019 if h > 1 && uniform(x, y, w, h / 2) && uniform(x, y + h / 2, w, h / 2) {
5020 emit(x, y, w, h / 2);
5021 emit(x, y + h / 2, w, h / 2);
5022 return;
5023 }
5024 if w > 1 && uniform(x, y, w / 2, h) && uniform(x + w / 2, y, w / 2, h) {
5025 emit(x, y, w / 2, h);
5026 emit(x + w / 2, y, w / 2, h);
5027 return;
5028 }
5029 match (w > 1, h > 1) {
5030 (true, true) => {
5031 for q in 0..4usize {
5032 Self::coalesce_region(x + (q % 2) * (w / 2), y + (q / 2) * (h / 2), w / 2, h / 2, uniform, emit);
5033 }
5034 }
5035 (true, false) => {
5036 Self::coalesce_region(x, y, w / 2, h, uniform, emit);
5037 Self::coalesce_region(x + w / 2, y, w / 2, h, uniform, emit);
5038 }
5039 (false, true) => {
5040 Self::coalesce_region(x, y, w, h / 2, uniform, emit);
5041 Self::coalesce_region(x, y + h / 2, w, h / 2, uniform, emit);
5042 }
5043 (false, false) => emit(x, y, 1, 1),
5044 }
5045 }
5046
5047 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]) {
5048 if !self.direct_spatial {
5049 return self.decode_b_direct_temporal(mb_x, mb_y, px, py, rw, rh, pred_y, c_pred);
5050 }
5051 let (n0, n1) = self.b_direct_nbrs(mb_x, mb_y);
5052 self.decode_b_direct_n(mb_x, mb_y, px, py, rw, rh, pred_y, c_pred, n0, n1);
5053 }
5054
5055 /// Spec §8.4.1.2.2: the spatial-direct reference indices (min positive over
5056 /// the three neighbors, per list) and the predicted MVs. `direct_zero` is
5057 /// the both-lists-unavailable case, which forces ref 0 / mv (0,0). Shared
5058 /// by `decode_b_direct_n` and the B_Skip zero-bi fast path — ONE derivation,
5059 /// no drift.
5060 #[inline]
5061 fn b_direct_refs_mvs(n0: &[MvNeighbor; 3], n1: &[MvNeighbor; 3]) -> (i32, i32, (i32, i32), (i32, i32), bool) {
5062 let min_pos = |a: i32, b: i32| if a < 0 { b } else if b < 0 { a } else { a.min(b) };
5063 let rid = |n: &[MvNeighbor; 3]| min_pos(min_pos(n[0].ref_idx, n[1].ref_idx), n[2].ref_idx);
5064 let (mut refi0, mut refi1) = (rid(n0), rid(n1));
5065 let direct_zero = refi0 < 0 && refi1 < 0;
5066 if direct_zero {
5067 refi0 = 0;
5068 refi1 = 0;
5069 }
5070 let mv0 = if refi0 >= 0 && !direct_zero { predict_mv(n0[0], n0[1], n0[2], refi0) } else { (0, 0) };
5071 let mv1 = if refi1 >= 0 && !direct_zero { predict_mv(n1[0], n1[1], n1[2], refi1) } else { (0, 0) };
5072 (refi0, refi1, mv0, mv1, direct_zero)
5073 }
5074
5075 fn decode_b_direct_n(
5076 &mut self,
5077 mb_x: usize,
5078 mb_y: usize,
5079 px: usize,
5080 py: usize,
5081 rw: usize,
5082 rh: usize,
5083 pred_y: &mut [u8; 256],
5084 c_pred: &mut [[u8; 64]; 2],
5085 n0: [MvNeighbor; 3],
5086 n1: [MvNeighbor; 3],
5087 ) {
5088 let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDirect);
5089 // H-48: DERIVATION-ONLY scope, dropped before the MC loop below. DecBDirect
5090 // wraps this function whole and therefore INCLUDES the `b_mc` calls it makes,
5091 // so its 1460 ns/call was never "MV derivation is slow" — that read was wrong.
5092 // This guard is what separates the two.
5093 let gd = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBDeriv);
5094 let derived = Self::b_direct_refs_mvs(&n0, &n1);
5095 drop(gd);
5096 self.b_direct_region(mb_x, mb_y, px, py, rw, rh, pred_y, c_pred, derived);
5097 }
5098
5099 /// The post-derivation half of spatial direct: czg probing (gated), region
5100 /// coalescing, MC + motion commit. Split out so B_8x8 direct subs derive
5101 /// ONCE per MB (the A/B/C neighbours and the rid/median result are
5102 /// MB-level; only czg varies per 8x8).
5103 #[allow(clippy::too_many_arguments)]
5104 fn b_direct_region(
5105 &mut self,
5106 mb_x: usize,
5107 mb_y: usize,
5108 px: usize,
5109 py: usize,
5110 rw: usize,
5111 rh: usize,
5112 pred_y: &mut [u8; 256],
5113 c_pred: &mut [[u8; 64]; 2],
5114 derived: (i32, i32, (i32, i32), (i32, i32), bool),
5115 ) {
5116 let (refi0, refi1, mv0, mv1, direct_zero) = derived;
5117 // Per 4×4 sub-block: colZeroFlag zeroes the ref-0 motion vector. cz is the
5118 // ONLY per-block variable (two possible (m0,m1) values for the region), and
5119 // the MC filters + bi-blend are per-output-pixel — so sub-blocks with equal
5120 // cz coalesce into one wider `b_mc`, BIT-IDENTICAL. A 16×16 direct MB paid
5121 // 16 bi-pred b_mc calls (~96 MC kernel entries) before this; typically 1 now.
5122 let (bx0, by0, bw, bh) = (px / 4, py / 4, rw / 4, rh / 4);
5123 // MASKED at every use below. The loop bounds are the region's `bw`/`bh` in
5124 // 4x4 units, which are always <= 4 for a macroblock but are RUNTIME values,
5125 // so none of the eight indexes into this fixed 4x4 grid was provable.
5126 let mut czg = [[false; 4]; 4]; // region-local, [dy][dx]
5127 // colZeroFlag can only change a list whose ref is 0 AND whose predicted
5128 // MV is nonzero (it zeroes MVs; zeroing (0,0) is a no-op). When neither
5129 // list qualifies, skip the probing entirely: czg stays false, the
5130 // region is uniform, and the m values are identical — the only change
5131 // is FEWER b_mc calls where the old czg would have split rects with
5132 // equal values (tiles of the same math, byte-identical).
5133 let cz_matters = (refi0 == 0 && mv0 != (0, 0)) || (refi1 == 0 && mv1 != (0, 0));
5134 // Uniformity is KNOWN after the probe fill — when every probe agreed,
5135 // the 16-bool coalesce scan and its recursion are skipped outright.
5136 let mut cz_mixed = false;
5137 if cz_matters && !direct_zero && self.direct_8x8_inference {
5138 // Under direct_8x8_inference every 4×4 in an 8×8 shares one colZeroFlag
5139 // (col_block collapses to the MB-corner). Probe once per 8×8 — same
5140 // czg, fewer wait_motion_ready / meta locks on the live-ref path.
5141 let mut oy = 0usize;
5142 while oy < bh {
5143 let h = (bh - oy).min(2);
5144 let mut ox = 0usize;
5145 while ox < bw {
5146 let w = (bw - ox).min(2);
5147 let (colx, coly) = self.col_block(bx0 + ox, by0 + oy);
5148 let cz = self.col_zero(mb_x * 4 + colx, mb_y * 4 + coly);
5149 cz_mixed |= cz != czg[0][0] && !(ox == 0 && oy == 0);
5150 for dy in oy..oy + h {
5151 for dx in ox..ox + w {
5152 czg[dy & 3][dx & 3] = cz;
5153 }
5154 }
5155 ox += w;
5156 }
5157 oy += h;
5158 }
5159 } else if cz_matters && !direct_zero {
5160 for dy in 0..bh {
5161 for dx in 0..bw {
5162 let (colx, coly) = self.col_block(bx0 + dx, by0 + dy);
5163 let cz = self.col_zero(mb_x * 4 + colx, mb_y * 4 + coly);
5164 cz_mixed |= cz != czg[0][0] && !(dx == 0 && dy == 0);
5165 czg[dy & 3][dx & 3] = cz;
5166 }
5167 }
5168 }
5169 let mut rects: [(usize, usize, usize, usize); 16] = [(0, 0, 0, 0); 16];
5170 let mut n = 0usize;
5171 if !cz_mixed {
5172 rects[0] = (0, 0, bw, bh);
5173 n = 1;
5174 } else {
5175 let uniform = |x: usize, y: usize, w: usize, h: usize| -> bool {
5176 let t = czg[y & 3][x & 3];
5177 (y..y + h).all(|dy| (x..x + w).all(|dx| czg[dy & 3][dx & 3] == t))
5178 };
5179 Self::coalesce_region(0, 0, bw, bh, &uniform, &mut |x, y, w, h| {
5180 rects[n & 15] = (x, y, w, h);
5181 n += 1;
5182 });
5183 }
5184 let recording = self.edc_regions.is_some();
5185 if rw == 16 && rh == 16 {
5186 edcstat::bump(&edcstat::BSK_FULLMB, 1);
5187 if n == 1 {
5188 edcstat::bump(&edcstat::BSK_1RECT, 1);
5189 let cz = czg[0][0];
5190 let m0 = if refi0 == 0 && cz { (0, 0) } else { mv0 };
5191 let m1 = if refi1 == 0 && cz { (0, 0) } else { mv1 };
5192 let (a0, a1) = (refi0 >= 0, refi1 >= 0);
5193 if a0 && a1 && m0 == (0, 0) && m1 == (0, 0) {
5194 edcstat::bump(&edcstat::BSK_ZBI, 1);
5195 } else if (a0 != a1) && (if a0 { m0 } else { m1 }) == (0, 0) {
5196 edcstat::bump(&edcstat::BSK_ZUNI, 1);
5197 } else if (!a0 || (m0.0 % 4 == 0 && m0.1 % 4 == 0))
5198 && (!a1 || (m1.0 % 4 == 0 && m1.1 % 4 == 0))
5199 {
5200 edcstat::bump(&edcstat::BSK_FP, 1);
5201 }
5202 if self.bsk_last == Some((mb_x.wrapping_sub(1), mb_y, refi0, refi1, m0, m1)) {
5203 edcstat::bump(&edcstat::BSK_RUNCONT, 1);
5204 }
5205 self.bsk_last = Some((mb_x, mb_y, refi0, refi1, m0, m1));
5206 } else {
5207 self.bsk_last = None;
5208 }
5209 }
5210 for &(x, y, w, h) in &rects[..n] {
5211 let cz = czg[y & 3][x & 3];
5212 let m0 = if refi0 == 0 && cz { (0, 0) } else { mv0 };
5213 let m1 = if refi1 == 0 && cz { (0, 0) } else { mv1 };
5214 let (lx, ly, lw, lh) = ((bx0 + x) * 4, (by0 + y) * 4, w * 4, h * 4);
5215 if recording {
5216 self.b_mc_or_record(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1, pred_y, c_pred);
5217 } else {
5218 self.b_mc(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1, pred_y, c_pred);
5219 }
5220 self.b_set_motion(mb_x, mb_y, lx, ly, lw, lh, refi0, m0, refi1, m1);
5221 }
5222 }
5223
5224 /// Temporal direct prediction for a region (spec §8.4.1.2.3): for each 4×4
5225 /// (or per-8×8 corner under `direct_8x8_inference`), take the co-located
5226 /// List-0 motion from `RefPicList1[0]`, map its reference into the current
5227 /// List-0 by POC, and scale the motion vector by the POC distances.
5228 #[allow(clippy::too_many_arguments)]
5229 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]) {
5230 let poc1 = self.refs1.first().map_or(0, |f| f.pic_poc());
5231 let infer = self.direct_8x8_inference;
5232 // Under direct_8x8_inference every 4×4 in an 8×8 takes the same MB-corner
5233 // co-located motion, so motion-compensate the whole 8×8 in one call — this
5234 // hits the width-8 MC asm and pays the per-call tile/blend setup 4× less.
5235 // Without inference, motion is genuinely per-4×4. Bit-identical either way
5236 // (MC of an 8×8 with one MV == four 4×4 MCs with that same MV).
5237 let step = if infer { 8 } else { 4 };
5238 let mut sy = py;
5239 while sy < py + rh {
5240 let mut sx = px;
5241 while sx < px + rw {
5242 // Co-located 4×4 (the 8×8's MB-corner under inference) — shared with
5243 // the spatial path's colZeroFlag, which must map identically.
5244 let (colx, coly) = self.col_block(sx / 4, sy / 4);
5245 let (mvcol, refpoc) = {
5246 let Some(col) = self.refs1.first() else { return };
5247 if let Some(live) = col.live.as_ref() {
5248 col.wait_motion_ready();
5249 let meta = live.meta.read().unwrap();
5250 let idx = (mb_y * 4 + coly) * meta.w4 + (mb_x * 4 + colx);
5251 // `mv` and `ref_poc` are PARALLEL Vecs: the `mv.len()`
5252 // guard said nothing about `ref_poc`.
5253 match (meta.mv.get(idx), meta.ref_poc.get(idx)) {
5254 (Some(&m), Some(&pc)) if meta.w4 != 0 && pc != i32::MIN => (m, pc),
5255 _ => ((0, 0), i32::MIN),
5256 }
5257 } else {
5258 let idx = (mb_y * 4 + coly) * col.w4 + (mb_x * 4 + colx);
5259 // intra co-located → zero motion, refIdxL0 = 0
5260 match (col.mv.get(idx), col.ref_poc.get(idx)) {
5261 (Some(&m), Some(&pc)) if col.w4 != 0 && pc != i32::MIN => (m, pc),
5262 _ => ((0, 0), i32::MIN),
5263 }
5264 }
5265 };
5266 // MapColToList0: the current-list index of the co-located reference.
5267 let (refi0, mvc) = if refpoc == i32::MIN {
5268 (0, (0, 0))
5269 } else {
5270 let r = self
5271 .refs
5272 .iter()
5273 .position(|f| f.pic_poc() == refpoc)
5274 .unwrap_or(0) as i32;
5275 (r, mvcol)
5276 };
5277 let Some(r0) = self.refs.get(refi0 as usize) else { return };
5278 let poc0 = r0.pic_poc();
5279 let td = (poc1 - poc0).clamp(-128, 127);
5280 let tb = (self.cur_poc - poc0).clamp(-128, 127);
5281 let (mv0, mv1) = if td == 0 || r0.long_term {
5282 (mvc, (0, 0))
5283 } else {
5284 let tx = tx_for_td(td);
5285 let dsf = ((tb * tx + 32) >> 6).clamp(-1024, 1023);
5286 let m0 = ((dsf * mvc.0 + 128) >> 8, (dsf * mvc.1 + 128) >> 8);
5287 (m0, (m0.0 - mvc.0, m0.1 - mvc.1))
5288 };
5289 self.b_mc_or_record(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1, pred_y, c_pred);
5290 self.b_set_motion(mb_x, mb_y, sx, sy, step, step, refi0, mv0, 0, mv1);
5291 sx += step;
5292 }
5293 sy += step;
5294 }
5295 }
5296
5297 /// Reads `ref_idx_lX` for a B partition (te(v)/ue(v) by the list's active
5298 /// count), bounds-checked against the available reference count.
5299 fn read_b_ref(&self, r: &mut BitReader, list: usize) -> Result<i32, MbError> {
5300 let (active, avail) = if list == 0 {
5301 (self.num_ref_active, self.refs.len())
5302 } else {
5303 (self.num_ref_active1, self.refs1.len())
5304 };
5305 let v = if active > 1 { read_ref_idx(r, active)? } else { 0 };
5306 if v as usize >= avail {
5307 return Err(MbError::Truncated);
5308 }
5309 Ok(v)
5310 }
5311
5312 /// Reconstructs a `B_Skip` macroblock: spatial-direct prediction, no residual.
5313 /// Defer one fast B_Skip's grid commit + recon into the pending row span.
5314 /// Continuation requires position adjacency AND kind equality.
5315 #[inline]
5316 fn bz_push(&mut self, mb_x: usize, mb_y: usize, kind: BzKind) {
5317 match self.bzspan {
5318 Some((row, x0, ref mut n, k)) if row == mb_y && x0 + *n == mb_x && k == kind => {
5319 *n += 1
5320 }
5321 _ => {
5322 self.bz_flush();
5323 self.bzspan = Some((mb_y, mb_x, 1, kind));
5324 }
5325 }
5326 }
5327
5328 /// Range-fill the pending span's grids. MUST run before anything reads
5329 /// the motion/coded/mode/nnz grids of a deferred MB: decode_b_mb entry,
5330 /// decode_b_skip's b_direct_nbrs, bS derivation (derive_bs_row callers
5331 /// via row_hook + deblock), and slice end (edc_flush covers it).
5332 #[inline(always)]
5333 fn bz_flush(&mut self) {
5334 if self.bzspan.is_some() {
5335 self.bz_flush_slow();
5336 }
5337 }
5338
5339 fn bz_flush_slow(&mut self) {
5340 let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BSpanRecon);
5341 let Some((row, x0, n, kind)) = self.bzspan.take() else { return };
5342 edcstat::bump(&edcstat::BZ_SPANS, 1);
5343 edcstat::bump(&edcstat::BZ_SPAN_MBS, n as u64);
5344 // Grid values per kind (mirrors b_set_motion's inactive-list zeroing).
5345 let (g_r0, g_r1, g_m0, g_m1): (i32, i32, (i32, i32), (i32, i32)) = match kind {
5346 BzKind::ZeroBi => (0, 0, (0, 0), (0, 0)),
5347 BzKind::ZeroUni(0, ri) => (ri as i32, -1, (0, 0), (0, 0)),
5348 BzKind::ZeroUni(_, ri) => (-1, ri as i32, (0, 0), (0, 0)),
5349 BzKind::Fp { r0, r1, m0, m1 } => {
5350 let mm0 = if r0 >= 0 { (m0.0 as i32, m0.1 as i32) } else { (0, 0) };
5351 let mm1 = if r1 >= 0 { (m1.0 as i32, m1.1 as i32) } else { (0, 0) };
5352 (r0 as i32, r1 as i32, mm0, mm1)
5353 }
5354 };
5355 let w4 = self.mb_w * 4;
5356 let (b0, len) = (x0 * 4, n * 4);
5357 for dy in 0..4 {
5358 let a = (row * 4 + dy) * w4 + b0;
5359 self.ref_idx_y[a..a + len].fill(g_r0);
5360 self.mv_y[a..a + len].fill(g_m0);
5361 self.ref_idx1[a..a + len].fill(g_r1);
5362 self.mv1[a..a + len].fill(g_m1);
5363 self.inter_y[a..a + len].fill(true);
5364 self.coded_y[a..a + len].fill(true);
5365 self.modes_y[a..a + len].fill(2);
5366 self.nnz_y[a..a + len].fill(0);
5367 }
5368 match kind {
5369 BzKind::ZeroBi => self.bz_recon_band_bi(row, x0, n),
5370 BzKind::ZeroUni(list, ri) => self.bz_recon_band_copy(row, x0, n, list as usize, ri as usize, (0, 0)),
5371 BzKind::Fp { r0, r1, m0, m1 } => {
5372 let mv0 = (m0.0 as i32, m0.1 as i32);
5373 let mv1 = (m1.0 as i32, m1.1 as i32);
5374 match (r0 >= 0, r1 >= 0) {
5375 (true, true) => self.bz_recon_band_fp_bi(row, x0, n, r0 as usize, r1 as usize, mv0, mv1),
5376 (true, false) => self.bz_recon_band_copy(row, x0, n, 0, r0 as usize, mv0),
5377 _ => self.bz_recon_band_copy(row, x0, n, 1, r1 as usize, mv1),
5378 }
5379 }
5380 }
5381 }
5382
5383 /// ZeroBi band: rows of 16n averaged from both padded refs (index 0).
5384 fn bz_recon_band_bi(&mut self, row: usize, x0: usize, n: usize) {
5385 // Byte-identical to n recon_b_skip_zero_bi(_, _, 0, 0) calls: each
5386 // output byte is the same (a + b + 1) >> 1 of the same source bytes.
5387 let w = n * 16;
5388 let Some(rf0) = self.refs.first() else { return };
5389 let Some(rf1) = self.refs1.first() else { return };
5390 let (l0st, l1st) = (rf0.lstride(), rf1.lstride());
5391 {
5392 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
5393 let (ly0, ly1) = (rf0.luma_guard(rf0.ch), rf1.luma_guard(rf1.ch));
5394 for dy in 0..16 {
5395 let y = row * 16 + dy;
5396 let s0 = (y + crate::LPAD) * l0st + crate::LPAD + x0 * 16;
5397 let s1 = (y + crate::LPAD) * l1st + crate::LPAD + x0 * 16;
5398 let d = y * self.cw + x0 * 16;
5399 for ((dst, a), b) in self.rec_y[d..d + w].iter_mut().zip(&ly0[s0..s0 + w]).zip(&ly1[s1..s1 + w]) {
5400 *dst = ((*a as u16 + *b as u16 + 1) >> 1) as u8;
5401 }
5402 }
5403 }
5404 let (c0st, c1st) = (rf0.cstride(), rf1.cstride());
5405 let wc = n * 8;
5406 for c in 0..2 {
5407 let rc0 = rf0.chroma_guard(c, rf0.ch);
5408 let rc1 = rf1.chroma_guard(c, rf1.ch);
5409 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
5410 for dy in 0..8 {
5411 let y = row * 8 + dy;
5412 let s0 = (y + crate::CPAD) * c0st + crate::CPAD + x0 * 8;
5413 let s1 = (y + crate::CPAD) * c1st + crate::CPAD + x0 * 8;
5414 let d = y * self.ccw + x0 * 8;
5415 for ((dst, a), b) in plane[d..d + wc].iter_mut().zip(&rc0[s0..s0 + wc]).zip(&rc1[s1..s1 + wc]) {
5416 *dst = ((*a as u16 + *b as u16 + 1) >> 1) as u8;
5417 }
5418 }
5419 }
5420 }
5421
5422 /// Full-pel offset copy band from ONE list's padded planes. The caller
5423 /// prevalidated (per MB, at push): mv%8 == 0 both components and luma +
5424 /// chroma windows inside the padded planes — contiguous MB windows tile,
5425 /// so the span's union window is valid by induction.
5426 #[allow(clippy::too_many_arguments)]
5427 fn bz_recon_band_copy(&mut self, row: usize, x0: usize, n: usize, list: usize, ri: usize, mv: (i32, i32)) {
5428 let Some(rf) = (if list == 0 { self.refs.get(ri) } else { self.refs1.get(ri) }) else {
5429 return;
5430 };
5431 let lst = rf.lstride();
5432 let (dx, dy) = ((mv.0 / 4) as isize, (mv.1 / 4) as isize);
5433 let w = n * 16;
5434 {
5435 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
5436 let ly = rf.luma_guard(rf.ch);
5437 for r in 0..16 {
5438 let y = (row * 16 + r) as isize + dy;
5439 let src = ((y + crate::LPAD as isize) * lst as isize
5440 + crate::LPAD as isize
5441 + x0 as isize * 16
5442 + dx) as usize;
5443 let d = (row * 16 + r) * self.cw + x0 * 16;
5444 self.rec_y[d..d + w].copy_from_slice(&ly[src..src + w]);
5445 }
5446 }
5447 let cst = rf.cstride();
5448 let (cdx, cdy) = ((mv.0 / 8) as isize, (mv.1 / 8) as isize);
5449 let wc = n * 8;
5450 for c in 0..2 {
5451 let rc = rf.chroma_guard(c, rf.ch);
5452 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
5453 for r in 0..8 {
5454 let y = (row * 8 + r) as isize + cdy;
5455 let src = ((y + crate::CPAD as isize) * cst as isize
5456 + crate::CPAD as isize
5457 + x0 as isize * 8
5458 + cdx) as usize;
5459 let d = (row * 8 + r) * self.ccw + x0 * 8;
5460 plane[d..d + wc].copy_from_slice(&rc[src..src + wc]);
5461 }
5462 }
5463 }
5464
5465 /// Full-pel bi band: rows of 16n averaged from two offset windows
5466 /// (prevalidated as above; implicit weights None/(32,32) guaranteed by
5467 /// the pushing arm).
5468 #[allow(clippy::too_many_arguments)]
5469 fn bz_recon_band_fp_bi(&mut self, row: usize, x0: usize, n: usize, r0: usize, r1: usize, mv0: (i32, i32), mv1: (i32, i32)) {
5470 let (Some(rf0), Some(rf1)) = (self.refs.get(r0), self.refs1.get(r1)) else {
5471 return;
5472 };
5473 let (l0, l1) = (rf0.lstride(), rf1.lstride());
5474 let off = |st: usize, pad: isize, yy: isize, xx: isize| (((yy + pad) * st as isize) + pad + xx) as usize;
5475 let w = n * 16;
5476 {
5477 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
5478 let (ly0, ly1) = (rf0.luma_guard(rf0.ch), rf1.luma_guard(rf1.ch));
5479 for r in 0..16 {
5480 let y = (row * 16 + r) as isize;
5481 let s0 = off(l0, crate::LPAD as isize, y + (mv0.1 / 4) as isize, x0 as isize * 16 + (mv0.0 / 4) as isize);
5482 let s1 = off(l1, crate::LPAD as isize, y + (mv1.1 / 4) as isize, x0 as isize * 16 + (mv1.0 / 4) as isize);
5483 let d = (row * 16 + r) * self.cw + x0 * 16;
5484 for ((dst, a), b) in self.rec_y[d..d + w].iter_mut().zip(&ly0[s0..s0 + w]).zip(&ly1[s1..s1 + w]) {
5485 *dst = ((*a as u16 + *b as u16 + 1) >> 1) as u8;
5486 }
5487 }
5488 }
5489 let (c0, c1) = (rf0.cstride(), rf1.cstride());
5490 let wc = n * 8;
5491 for c in 0..2 {
5492 let rc0 = rf0.chroma_guard(c, rf0.ch);
5493 let rc1 = rf1.chroma_guard(c, rf1.ch);
5494 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
5495 for r in 0..8 {
5496 let y = (row * 8 + r) as isize;
5497 let s0 = off(c0, crate::CPAD as isize, y + (mv0.1 / 8) as isize, x0 as isize * 8 + (mv0.0 / 8) as isize);
5498 let s1 = off(c1, crate::CPAD as isize, y + (mv1.1 / 8) as isize, x0 as isize * 8 + (mv1.0 / 8) as isize);
5499 let d = (row * 8 + r) * self.ccw + x0 * 8;
5500 for ((dst, a), b) in plane[d..d + wc].iter_mut().zip(&rc0[s0..s0 + wc]).zip(&rc1[s1..s1 + wc]) {
5501 *dst = ((*a as u16 + *b as u16 + 1) >> 1) as u8;
5502 }
5503 }
5504 }
5505 }
5506
5507 /// Per-MB prevalidation for an Fp span push: mv%8 both components (chroma
5508 /// copies at mv/8) and luma + chroma windows inside the padded planes for
5509 /// every active list. False ⇒ the arm recons immediately instead.
5510 fn bz_fp_valid(&self, mbx: usize, mby: usize, r0: Option<usize>, r1: Option<usize>, mv0: (i32, i32), mv1: (i32, i32)) -> bool {
5511 let chk = |rf: &crate::Ref, mv: (i32, i32)| -> bool {
5512 if mv.0 % 8 != 0 || mv.1 % 8 != 0 {
5513 return false;
5514 }
5515 let lst = rf.lstride();
5516 let lpad = crate::LPAD as isize;
5517 let cx = mbx as isize * 16 + (mv.0 / 4) as isize + lpad;
5518 let cy = mby as isize * 16 + (mv.1 / 4) as isize + lpad;
5519 let lrows = rf.lrows() as isize;
5520 if cx < 0 || cx + 16 > lst as isize || cy < 0 || cy + 16 > lrows {
5521 return false;
5522 }
5523 let cst = rf.cstride();
5524 let cpad = crate::CPAD as isize;
5525 let ccx = mbx as isize * 8 + (mv.0 / 8) as isize + cpad;
5526 let ccy = mby as isize * 8 + (mv.1 / 8) as isize + cpad;
5527 let crows = rf.crows() as isize;
5528 ccx >= 0 && ccx + 8 <= cst as isize && ccy >= 0 && ccy + 8 <= crows
5529 };
5530 r0.is_none_or(|i| self.refs.get(i).is_some_and(|r| chk(r, mv0)))
5531 && r1.is_none_or(|i| self.refs1.get(i).is_some_and(|r| chk(r, mv1)))
5532 }
5533
5534 /// Defer one (0,0) P_Skip's grid commit into the pending P span.
5535 #[inline]
5536 fn pz_push(&mut self, mb_x: usize, mb_y: usize, recon: bool) {
5537 match self.pzspan {
5538 Some((row, x0, ref mut n, r)) if row == mb_y && x0 + *n == mb_x && r == recon => {
5539 *n += 1
5540 }
5541 _ => {
5542 self.pz_flush();
5543 self.pzspan = Some((mb_y, mb_x, 1, recon));
5544 }
5545 }
5546 }
5547
5548 /// Range-fill the pending P span's grids (the deferral constants of
5549 /// decode_p_skip's commit: list-0 ref 0, mv (0,0), inter, coded, DC mode,
5550 /// deblock kind SKIP). Same flush points as the B span (span_flush).
5551 #[inline(always)]
5552 fn pz_flush(&mut self) {
5553 if self.pzspan.is_some() {
5554 self.pz_flush_slow();
5555 }
5556 }
5557
5558 fn pz_flush_slow(&mut self) {
5559 let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BSpanRecon);
5560 let Some((row, x0, n, recon)) = self.pzspan.take() else { return };
5561 edcstat::bump(&edcstat::PZ_SPANS, 1);
5562 edcstat::bump(&edcstat::PZ_SPAN_MBS, n as u64);
5563 let w4 = self.mb_w * 4;
5564 let (b0, len) = (x0 * 4, n * 4);
5565 for dy in 0..4 {
5566 let a = (row * 4 + dy) * w4 + b0;
5567 self.mv_y[a..a + len].fill((0, 0));
5568 self.inter_y[a..a + len].fill(true);
5569 self.ref_idx_y[a..a + len].fill(0);
5570 self.coded_y[a..a + len].fill(true);
5571 self.modes_y[a..a + len].fill(2);
5572 }
5573 self.mb_kind[row * self.mb_w + x0..row * self.mb_w + x0 + n]
5574 .fill(rusty_h264_common::deblock::MB_KIND_SKIP);
5575 if recon {
5576 // The recon deferred with the commit: one band copy from ref[0]
5577 // replaces n queued EdcJob::Skip pushes AND the flush-time
5578 // run-coalescing scan that used to rediscover this very run.
5579 self.recon_p_skip_band(x0, row, n);
5580 }
5581 }
5582
5583 /// Flush BOTH pending grid spans — the one call every grid reader makes.
5584 #[inline]
5585 fn span_flush(&mut self) {
5586 self.bz_flush();
5587 self.pz_flush();
5588 }
5589
5590 /// Hot prefix of decode_b_skip: the FORCED zero-bi run continuation,
5591 /// small enough to inline into the slice loops. Returns true when the MB
5592 /// was fully handled (deferred into the span); false falls to the cold
5593 /// body. Exactly the forced arm's conditions — no behavior change.
5594 #[inline(always)]
5595 fn b_skip_hot(&mut self, mb_x: usize, mb_y: usize) -> bool {
5596 if !self.direct_spatial || self.edc_tx.is_some() || self.k_no_bskipfast {
5597 return false;
5598 }
5599 let mbw = self.mb_w;
5600 let addr = mb_y * mbw + mb_x;
5601 // ONE slice that ENDS at `addr` proves all three neighbour reads. Each
5602 // was a separate check against the whole `bzero` grid even though every
5603 // index is strictly below `addr` (the guards above establish that), and
5604 // the trailing `bzero[addr] = true` a fourth. `get(..=addr)` states the
5605 // bound once, in the form the neighbour indexes are already inside.
5606 // The `get(..=addr)` slice did NOT fold these: it bounds the reads from
5607 // above, but LLVM still has to chain `mb_x > 0 => addr >= 1` and
5608 // `mb_y > 0 => addr >= mbw` through the `&&` sequence, and it will not.
5609 // Fallible reads state each one where it is made; a missing neighbour is
5610 // "not zero", which is the conservative answer this guard wants.
5611 let up = addr.wrapping_sub(mbw);
5612 if !(mb_x > 0
5613 && mb_y > 0
5614 && mb_x + 1 < mbw
5615 && up >= self.slice_first_mb
5616 && self.bzero.get(addr - 1).copied().unwrap_or(false)
5617 && self.bzero.get(up).copied().unwrap_or(false)
5618 && self.bzero.get(up + 1).copied().unwrap_or(false))
5619 {
5620 return false;
5621 }
5622 let wgt = self.iw00();
5623 if !(wgt.is_none() || wgt == Some((32, 32))) {
5624 return false;
5625 }
5626 self.route_skip_mbs += 1;
5627 edcstat::bump(&edcstat::BSKB_FORCED, 1);
5628 let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BSkipHot);
5629 edcstat::bump(&edcstat::BSKB_FAST, 1);
5630 self.bz_push(mb_x, mb_y, BzKind::ZeroBi);
5631 if let Some(b) = self.bzero.get_mut(addr) {
5632 *b = true;
5633 }
5634 true
5635 }
5636
5637 /// Zero this macroblock's 16 luma nnz entries, walking rows by adding `w4`
5638 /// instead of recomputing `(mb_y * 4 + dy) * w4 + mb_x * 4` four times.
5639 /// Shared by the three sites in decode_b_skip that had it open-coded.
5640 #[inline]
5641 fn clear_mb_nnz(&mut self, mb_x: usize, mb_y: usize, w4: usize) {
5642 let mut a = (mb_y * 4) * w4 + mb_x * 4;
5643 for _ in 0..4 {
5644 self.nnz_y[a..a + 4].fill(0);
5645 a += w4;
5646 }
5647 }
5648
5649 /// The B_Skip SLOW half: full spatial-direct recon plus the zero-residual
5650 /// plane copy. Split out so `pred_y`/`c_pred` (384 B) are built ONLY when a
5651 /// macroblock actually reaches it — the zero-bi / zero-uni / full-pel fast
5652 /// paths in decode_b_skip all return before this, and on LIGHT streams they
5653 /// take 90%+ of the calls.
5654 fn b_skip_slow(
5655 &mut self,
5656 mb_x: usize,
5657 mb_y: usize,
5658 nbrs: Option<([MvNeighbor; 3], [MvNeighbor; 3])>,
5659 w4: usize,
5660 ) -> Result<(), MbError> {
5661 let mut pred_y = [0u8; 256];
5662 let mut c_pred = [[0u8; 64]; 2];
5663 match nbrs {
5664 // Reuses the already-probed neighbours — the grid walk is the
5665 // expensive half of the derivation and paying it twice leaned on
5666 // fall-through-heavy streams (stockholm).
5667 Some((n0, n1)) => {
5668 self.decode_b_direct_n(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred, n0, n1)
5669 }
5670 None => self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred),
5671 }
5672 if let Some(regions) = self.edc_regions.take() {
5673 // nnz clears are PARSE state; the pixel copy is the worker's.
5674 self.clear_mb_nnz(mb_x, mb_y, w4);
5675 self.edc_giveback();
5676 self.edc_send_job(EdcJob::BSkip { mbx: mb_x, mby: mb_y, regions });
5677 return Ok(());
5678 }
5679 // Zero residual: the prediction IS the reconstruction — copy it row-wise,
5680 // stepping the destination by the stride rather than multiplying per row.
5681 let mut d = (mb_y * 16) * self.cw + mb_x * 16;
5682 for dy in 0..16 {
5683 self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
5684 d += self.cw;
5685 }
5686 let (ccw, cx0) = (self.ccw, mb_x * 8);
5687 for c in 0..2 {
5688 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
5689 let mut d = (mb_y * 8) * ccw + cx0;
5690 for dy in 0..8 {
5691 plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
5692 d += ccw;
5693 }
5694 }
5695 // nnz stays 0 (no residual) — clear the grids for neighbour context.
5696 self.clear_mb_nnz(mb_x, mb_y, w4);
5697 Ok(())
5698 }
5699
5700 fn decode_b_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
5701 let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::BSkipCold);
5702 self.route_skip_mbs += 1;
5703 self.wait_refs_for_mb(mb_y);
5704 // Each list length read ONCE: the emptiness test and every later
5705 // `len() - 1` clamp (six reads in all) come from these two.
5706 let (nref0, nref1) = (self.refs.len(), self.refs1.len());
5707 if nref0 == 0 || nref1 == 0 {
5708 return Err(MbError::Unsupported("B without references"));
5709 }
5710 let (lref0, lref1) = (nref0 - 1, nref1 - 1);
5711 let mbw = self.mb_w;
5712 let addr = mb_y * mbw + mb_x;
5713 let w4 = mbw * 4;
5714 // ZERO-BI FAST PATH: when spatial direct derives (0,0)/(0,0) bi motion,
5715 // colZeroFlag is IRRELEVANT (it only zeroes MVs that are already zero),
5716 // so the colocated probing, region split, b_mc staging and the pred
5717 // round-trip all collapse into one fused row-average from the two
5718 // padded refs straight into the recon planes. b_mc's bi blend applies
5719 // only IMPLICIT weights, so `None` or (32,32) (both exactly
5720 // (a+b+1)>>1) is the whole identity condition. FourPeople-class LIGHT
5721 // streams put 90%+ of their B_Skips here (BSK_ZBI counter).
5722 // pred_y / c_pred now live in `b_skip_slow` — see there.
5723 if self.direct_spatial && self.edc_tx.is_none() && !self.k_no_bskipfast {
5724 // (The FORCED zero-bi run continuation lives in b_skip_hot, inlined
5725 // at the loop call sites — this cold body only sees non-forced MBs.)
5726 // A pending span can only occupy the LEFT gather position, and the
5727 // left member's committed grid values are fully determined by the
5728 // span KIND — synthesize them instead of flushing, for EVERY kind
5729 // (the same mapping bz_flush writes).
5730 let patch = match self.bzspan {
5731 Some((r, x0, n, k)) if r == mb_y && x0 + n == mb_x => Some(k),
5732 _ => None,
5733 };
5734 if patch.is_none() {
5735 self.bz_flush();
5736 }
5737 let (mut n0, mut n1) = self.b_direct_nbrs(mb_x, mb_y);
5738 if let Some(k) = patch {
5739 let (r0, r1, m0, m1): (i32, i32, (i32, i32), (i32, i32)) = match k {
5740 BzKind::ZeroBi => (0, 0, (0, 0), (0, 0)),
5741 BzKind::ZeroUni(0, ri) => (ri as i32, -1, (0, 0), (0, 0)),
5742 BzKind::ZeroUni(_, ri) => (-1, ri as i32, (0, 0), (0, 0)),
5743 BzKind::Fp { r0, r1, m0, m1 } => {
5744 let mm0 = if r0 >= 0 { (m0.0 as i32, m0.1 as i32) } else { (0, 0) };
5745 let mm1 = if r1 >= 0 { (m1.0 as i32, m1.1 as i32) } else { (0, 0) };
5746 (r0 as i32, r1 as i32, mm0, mm1)
5747 }
5748 };
5749 n0[0] = MvNeighbor { available: true, mv: m0, ref_idx: r0 };
5750 n1[0] = MvNeighbor { available: true, mv: m1, ref_idx: r1 };
5751 }
5752 let (refi0, refi1, mv0, mv1, _dz) = Self::b_direct_refs_mvs(&n0, &n1);
5753 let (a0, a1) = (refi0 >= 0, refi1 >= 0);
5754 let fast = if a0 && a1 && mv0 == (0, 0) && mv1 == (0, 0) {
5755 // Same malformed-stream ref clamp as b_mc.
5756 let r0 = (refi0 as usize).min(lref0);
5757 let r1 = (refi1 as usize).min(lref1);
5758 // (0,0) dominates here and is already cached slice-wide by iw00.
5759 let wgt = if r0 == 0 && r1 == 0 {
5760 self.iw00()
5761 } else {
5762 self.implicit_weights(r0 as i32, r1 as i32)
5763 };
5764 if wgt.is_none() || wgt == Some((32, 32)) {
5765 if refi0 == 0 && refi1 == 0 {
5766 if let Some(b) = self.bzero.get_mut(addr) {
5767 *b = true;
5768 }
5769 // Same constants as the forced arm: recon AND commit
5770 // both defer into the span.
5771 self.bz_push(mb_x, mb_y, BzKind::ZeroBi);
5772 edcstat::bump(&edcstat::BSKB_FAST, 1);
5773 return Ok(());
5774 }
5775 self.recon_b_skip_zero_bi(mb_x, mb_y, r0, r1);
5776 true
5777 } else {
5778 false
5779 }
5780 } else if a0 != a1 && (if a0 { mv0 } else { mv1 }) == (0, 0) {
5781 // ZERO-UNI: one active list at (0,0) — b_mc's uni arms are plain
5782 // unweighted copies. Defer as a memcpy-band span.
5783 let (list, ri) = if a0 {
5784 (0u8, (refi0 as usize).min(lref0) as u8)
5785 } else {
5786 (1u8, (refi1 as usize).min(lref1) as u8)
5787 };
5788 self.bz_push(mb_x, mb_y, BzKind::ZeroUni(list, ri));
5789 edcstat::bump(&edcstat::BSKB_FAST, 1);
5790 return Ok(());
5791 } else if (!a0 || (mv0.0 % 4 == 0 && mv0.1 % 4 == 0))
5792 && (!a1 || (mv1.0 % 4 == 0 && mv1.1 % 4 == 0))
5793 && self.direct_8x8_inference
5794 {
5795 // FULL-PEL arm (the pan case — shields): nonzero MVs make
5796 // colZeroFlag matter, so probe the four 8x8 corners; a UNIFORM
5797 // czg keeps the MB one region and the MC is an offset read.
5798 // Non-uniform czg or an out-of-pad window falls through.
5799 let mut cz_ok = true;
5800 let mut czv = false;
5801 // Same cz-relevance gate as decode_b_direct_n: only a ref-0
5802 // list with a nonzero MV can be changed by colZeroFlag (fp-arm
5803 // MVs are nonzero by construction, so refs alone decide).
5804 let cz_need = refi0 == 0 || refi1 == 0;
5805 // LOOP-INVARIANT: this gated a `break` INSIDE the probe loop, so
5806 // a fact known before the loop was re-tested on every probe.
5807 if cz_need {
5808 // Absolute block base, hoisted out of the four probes.
5809 let (bx4, by4) = (mb_x * 4, mb_y * 4);
5810 for (k, &(ox, oy)) in [(0usize, 0usize), (2, 0), (0, 2), (2, 2)].iter().enumerate() {
5811 // col_block takes MB-LOCAL block coords (the whole MB is
5812 // the region here); col_zero takes absolute — same
5813 // convention as decode_b_direct_n's probe loop.
5814 let (colx, coly) = self.col_block(ox, oy);
5815 let cz = self.col_zero(bx4 + colx, by4 + coly);
5816 if k == 0 {
5817 czv = cz;
5818 } else if cz != czv {
5819 cz_ok = false;
5820 break;
5821 }
5822 }
5823 }
5824 if cz_ok {
5825 let m0 = if refi0 == 0 && czv { (0, 0) } else { mv0 };
5826 let m1 = if refi1 == 0 && czv { (0, 0) } else { mv1 };
5827 let r0c = (refi0 as usize).min(lref0);
5828 let r1c = (refi1 as usize).min(lref1);
5829 let (or0, or1) = (a0.then_some(r0c), a1.then_some(r1c));
5830 let wgt_ok = !(a0 && a1) || {
5831 let wgt = self.implicit_weights(r0c as i32, r1c as i32);
5832 wgt.is_none() || wgt == Some((32, 32))
5833 };
5834 // SPAN path: prevalidated windows + %8 chroma ⇒ defer as
5835 // an offset copy/avg band; otherwise the immediate per-MB
5836 // recon below (which handles interpolating chroma).
5837 if wgt_ok && self.bz_fp_valid(mb_x, mb_y, or0, or1, m0, m1) {
5838 let kind = BzKind::Fp {
5839 r0: if a0 { r0c as i8 } else { -1 },
5840 r1: if a1 { r1c as i8 } else { -1 },
5841 m0: (m0.0 as i16, m0.1 as i16),
5842 m1: (m1.0 as i16, m1.1 as i16),
5843 };
5844 self.bz_push(mb_x, mb_y, kind);
5845 edcstat::bump(&edcstat::BSKB_FP, 1);
5846 return Ok(());
5847 }
5848 let ok = wgt_ok && self.recon_b_skip_fp(mb_x, mb_y, or0, or1, m0, m1);
5849 if ok {
5850 edcstat::bump(&edcstat::BSKB_FP, 1);
5851 self.b_set_motion(mb_x, mb_y, 0, 0, 16, 16, refi0, m0, refi1, m1);
5852 self.clear_mb_nnz(mb_x, mb_y, w4);
5853 return Ok(());
5854 }
5855 }
5856 false
5857 } else {
5858 false
5859 };
5860 if fast {
5861 edcstat::bump(&edcstat::BSKB_FAST, 1);
5862 self.b_set_motion(mb_x, mb_y, 0, 0, 16, 16, refi0, (0, 0), refi1, (0, 0));
5863 self.clear_mb_nnz(mb_x, mb_y, w4);
5864 return Ok(());
5865 }
5866 // Fall-through hands the ALREADY-probed neighbours to the slow
5867 // half so the grid walk is not paid twice.
5868 return self.b_skip_slow(mb_x, mb_y, Some((n0, n1)), w4);
5869 } else {
5870 if self.edc_tx.is_some() {
5871 self.edc_regions = Some(Vec::with_capacity(4));
5872 }
5873 return self.b_skip_slow(mb_x, mb_y, None, w4);
5874 }
5875 }
5876
5877 /// Reconstructs a B macroblock (spec Table 7-14): direct, L0/L1/Bi partitions,
5878 /// `B_8x8`, or intra.
5879 fn decode_b_mb(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
5880 // Non-skip B MBs gather neighbor motion/modes — flush any deferred
5881 // spans before those grids are read.
5882 self.span_flush();
5883 self.wait_refs_for_mb(mb_y);
5884 let mb_type = r.read_ue()?;
5885 if mb_type >= 23 {
5886 return self.decode_intra_mb(r, mb_x, mb_y, mb_type - 23);
5887 }
5888 if self.refs.is_empty() || self.refs1.is_empty() {
5889 return Err(MbError::Unsupported("B without references"));
5890 }
5891 let mut pred_y = [0u8; 256];
5892 let mut c_pred = [[0u8; 64]; 2];
5893
5894 if mb_type == 0 {
5895 // B_Direct_16x16 — 8×8 transform allowed only with direct_8x8_inference.
5896 // FORCED derivation via the zero-bi bitmap (CAVLC parity with the
5897 // CABAC direct arm): gather + rid/median skipped, chains extended.
5898 let addr_f = mb_y * self.mb_w + mb_x;
5899 let forced = self.direct_spatial
5900 && self.edc_tx.is_none()
5901 && mb_x > 0
5902 && mb_y > 0
5903 && mb_x + 1 < self.mb_w
5904 && addr_f - self.mb_w >= self.slice_first_mb
5905 && self.bzero.get(addr_f - 1).copied().unwrap_or(false)
5906 && self.bzero.get(addr_f.wrapping_sub(self.mb_w)).copied().unwrap_or(false)
5907 && self
5908 .bzero
5909 .get(addr_f.wrapping_sub(self.mb_w) + 1)
5910 .copied()
5911 .unwrap_or(false);
5912 if forced {
5913 self.b_direct_region(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred, (0, 0, (0, 0), (0, 0), false));
5914 if let Some(b) = self.bzero.get_mut(addr_f) {
5915 *b = true;
5916 }
5917 } else {
5918 self.decode_b_direct(mb_x, mb_y, 0, 0, 16, 16, &mut pred_y, &mut c_pred);
5919 }
5920 return self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, self.direct_8x8_inference, false);
5921 }
5922 if mb_type == 22 {
5923 return self.decode_b_8x8(r, mb_x, mb_y);
5924 }
5925
5926 // 16x16 / 16x8 / 8x16 partitions with per-partition L0/L1/Bi.
5927 let (layout, mvmode, preds) = b_inter_layout(mb_type);
5928 // mb_pred order: ref_idx_l0 (all L0 parts), ref_idx_l1, mvd_l0, mvd_l1.
5929 let mut refi = [[-1i32; 2]; 2]; // [part][list]
5930 for (p, &(_, _, _, _)) in layout.iter().enumerate() {
5931 if preds[p & 1].uses(0) {
5932 refi[p][0] = self.read_b_ref(r, 0)?;
5933 }
5934 }
5935 for (p, _) in layout.iter().enumerate() {
5936 if preds[p & 1].uses(1) {
5937 refi[p][1] = self.read_b_ref(r, 1)?;
5938 }
5939 }
5940 let mut mvd = [[(0i32, 0i32); 2]; 2];
5941 for (p, _) in layout.iter().enumerate() {
5942 if preds[p & 1].uses(0) {
5943 mvd[p][0] = (read_mvd(r)?, read_mvd(r)?);
5944 }
5945 }
5946 for (p, _) in layout.iter().enumerate() {
5947 if preds[p & 1].uses(1) {
5948 mvd[p][1] = (read_mvd(r)?, read_mvd(r)?);
5949 }
5950 }
5951 // Per partition: predict + commit each list's MV, then motion-compensate.
5952 for (p, &(rx, ry, rw, rh)) in layout.iter().enumerate() {
5953 let (pbx, pby) = ((mb_x * 4 + rx / 4) as isize, (mb_y * 4 + ry / 4) as isize);
5954 let pwb = (rw / 4) as isize;
5955 let mut mv = [(0i32, 0i32); 2];
5956 for list in 0..2 {
5957 if refi[p][list] >= 0 {
5958 let n = self.mv_neighbors_list(pbx, pby, pwb, list);
5959 let pmv = predict_partition_mv(mvmode, p, n[0], n[1], n[2], refi[p][list]);
5960 mv[list] = (pmv.0 + mvd[p][list].0, pmv.1 + mvd[p][list].1);
5961 }
5962 }
5963 self.b_set_motion(mb_x, mb_y, rx, ry, rw, rh, refi[p & 3][0], mv[0], refi[p & 3][1], mv[1]);
5964 // Spec-correct bi-prediction (average of L0 and L1), matching the CABAC
5965 // path. This used to replicate an openh264 bug for a Bi 16x8/8x16
5966 // partition -- openh264 mis-handles the destination buffer there, so
5967 // partition 0 came out List-1-only and partition 1 List-0-only. That was
5968 // deliberate when openh264's h264dec WAS the conformance oracle, but the
5969 // gate is ffmpeg now and the CABAC path already went spec-correct; the
5970 // CAVLC path was simply left behind. Measured: mb_type 12..21 (every B
5971 // 16x8/8x16 with at least one Bi partition) were 100% wrong vs ffmpeg,
5972 // while 1..11 (no Bi partition) were only collaterally damaged.
5973 self.b_mc_or_record(mb_x, mb_y, rx, ry, rw, rh, refi[p & 3][0], mv[0], refi[p & 3][1], mv[1], &mut pred_y, &mut c_pred);
5974 }
5975 self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, true, false)
5976 }
5977
5978 /// Reconstructs a `B_8x8` macroblock: four 8×8 sub-macroblock partitions, each
5979 /// direct or L0/L1/Bi with its own sub-partitioning (spec Table 7-18).
5980 fn decode_b_8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
5981 let mut sub = [0u32; 4];
5982 for s in sub.iter_mut() {
5983 let v = r.read_ue()?;
5984 if v > 12 {
5985 return Err(MbError::Unsupported("invalid B sub_mb_type"));
5986 }
5987 *s = v;
5988 }
5989 let mut pred_y = [0u8; 256];
5990 let mut c_pred = [[0u8; 64]; 2];
5991 // ref_idx for all 8×8 partitions (L0 batch, then L1 batch), for the
5992 // non-direct sub-partitions.
5993 let mut refi = [[-1i32; 2]; 4];
5994 for (p, &st) in sub.iter().enumerate() {
5995 if st != 0 && b_sub_uses(st, 0) {
5996 refi[p][0] = self.read_b_ref(r, 0)?;
5997 }
5998 }
5999 for (p, &st) in sub.iter().enumerate() {
6000 if st != 0 && b_sub_uses(st, 1) {
6001 refi[p][1] = self.read_b_ref(r, 1)?;
6002 }
6003 }
6004 // mvd: all mvd_l0 (partition-major, sub-partition order), then all mvd_l1.
6005 //
6006 // FIXED ARRAYS, NOT `Vec::new()` + push. These are per-MACROBLOCK on every
6007 // B_8x8, and a growing Vec allocated (and reallocated) twice per MB — on a
6008 // B-heavy stream that is thousands of allocations per frame for data whose
6009 // maximum size is a compile-time constant: 4 partitions x at most 4
6010 // sub-partitions = 16 entries. Indexing a fixed array cannot exceed that, and
6011 // an out-of-range index would panic rather than misbehave, so the bound is
6012 // enforced either way and this crate stays forbid(unsafe).
6013 const MAX_MVD: usize = 16;
6014 let mut mvd0 = [(0i32, 0i32); MAX_MVD];
6015 let mut mvd1 = [(0i32, 0i32); MAX_MVD];
6016 let (mut n0, mut n1) = (0usize, 0usize);
6017 for &st in &sub {
6018 if st != 0 && b_sub_uses(st, 0) {
6019 for _ in b_sub_parts(st) {
6020 mvd0[n0] = (read_mvd(r)?, read_mvd(r)?);
6021 n0 += 1;
6022 }
6023 }
6024 }
6025 for &st in &sub {
6026 if st != 0 && b_sub_uses(st, 1) {
6027 for _ in b_sub_parts(st) {
6028 mvd1[n1] = (read_mvd(r)?, read_mvd(r)?);
6029 n1 += 1;
6030 }
6031 }
6032 }
6033 // Decode each 8×8 partition.
6034 // Spatial-direct A/B/C are MB-level — walk once if any sub is direct.
6035 // `dmemo=0` rewalks every direct 8×8 (A/B oracle).
6036 let hoisted = if self.direct_spatial
6037 && direct_memo_on()
6038 && sub.iter().any(|&t| t == 0)
6039 {
6040 Some(self.b_direct_nbrs(mb_x, mb_y))
6041 } else {
6042 None
6043 };
6044 let (mut i0, mut i1) = (0usize, 0usize);
6045 for (p, &st) in sub.iter().enumerate() {
6046 let (b8x, b8y) = ((p % 2) * 8, (p / 2) * 8);
6047 if st == 0 {
6048 match hoisted {
6049 Some((n0, n1)) => self.decode_b_direct_n(
6050 mb_x, mb_y, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred, n0, n1,
6051 ),
6052 None => self.decode_b_direct(
6053 mb_x, mb_y, b8x, b8y, 8, 8, &mut pred_y, &mut c_pred,
6054 ),
6055 }
6056 continue;
6057 }
6058 for &(sx, sy, sw, sh) in b_sub_parts(st) {
6059 let (px, py) = (b8x + sx, b8y + sy);
6060 let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
6061 let pwb = (sw / 4) as isize;
6062 let mut mv = [(0i32, 0i32); 2];
6063 if b_sub_uses(st, 0) {
6064 let n = self.mv_neighbors_list(pbx, pby, pwb, 0);
6065 let pmv = predict_mv(n[0], n[1], n[2], refi[p & 3][0]);
6066 let d = mvd0[i0 & 15];
6067 i0 += 1;
6068 mv[0] = (pmv.0 + d.0, pmv.1 + d.1);
6069 }
6070 if b_sub_uses(st, 1) {
6071 let n = self.mv_neighbors_list(pbx, pby, pwb, 1);
6072 let pmv = predict_mv(n[0], n[1], n[2], refi[p & 3][1]);
6073 let d = mvd1[i1 & 15];
6074 i1 += 1;
6075 mv[1] = (pmv.0 + d.0, pmv.1 + d.1);
6076 }
6077 self.b_set_motion(mb_x, mb_y, px, py, sw, sh, refi[p & 3][0], mv[0], refi[p & 3][1], mv[1]);
6078 self.b_mc_or_record(mb_x, mb_y, px, py, sw, sh, refi[p & 3][0], mv[0], refi[p & 3][1], mv[1], &mut pred_y, &mut c_pred);
6079 }
6080 }
6081 // noSubMbPartSizeLessThan8x8: each sub-partition must be ≥ 8×8 (direct
6082 // counts only with the 8×8 inference flag).
6083 let allow_8x8 = sub
6084 .iter()
6085 .all(|&st| if st == 0 { self.direct_8x8_inference } else { st <= 3 });
6086 self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8, false)
6087 }
6088
6089 /// Reconstructs a `P_8x8` macroblock: four 8×8 sub-macroblock partitions,
6090 /// each independently split (8×8 / 8×4 / 4×8 / 4×4) with its own motion
6091 /// vector(s). `ref0` is `P_8x8ref0` (every `ref_idx` forced to 0, not coded).
6092 fn decode_p8x8(
6093 &mut self,
6094 r: &mut BitReader,
6095 mb_x: usize,
6096 mb_y: usize,
6097 ref0: bool,
6098 ) -> Result<(), MbError> {
6099 if self.refs.is_empty() {
6100 return Err(MbError::Unsupported("inter without reference"));
6101 }
6102 let w4 = self.mb_w * 4;
6103 let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
6104 let num_refs = self.refs.len();
6105
6106 // mb_pred order (spec §7.3.5.2): all sub_mb_type, then all ref_idx_l0,
6107 // then all mvd_l0 (partition-major, sub-partition order within each).
6108 let mut sub_types = [0u32; 4];
6109 for st in sub_types.iter_mut() {
6110 let v = r.read_ue()?;
6111 if v > 3 {
6112 return Err(MbError::Unsupported("B-slice / invalid sub_mb_type"));
6113 }
6114 *st = v;
6115 }
6116 let mut ref_idxs = [0i32; 4];
6117 if self.num_ref_active > 1 && !ref0 {
6118 for ri in ref_idxs.iter_mut() {
6119 *ri = read_ref_idx(r, self.num_ref_active)?;
6120 if *ri as usize >= num_refs {
6121 return Err(MbError::Truncated); // references a non-existent picture
6122 }
6123 }
6124 }
6125
6126 // Per sub-partition (in decoding order): median MV prediction from the
6127 // committed neighbor grid, mvd, commit, then motion-compensate. Committing
6128 // before the next prediction is what lets sub-partitions chain correctly.
6129 // D14b: P_8x8 defers too. Syncing before it instead cost 9,772 pipeline
6130 // drains per stream (45 per 1000 MBs, vs CABAC's 3.3) because P_8x8 is
6131 // common in CAVLC P slices — and the seam measured 1.65-1.97x SLOWER
6132 // for it. Deferring is byte-identical for sub-partitions: the worker
6133 // motion-compensates per 4x4 from the committed grids, which is exactly
6134 // how the CABAC path already handles P_8x8, and a 6-tap filter applied
6135 // per-4x4 with the same MV gives the same pixels as one 8x8 call.
6136 let defer = self.edc_tx.is_some() || self.edc_active;
6137
6138 // ── PHASE 1: PARSE + COMMIT. Must always run to completion. ──────────
6139 //
6140 // These two are interleaved BY NECESSITY: each sub-partition's MV
6141 // prediction reads the grids the previous one committed, so they cannot
6142 // be separated from each other. But they consume BITSTREAM, so nothing
6143 // here may ever be skipped conditionally.
6144 //
6145 // This phase split is a STRUCTURAL guard, not a tidy-up. When MC lived
6146 // inside this loop, deferring it was written as a `break` — which also
6147 // skipped the `read_se` mvd reads, desynced the bitstream, and
6148 // mis-parsed as a B-slice sub_mb_type on a Baseline stream. It happened
6149 // to crash; a desync that stayed IN RANGE would have produced plausible
6150 // garbage instead. With MC in its own pass below, skipping pixel work
6151 // cannot reach the bitstream at all — the mistake is unavailable.
6152 let mut regions: [(usize, usize, usize, usize, i32, (i32, i32)); 16] =
6153 [(0, 0, 0, 0, 0, (0, 0)); 16];
6154 let mut nreg = 0usize;
6155 for part in 0..4usize {
6156 let refi = ref_idxs[part];
6157 let (b8x, b8y) = ((part % 2) * 8, (part / 2) * 8);
6158 for &(srx, sry, srw, srh) in sub_mb_partitions(sub_types[part]) {
6159 let (px, py) = (b8x + srx, b8y + sry);
6160 let (pbx, pby) = ((mb_x * 4 + px / 4) as isize, (mb_y * 4 + py / 4) as isize);
6161 let [a, b, c] = self.mv_neighbors_block(pbx, pby, (srw / 4) as isize);
6162 let pmv = predict_mv(a, b, c, refi);
6163 let mvd_x = read_mvd(r)?;
6164 let mvd_y = read_mvd(r)?;
6165 let mv = (pmv.0 + mvd_x, pmv.1 + mvd_y);
6166 for by in py / 4..py / 4 + srh / 4 {
6167 for bx in px / 4..px / 4 + srw / 4 {
6168 let idx = (mb_y * 4 + by) * w4 + (mb_x * 4 + bx);
6169 if let (Some(m), Some(it), Some(rf), Some(cd)) = (
6170 self.mv_y.get_mut(idx),
6171 self.inter_y.get_mut(idx),
6172 self.ref_idx_y.get_mut(idx),
6173 self.coded_y.get_mut(idx),
6174 ) {
6175 (*m, *it, *rf, *cd) = (mv, true, refi, true);
6176 }
6177 }
6178 }
6179 regions[nreg & 15] = (px, py, srw, srh, refi, mv);
6180 nreg += 1;
6181 }
6182 }
6183
6184 // ── PHASE 2: PIXEL WORK ONLY. Reads no bitstream; safe to skip. ──────
6185 let mut pred_y = [0u8; 256];
6186 let mut c_pred = [[0u8; 64]; 2];
6187 if !defer {
6188 for &(px, py, srw, srh, refi, mv) in ®ions[..nreg] {
6189 let Some(reference) = self.refs.get(refi as usize) else { continue };
6190 let mut tmp = [0u8; 256];
6191 mc_luma_padded(&*reference.luma_guard(reference.ch), reference.lstride(), crate::LPAD, self.cw, ch, mb_x * 16 + px, mb_y * 16 + py, srw, srh, mv.0, mv.1, &mut tmp);
6192 restride(&mut pred_y, 16, px, py, &tmp, srw, srh);
6193 let (crx, cry, crw, crh) = (px / 2, py / 2, srw / 2, srh / 2);
6194 for cc in 0..2 {
6195 let rc = if cc == 0 { &*reference.chroma_guard(0, reference.ch) } else { &*reference.chroma_guard(1, reference.ch) };
6196 let mut tc = [0u8; 64];
6197 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);
6198 restride(&mut c_pred[cc], 8, crx, cry, &tc, crw, crh);
6199 }
6200 self.weight_partition(
6201 &mut pred_y, &mut c_pred, 0, refi as usize, px, py, srw, srh,
6202 );
6203 }
6204 }
6205
6206 // P_8x8 allows the 8×8 transform only when every sub-partition is 8×8.
6207 let allow_8x8 = sub_types.iter().all(|&t| t == 0);
6208 self.inter_finish(r, mb_x, mb_y, &pred_y, &c_pred, allow_8x8, defer)
6209 }
6210
6211 /// Reconstructs a `P_Skip` macroblock: motion-compensate from the reference
6212 /// at the skip MV, with no residual.
6213 /// Records a B MC region (threaded mode) or executes it inline — the SAME
6214 /// arguments as `b_mc`; the recorded arm resolves the implicit weights at
6215 /// parse time (identical function, parse-side data).
6216 #[allow(clippy::too_many_arguments)]
6217 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]) {
6218 if self.edc_regions.is_some() {
6219 // Mirror `b_mc`'s malformed-stream armor EXACTLY before touching the
6220 // ref lists: the inline path clamps the indices and bails on empty
6221 // lists BEFORE computing weights; calling `implicit_weights` with the
6222 // raw indices re-introduced the panic the armor exists to prevent
6223 // (found by the fuzzer, via the unwind-guard that turned the
6224 // resulting worker deadlock back into a diagnosable failure).
6225 let cr0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
6226 let cr1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
6227 let w = if (cr0 >= 0 && self.refs.is_empty()) || (cr1 >= 0 && self.refs1.is_empty()) {
6228 None // the worker's port returns before reading the weights
6229 } else {
6230 self.implicit_weights(cr0, cr1)
6231 };
6232 // Cannot bind above: `implicit_weights` needs `&mut self` between the
6233 // guard and here. The guard already proved this is `Some`, so the
6234 // `else` is unreachable -- but an unreachable no-op is the correct
6235 // shape for armor code, not an `unwrap` that can abort a decode.
6236 if let Some(regions) = self.edc_regions.as_mut() {
6237 regions.push(BRegion { px, py, rw, rh, refi0, refi1, mv0, mv1, w });
6238 }
6239 return;
6240 }
6241 self.b_mc(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, pred_y, c_pred);
6242 }
6243
6244 /// Builds the worker's owned pixel context from `self` (planes MOVED out,
6245 /// shared read-only state cloned, filter inputs snapshotted).
6246 fn edc_take_ctx(&mut self) -> PixelCtx {
6247 PixelCtx {
6248 rec_y: std::mem::take(&mut self.rec_y),
6249 rec_u: std::mem::take(&mut self.rec_u),
6250 rec_v: std::mem::take(&mut self.rec_v),
6251 bak_y: std::mem::take(&mut self.bak_y),
6252 bak_u: std::mem::take(&mut self.bak_u),
6253 bak_v: std::mem::take(&mut self.bak_v),
6254 refs: self.refs.clone(),
6255 refs1: self.refs1.clone(),
6256 weights: self.weights.clone(),
6257 weights_l0id: self.weights_l0id,
6258 scaling: self.scaling,
6259 scaling8: self.scaling8,
6260 cw: self.cw,
6261 ccw: self.ccw,
6262 mb_w: self.mb_w,
6263 mb_h: self.mb_h,
6264 chroma_qp_offset: self.chroma_qp_offset,
6265 flt_rows: self.flt_rows,
6266 db_ena: self.db_ena,
6267 db_oa: self.db_oa,
6268 db_ob: self.db_ob,
6269 cur_qp: self.cur_qp,
6270 qp_grid: self.mb_qp.clone(),
6271 t8_grid: self.mb_t8x8.clone(),
6272 bs_store: self.bs_frame.clone(),
6273 progress: self.progress.clone(),
6274 }
6275 }
6276
6277 /// Restores the planes (and the filter watermark) from a returned context.
6278 fn edc_restore_ctx(&mut self, ctx: PixelCtx) {
6279 self.rec_y = ctx.rec_y;
6280 self.rec_u = ctx.rec_u;
6281 self.rec_v = ctx.rec_v;
6282 self.bak_y = ctx.bak_y;
6283 self.bak_u = ctx.bak_u;
6284 self.bak_v = ctx.bak_v;
6285 self.flt_rows = ctx.flt_rows;
6286 }
6287
6288 /// Intra macroblocks read neighbour PIXELS: fetch the context from the
6289 /// worker (which drains all prior jobs first — the channel is FIFO) and
6290 /// install the planes so the inline intra path runs unchanged. The context
6291 /// is given back lazily at the next job/row/slice-end (`edc_giveback`), so
6292 /// consecutive intra macroblocks pay ONE round-trip.
6293 /// Queue a pixel job for the worker (D10). Batched per row; see `EdcMsg::Batch`.
6294 #[inline]
6295 fn edc_send_job(&mut self, job: EdcJob) {
6296 edcstat::bump(&edcstat::JOBS, 1);
6297 if !batch_on() {
6298 self.edc_tx
6299 .as_ref()
6300 .unwrap()
6301 .send(EdcMsg::Job(job))
6302 .expect("worker alive");
6303 return;
6304 }
6305 self.edc_batch.push(job);
6306 }
6307
6308 /// Ship the accumulated row batch. MUST be called before anything that
6309 /// depends on those jobs having been applied: the row's `Row` filter
6310 /// message, a `NeedCtx` handover, and slice end.
6311 fn edc_flush_batch(&mut self) {
6312 if self.edc_batch.is_empty() {
6313 return;
6314 }
6315 // Replace with a PRE-RESERVED buffer rather than the empty Vec
6316 // `mem::take` would leave: otherwise each row reallocs and regrows from
6317 // zero, trading 208k channel sends for ~7 reallocs x 3k rows.
6318 let cap = self.edc_batch.capacity().max(self.mb_w);
6319 let jobs = std::mem::replace(&mut self.edc_batch, Vec::with_capacity(cap));
6320 edcstat::bump(&edcstat::BATCHES, 1);
6321 self.edc_tx
6322 .as_ref()
6323 .unwrap()
6324 .send(EdcMsg::Batch(jobs))
6325 .expect("worker alive");
6326 }
6327
6328 fn edc_intra_sync(&mut self) {
6329 if self.edc_tx.is_none() {
6330 self.edc_flush();
6331 return;
6332 }
6333 if self.edc_parked.is_some() {
6334 return; // already holding
6335 }
6336 // ORDER: drain the batch before taking the planes, or those jobs would
6337 // be applied to a context the parse thread is concurrently holding.
6338 self.edc_flush_batch();
6339 edcstat::bump(&edcstat::NEEDCTX, 1);
6340 self.edc_tx.as_ref().unwrap().send(EdcMsg::NeedCtx).expect("worker alive");
6341 let mut ctx = self.edc_ctx_rx.as_ref().unwrap().recv().expect("worker ctx");
6342 self.rec_y = std::mem::take(&mut ctx.rec_y);
6343 self.rec_u = std::mem::take(&mut ctx.rec_u);
6344 self.rec_v = std::mem::take(&mut ctx.rec_v);
6345 self.bak_y = std::mem::take(&mut ctx.bak_y);
6346 self.bak_u = std::mem::take(&mut ctx.bak_u);
6347 self.bak_v = std::mem::take(&mut ctx.bak_v);
6348 self.flt_rows = ctx.flt_rows;
6349 self.edc_parked = Some(ctx);
6350 }
6351
6352 /// Returns a held context to the worker (inverse of `edc_intra_sync`).
6353 /// NOTE (D10): this is called per-macroblock on the job paths, so it must
6354 /// NOT flush the batch — that would undo the batching. It is safe: the
6355 /// parked state is only ever entered through `edc_intra_sync`, which
6356 /// flushes before taking the planes, so nothing can be queued-but-unsent
6357 /// while the parse thread holds them.
6358 fn edc_giveback(&mut self) {
6359 if let Some(mut parked) = self.edc_parked.take() {
6360 parked.rec_y = std::mem::take(&mut self.rec_y);
6361 parked.rec_u = std::mem::take(&mut self.rec_u);
6362 parked.rec_v = std::mem::take(&mut self.rec_v);
6363 parked.bak_y = std::mem::take(&mut self.bak_y);
6364 parked.bak_u = std::mem::take(&mut self.bak_u);
6365 parked.bak_v = std::mem::take(&mut self.bak_v);
6366 parked.flt_rows = self.flt_rows;
6367 // Fail-soft: on the unwind path the worker may already be gone;
6368 // dropping the parked context is acceptable there (the planes are
6369 // lost, but the panic is being propagated anyway).
6370 let _ = self.edc_back_tx.as_ref().unwrap().send(parked);
6371 }
6372 }
6373
6374 /// Parse-side twin of the nnz/coded grid writes `add_inter_residual` does
6375 /// inline — the worker's port omits them (they are PARSE state: deblock
6376 /// derivation and the CAVLC nC contexts read them), so the threaded path
6377 /// commits them here, from the same parsed counts (their equality with the
6378 /// recon-side recount is the Part 8 nnz-threading brick's own invariant).
6379 fn edc_commit_nnz(&mut self, mbx: usize, mby: usize, t8: bool, nnzs: &[u8; 24], cbp_chroma: u32) {
6380 // BUILD THE RASTER LOCALLY, THEN COPY ROW-WISE. Both luma arms scattered
6381 // sixteen individually bounds-checked stores into the frame grid (the t8
6382 // arm through a 2x2-of-2x2 nest, the other through the z-order scan
6383 // table); chroma scattered eight more. Assembling the macroblock's own
6384 // 4x4 raster on the stack first turns that into four contiguous row
6385 // copies per plane.
6386 let (w4r, w2r) = (self.mb_w * 4, self.mb_w * 2);
6387 let mut raster = [0u8; 16];
6388 if t8 {
6389 for b8 in 0..4usize {
6390 let (b8x, b8y) = (b8 % 2, b8 / 2);
6391 let n = nnzs[b8 * 4];
6392 for sy in 0..2 {
6393 for sx in 0..2 {
6394 raster[(b8y * 2 + sy) * 4 + b8x * 2 + sx] = n;
6395 }
6396 }
6397 }
6398 } else {
6399 for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
6400 raster[lby * 4 + lbx] = nnzs[blk];
6401 }
6402 }
6403 for by in 0..4usize {
6404 let a = (mby * 4 + by) * w4r + mbx * 4;
6405 self.nnz_y[a..a + 4].copy_from_slice(&raster[by * 4..by * 4 + 4]);
6406 }
6407 if cbp_chroma == 2 {
6408 for c in 0..2usize {
6409 let mut cr = [0u8; 4];
6410 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
6411 cr[by * 2 + bx] = nnzs[(16 + c * 4 + by * 2 + bx).min(23)];
6412 }
6413 for by in 0..2usize {
6414 let a = (mby * 2 + by) * w2r + mbx * 2;
6415 self.nnz_c[c][a..a + 2].copy_from_slice(&cr[by * 2..by * 2 + 2]);
6416 }
6417 }
6418 }
6419 }
6420
6421 /// Flush the entropy-decouple job queue: replay every deferred pixel job
6422 /// in parse order. Called before any intra macroblock (its reconstruction
6423 /// reads neighbour PIXELS), before row filtering, at B-branch entry, at
6424 /// slice end, and at `deblock()` as a backstop.
6425 /// Per-macroblock guard: the B arm calls this once per B MB — the empty
6426 /// test inlines, the drain body stays outlined.
6427 #[inline(always)]
6428 fn edc_flush(&mut self) {
6429 if self.edc_jobs.is_empty() {
6430 return;
6431 }
6432 self.edc_flush_slow();
6433 }
6434
6435 fn edc_flush_slow(&mut self) {
6436 let jobs = std::mem::take(&mut self.edc_jobs);
6437 // Band identity holds unweighted OR under identity weights (the x264
6438 // weightp=2 common case: a table in every P slice, identity outside fades).
6439 let band_ok = self.weights.is_none() || self.weights_id0;
6440 // Measurement: nonzero-MV skips sitting in same-row runs of >=2 EQUAL
6441 // MVs (the next band candidate). counted_until stops double-counting.
6442 let mut counted_until = 0usize;
6443 let mut i = 0usize;
6444 while i < jobs.len() {
6445 let j = &jobs[i];
6446 match j {
6447 EdcJob::Skip { mbx, mby, mv } => {
6448 // mb_skip_run band coalescing: gather the maximal run of
6449 // consecutive same-row (0,0)-skips and recon it as ONE
6450 // band copy (weighted-P falls back — the copy identity
6451 // only holds unweighted).
6452 if *mv == (0, 0) && band_ok && !no_skipband() {
6453 let (x0, y0) = (*mbx, *mby);
6454 let mut n = 1usize;
6455 while let Some(EdcJob::Skip { mbx: nx, mby: ny, mv: nmv }) = jobs.get(i + n) {
6456 if *ny == y0 && *nx == x0 + n && *nmv == (0, 0) {
6457 n += 1;
6458 } else {
6459 break;
6460 }
6461 }
6462 self.recon_p_skip_band(x0, y0, n);
6463 if double_recon() {
6464 edcstat::bump(&edcstat::DOUBLED, n as u64);
6465 self.recon_p_skip_band(x0, y0, n);
6466 }
6467 i += n;
6468 continue;
6469 }
6470 edcstat::bump(&edcstat::SKIP_SINGLES, 1);
6471 if *mv != (0, 0) && i >= counted_until && edcstat::on() {
6472 let (x0, y0) = (*mbx, *mby);
6473 let mut n2 = 1usize;
6474 while let Some(EdcJob::Skip { mbx: nx, mby: ny, mv: nmv }) = jobs.get(i + n2) {
6475 if *ny == y0 && *nx == x0 + n2 && nmv == mv { n2 += 1; } else { break; }
6476 }
6477 if n2 >= 2 {
6478 let fp = mv.0 % 4 == 0 && mv.1 % 4 == 0;
6479 edcstat::bump(if fp { &edcstat::PEQ_FP } else { &edcstat::PEQ_FRAC }, n2 as u64);
6480 }
6481 counted_until = i + n2;
6482 }
6483 self.recon_p_skip(*mbx, *mby, *mv);
6484 if double_recon() {
6485 edcstat::bump(&edcstat::DOUBLED, 1);
6486 self.recon_p_skip(*mbx, *mby, *mv);
6487 }
6488 }
6489 EdcJob::Inter(job) => {
6490 self.recon_p_inter(job);
6491 if double_recon() {
6492 edcstat::bump(&edcstat::DOUBLED, 1);
6493 self.recon_p_inter(job);
6494 }
6495 }
6496 EdcJob::InterNoRes(job) => {
6497 // D9b: do NOT to_full() — that memset ~2.5 KB of zeros per MB
6498 // then walked add_inter_residual's all-zero path. MC + plane copy.
6499 self.recon_p_inter_nores(job);
6500 if double_recon() {
6501 edcstat::bump(&edcstat::DOUBLED, 1);
6502 self.recon_p_inter_nores(job);
6503 }
6504 }
6505 // B jobs exist only in worker (MT) mode and are never queued
6506 // here — the single-thread seam keeps B inline. Not reachable
6507 // from any input (the push sites are gated on `edc_tx`).
6508 EdcJob::B(_) | EdcJob::BSkip { .. } => unreachable!("B jobs are worker-only"),
6509 }
6510 i += 1;
6511 }
6512 // Hand the (now empty) Vec back so its allocation is reused.
6513 self.edc_jobs = jobs;
6514 self.edc_jobs.clear();
6515 }
6516
6517 /// Reconstructs one CABAC P inter macroblock from its parse job — the
6518 /// pixel half of the entropy-decouple seam (docs/entropy-decouple-plan.md
6519 /// E1). Reads NOTHING from parse state except the frame grids this MB's
6520 /// parse already committed (its own block MVs/refs, re-gathered below —
6521 /// stable after commit) and the immutable DPB; called either inline
6522 /// (seam off / flush disabled) or in-order at a flush point. Byte-
6523 /// identical to the former inline block by construction: replay order
6524 /// equals inline order at every pixel-observable point (intra reads, row
6525 /// filtering) because flushes precede both.
6526 /// The P-inter reconstruction body, taking its inputs directly rather than
6527 /// through a `PInterJob`. Split out so the residual planes can be passed as
6528 /// `Option`s (absent == nothing was parsed) instead of forcing every caller
6529 /// to own a zeroed copy. NOTE: it re-gathers `gmv`/`gref` from the frame
6530 /// grids rather than reading the job's copies — those are for the worker,
6531 /// which must not touch the parse thread's grids.
6532 #[allow(clippy::too_many_arguments)]
6533 fn recon_p_inter_parts(
6534 &mut self,
6535 mbx: usize,
6536 mby: usize,
6537 qp: u8,
6538 cbp_chroma: u32,
6539 luma_scan: Option<&[[i32; 16]; 16]>,
6540 luma8: Option<&[[i32; 64]; 4]>,
6541 cdc: &[[i32; 4]; 2],
6542 cac: Option<&[[[i32; 16]; 4]; 2]>,
6543 nnzs: &[u8; 24],
6544 ) {
6545 let mbw = self.mb_w;
6546 // `add_inter_residual` (and anything under it) reads `self.cur_qp`,
6547 // which at FLUSH time belongs to a later macroblock — replay must
6548 // restore this MB's qp. The x264 corpus (near-constant QP) could not
6549 // see this; the encoder's delta-QP roundtrip stream caught it.
6550 let saved_qp = self.cur_qp;
6551 self.cur_qp = qp;
6552 // ---- Recon: motion-comp (per 4×4 luma / co-located 2×2 chroma using the
6553 // committed grid MV — the 6-tap/bilinear filter is per-output-pixel, so
6554 // per-block MC is bit-identical to per-partition MC) + residual add via the
6555 // SAME reconstruct_4x4 as intra, with the MC output as the prediction.
6556 let w4r = mbw * 4;
6557 let mut pred_y = [0u8; 256];
6558 let mut c_pred = [[0u8; 64]; 2];
6559 {
6560 // MC-CALL COALESCING (side-by-side descent, dec target #2): the old
6561 // loop paid 16 mc_luma(4×4) + 32 mc_chroma(2×2) per MB regardless of
6562 // partitioning — 48 calls even for a single-MV 16×16 MB, and the
6563 // per-call glue around 2.4M calls was ~40% of decoding real-world
6564 // (x264) streams. The 6-tap/bilinear filters are per-output-pixel,
6565 // so merging blocks with equal (mv, ref) into one wider MC call is
6566 // BIT-IDENTICAL; the rect ladder mirrors the partition shapes.
6567 let _ms = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMcStage);
6568 let (rh16, cch) = (self.mb_h * 16, self.mb_h * 8);
6569 let mut gmv = [(0i32, 0i32); 16];
6570 let mut gref = [0usize; 16];
6571 let _gg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::MvGrid);
6572 let nrefs = self.refs.len() - 1;
6573 for by in 0..4usize {
6574 // Row-contiguous: one slice copy for the MVs.
6575 let row = (mby * 4 + by) * w4r + mbx * 4;
6576 gmv[by * 4..by * 4 + 4].copy_from_slice(&self.mv_y[row..row + 4]);
6577 let ridx = &self.ref_idx_y[row..row + 4];
6578 for bx in 0..4usize {
6579 // Per-block reference (multi-ref P): ref_idx_l0 committed to the
6580 // grid. Clamp — a corrupt stream can over-range it (never panic).
6581 gref[by * 4 + bx] = (ridx[bx].max(0) as usize).min(nrefs);
6582 }
6583 }
6584 drop(_gg);
6585 // All blocks of the rect (in 4×4-block units) match its top-left?
6586 let rect_eq = |x4: usize, y4: usize, w4: usize, h4: usize| -> bool {
6587 let t = y4 * 4 + x4;
6588 (0..h4).all(|dy| {
6589 (0..w4).all(|dx| {
6590 let b = ((y4 + dy) * 4 + (x4 + dx)) & 15;
6591 gmv[b] == gmv[t] && gref[b] == gref[t]
6592 })
6593 })
6594 };
6595 let refs = &self.refs;
6596 let (cw, ccw) = (self.cw, self.ccw);
6597 let mc_rect = |x4: usize,
6598 y4: usize,
6599 w4: usize,
6600 h4: usize,
6601 pred_y: &mut [u8; 256],
6602 c_pred: &mut [[u8; 64]; 2]| {
6603 let b = y4 * 4 + x4;
6604 let (mv, gr) = (gmv[b & 15], gref[b & 15]);
6605 // `gref` holds a reference-list slot; a slot the list
6606 // does not have means there is nothing to predict from,
6607 // so the rect is left as it stands.
6608 let Some(reference) = refs.get(gr) else { return };
6609 let (w, h) = (w4 * 4, h4 * 4);
6610 // A FULL-WIDTH rect (w == 16, so x4 == 0) occupies contiguous
6611 // whole rows of `pred_y` — the MC output layout and the
6612 // destination layout coincide, so MC writes the prediction
6613 // buffer DIRECTLY. The staging copy exists only for narrow
6614 // rects, whose rows really are strided in `pred_y`. This is
6615 // the diagnosis's "stage-boundary materialization" tax paid
6616 // by the dominant 16×16/16×8 shapes: 256 B of `t` zeroing
6617 // plus a 256 B copy per rect, for nothing.
6618 if w == 16 {
6619 rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &*reference.luma_guard(reference.ch), reference.lstride(), crate::LPAD, cw, rh16, mbx * 16, mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut pred_y[y4 * 64..y4 * 64 + w * h]));
6620 } else {
6621 let mut t = [0u8; 256];
6622 rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &*reference.luma_guard(reference.ch), reference.lstride(), crate::LPAD, cw, rh16, mbx * 16 + x4 * 4, mby * 16 + y4 * 4, w, h, mv.0, mv.1, &mut t[..w * h]));
6623 let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
6624 for dy in 0..h {
6625 pred_y[(y4 * 4 + dy) * 16 + x4 * 4..][..w]
6626 .copy_from_slice(&t[dy * w..dy * w + w]);
6627 }
6628 }
6629 let (cw4, ch4) = (w4 * 2, h4 * 2);
6630 let nc = cw4 * ch4;
6631 let (gu, gv) = (reference.chroma_guard(0, reference.ch), reference.chroma_guard(1, reference.ch));
6632 // U+V paired: one setup serves both planes (see
6633 // mc_chroma_padded_pair). Full-width coincidence:
6634 // cw4 == 8 rows are contiguous in the 8-wide plane.
6635 if cw4 == 8 {
6636 let [cu, cv] = &mut *c_pred;
6637 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, reference.cstride(), crate::CPAD, ccw, cch, mbx * 8, mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut cu[y4 * 16..y4 * 16 + nc], &mut cv[y4 * 16..y4 * 16 + nc]);
6638 } else {
6639 let (mut tu, mut tv) = ([0u8; 64], [0u8; 64]);
6640 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, reference.cstride(), crate::CPAD, ccw, cch, mbx * 8 + x4 * 2, mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut tu[..nc], &mut tv[..nc]);
6641 let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
6642 for (cc, tc) in [(0usize, &tu), (1, &tv)] {
6643 for dy in 0..ch4 {
6644 c_pred[cc][(y4 * 2 + dy) * 8 + x4 * 2..][..cw4]
6645 .copy_from_slice(&tc[dy * cw4..dy * cw4 + cw4]);
6646 }
6647 }
6648 }
6649 };
6650 if rect_eq(0, 0, 4, 4) {
6651 mc_rect(0, 0, 4, 4, &mut pred_y, &mut c_pred);
6652 } else if rect_eq(0, 0, 4, 2) && rect_eq(0, 2, 4, 2) {
6653 mc_rect(0, 0, 4, 2, &mut pred_y, &mut c_pred);
6654 mc_rect(0, 2, 4, 2, &mut pred_y, &mut c_pred);
6655 } else if rect_eq(0, 0, 2, 4) && rect_eq(2, 0, 2, 4) {
6656 mc_rect(0, 0, 2, 4, &mut pred_y, &mut c_pred);
6657 mc_rect(2, 0, 2, 4, &mut pred_y, &mut c_pred);
6658 } else {
6659 for q in 0..4usize {
6660 let (qx, qy) = ((q % 2) * 2, (q / 2) * 2);
6661 if rect_eq(qx, qy, 2, 2) {
6662 mc_rect(qx, qy, 2, 2, &mut pred_y, &mut c_pred);
6663 } else if rect_eq(qx, qy, 2, 1) && rect_eq(qx, qy + 1, 2, 1) {
6664 mc_rect(qx, qy, 2, 1, &mut pred_y, &mut c_pred);
6665 mc_rect(qx, qy + 1, 2, 1, &mut pred_y, &mut c_pred);
6666 } else if rect_eq(qx, qy, 1, 2) && rect_eq(qx + 1, qy, 1, 2) {
6667 mc_rect(qx, qy, 1, 2, &mut pred_y, &mut c_pred);
6668 mc_rect(qx + 1, qy, 1, 2, &mut pred_y, &mut c_pred);
6669 } else {
6670 for j in 0..4usize {
6671 mc_rect(qx + (j % 2), qy + (j / 2), 1, 1, &mut pred_y, &mut c_pred);
6672 }
6673 }
6674 }
6675 }
6676 // EXPLICIT WEIGHTED PREDICTION (spec 8.4.2.3). The CAVLC inter
6677 // path weights each partition after MC; the MC-call-coalescing
6678 // rewrite of this CABAC path lost it, and nothing caught that
6679 // because the effect is invisible unless a stream actually
6680 // carries non-default weights. x264's `weightp` DUPLICATES a
6681 // reference and distinguishes the copy ONLY by its weights, so
6682 // every macroblock picking the weighted index decoded unweighted
6683 // -- a silent, accumulating luma drift.
6684 //
6685 // Applied per 4x4 block rather than per partition: the weight
6686 // depends solely on the block's reference index, so the two are
6687 // equivalent, and `gref` already holds it for every block
6688 // regardless of which rect ladder rung ran.
6689 if self.weights.is_some() {
6690 for by in 0..4usize {
6691 for bx in 0..4usize {
6692 let refi = gref[by * 4 + bx];
6693 self.weight_partition(
6694 &mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4,
6695 );
6696 }
6697 }
6698 }
6699 }
6700 // Residual add — the SAME helper the B path uses (this inline
6701 // copy was a duplicate; deduped when the zero-block fast path
6702 // landed so both paths share it).
6703 self.add_inter_residual(mbx, mby, &pred_y, &c_pred, luma_scan, luma8, cdc, cac, cbp_chroma, nnzs);
6704 self.cur_qp = saved_qp;
6705 }
6706
6707 /// Job-shaped entry point (EDC replay + the `double_recon` A/B knob).
6708 fn recon_p_inter(&mut self, j: &PInterJob) {
6709 self.recon_p_inter_parts(
6710 j.mbx,
6711 j.mby,
6712 j.qp,
6713 j.cbp_chroma,
6714 j.luma_scan.as_ref(),
6715 j.luma8.as_ref(),
6716 &j.cdc,
6717 j.cac.as_ref(),
6718 &j.nnzs,
6719 );
6720 }
6721
6722 /// D9b: P inter with `cbp == 0` — MC + plane copy, no coeff memset / residual walk.
6723 /// Byte-identical to `recon_p_inter` on a zero-residual job: prediction is the recon.
6724 fn recon_p_inter_nores(&mut self, j: &PInterNoResJob) {
6725 let w4r = self.mb_w * 4;
6726 // Parse-side nnz for single-thread EDC flush (MT commits earlier via edc_commit_nnz).
6727 // ROW FILLS. Both arms touch the same sixteen cells - four contiguous
6728 // per macroblock row - and wrote them one indexed store at a time (the
6729 // t8 arm through a 2x2-of-2x2 nest, the other through the z-order scan).
6730 for by in 0..4usize {
6731 let r = (j.mby * 4 + by) * w4r + j.mbx * 4;
6732 self.nnz_y[r..r + 4].fill(0);
6733 if j.t8 {
6734 self.coded_y[r..r + 4].fill(true);
6735 }
6736 }
6737 let mut pred_y = [0u8; 256];
6738 let mut c_pred = [[0u8; 64]; 2];
6739 // `self.refs.len() - 1` was re-loaded on every one of the sixteen
6740 // iterations; it is a per-macroblock invariant. Written once, not
6741 // zeroed-then-filled.
6742 let nrefs = self.refs.len() - 1;
6743 let gref: [usize; 16] = core::array::from_fn(|k| (j.gref[k] as usize).min(nrefs));
6744 coalesce_p_inter_mc(
6745 &self.refs,
6746 self.cw,
6747 self.ccw,
6748 self.mb_h,
6749 j.mbx,
6750 j.mby,
6751 &j.gmv,
6752 &gref,
6753 &mut pred_y,
6754 &mut c_pred,
6755 );
6756 // The identity early-out lives INSIDE `weight_partition`, so an x264
6757 // stream - which carries a pred_weight_table in EVERY P slice, identity
6758 // outside fades - still paid sixteen calls per macroblock to be told
6759 // there was nothing to do. Hoisted to one test.
6760 if self.weights.is_some() && !self.weights_l0id {
6761 for by in 0..4usize {
6762 for bx in 0..4usize {
6763 let refi = gref[by * 4 + bx];
6764 self.weight_partition(&mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4);
6765 }
6766 }
6767 }
6768 // ONE span per plane: the destination is a stack of contiguous runs a
6769 // fixed stride apart, so the rows share a single bounds check instead of
6770 // one each (16 + 8 + 8 of them).
6771 let (cw, ccw) = (self.cw, self.ccw);
6772 let ybase = (j.mby * 16) * cw + j.mbx * 16;
6773 let ywin = &mut self.rec_y[ybase..ybase + 15 * cw + 16];
6774 for dy in 0..16 {
6775 ywin[dy * cw..dy * cw + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
6776 }
6777 let cbase = (j.mby * 8) * ccw + j.mbx * 8;
6778 for c in 0..2 {
6779 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
6780 let cwin = &mut plane[cbase..cbase + 7 * ccw + 8];
6781 for dy in 0..8 {
6782 cwin[dy * ccw..dy * ccw + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
6783 }
6784 }
6785 }
6786
6787 fn decode_p_skip(&mut self, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
6788 self.route_skip_mbs += 1;
6789 self.wait_refs_for_mb(mb_y);
6790 // DEBLOCK CLASS: a P_Skip macroblock carries no coefficients and one
6791 // (ref, mv) for all 16 blocks, so every internal boundary strength is 0 by
6792 // §8.7.2.1 and the loop filter needs 9 block loads instead of 24. This is
6793 // the single highest-value classification: the MB-kind census measures Skip
6794 // at 36.4% (CAVLC) / 65.0% (main) / 57.8% (high) of real x264 corpora.
6795 // Written HERE because both the CAVLC and the CABAC slice loops funnel
6796 // through this one function.
6797 //
6798 // Deliberately NOT done for `B_Skip` — its motion is direct-derived and can
6799 // differ per 4×4 sub-block, so its internal edges can legally reach
6800 // strength 1. B_Skip stays UNSET and takes the blind path.
6801 // (kind commit moved into the span/immediate arms below.)
6802 // P_Skip always references index 0 (the most recent picture). Borrow it —
6803 // a full-frame `.cloned()` here was ~86% of total decode time (one ~3 MB
6804 // plane copy per skip MB, thousands per frame).
6805 if self.refs.is_empty() {
6806 return Err(MbError::Unsupported("P_Skip without reference"));
6807 }
6808 let addr = mb_y * self.mb_w + mb_x;
6809 // mb_x == 0: the left neighbor is off-frame, so §8.4.1.1's
6810 // unavailability rule forces (0,0) with no gather at all.
6811 let mv = if (mb_x == 0 || self.skip_zero_next == addr) && !self.k_no_runmv {
6812 edcstat::bump(&edcstat::SKIPMV_FORCED, 1);
6813 (0, 0)
6814 } else {
6815 // The gather reads the grids; the pending spans cannot contain
6816 // this MB's left (a non-forced skip means the previous MB was not
6817 // a (0,0) committer) — flush both before deriving.
6818 self.span_flush();
6819 edcstat::bump(&edcstat::SKIPMV_DERIVED, 1);
6820 self.skip_mv(mb_x, mb_y)
6821 };
6822 self.skip_zero_next = if mv == (0, 0) { addr + 1 } else { usize::MAX };
6823 // Grid commits are PARSE state — (0,0) skips defer them into the P
6824 // span (the deferral constants); nonzero-MV skips commit immediately.
6825 if mv == (0, 0) {
6826 // Recon defers too when the band identity holds (1T, no/identity
6827 // weights) — the span flush band-copies instead of queueing jobs.
6828 let recon = self.edc_tx.is_none() && (self.weights.is_none() || self.weights_id0);
6829 self.pz_push(mb_x, mb_y, recon);
6830 if recon {
6831 return Ok(());
6832 }
6833 } else {
6834 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
6835 if let Some(p) = self.mb_kind.get_mut(mb_y * self.mb_w + mb_x) {
6836 *p = rusty_h264_common::deblock::MB_KIND_SKIP;
6837 }
6838 self.set_mb_mv(mb_x, mb_y, mv, true, 0);
6839 let w4 = self.mb_w * 4;
6840 for dy in 0..4 {
6841 let a = (mb_y * 4 + dy) * w4 + mb_x * 4;
6842 self.coded_y[a..a + 4].fill(true);
6843 self.modes_y[a..a + 4].fill(2);
6844 }
6845 }
6846 if self.edc_tx.is_some() {
6847 self.edc_giveback();
6848 self.edc_send_job(EdcJob::Skip { mbx: mb_x, mby: mb_y, mv });
6849 return Ok(());
6850 }
6851 if self.edc_active {
6852 self.edc_jobs.push(EdcJob::Skip { mbx: mb_x, mby: mb_y, mv });
6853 return Ok(());
6854 }
6855 self.recon_p_skip(mb_x, mb_y, mv);
6856 if double_recon() {
6857 self.recon_p_skip(mb_x, mb_y, mv);
6858 }
6859 Ok(())
6860 }
6861
6862 /// Pixel half of P_Skip (see the E1 seam note on `recon_p_inter`).
6863 /// Run-coalesced P_Skip reconstruction: `n` consecutive skip MBs on one
6864 /// row, all with `mv == (0,0)` and NO weighted prediction. At full-pel
6865 /// zero motion, MC is the identity read of `refs[0]` — so the recon is a
6866 /// straight band copy from the padded reference plane, no staging, no MC
6867 /// calls. Byte-identical to `n` calls of `recon_p_skip` by construction
6868 /// (mc_luma_padded/mc_chroma_padded at mv 0 return exactly these bytes).
6869 ///
6870 /// The (0,0) run theorem that makes ONE derivation cover the run: once a
6871 /// skip MB commits (ref 0, mv (0,0)), every later MB of the run derives
6872 /// (0,0) too — its left neighbour (or a row-start's missing neighbour)
6873 /// triggers §8.4.1.1's zero-MV rule in `skip_mv`.
6874 fn recon_p_skip_band(&mut self, mbx0: usize, mby: usize, n: usize) {
6875 let w = n * 16;
6876 let Some(rf0) = self.refs.first() else { return };
6877 let lst = rf0.lstride();
6878 {
6879 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
6880 let ly = rf0.luma_guard(rf0.ch);
6881 for dy in 0..16 {
6882 let y = mby * 16 + dy;
6883 let src = (y + crate::LPAD) * lst + crate::LPAD + mbx0 * 16;
6884 let dst = y * self.cw + mbx0 * 16;
6885 // ARMOR (fuzz find, inline-execution.md 11.11): on a MALFORMED
6886 // stream a reference can carry geometry that does not match the
6887 // open picture (mutated parameter sets / truncated ref planes),
6888 // so the in-frame band's ref window can leave the guard slice.
6889 // The band REFUSES rather than panics; a conformant stream
6890 // cannot take this arm (repro: scratchpad repro.264, plane 6400
6891 // vs index 6416). Output on malformed input is unspecified —
6892 // absence of panic is the contract the fuzz gate enforces.
6893 let (Some(d), Some(sr)) = (
6894 self.rec_y.get_mut(dst..dst + w),
6895 ly.get(src..src + w),
6896 ) else {
6897 return;
6898 };
6899 d.copy_from_slice(sr);
6900 }
6901 }
6902 let cst = rf0.cstride();
6903 let wc = n * 8;
6904 for c in 0..2 {
6905 let rc = rf0.chroma_guard(c, rf0.ch);
6906 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
6907 for dy in 0..8 {
6908 let y = mby * 8 + dy;
6909 let src = (y + crate::CPAD) * cst + crate::CPAD + mbx0 * 8;
6910 let dst = y * self.ccw + mbx0 * 8;
6911 // Same armor as the luma band above.
6912 let (Some(d), Some(sr)) = (plane.get_mut(dst..dst + wc), rc.get(src..src + wc))
6913 else {
6914 return;
6915 };
6916 d.copy_from_slice(sr);
6917 }
6918 }
6919 edcstat::bump(&edcstat::SKIPBAND_MBS, n as u64);
6920 edcstat::bump(&edcstat::SKIPBAND_RUNS, 1);
6921 }
6922
6923 /// B_Skip zero-bi recon: rec = avg(ref0, ref1') row-wise, straight from the
6924 /// padded planes. Byte-identical to b_mc at mv (0,0)/(0,0): full-pel MC is
6925 /// an identity read and the unweighted bi blend is exactly (a+b+1)>>1 —
6926 /// which the compiler emits as pavgb over the sliced zips, same as b_mc's
6927 /// blend site.
6928 fn recon_b_skip_zero_bi(&mut self, mbx: usize, mby: usize, r0i: usize, r1i: usize) {
6929 let (Some(rf0), Some(rf1)) = (self.refs.get(r0i), self.refs1.get(r1i)) else {
6930 return;
6931 };
6932 let (l0st, l1st) = (rf0.lstride(), rf1.lstride());
6933 {
6934 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
6935 let (ly0, ly1) = (rf0.luma_guard(rf0.ch), rf1.luma_guard(rf1.ch));
6936 for dy in 0..16 {
6937 let y = mby * 16 + dy;
6938 let s0 = (y + crate::LPAD) * l0st + crate::LPAD + mbx * 16;
6939 let s1 = (y + crate::LPAD) * l1st + crate::LPAD + mbx * 16;
6940 let d = y * self.cw + mbx * 16;
6941 for ((dst, a), b) in self.rec_y[d..d + 16].iter_mut().zip(&ly0[s0..s0 + 16]).zip(&ly1[s1..s1 + 16]) {
6942 *dst = ((*a as u16 + *b as u16 + 1) >> 1) as u8;
6943 }
6944 }
6945 }
6946 let (c0st, c1st) = (rf0.cstride(), rf1.cstride());
6947 for c in 0..2 {
6948 let rc0 = rf0.chroma_guard(c, rf0.ch);
6949 let rc1 = rf1.chroma_guard(c, rf1.ch);
6950 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
6951 for dy in 0..8 {
6952 let y = mby * 8 + dy;
6953 let s0 = (y + crate::CPAD) * c0st + crate::CPAD + mbx * 8;
6954 let s1 = (y + crate::CPAD) * c1st + crate::CPAD + mbx * 8;
6955 let d = y * self.ccw + mbx * 8;
6956 for ((dst, a), b) in plane[d..d + 8].iter_mut().zip(&rc0[s0..s0 + 8]).zip(&rc1[s1..s1 + 8]) {
6957 *dst = ((*a as u16 + *b as u16 + 1) >> 1) as u8;
6958 }
6959 }
6960 }
6961 }
6962
6963 /// Full-pel P_Skip single: rec window = ref\[0\] window at an integer offset.
6964 /// Returns false (touching nothing) when any window leaves the padded
6965 /// plane — mc's edge clamping takes over there. Luma needs mv%4==0;
6966 /// chroma needs mv%8==0 (its frac is mv&7), so odd full-pel MVs copy luma
6967 /// and interpolate chroma.
6968 fn recon_p_skip_fullpel(&mut self, mbx: usize, mby: usize, mv: (i32, i32)) -> bool {
6969 let (dx, dy) = ((mv.0 / 4) as isize, (mv.1 / 4) as isize);
6970 let Some(rf0) = self.refs.first() else { return false };
6971 let lst = rf0.lstride();
6972 let lpad = crate::LPAD as isize;
6973 let cx = mbx as isize * 16 + dx + lpad;
6974 let cy = mby as isize * 16 + dy + lpad;
6975 {
6976 let ly = rf0.luma_guard(rf0.ch);
6977 let lrows = rf0.lrows() as isize;
6978 if cx < 0 || cx + 16 > lst as isize || cy < 0 || cy + 16 > lrows {
6979 return false;
6980 }
6981 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
6982 // ONE span each side: sixteen rows a fixed stride apart, so the
6983 // source and destination checks are paid once instead of per row.
6984 let (cw, sb) = (self.cw, cy as usize * lst + cx as usize);
6985 let db = (mby * 16) * cw + mbx * 16;
6986 let sw = &ly[sb..sb + 15 * lst + 16];
6987 let dw = &mut self.rec_y[db..db + 15 * cw + 16];
6988 for r in 0..16 {
6989 dw[r * cw..r * cw + 16].copy_from_slice(&sw[r * lst..r * lst + 16]);
6990 }
6991 }
6992 let cst = rf0.cstride();
6993 let cpad = crate::CPAD as isize;
6994 if mv.0 % 8 == 0 && mv.1 % 8 == 0 {
6995 let ccx = mbx as isize * 8 + (mv.0 / 8) as isize + cpad;
6996 let ccy = mby as isize * 8 + (mv.1 / 8) as isize + cpad;
6997 let crows = rf0.crows() as isize;
6998 if ccx >= 0 && ccx + 8 <= cst as isize && ccy >= 0 && ccy + 8 <= crows {
6999 for c in 0..2 {
7000 let rc = rf0.chroma_guard(c, rf0.ch);
7001 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
7002 let (ccw, sb) = (self.ccw, ccy as usize * cst + ccx as usize);
7003 let db = (mby * 8) * ccw + mbx * 8;
7004 let sw = &rc[sb..sb + 7 * cst + 8];
7005 let dw = &mut plane[db..db + 7 * ccw + 8];
7006 for r in 0..8 {
7007 dw[r * ccw..r * ccw + 8].copy_from_slice(&sw[r * cst..r * cst + 8]);
7008 }
7009 }
7010 edcstat::bump(&edcstat::SKIP_FP_FULL, 1);
7011 return true;
7012 }
7013 }
7014 // Odd full-pel (or chroma window out of pad): luma is already copied,
7015 // chroma still interpolates — identical to the mc path's chroma half.
7016 let cch = self.mb_h * 8;
7017 for c in 0..2 {
7018 let mut pc = [0u8; 64];
7019 let rc = if c == 0 { &*rf0.chroma_guard(0, rf0.ch) } else { &*rf0.chroma_guard(1, rf0.ch) };
7020 mc_chroma_padded(rc, cst, crate::CPAD, self.ccw, cch, mbx * 8, mby * 8, 8, 8, mv.0, mv.1, &mut pc);
7021 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
7022 for r in 0..8 {
7023 let d = (mby * 8 + r) * self.ccw + mbx * 8;
7024 plane[d..d + 8].copy_from_slice(&pc[r * 8..r * 8 + 8]);
7025 }
7026 }
7027 edcstat::bump(&edcstat::SKIP_FP_LUMA, 1);
7028 true
7029 }
7030
7031 /// B_Skip full-pel recon with per-list integer offsets: uni = offset row
7032 /// copy, bi = offset row average ((a+b+1)>>1). Luma always copies here
7033 /// (caller guarantees mv%4==0); chroma copies at mv%8==0 and otherwise
7034 /// interpolates via b_mc_chroma — identical to the b_mc path's chroma
7035 /// half with no implicit weights. Returns false untouched when a luma
7036 /// window leaves the padded plane.
7037 fn recon_b_skip_fp(&mut self, mbx: usize, mby: usize, r0: Option<usize>, r1: Option<usize>, mv0: (i32, i32), mv1: (i32, i32)) -> bool {
7038 let lpad = crate::LPAD as isize;
7039 // (start-row, start-col, stride, rows) of each active list's luma window.
7040 let win = |rf: &crate::RefFrame, mv: (i32, i32)| -> Option<(usize, usize)> {
7041 let lst = rf.lstride();
7042 let cx = mbx as isize * 16 + (mv.0 / 4) as isize + lpad;
7043 let cy = mby as isize * 16 + (mv.1 / 4) as isize + lpad;
7044 let lrows = rf.lrows() as isize;
7045 if cx < 0 || cx + 16 > lst as isize || cy < 0 || cy + 16 > lrows {
7046 None
7047 } else {
7048 Some((cy as usize * lst + cx as usize, lst))
7049 }
7050 };
7051 let w0 = match r0 {
7052 Some(i) => match self.refs.get(i).and_then(|rr| win(rr, mv0)) {
7053 Some(w) => Some(w),
7054 None => return false,
7055 },
7056 None => None,
7057 };
7058 let w1 = match r1 {
7059 Some(i) => match self.refs1.get(i).and_then(|rr| win(rr, mv1)) {
7060 Some(w) => Some(w),
7061 None => return false,
7062 },
7063 None => None,
7064 };
7065 {
7066 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
7067 match (w0, w1) {
7068 (Some((s0, l0)), Some((s1, l1))) => {
7069 let Some(rf0) = r0.and_then(|i| self.refs.get(i)) else { return false };
7070 // Same shape as the `rf0` line above: `w1` is `Some` only when
7071 // `r1` is `Some(i)` AND `refs1.get(i)` returned a frame, so the
7072 // `else` is unreachable -- but it is a REFUSAL, not a panic, and
7073 // `recon_b_skip_fp` already answers `false` for every case it
7074 // cannot fast-path. Indexing a `Vec` with an unwrapped `Option`
7075 // spent a discriminant test, an `unwrap_failed` call and a bounds
7076 // check to restate an invariant the caller had already proven.
7077 let Some(rf1) = r1.and_then(|i| self.refs1.get(i)) else { return false };
7078 let (ly0, ly1) = (rf0.luma_guard(rf0.ch), rf1.luma_guard(rf1.ch));
7079 for r in 0..16 {
7080 let d = (mby * 16 + r) * self.cw + mbx * 16;
7081 let (a, b) = (&ly0[s0 + r * l0..s0 + r * l0 + 16], &ly1[s1 + r * l1..s1 + r * l1 + 16]);
7082 for ((dst, p), q) in self.rec_y[d..d + 16].iter_mut().zip(a).zip(b) {
7083 *dst = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
7084 }
7085 }
7086 }
7087 (Some((s0, l0)), None) => {
7088 let Some(rf0) = r0.and_then(|i| self.refs.get(i)) else { return false };
7089 let ly = rf0.luma_guard(rf0.ch);
7090 for r in 0..16 {
7091 let d = (mby * 16 + r) * self.cw + mbx * 16;
7092 self.rec_y[d..d + 16].copy_from_slice(&ly[s0 + r * l0..s0 + r * l0 + 16]);
7093 }
7094 }
7095 (None, Some((s1, l1))) => {
7096 // Same shape as the `rf0` line above: `w1` is `Some` only when
7097 // `r1` is `Some(i)` AND `refs1.get(i)` returned a frame, so the
7098 // `else` is unreachable -- but it is a REFUSAL, not a panic, and
7099 // `recon_b_skip_fp` already answers `false` for every case it
7100 // cannot fast-path. Indexing a `Vec` with an unwrapped `Option`
7101 // spent a discriminant test, an `unwrap_failed` call and a bounds
7102 // check to restate an invariant the caller had already proven.
7103 let Some(rf1) = r1.and_then(|i| self.refs1.get(i)) else { return false };
7104 let ly = rf1.luma_guard(rf1.ch);
7105 for r in 0..16 {
7106 let d = (mby * 16 + r) * self.cw + mbx * 16;
7107 self.rec_y[d..d + 16].copy_from_slice(&ly[s1 + r * l1..s1 + r * l1 + 16]);
7108 }
7109 }
7110 (None, None) => unreachable!("caller guarantees an active list"),
7111 }
7112 }
7113 // Chroma: interpolating half — route through b_mc_chroma into a stage
7114 // (weights None: the caller only enters at implicit None/(32,32), and
7115 // (32,32) IS the plain average b_mc_chroma computes for None).
7116 let cch = self.mb_h * 8;
7117 let mut c_pred = [[0u8; 64]; 2];
7118 let (ri0, ri1) = (r0.map_or(-1, |i| i as i32), r1.map_or(-1, |i| i as i32));
7119 self.b_mc_chroma(mbx, mby, 0, 0, 16, 16, ri0, mv0, ri1, mv1, &mut c_pred, None, cch);
7120 for c in 0..2 {
7121 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
7122 for r in 0..8 {
7123 let d = (mby * 8 + r) * self.ccw + mbx * 8;
7124 plane[d..d + 8].copy_from_slice(&c_pred[c][r * 8..r * 8 + 8]);
7125 }
7126 }
7127 true
7128 }
7129
7130 fn recon_p_skip(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32)) {
7131 // Full-pel + identity/no weights: MC is a pure offset read, so copy the
7132 // ref window straight into rec — no pred staging, no weight pass. The
7133 // window must sit inside the PADDED plane (outside it, mc's clamping
7134 // differs from a raw offset read, so fall through).
7135 if (self.weights.is_none() || self.weights_id0)
7136 && mv.0 % 4 == 0
7137 && mv.1 % 4 == 0
7138 && !no_skipfp()
7139 && self.recon_p_skip_fullpel(mb_x, mb_y, mv)
7140 {
7141 return;
7142 }
7143 let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
7144
7145 let mut pred = [0u8; 256];
7146 let Some(rf0) = self.refs.first() else { return };
7147 mc_luma_padded(&*rf0.luma_guard(rf0.ch), rf0.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16, 16, 16, mv.0, mv.1, &mut pred);
7148 if let Some(wt) = &self.weights {
7149 if !self.weights_id0 || no_skipfp() {
7150 for p in pred.iter_mut() {
7151 *p = wt.apply_luma(*p, 0, 0);
7152 }
7153 }
7154 }
7155 {
7156 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
7157 // ONE span for the sixteen rows (see `recon_p_inter_nores`).
7158 let (cw, base) = (self.cw, (mb_y * 16) * self.cw + mb_x * 16);
7159 let win = &mut self.rec_y[base..base + 15 * cw + 16];
7160 for dy in 0..16 {
7161 win[dy * cw..dy * cw + 16].copy_from_slice(&pred[dy * 16..dy * 16 + 16]);
7162 }
7163 }
7164 let (mut pu, mut pv) = ([0u8; 64], [0u8; 64]);
7165 {
7166 let Some(rf0) = self.refs.first() else { return };
7167 let (gu, gv) = (rf0.chroma_guard(0, rf0.ch), rf0.chroma_guard(1, rf0.ch));
7168 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, rf0.cstride(), crate::CPAD, self.ccw, cch, mb_x * 8, mb_y * 8, 8, 8, mv.0, mv.1, &mut pu, &mut pv);
7169 }
7170 if let Some(wt) = &self.weights {
7171 if !self.weights_id0 || no_skipfp() {
7172 for p in pu.iter_mut() {
7173 *p = wt.apply_chroma(*p, 0, 0, 0);
7174 }
7175 for p in pv.iter_mut() {
7176 *p = wt.apply_chroma(*p, 0, 0, 1);
7177 }
7178 }
7179 }
7180 for (pc, plane) in [(&pu, &mut self.rec_u), (&pv, &mut self.rec_v)] {
7181 for dy in 0..8 {
7182 let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
7183 plane[d..d + 8].copy_from_slice(&pc[dy * 8..dy * 8 + 8]);
7184 }
7185 }
7186 }
7187
7188 /// Predicted `Intra_4x4` mode for the block at absolute coords `(bx, by)`.
7189 /// If either the left or top neighbor is outside the frame or in another
7190 /// slice, the prediction is DC (mode 2) (spec §8.3.1.1).
7191 /// `predict_i4_mode` with the MACROBLOCK-level availability already
7192 /// resolved by the caller (`top_ok` / `left_ok`).
7193 ///
7194 /// Twelve of a macroblock's sixteen blocks have BOTH neighbours inside the
7195 /// same macroblock, so their slice test is a foregone conclusion - yet the
7196 /// general form recomputed two `nbr_in_slice` products and two
7197 /// `intra_nbr_ok` tests for every one of them, ~1M times on all-intra
7198 /// content.
7199 ///
7200 /// `constrained_intra` is excluded deliberately: there the general form
7201 /// tests THIS macroblock's own `inter_y` cells for interior blocks, which
7202 /// the macroblock-level flags do not model, so that case defers unchanged.
7203 #[inline]
7204 fn predict_i4_mode_fast(
7205 &self,
7206 bx: usize,
7207 by: usize,
7208 lbx: usize,
7209 lby: usize,
7210 top_ok: bool,
7211 left_ok: bool,
7212 ) -> u8 {
7213 if self.constrained_intra {
7214 return self.predict_i4_mode(bx, by);
7215 }
7216 if !((lbx > 0 || left_ok) && (lby > 0 || top_ok)) {
7217 return 2;
7218 }
7219 let w4 = self.mb_w * 4;
7220 // Fallible reads: two checks on the same grid at two indexes. `2` is DC,
7221 // which is exactly what every unavailable-neighbour path above already
7222 // returns, so the fallback is the function's own existing semantics.
7223 let l = self.modes_y.get(by * w4 + (bx - 1)).copied().unwrap_or(2);
7224 let t = self.modes_y.get((by - 1) * w4 + bx).copied().unwrap_or(2);
7225 l.min(t)
7226 }
7227
7228 fn predict_i4_mode(&self, bx: usize, by: usize) -> u8 {
7229 if bx == 0 || by == 0 {
7230 return 2;
7231 }
7232 // Left neighbor block (bx-1,by); top neighbor block (bx,by-1). A neighbor
7233 // in another slice — or, under constrained_intra, an inter neighbor — is
7234 // unavailable, forcing the predicted mode to DC.
7235 if !self.nbr_in_slice((bx - 1) / 4, by / 4)
7236 || !self.nbr_in_slice(bx / 4, (by - 1) / 4)
7237 || !self.intra_nbr_ok(bx - 1, by)
7238 || !self.intra_nbr_ok(bx, by - 1)
7239 {
7240 return 2;
7241 }
7242 let w4 = self.mb_w * 4;
7243 // Fallible reads: two checks on the same grid at two indexes. `2` is DC,
7244 // which is exactly what every unavailable-neighbour path above already
7245 // returns, so the fallback is the function's own existing semantics.
7246 let l = self.modes_y.get(by * w4 + (bx - 1)).copied().unwrap_or(2);
7247 let t = self.modes_y.get((by - 1) * w4 + bx).copied().unwrap_or(2);
7248 l.min(t)
7249 }
7250
7251 /// Gathers 4×4 luma intra neighbors at pixel `(px, py)` from `rec_y`.
7252 fn gather_i4(
7253 &self,
7254 px: usize,
7255 py: usize,
7256 avail_top: bool,
7257 avail_left: bool,
7258 bx: usize,
7259 by: usize,
7260 ) -> ([u8; 8], [u8; 4], u8) {
7261 let (cw, w4) = (self.cw, self.mb_w * 4);
7262 let mut top = [0u8; 8];
7263 let mut left = [0u8; 4];
7264 let mut corner = 0;
7265 if avail_top {
7266 // Row-slice loads: the bak-vs-rec source branch runs once per row
7267 // segment instead of once per PIXEL (top_y_px paid it 8 times).
7268 top[..4].copy_from_slice(self.top_y_row(py, px, 4));
7269 let tr_avail = bx + 1 < w4
7270 && self.coded_y.get((by - 1) * w4 + (bx + 1)).copied().unwrap_or(false)
7271 && self.nbr_in_slice((bx + 1) / 4, (by - 1) / 4)
7272 && self.intra_nbr_ok(bx + 1, by - 1);
7273 if tr_avail {
7274 top[4..8].copy_from_slice(self.top_y_row(py, px + 4, 4));
7275 } else {
7276 let t3 = top[3];
7277 top[4..8].fill(t3);
7278 }
7279 }
7280 if avail_left {
7281 // STRIDED WALK. The span form this replaces claimed `i * cw <= 3 * cw`
7282 // was provable against the span's length; it is not — the same shape
7283 // left five live checks on `recon_i16_luma`'s sixteen-sample column.
7284 // `step_by` does the stride with no index, and `zip` bounds the count.
7285 let base = py * cw + px - 1;
7286 for (s, &v) in left.iter_mut().zip(self.rec_y[base..].iter().step_by(cw)) {
7287 *s = v;
7288 }
7289 }
7290 // The above-left corner has its own availability (block D); under
7291 // constrained_intra it is gone if that block is inter.
7292 if avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1) {
7293 corner = self.top_y_px(py, px - 1);
7294 }
7295 (top, left, corner)
7296 }
7297
7298 /// Reconstructs an `I_PCM` macroblock: byte-aligned raw 8-bit samples, no
7299 /// prediction/transform/quant (spec §7.3.5, §8.3.5).
7300 fn decode_ipcm(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
7301 r.align_to_byte()?;
7302 // ROW SLICES throughout. A PCM macroblock wrote 384 samples one at a
7303 // time, each a separately bounds-checked index into a whole plane, and
7304 // then 104 more scattered context stores below. Every extent here is a
7305 // literal (16, 8, 4, 2), so one slice per row makes the inner index
7306 // provable and the stores become a straight walk.
7307 let (lx, ly) = (mb_x * 16, mb_y * 16);
7308 for dy in 0..16 {
7309 let row = &mut self.rec_y[(ly + dy) * self.cw + lx..][..16];
7310 for px in row.iter_mut() {
7311 *px = r.read_bits(8)? as u8;
7312 }
7313 }
7314 let (cx, cy) = (mb_x * 8, mb_y * 8);
7315 let ccw = self.ccw;
7316 for plane in [&mut self.rec_u, &mut self.rec_v] {
7317 for dy in 0..8 {
7318 let row = &mut plane[(cy + dy) * ccw + cx..][..8];
7319 for px in row.iter_mut() {
7320 *px = r.read_bits(8)? as u8;
7321 }
7322 }
7323 }
7324 // Neighbor context: an I_PCM block contributes TotalCoeff = 16, counts as
7325 // intra with DC mode for prediction, and has no motion (§9.2.1, §8.3.1.2.2).
7326 let (w4, w2) = (self.mb_w * 4, self.mb_w * 2);
7327 // The scan order is irrelevant when every written value is a constant:
7328 // the sixteen `LUMA_4X4_SCAN_XY` positions are exactly the macroblock's
7329 // 4x4 rectangle, so four row fills per array cover them.
7330 for ry in 0..4 {
7331 let base = (mb_y * 4 + ry) * w4 + mb_x * 4;
7332 self.nnz_y[base..][..4].fill(16);
7333 self.modes_y[base..][..4].fill(2);
7334 self.coded_y[base..][..4].fill(true);
7335 self.inter_y[base..][..4].fill(false);
7336 self.ref_idx_y[base..][..4].fill(-1);
7337 self.mv_y[base..][..4].fill((0, 0));
7338 }
7339 for c in 0..2 {
7340 for by in 0..2 {
7341 self.nnz_c[c][(mb_y * 2 + by) * w2 + mb_x * 2..][..2].fill(16);
7342 }
7343 }
7344 Ok(())
7345 }
7346
7347 fn decode_i4x4(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
7348 let vt = vlc_tables();
7349 let w4 = self.mb_w * 4;
7350
7351 // intra4x4 mode signalling
7352 let mut modes = [2u8; 16]; // raster [lby*4+lbx]
7353 for &(lbx, lby) in &LUMA_4X4_SCAN_XY {
7354 let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
7355 let predicted = self.predict_i4_mode(bx, by);
7356 let actual = if r.read_bit()? {
7357 predicted
7358 } else {
7359 let rem = r.read_bits(3)? as u8;
7360 if rem < predicted {
7361 rem
7362 } else {
7363 rem + 1
7364 }
7365 };
7366 if let Some(p) = self.modes_y.get_mut(by * w4 + bx) {
7367 *p = actual;
7368 }
7369 modes[(lby & 3) * 4 + (lbx & 3)] = actual;
7370 }
7371
7372 let chroma_mode = r.read_ue()? as u8;
7373 let cbp = read_cbp_intra(r)?;
7374 let cbp_luma = cbp & 15;
7375 let cbp_chroma = cbp >> 4;
7376 if cbp != 0 {
7377 self.step_qp(r.read_se()?)?;
7378 }
7379 let qp = self.cur_qp;
7380
7381 // luma residuals + serial reconstruction. Cross-MB neighbors are only
7382 // available when the adjacent macroblock is in this slice (and, under
7383 // constrained_intra_pred, is itself intra-coded).
7384 let top_mb_avail = mb_y > 0
7385 && self.nbr_in_slice(mb_x, mb_y - 1)
7386 && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
7387 let left_mb_avail = mb_x > 0
7388 && self.nbr_in_slice(mb_x - 1, mb_y)
7389 && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
7390 self.nnz_cache_load(mb_x, mb_y);
7391 let mut nnz_raster = [0u8; 16];
7392 for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
7393 let (bx, by) = (mb_x * 4 + lbx, mb_y * 4 + lby);
7394 let avail_top = lby > 0 || top_mb_avail;
7395 let avail_left = lbx > 0 || left_mb_avail;
7396 let mut scan16 = [0i32; 16];
7397 let total = if cbp_luma & (1 << (blk / 4)) != 0 {
7398 let nc = self.nc_pred(lbx, lby);
7399 let t;
7400 (scan16, t) = decode_residual_block_with(vt, r, 16, nc)?;
7401 t
7402 } else {
7403 0
7404 };
7405 self.nnz_cache_set(lbx, lby, total);
7406 nnz_raster[(lby & 3) * 4 + (lbx & 3)] = total;
7407 self.recon_i4_block(bx, by, modes[(lby & 3) * 4 + (lbx & 3)], avail_top, avail_left, &scan16, total, qp);
7408 }
7409 // Deferred: `recon_i4_block` gathers from `coded_y` / `rec_y`, never
7410 // from `nnz_y`, so one contiguous copy per row replaces sixteen stores.
7411 for by in 0..4usize {
7412 let a = (mb_y * 4 + by) * w4 + mb_x * 4;
7413 self.nnz_y[a..a + 4].copy_from_slice(&nnz_raster[by * 4..by * 4 + 4]);
7414 }
7415
7416 self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
7417 }
7418
7419 /// Decodes an `I_8x8` macroblock (High profile): four 8×8 luma blocks, each
7420 /// with its own intra mode, 8×8 transform residual (CAVLC = four interleaved
7421 /// 4×4 blocks), and 8×8 intra prediction.
7422 fn decode_i8x8(&mut self, r: &mut BitReader, mb_x: usize, mb_y: usize) -> Result<(), MbError> {
7423 let vt = vlc_tables();
7424 let w4 = self.mb_w * 4;
7425 if let Some(p) = self.mb_t8x8.get_mut(mb_y * self.mb_w + mb_x) {
7426 *p = true;
7427 }
7428 self.any_t8 = true;
7429
7430 // intra8x8 mode signalling — one mode per 8×8 block (raster 0..3),
7431 // stored into all four of its 4×4 cells so neighbors can read it.
7432 let mut modes8 = [2u8; 4];
7433 for (b8, mode) in modes8.iter_mut().enumerate() {
7434 let (b8x, b8y) = (b8 % 2, b8 / 2);
7435 let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
7436 let predicted = self.predict_i4_mode(bx, by);
7437 let actual = if r.read_bit()? {
7438 predicted
7439 } else {
7440 let rem = r.read_bits(3)? as u8;
7441 if rem < predicted { rem } else { rem + 1 }
7442 };
7443 *mode = actual;
7444 for sy in 0..2 {
7445 // Row fill: the 2x2 cell block is two contiguous PAIRS.
7446 self.modes_y[(by + sy) * w4 + bx..][..2].fill(actual);
7447 }
7448 }
7449
7450 let chroma_mode = r.read_ue()? as u8;
7451 let cbp = read_cbp_intra(r)?;
7452 let cbp_luma = cbp & 15;
7453 let cbp_chroma = cbp >> 4;
7454 if cbp != 0 {
7455 self.step_qp(r.read_se()?)?;
7456 }
7457 let qp = self.cur_qp;
7458
7459 let top_mb_avail = mb_y > 0
7460 && self.nbr_in_slice(mb_x, mb_y - 1)
7461 && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
7462 let left_mb_avail = mb_x > 0
7463 && self.nbr_in_slice(mb_x - 1, mb_y)
7464 && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
7465 self.nnz_cache_load(mb_x, mb_y);
7466
7467 for b8 in 0..4 {
7468 let (b8x, b8y) = (b8 % 2, b8 / 2);
7469 let (bx, by) = (mb_x * 4 + b8x * 2, mb_y * 4 + b8y * 2);
7470
7471 // residual: 8×8 CAVLC = four 4×4 sub-blocks, coeff k of sub-block s
7472 // mapping to 8×8 scan position 4·k + s (spec §7.3.5.3.2).
7473 let mut scan8 = [0i32; 64];
7474 let coded8 = cbp_luma & (1 << b8) != 0;
7475 if coded8 {
7476 for sub in 0..4 {
7477 let (sx, sy) = (sub % 2, sub / 2);
7478 let (cx, cy) = (b8x * 2 + sx, b8y * 2 + sy);
7479 let nc = self.nc_pred(cx, cy);
7480 let (blk, total) = decode_residual_block_with(vt, r, 16, nc)?;
7481 self.nnz_cache_set(cx, cy, total);
7482 if let Some(c) = self.nnz_y.get_mut((by + sy) * w4 + (bx + sx)) {
7483 *c = total;
7484 }
7485 for k in 0..16 {
7486 scan8[4 * k + sub] = blk[k];
7487 }
7488 }
7489 } else {
7490 for sub in 0..4 {
7491 let (sx, sy) = (sub % 2, sub / 2);
7492 self.nnz_cache_set(b8x * 2 + sx, b8y * 2 + sy, 0);
7493 if let Some(c) = self.nnz_y.get_mut((by + sy) * w4 + (bx + sx)) {
7494 *c = 0;
7495 }
7496 }
7497 }
7498
7499 let avail_top = b8y > 0 || top_mb_avail;
7500 let avail_left = b8x > 0 || left_mb_avail;
7501 self.recon_i8_block(bx, by, modes8[b8], avail_top, avail_left, coded8.then_some(&scan8), qp);
7502 for sy in 0..2 {
7503 // Row fill: the 2x2 cell block is two contiguous PAIRS.
7504 self.coded_y[(by + sy) * w4 + bx..][..2].fill(true);
7505 }
7506 }
7507
7508 self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
7509 }
7510
7511 /// Dequantizes + inverse-transforms an 8×8 luma block, applying the scaling
7512 /// matrix `list` (0 = intra, 1 = inter) or flat weights.
7513 fn inv_quant8(&self, raster: &[i32; 64], qp: u8, list: usize) -> [i32; 64] {
7514 match &self.scaling8 {
7515 Some(s) => inverse_quant_8x8(raster, qp, &s[list]),
7516 None => inverse_quant_8x8(raster, qp, &[16i32; 64]),
7517 }
7518 }
7519
7520 /// Gathers the 8×8 luma intra reference samples at pixel `(px, py)`: the 16
7521 /// top samples (8..15 substituted from the last when no top-right), 8 left
7522 /// samples, the above-left corner, and whether the corner is available.
7523 #[allow(clippy::too_many_arguments)]
7524 fn gather_i8(
7525 &self,
7526 px: usize,
7527 py: usize,
7528 avail_top: bool,
7529 avail_left: bool,
7530 bx: usize,
7531 by: usize,
7532 ) -> ([u8; 16], [u8; 8], u8, bool) {
7533 let (cw, w4) = (self.cw, self.mb_w * 4);
7534 let mut top = [0u8; 16];
7535 let mut left = [0u8; 8];
7536 let mut corner = 0;
7537 if avail_top {
7538 // Row-slice loads: one source branch per segment, not per pixel.
7539 top[..8].copy_from_slice(self.top_y_row(py, px, 8));
7540 let tr_avail = bx + 2 < w4
7541 && self.coded_y.get((by - 1) * w4 + (bx + 2)).copied().unwrap_or(false)
7542 && self.nbr_in_slice((bx + 2) / 4, (by - 1) / 4)
7543 && self.intra_nbr_ok(bx + 2, by - 1);
7544 if tr_avail {
7545 top[8..16].copy_from_slice(self.top_y_row(py, px + 8, 8));
7546 } else {
7547 let t7 = top[7];
7548 top[8..16].fill(t7);
7549 }
7550 }
7551 if avail_left {
7552 // STRIDED WALK, NOT STRIDED INDEXING. A span plus `col[i * cw]` still
7553 // checks, because `i * cw <= 7 * cw` is not something LLVM derives
7554 // against the span's own length; `step_by` on the iterator does the
7555 // stride with no index at all, and `zip` bounds the count.
7556 let base = py * cw + px - 1;
7557 for (s, &v) in left.iter_mut().zip(self.rec_y[base..].iter().step_by(cw)) {
7558 *s = v;
7559 }
7560 }
7561 let avail_corner = avail_top && avail_left && self.intra_nbr_ok(bx - 1, by - 1);
7562 if avail_corner {
7563 corner = self.top_y_px(py, px - 1);
7564 }
7565 (top, left, corner, avail_corner)
7566 }
7567
7568 fn decode_i16(
7569 &mut self,
7570 r: &mut BitReader,
7571 mb_x: usize,
7572 mb_y: usize,
7573 mt: u32,
7574 ) -> Result<(), MbError> {
7575 let vt = vlc_tables();
7576 let pred_mode = I16Mode::from_id(mt % 4);
7577 let cbp_chroma = (mt % 12) / 4;
7578 let cbp_luma_15 = mt / 12 == 1;
7579 let chroma_mode = r.read_ue()? as u8;
7580 self.step_qp(r.read_se()?)?;
7581 let qp = self.cur_qp;
7582 let w4 = self.mb_w * 4;
7583
7584 // luma DC
7585 self.nnz_cache_load(mb_x, mb_y);
7586 let nc_dc = self.nc_pred(0, 0);
7587 let (dc_scan, _) = decode_residual_block_with(vt, r, 16, nc_dc)?;
7588 let dc_levels = un_scan_4x4_dcac(&dc_scan);
7589 let recon_dc = self.dequant_luma_dc(&dc_levels, qp, 0);
7590
7591 // luma AC (nnz set for all 16 blocks: 0 when DC-only, matching the encoder)
7592 let mut nnz_raster = [0u8; 16];
7593 let mut q_blocks = [[0i32; 16]; 16];
7594 for &(bx, by) in &LUMA_4X4_SCAN_XY {
7595 let total = if cbp_luma_15 {
7596 let nc = self.nc_pred(bx, by);
7597 let (ac, t) = decode_residual_block_with(vt, r, 15, nc)?;
7598 // Zero-skip: an empty AC block leaves the fresh-zero raster
7599 // block untouched (un-scanning 16 zeros wrote zeros on zeros).
7600 if t != 0 {
7601 un_scan_4x4_ac_into(&ac, &mut q_blocks[(by & 3) * 4 + (bx & 3)]);
7602 }
7603 t
7604 } else {
7605 0
7606 };
7607 self.nnz_cache_set(bx, by, total);
7608 nnz_raster[(by & 3) * 4 + (bx & 3)] = total;
7609 }
7610 // ONE contiguous copy per row (nothing reads `nnz_y` for this
7611 // macroblock in between - `nc_pred` predicts from `nnz_cache`).
7612 for by in 0..4usize {
7613 let a = (mb_y * 4 + by) * w4 + mb_x * 4;
7614 self.nnz_y[a..a + 4].copy_from_slice(&nnz_raster[by * 4..by * 4 + 4]);
7615 }
7616
7617 // prediction + reconstruction
7618 let avail_top = mb_y > 0
7619 && self.nbr_in_slice(mb_x, mb_y - 1)
7620 && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
7621 let avail_left = mb_x > 0
7622 && self.nbr_in_slice(mb_x - 1, mb_y)
7623 && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
7624 self.recon_i16_luma(mb_x, mb_y, pred_mode, avail_top, avail_left, Some(&q_blocks), &recon_dc, qp);
7625 // I_16x16 blocks are treated as DC for neighbor mode prediction.
7626 for lby in 0..4usize {
7627 let a = (mb_y * 4 + lby) * w4 + mb_x * 4;
7628 self.modes_y[a..a + 4].fill(2);
7629 }
7630
7631 self.decode_chroma(r, mb_x, mb_y, cbp_chroma, chroma_mode)
7632 }
7633
7634 /// Reads and reconstructs the chroma residual (shared by both luma types).
7635 fn decode_chroma(
7636 &mut self,
7637 r: &mut BitReader,
7638 mb_x: usize,
7639 mb_y: usize,
7640 cbp_chroma: u32,
7641 chroma_mode: u8,
7642 ) -> Result<(), MbError> {
7643 let vt = vlc_tables();
7644 let qpc = self.chroma_qp_for(self.cur_qp);
7645 let avail_top = mb_y > 0
7646 && self.nbr_in_slice(mb_x, mb_y - 1)
7647 && self.intra_nbr_ok(mb_x * 4, mb_y * 4 - 1);
7648 let avail_left = mb_x > 0
7649 && self.nbr_in_slice(mb_x - 1, mb_y)
7650 && self.intra_nbr_ok(mb_x * 4 - 1, mb_y * 4);
7651
7652 let mut c_recon_dc = [[0i32; 4]; 2];
7653 if cbp_chroma != 0 {
7654 for (c, slot) in c_recon_dc.iter_mut().enumerate() {
7655 let (dc, _) = decode_residual_block_with(vt, r, 4, -1)?;
7656 *slot = self.dequant_chroma_dc(&[dc[0], dc[1], dc[2], dc[3]], qpc, 1 + c);
7657 }
7658 }
7659 let mut c_q_blocks = [[[0i32; 16]; 4]; 2];
7660 if cbp_chroma == 2 {
7661 self.chroma_cache_load(mb_x, mb_y);
7662 let w2 = self.mb_w * 2;
7663 for c in 0..2 {
7664 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
7665 let nc = self.chroma_nc_pred(c, bx, by);
7666 let (ac, total) = decode_residual_block_with(vt, r, 15, nc)?;
7667 self.chroma_nnz_cache_set(c, bx, by, total);
7668 if let Some(n) = self.nnz_c[c & 1].get_mut((mb_y * 2 + by) * w2 + (mb_x * 2 + bx)) {
7669 *n = total;
7670 }
7671 // Zero-skip: empty AC leaves the fresh-zero raster block.
7672 if total != 0 {
7673 un_scan_4x4_ac_into(&ac, &mut c_q_blocks[c][by * 2 + bx]);
7674 }
7675 }
7676 }
7677 }
7678 self.recon_chroma_blocks(mb_x, mb_y, chroma_mode, avail_top, avail_left, &c_q_blocks, &c_recon_dc, qpc);
7679 Ok(())
7680 }
7681
7682 /// Applies the in-loop deblocking filter to the reconstructed frame, with
7683 /// the slice's `FilterOffsetA`/`FilterOffsetB` (each = the coded `*_div2`
7684 /// value × 2).
7685 /// Per-frame per-MB dump for conformance bisection, keyed on `RH264_DUMP_MB`.
7686 /// Prints one char per macroblock: `i` = intra, otherwise the List-0 reference
7687 /// index of the MB's top-left 4x4 block. Directly comparable with ffmpeg's
7688 /// `-debug mb_type` map, which is the only per-MB ground truth we can get out
7689 /// of the reference decoder.
7690 fn dump_mb_map(&self) {
7691 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7692 if !*ON.get_or_init(|| std::env::var_os("RH264_DUMP_MB").is_some()) {
7693 return;
7694 }
7695 let w4 = self.mb_w * 4;
7696 let mut hist = [0usize; 4];
7697 eprintln!("--- frame poc {} ---", self.cur_poc);
7698 for mb_y in 0..self.mb_h {
7699 let mut row = String::new();
7700 for mb_x in 0..self.mb_w {
7701 let b = (mb_y * 4) * w4 + mb_x * 4;
7702 let Some(&r) = self.ref_idx_y.get(b) else { continue };
7703 if r < 0 {
7704 row.push('i');
7705 } else {
7706 if (r as usize) < 4 {
7707 hist[r as usize] += 1;
7708 }
7709 row.push((b'0' + (r as u8).min(9)) as char);
7710 }
7711 }
7712 eprintln!("{row}");
7713 }
7714 eprintln!(
7715 "ref histogram: {hist:?} num_ref_active={} refs.len()={} OUT-OF-RANGE={}",
7716 self.num_ref_active,
7717 self.refs.len(),
7718 hist.iter().skip(self.refs.len()).sum::<usize>()
7719 );
7720 let list: Vec<String> = self
7721 .refs
7722 .iter()
7723 .enumerate()
7724 .map(|(i, f)| {
7725 // A synthesized frame_num-gap frame is uniform grey with w4 == 0;
7726 // flag it, because it silently displaces real pictures in the list.
7727 let synth = if f.w4 == 0 { " SYNTH-GREY" } else { "" };
7728 format!("[{i}] poc={} fn={}{synth}", f.poc, f.frame_num)
7729 })
7730 .collect();
7731 eprintln!(" RefPicList0: {}", list.join(" "));
7732 }
7733
7734 pub fn deblock(&mut self, offset_a: i32, offset_b: i32) {
7735 self.span_flush(); // deferred grid spans feed bS derivation below
7736 self.edc_flush(); // backstop: no pixel job may survive to filtering
7737 self.dump_mb_map();
7738 // ROW MODE: finish any rows not derived during decode (mid-row slice
7739 // ends, error paths) FIRST, while `self` is still mutably borrowable.
7740 if rowdb_on() {
7741 while self.bs_rows < self.mb_h {
7742 let r = self.bs_rows;
7743 self.derive_bs_row(r);
7744 self.bs_rows += 1;
7745 }
7746 }
7747 // Deblock boundary strength uses the *transform block's* coded status. For
7748 // an 8×8-transform macroblock the unit is the whole 8×8, so every 4×4 cell
7749 // shares the 8×8's coefficient presence (OR of its four sub-block counts)
7750 // — distinct from the per-sub-block `nnz_y` used for the CAVLC nC context.
7751 // Only differs from `nnz_y` when some MB uses the 8×8 transform (High
7752 // profile). On Baseline (no 8×8) it's identical — skip the clone + rewrite.
7753 // Row-interleave already derived bS into `bs_frame`; the filter then
7754 // reads only `bs`+`t8x8`+qp — do not clone nnz or rebuild POC maps.
7755 let rowdb = rowdb_on();
7756 let nnz_db_storage;
7757 let nnz_db: &[u8] = if rowdb {
7758 &[]
7759 } else if self.mb_t8x8.iter().any(|&t| t) {
7760 let mut n = self.nnz_y.clone();
7761 let w4 = self.mb_w * 4;
7762 for mb_y in 0..self.mb_h {
7763 for mb_x in 0..self.mb_w {
7764 if !self.mb_t8x8.get(mb_y * self.mb_w + mb_x).copied().unwrap_or(false) {
7765 continue;
7766 }
7767 for b8 in 0..4 {
7768 let (bx, by) = (mb_x * 4 + (b8 % 2) * 2, mb_y * 4 + (b8 / 2) * 2);
7769 let any = (0..2)
7770 .any(|sy| self.nnz_y[(by + sy) * w4 + bx..][..2].iter().any(|&v| v > 0));
7771 for sy in 0..2 {
7772 // Row fill: the 2x2 cell block is two contiguous PAIRS.
7773 n[(by + sy) * w4 + bx..][..2].fill(u8::from(any));
7774 }
7775 }
7776 }
7777 }
7778 nnz_db_storage = n;
7779 &nnz_db_storage
7780 } else {
7781 &self.nnz_y
7782 };
7783 let mut info = rusty_h264_common::deblock::BlockInfo {
7784 inter: if rowdb { &[] } else { &self.inter_y },
7785 nnz: nnz_db,
7786 mv: if rowdb { &[] } else { &self.mv_y },
7787 ref_id: if rowdb { &[] } else { &self.ref_idx_y },
7788 mv1: if rowdb { &[] } else { &self.mv1 },
7789 ref_id1: if rowdb || self.ref_poc1.is_empty() {
7790 &[]
7791 } else {
7792 &self.ref_idx1
7793 },
7794 w4: self.mb_w * 4,
7795 t8x8: &self.mb_t8x8,
7796 bs: &[],
7797 poc0: if rowdb { &[] } else { &self.ref_poc0 },
7798 poc1: if rowdb { &[] } else { &self.ref_poc1 },
7799 kind: &self.mb_kind,
7800 };
7801 // ROW MODE (R2): rows were derived during decode; the remainder was
7802 // finished above (before `info` borrowed the grids). Fallback: the
7803 // Part 16/17 picture-end precompute; `RS_H264_BS_PRE=0` further falls
7804 // back to the pack-then-derive-in-loop pipeline.
7805 let bs_store;
7806 if rowdb {
7807 bs_store = std::mem::take(&mut self.bs_frame);
7808 info.bs = &bs_store;
7809 } else if bs_pre_on() {
7810 let mut buf = Vec::new();
7811 rusty_h264_common::deblock::precompute_bs_frame(&info, self.mb_w, self.mb_h, &mut buf);
7812 bs_store = buf;
7813 info.bs = &bs_store;
7814 } else {
7815 bs_store = Vec::new();
7816 }
7817 let first_row = if rowdb { self.flt_rows } else { 0 };
7818 rusty_h264_common::deblock::filter_frame_rows(
7819 &mut self.rec_y,
7820 &mut self.rec_u,
7821 &mut self.rec_v,
7822 self.mb_w,
7823 self.mb_h,
7824 first_row..self.mb_h,
7825 &self.mb_qp,
7826 self.chroma_qp_offset,
7827 offset_a,
7828 offset_b,
7829 &info,
7830 );
7831 drop(info);
7832 if rowdb {
7833 self.bs_frame = bs_store;
7834 }
7835 }
7836
7837 /// Crops the reconstructed coded-size planes to the display window.
7838 /// `into_frame`, additionally handing the per-picture grids back for reuse by
7839 /// the next picture. See `GridPool` for why this is worth doing.
7840 pub fn into_frame_recycle(mut self, crop_r: usize, crop_b: usize) -> (YuvFrame, GridPool) {
7841 let [c0, c1] = std::mem::take(&mut self.nnz_c);
7842 let pool = GridPool {
7843 bits_per_mb: self.bits_per_mb,
7844 mb_qp: std::mem::take(&mut self.mb_qp),
7845 bs_frame: std::mem::take(&mut self.bs_frame),
7846 pk_prev: std::mem::take(&mut self.pk_prev),
7847 pk_cur: std::mem::take(&mut self.pk_cur),
7848 nnz_dbr: std::mem::take(&mut self.nnz_dbr),
7849 bak_y: std::mem::take(&mut self.bak_y),
7850 bak_u: std::mem::take(&mut self.bak_u),
7851 bak_v: std::mem::take(&mut self.bak_v),
7852 nnz_y: std::mem::take(&mut self.nnz_y),
7853 nnz_c0: c0,
7854 nnz_c1: c1,
7855 modes_y: std::mem::take(&mut self.modes_y),
7856 coded_y: std::mem::take(&mut self.coded_y),
7857 mv_y: std::mem::take(&mut self.mv_y),
7858 inter_y: std::mem::take(&mut self.inter_y),
7859 ref_idx_y: std::mem::take(&mut self.ref_idx_y),
7860 mv1: std::mem::take(&mut self.mv1),
7861 ref_idx1: std::mem::take(&mut self.ref_idx1),
7862 mb_t8x8: std::mem::take(&mut self.mb_t8x8),
7863 mb_kind: std::mem::take(&mut self.mb_kind),
7864 bzero: std::mem::take(&mut self.bzero),
7865 sc_cat: std::mem::take(&mut self.sc_cat),
7866 sc_cbp: std::mem::take(&mut self.sc_cbp),
7867 sc_cmode: std::mem::take(&mut self.sc_cmode),
7868 sc_nzc: std::mem::take(&mut self.sc_nzc),
7869 sc_cbfdc: std::mem::take(&mut self.sc_cbfdc),
7870 sc_skip: std::mem::take(&mut self.sc_skip),
7871 sc_ref: std::mem::take(&mut self.sc_ref),
7872 sc_mvd: std::mem::take(&mut self.sc_mvd),
7873 sc_ref1: std::mem::take(&mut self.sc_ref1),
7874 sc_mvd1: std::mem::take(&mut self.sc_mvd1),
7875 sc_direct: std::mem::take(&mut self.sc_direct),
7876 ref_poc0: std::mem::take(&mut self.ref_poc0),
7877 ref_poc1: std::mem::take(&mut self.ref_poc1),
7878 };
7879 (self.into_frame(crop_r, crop_b), pool)
7880 }
7881
7882 pub fn into_frame(self, crop_r: usize, crop_b: usize) -> YuvFrame {
7883 // No cropping (the common case): the reconstruction planes ARE the output —
7884 // move them out instead of allocating + copying three full planes per frame.
7885 if crop_r == 0 && crop_b == 0 {
7886 return YuvFrame {
7887 width: self.cw,
7888 height: self.ch,
7889 y: self.rec_y,
7890 u: self.rec_u,
7891 v: self.rec_v,
7892 };
7893 }
7894 let dw = self.cw - 2 * crop_r;
7895 let dh = self.ch - 2 * crop_b;
7896 let mut y = vec![0u8; dw * dh];
7897 for row in 0..dh {
7898 y[row * dw..row * dw + dw].copy_from_slice(&self.rec_y[row * self.cw..row * self.cw + dw]);
7899 }
7900 let (cdw, cdh) = (dw / 2, dh / 2);
7901 let mut u = vec![0u8; cdw * cdh];
7902 let mut v = vec![0u8; cdw * cdh];
7903 for row in 0..cdh {
7904 u[row * cdw..row * cdw + cdw]
7905 .copy_from_slice(&self.rec_u[row * self.ccw..row * self.ccw + cdw]);
7906 v[row * cdw..row * cdw + cdw]
7907 .copy_from_slice(&self.rec_v[row * self.ccw..row * self.ccw + cdw]);
7908 }
7909 let _ = self.cch;
7910 YuvFrame {
7911 width: dw,
7912 height: dh,
7913 y,
7914 u,
7915 v,
7916 }
7917 }
7918}
7919
7920/// Reads `ref_idx_l0` as `te(v)` with range `num_ref_active - 1`: a single flag
7921/// when exactly two references are active (cMax == 1), else `ue(v)`.
7922// ---- CABAC binarization engine helpers (openh264 cabac_decoder.cpp) ----
7923
7924/// Unary bin (`DecodeUnaryBinCabac`): bin0 at `ctx`; if 1, count bins at `ctx+off`
7925/// (including the terminating 0) until a 0.
7926/// CAVLC `mvd_lX` with a sanity bound. `se(v)` can legally code ±2^31-1, but
7927/// every profile/level caps |MV| far below ±2^17 quarter-pel units; beyond
7928/// that is a corrupt stream, and the unchecked `pmv + mvd` addition would
7929/// overflow i32 (debug panic / release wrap into an absurd vector).
7930fn read_mvd(r: &mut BitReader) -> Result<i32, MbError> {
7931 let v = r.read_se()?;
7932 if v.unsigned_abs() > (1 << 17) {
7933 return Err(MbError::Truncated);
7934 }
7935 Ok(v)
7936}
7937
7938fn cabac_unary(cab: &mut crate::cabac::Cabac, ctx: usize, off: usize) -> u32 {
7939 if cab.decode_decision(ctx) == 0 {
7940 return 0;
7941 }
7942 let mut sym = 0;
7943 loop {
7944 let bin = cab.decode_decision(ctx + off);
7945 sym += 1;
7946 // Cap the unary run: no valid H.264 element coded through this helper
7947 // (mb_qp_delta) exceeds a few dozen bins, but on malformed / buffer-exhausted
7948 // input the arithmetic engine keeps yielding 1s (it zero-fills past the end),
7949 // which would loop forever. 512 is far beyond any legal value.
7950 if bin == 0 || sym >= 512 {
7951 break;
7952 }
7953 }
7954 sym
7955}
7956
7957/// k-th order Exp-Golomb in bypass (`DecodeExpBypassCabac`).
7958fn cabac_exp_bypass(cab: &mut crate::cabac::Cabac, mut count: i32) -> u32 {
7959 let mut sym = 0u32;
7960 loop {
7961 let c = cab.decode_bypass();
7962 if c == 1 {
7963 sym += 1 << count;
7964 count += 1;
7965 }
7966 if c == 0 || count == 16 {
7967 break;
7968 }
7969 }
7970 let mut sym2 = 0u32;
7971 while count > 0 {
7972 count -= 1;
7973 if cab.decode_bypass() != 0 {
7974 sym2 |= 1 << count;
7975 }
7976 }
7977 sym + sym2
7978}
7979
7980/// UEG0 coeff-level suffix (`DecodeUEGLevelCabac`): TU prefix at `ctx` (≤13) then an
7981/// EG0 bypass suffix.
7982fn cabac_ueg_level(cab: &mut crate::cabac::Cabac, ctx: usize) -> u32 {
7983 if cab.decode_decision(ctx) == 0 {
7984 return 0;
7985 }
7986 let mut code = 0u32;
7987 let mut tmp;
7988 loop {
7989 tmp = cab.decode_decision(ctx);
7990 code += 1;
7991 if tmp == 0 || code == 12 {
7992 break;
7993 }
7994 }
7995 if tmp != 0 {
7996 code += cabac_exp_bypass(cab, 0) + 1;
7997 }
7998 code
7999}
8000
8001/// `mb_qp_delta` CABAC (`ParseDeltaQpCabac`): ctxIdxOffset 60, ctxInc = (prev delta ≠ 0).
8002pub fn parse_mb_qp_delta_cabac(cab: &mut crate::cabac::Cabac, last_delta_qp: &mut i32) -> i32 {
8003 const O: usize = 60;
8004 let ctx_inc = (*last_delta_qp != 0) as usize;
8005 let mut qp_delta = 0;
8006 if cab.decode_decision(O + ctx_inc) != 0 {
8007 let code = cabac_unary(cab, O + 2, 1) + 1;
8008 qp_delta = ((code + 1) >> 1) as i32;
8009 if code & 1 == 0 {
8010 qp_delta = -qp_delta;
8011 }
8012 }
8013 *last_delta_qp = qp_delta;
8014 qp_delta
8015}
8016
8017// Shared CABAC residual glue tables (NZC_CACHE, RES_*) now live in
8018// `cabac_tables` — both coders read the ONE copy.
8019use rusty_h264_common::cabac_tables::{NZC_CACHE, RES_CBF, RES_MAP, RES_MAXC2, RES_MAXPOS, RES_ONE};
8020// res-property values (post GetMbResProperty, CABAC): the ctx-table index.
8021const RP_I16_DC: usize = 1;
8022const RP_I16_AC: usize = 2;
8023const RP_LUMA_4X4: usize = 3;
8024const RP_CHROMA_DC: usize = 7; // U (V=8, same offsets)
8025const RP_CHROMA_AC: usize = 9; // U (V=10, same offsets)
8026/// Luma 8×8 (ctxBlockCat 5). Its RES_MAP/RES_CBF entries stay 0: cat 5 does NOT
8027/// share the `105 + off` / `166 + off` context bases the 4×4 categories use — it
8028/// has its own absolute bases (402 sig, 417 last) and its own per-position
8029/// ctxIdxInc maps below. RES_ONE[6] = 199 IS used, because 227 + 199 = 426 and
8030/// 232 + 199 = 431 reproduce the spec's coeff_abs_level_minus1 base exactly, so
8031/// the level loop needs no special case at all.
8032const RP_LUMA_8X8: usize = 6;
8033
8034// SIG8X8 / LAST8X8 moved to `rusty_h264_common::cabac_tables` (R6-1) so the encoder's
8035// ctxBlockCat 5 writer shares the exact spec data this reader is validated against.
8036use rusty_h264_common::cabac_tables::{LAST8X8, SIG8X8};
8037
8038/// One residual block (openh264 `ParseResidualBlockCabac`), generic over the 5 CABAC
8039/// block categories. `rp` selects the context offsets. DC categories (I16 luma DC,
8040/// chroma DC) take the cbf context from the per-MB `cbf_dc` bitmask + neighbour MB DC
8041/// cbf; AC categories from the padded nzc cache. Returns totalCoeffNum.
8042#[allow(clippy::too_many_arguments)]
8043fn parse_residual_cabac(
8044 cab: &mut crate::cabac::Cabac,
8045 nzc: &mut [u8; 48],
8046 cbf_dc: &mut u16,
8047 iz: usize,
8048 rp: usize,
8049 is_intra: bool,
8050 ndc: (Option<u16>, Option<u16>), // (top MB cbf_dc, left MB cbf_dc); None = unavailable
8051 out: &mut [i32], // scan-order coefficients written here (len ≥ maxPos+1)
8052) -> u32 {
8053 // The CABAC residual parse IS the decoder's entropy stage on Main-profile
8054 // streams — it was invisible (a ~47% residue) until this scope named it.
8055 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Entropy);
8056 // ---- coded_block_flag ----
8057 // ctxBlockCat 5 is the ONLY category with no coded_block_flag: its presence is
8058 // inferred from CodedBlockPatternLuma, so parsing one here would desync.
8059 let is8 = rp == RP_LUMA_8X8;
8060 let is_dc = rp == RP_I16_DC || rp == RP_CHROMA_DC || rp == RP_CHROMA_DC + 1;
8061 let (mut na, mut nb) = (is_intra as u8, is_intra as u8);
8062 // `nzc` is [u8; 48] and every `NZC_CACHE` entry lies in [9, 47], so this
8063 // clamp is a semantic no-op that hands LLVM BOTH bounds - which is what
8064 // makes `nzc[scan]`, `nzc[scan - 8]` and `nzc[scan - 1]` below provable
8065 // instead of three bounds checks. Same trick as `parse_mvd_partition`.
8066 let scan = NZC_CACHE[iz.min(23)].clamp(8, 47);
8067 if is_dc {
8068 if let Some(t) = ndc.0 {
8069 nb = ((t >> rp) & 1) as u8;
8070 }
8071 if let Some(l) = ndc.1 {
8072 na = ((l >> rp) & 1) as u8;
8073 }
8074 } else {
8075 let (nbc, nac) = (nzc[scan - 8], nzc[scan - 1]);
8076 if nbc != 0xff {
8077 nb = (nbc != 0) as u8;
8078 }
8079 if nac != 0xff {
8080 na = (nac != 0) as u8;
8081 }
8082 }
8083 if !is8 {
8084 let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntCbf);
8085 let cbf = cab.decode_decision(85 + RES_CBF[rp] + (na + (nb << 1)) as usize);
8086 if cbf == 0 {
8087 if !is_dc {
8088 nzc[scan] = 0;
8089 }
8090 return 0;
8091 }
8092 if is_dc {
8093 *cbf_dc |= 1 << rp;
8094 }
8095 }
8096 // ---- significance map ----
8097 let maxpos = RES_MAXPOS[rp] as usize;
8098 // cat 5 uses its own absolute bases; the 4×4 categories share 105/166 + offset.
8099 let (map, last) = if is8 {
8100 (402, 417)
8101 } else {
8102 let m = RES_MAP[rp];
8103 (105 + m, 166 + m)
8104 };
8105 // SPARSE significance map: record each significant POSITION in `pos[..n]`
8106 // instead of marking a dense 64-entry array. Three costs disappear — the
8107 // 256-byte `sig` zeroing per call, the level loop's data-dependent
8108 // `sig[i] != 0` re-scan of every position (a branch mispredict per
8109 // transition on typical 2-4-coeff blocks), and the final dense copy into
8110 // `out`. Bin ORDER is unchanged: levels were decoded at descending
8111 // significant positions, which is exactly `pos[..n]` reversed.
8112 //
8113 // CONTRACT with the callers (all 10 sites): `out` is freshly zeroed, so
8114 // writing only the significant entries leaves the same contents the dense
8115 // copy produced. A reused non-zero `out` would be a correctness bug.
8116 let mut pos = [0u8; 64];
8117 let mut n = 0usize;
8118 let mut last_hit = false;
8119 let _sg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntSig);
8120 // 4×4: ctxIdxInc IS the scan position. 8×8: it comes from the folded maps.
8121 // NOTE (4:2:2 landmine): `(i, i)` is correct for every 4:2:0 category
8122 // only because chroma-DC (cat 3) has NumC8x8 == 1 here; spec §9.3.3.1.3
8123 // wants `Min(i / NumC8x8, 2)` for its sig/last ctxIdxInc, which
8124 // diverges the day 4:2:2 (NumC8x8 == 2) is admitted.
8125 //
8126 let (mi_is8, li_is8) = (is8, is8);
8127 let _ = (mi_is8, li_is8);
8128 for i in 0..maxpos {
8129 // Both tables are [u8; 64] and `i < maxpos <= 63`, so `& 63` changes no
8130 // value and folds the two bounds checks.
8131 let (mi, li) = if is8 {
8132 (SIG8X8[i & 63] as usize, LAST8X8[i & 63] as usize)
8133 } else {
8134 (i, i)
8135 };
8136 if cab.decode_decision(map + mi) != 0 {
8137 pos[n & 63] = i as u8;
8138 n += 1;
8139 if cab.decode_decision(last + li) != 0 {
8140 last_hit = true;
8141 break;
8142 }
8143 }
8144 }
8145 if !last_hit {
8146 pos[n & 63] = maxpos as u8;
8147 n += 1;
8148 }
8149 let coeff_num = n as u32;
8150 // ---- levels ----
8151 let one = 227 + RES_ONE[rp];
8152 let abs = one + 5;
8153 let maxc2 = RES_MAXC2[rp];
8154 let (mut c1, mut c2) = (1i32, 0i32);
8155 drop(_sg);
8156 let _lg = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::EntLvl);
8157 for k in (0..n).rev() {
8158 let mut level = 1 + cab.decode_decision(one + c1 as usize) as i32;
8159 if level == 2 {
8160 level += cabac_ueg_level(cab, abs + c2 as usize) as i32;
8161 c2 = (c2 + 1).min(maxc2);
8162 c1 = 0;
8163 } else if c1 != 0 {
8164 c1 = (c1 + 1).min(4);
8165 }
8166 if cab.decode_bypass() != 0 {
8167 level = -level;
8168 }
8169 // NOT A MASK — `.get_mut`. `out` is `&mut [i32]`, a RUNTIME-length slice
8170 // of maxPos+1, so no constant bound can be correct: an `& 15` here wrote
8171 // 8x8 coefficients (maxPos = 63) to the WRONG positions and failed the
8172 // corpus on tempete high/default. The distinction that matters is that a
8173 // mask silently RELOCATES an out-of-range write while `.get_mut` can only
8174 // drop it, and dropping is unreachable here (`pos` holds values <= maxPos).
8175 if let Some(o) = out.get_mut(pos[k & 63] as usize) {
8176 *o = level;
8177 }
8178 }
8179 if is8 {
8180 // One 8×8 covers four consecutive z-order 4×4 cells. Every later
8181 // coded_block_flag ctxIdxInc reads this cache, so all four must carry the
8182 // count — writing only `scan` would corrupt the NEXT macroblock's contexts.
8183 let cn = coeff_num as u8;
8184 for k in 0..4 {
8185 nzc[NZC_CACHE[(iz + k).min(23)].min(47)] = cn;
8186 }
8187 } else if !is_dc {
8188 nzc[scan] = coeff_num as u8;
8189 }
8190 coeff_num
8191}
8192
8193
8194// ============================================================================
8195// Entropy-decouple E2: the OWNED pixel context that crosses the thread
8196// boundary (docs/entropy-decouple-plan.md). The worker owns the planes, the
8197// backup rows, the DPB Arcs and its own qp/t8/bs grids (fed by Row messages);
8198// the parse thread keeps every syntax grid. The methods below are ports of
8199// the FrameDecoder pixel halves — grid writes removed (parse commits its own
8200// grids), motion carried in the job instead of re-gathered.
8201// ============================================================================
8202
8203/// The committed-value class of a deferred B_Skip span. Continuation
8204/// requires kind EQUALITY: the flush range-fills grids and recons the band
8205/// from these values alone.
8206#[derive(Clone, Copy, PartialEq, Eq)]
8207enum BzKind {
8208 /// ref 0 both lists, (0,0)/(0,0) — band = avg of both padded refs.
8209 ZeroBi,
8210 /// One active list at (0,0): (list, clamped ref index) — band = memcpy.
8211 ZeroUni(u8, u8),
8212 /// Uniform full-pel direct: clamped refs (-1 = inactive) + adjusted MVs,
8213 /// windows PREVALIDATED per MB at push (contiguous tiles ⇒ the union
8214 /// window is valid) — band = offset copy/avg. Chroma requires mv%8==0
8215 /// (also prevalidated).
8216 Fp { r0: i8, r1: i8, m0: (i16, i16), m1: (i16, i16) },
8217}
8218
8219/// Messages from the parse thread to the pixel worker.
8220enum EdcMsg {
8221 Job(EdcJob),
8222 /// A ROW's worth of pixel jobs in one message (D10).
8223 ///
8224 /// The seam sent ONE message per macroblock: 208k sends per 60-frame pass
8225 /// against 2,596 rows. The overhead is per-JOB (channel lock, park/unpark)
8226 /// while the prize is proportional to pixel WORK, so the per-job send was
8227 /// dividing the payoff by ~80 for nothing. Batching per row cuts the
8228 /// synchronisation events by that factor and moves not one byte of pixel
8229 /// work off the worker.
8230 ///
8231 /// ORDER IS THE CORRECTNESS CONDITION: the worker must see a row's jobs
8232 /// before that row's `Row` filter message, so the batch is flushed at every
8233 /// row boundary, before `NeedCtx`, and at slice end.
8234 Batch(Vec<EdcJob>),
8235 /// A macroblock row finished parsing: install its qp/t8/bs and filter it.
8236 Row {
8237 r: usize,
8238 bs: Vec<rusty_h264_common::deblock::MbBs>,
8239 qp: Vec<u8>,
8240 t8: Vec<bool>,
8241 },
8242 /// An intra macroblock needs the planes on the parse thread: send the
8243 /// context over and wait for it to come back.
8244 NeedCtx,
8245}
8246
8247pub(crate) struct PixelCtx {
8248 rec_y: Vec<u8>,
8249 rec_u: Vec<u8>,
8250 rec_v: Vec<u8>,
8251 bak_y: Vec<u8>,
8252 bak_u: Vec<u8>,
8253 bak_v: Vec<u8>,
8254 refs: Vec<crate::Ref>,
8255 refs1: Vec<crate::Ref>,
8256 weights: Option<WeightTable>,
8257 /// `weights.list_identity(0)`, cached - mirrors `FrameDecoder::weights_l0id`
8258 /// so the worker twin does not have to walk the table per macroblock.
8259 weights_l0id: bool,
8260 scaling: Option<[[i32; 16]; 6]>,
8261 scaling8: Option<[[i32; 64]; 2]>,
8262 cw: usize,
8263 ccw: usize,
8264 mb_w: usize,
8265 mb_h: usize,
8266 chroma_qp_offset: i32,
8267 flt_rows: usize,
8268 db_ena: bool,
8269 db_oa: i32,
8270 db_ob: i32,
8271 cur_qp: u8,
8272 qp_grid: Vec<u8>,
8273 t8_grid: Vec<bool>,
8274 bs_store: Vec<rusty_h264_common::deblock::MbBs>,
8275 /// Frame-MT Phase B progress Arc (row publish from the EDC worker).
8276 progress: Option<crate::Ref>,
8277}
8278
8279impl PixelCtx {
8280 /// Frame-MT Phase B: copy filtered MB rows into the shared progress Arc.
8281 fn publish_progress_rows(&self) {
8282 let Some(slot) = &self.progress else {
8283 return;
8284 };
8285 if !self.db_ena || self.flt_rows == 0 {
8286 return;
8287 }
8288 publish_filtered_rows_to_slot(
8289 slot,
8290 &self.rec_y,
8291 &self.rec_u,
8292 &self.rec_v,
8293 self.cw,
8294 self.ccw,
8295 self.mb_h * 16,
8296 self.flt_rows,
8297 );
8298 }
8299
8300 fn chroma_qp_for(&self, qp: u8) -> u8 {
8301 rusty_h264_common::predict::chroma_qp(
8302 ((qp as i32 + self.chroma_qp_offset).clamp(0, 51)) as u8,
8303 )
8304 }
8305
8306 fn dequant(&self, levels: &[i32; 16], qp: u8, list: usize) -> [i32; 16] {
8307 match &self.scaling {
8308 Some(sc) => dequantize_weighted(levels, qp, &sc[list]),
8309 None => dequantize(levels, qp),
8310 }
8311 }
8312
8313 fn dequant_dc4(&self, level: i32, qp: u8, list: usize) -> i32 {
8314 rusty_h264_common::transform::dequantize_dc4(
8315 level,
8316 qp,
8317 self.scaling.as_ref().map(|sc| sc[list][0]),
8318 )
8319 }
8320
8321 fn inv_quant8(&self, raster: &[i32; 64], qp: u8, list: usize) -> [i32; 64] {
8322 match &self.scaling8 {
8323 Some(sc) => inverse_quant_8x8(raster, qp, &sc[list]),
8324 None => inverse_quant_8x8(raster, qp, &[16i32; 64]),
8325 }
8326 }
8327
8328 fn dequant_chroma_dc(&self, levels: &[i32; 4], qp: u8, list: usize) -> [i32; 4] {
8329 match &self.scaling {
8330 Some(sc) => inverse_quant_chroma_dc_weighted(levels, qp, sc[list][0]),
8331 None => inverse_quant_chroma_dc(levels, qp),
8332 }
8333 }
8334
8335 fn weight_partition(
8336 &self,
8337 pred_y: &mut [u8; 256],
8338 c_pred: &mut [[u8; 64]; 2],
8339 list: usize,
8340 refi: usize,
8341 rx: usize,
8342 ry: usize,
8343 rw: usize,
8344 rh: usize,
8345 ) {
8346 let Some(wt) = &self.weights else { return };
8347 // REFUTED, reverted: row-slicing these loops measured +28% instructions
8348 // on `weight_partition` (the extents are runtime values, so the slice
8349 // bounds cost more than the per-sample checks they replaced). The real
8350 // win for this function was hoisting the identity test to its CALLERS,
8351 // which stands.
8352 // RESOLVE ONCE, APPLY MANY. `apply_luma` re-read `self.luma[list][refi]`
8353 // — an array index, a Vec deref and an element index, two of them bounds
8354 // checked — on EVERY sample, up to 256 luma plus 128 chroma per call.
8355 // The weight is a property of the partition, not of the pixel. (The loop
8356 // SHAPE is untouched: row-slicing it is refuted above.)
8357 // MASKED, not row-sliced. `pred_y` is a fixed `[u8; 256]` and `c_pred` a
8358 // `[[u8; 64]; 2]`, so `& 255` / `& 63` are no-ops that prove the index
8359 // outright — where SLICING these loops cost +28% (the extents are runtime
8360 // values, so the slice bounds outweigh the checks). Same lesson as
8361 // `luma_centre`: the refutation was of one SHAPE, not of the goal.
8362 let (lw, lo) = wt.luma_wo(list, refi);
8363 for dy in 0..rh {
8364 for dx in 0..rw {
8365 let i = ((ry + dy) * 16 + (rx + dx)) & 255;
8366 pred_y[i] = wt.apply_luma_wo(pred_y[i], lw, lo);
8367 }
8368 }
8369 let (crx, cry, crw, crh) = (rx / 2, ry / 2, rw / 2, rh / 2);
8370 for cc in 0..2 {
8371 let (cw, co) = wt.chroma_wo(list, refi, cc);
8372 let plane = &mut c_pred[cc & 1];
8373 for dy in 0..crh {
8374 for dx in 0..crw {
8375 let i = ((cry + dy) * 8 + (crx + dx)) & 63;
8376 plane[i] = wt.apply_chroma_wo(plane[i], cw, co);
8377 }
8378 }
8379 }
8380 }
8381
8382 fn recon_p_inter(&mut self, j: &PInterJob) {
8383 crate::RefFrame::set_mc_row_need(j.mby, self.mb_h * 16);
8384 // `add_inter_residual` reads `self.cur_qp`; unlike the FrameDecoder copy
8385 // (which interleaves with parsing and must save/restore), every PixelCtx
8386 // job sets it from the job before any reader, so no restore is needed.
8387 self.cur_qp = j.qp;
8388 // ---- Recon: motion-comp (per 4×4 luma / co-located 2×2 chroma using the
8389 // committed grid MV — the 6-tap/bilinear filter is per-output-pixel, so
8390 // per-block MC is bit-identical to per-partition MC) + residual add via the
8391 // SAME reconstruct_4x4 as intra, with the MC output as the prediction.
8392 let mut pred_y = [0u8; 256];
8393 let mut c_pred = [[0u8; 64]; 2];
8394 {
8395 // MC-CALL COALESCING (side-by-side descent, dec target #2): the old
8396 // loop paid 16 mc_luma(4×4) + 32 mc_chroma(2×2) per MB regardless of
8397 // partitioning — 48 calls even for a single-MV 16×16 MB, and the
8398 // per-call glue around 2.4M calls was ~40% of decoding real-world
8399 // (x264) streams. The 6-tap/bilinear filters are per-output-pixel,
8400 // so merging blocks with equal (mv, ref) into one wider MC call is
8401 // BIT-IDENTICAL; the rect ladder mirrors the partition shapes.
8402 let _ms = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMcStage);
8403 let (rh16, cch) = (self.mb_h * 16, self.mb_h * 8);
8404 // E2: the worker owns no syntax grids — the job carries the
8405 // committed per-block motion (filled at parse time).
8406 let gmv = j.gmv;
8407 let mut gref = [0usize; 16];
8408 for k in 0..16 {
8409 gref[k] = (j.gref[k] as usize).min(self.refs.len() - 1);
8410 }
8411 // All blocks of the rect (in 4×4-block units) match its top-left?
8412 let rect_eq = |x4: usize, y4: usize, w4: usize, h4: usize| -> bool {
8413 let t = y4 * 4 + x4;
8414 (0..h4).all(|dy| {
8415 (0..w4).all(|dx| {
8416 let b = ((y4 + dy) * 4 + (x4 + dx)) & 15;
8417 gmv[b] == gmv[t] && gref[b] == gref[t]
8418 })
8419 })
8420 };
8421 let refs = &self.refs;
8422 let (cw, ccw) = (self.cw, self.ccw);
8423 let mc_rect = |x4: usize,
8424 y4: usize,
8425 w4: usize,
8426 h4: usize,
8427 pred_y: &mut [u8; 256],
8428 c_pred: &mut [[u8; 64]; 2]| {
8429 let b = y4 * 4 + x4;
8430 let (mv, gr) = (gmv[b & 15], gref[b & 15]);
8431 // `gref` holds a reference-list slot; a slot the list
8432 // does not have means there is nothing to predict from,
8433 // so the rect is left as it stands.
8434 let Some(reference) = refs.get(gr) else { return };
8435 let (w, h) = (w4 * 4, h4 * 4);
8436 // A FULL-WIDTH rect (w == 16, so x4 == 0) occupies contiguous
8437 // whole rows of `pred_y` — the MC output layout and the
8438 // destination layout coincide, so MC writes the prediction
8439 // buffer DIRECTLY. The staging copy exists only for narrow
8440 // rects, whose rows really are strided in `pred_y`. This is
8441 // the diagnosis's "stage-boundary materialization" tax paid
8442 // by the dominant 16×16/16×8 shapes: 256 B of `t` zeroing
8443 // plus a 256 B copy per rect, for nothing.
8444 if w == 16 {
8445 rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &*reference.luma_guard(reference.ch), 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]));
8446 } else {
8447 let mut t = [0u8; 256];
8448 rusty_h264_common::inter::with_mc_scratch(|scr| rusty_h264_common::inter::mc_luma_padded_pre(scr, &*reference.luma_guard(reference.ch), 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]));
8449 let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
8450 for dy in 0..h {
8451 pred_y[(y4 * 4 + dy) * 16 + x4 * 4..][..w]
8452 .copy_from_slice(&t[dy * w..dy * w + w]);
8453 }
8454 }
8455 let (cw4, ch4) = (w4 * 2, h4 * 2);
8456 let nc = cw4 * ch4;
8457 let (gu, gv) = (reference.chroma_guard(0, reference.ch), reference.chroma_guard(1, reference.ch));
8458 // U+V paired: one setup serves both planes (see
8459 // mc_chroma_padded_pair). Full-width coincidence:
8460 // cw4 == 8 rows are contiguous in the 8-wide plane.
8461 if cw4 == 8 {
8462 let [cu, cv] = &mut *c_pred;
8463 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut cu[y4 * 16..y4 * 16 + nc], &mut cv[y4 * 16..y4 * 16 + nc]);
8464 } else {
8465 let (mut tu, mut tv) = ([0u8; 64], [0u8; 64]);
8466 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, reference.cstride(), crate::CPAD, ccw, cch, j.mbx * 8 + x4 * 2, j.mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut tu[..nc], &mut tv[..nc]);
8467 let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
8468 for (cc, tc) in [(0usize, &tu), (1, &tv)] {
8469 for dy in 0..ch4 {
8470 c_pred[cc][(y4 * 2 + dy) * 8 + x4 * 2..][..cw4]
8471 .copy_from_slice(&tc[dy * cw4..dy * cw4 + cw4]);
8472 }
8473 }
8474 }
8475 };
8476 if rect_eq(0, 0, 4, 4) {
8477 mc_rect(0, 0, 4, 4, &mut pred_y, &mut c_pred);
8478 } else if rect_eq(0, 0, 4, 2) && rect_eq(0, 2, 4, 2) {
8479 mc_rect(0, 0, 4, 2, &mut pred_y, &mut c_pred);
8480 mc_rect(0, 2, 4, 2, &mut pred_y, &mut c_pred);
8481 } else if rect_eq(0, 0, 2, 4) && rect_eq(2, 0, 2, 4) {
8482 mc_rect(0, 0, 2, 4, &mut pred_y, &mut c_pred);
8483 mc_rect(2, 0, 2, 4, &mut pred_y, &mut c_pred);
8484 } else {
8485 for q in 0..4usize {
8486 let (qx, qy) = ((q % 2) * 2, (q / 2) * 2);
8487 if rect_eq(qx, qy, 2, 2) {
8488 mc_rect(qx, qy, 2, 2, &mut pred_y, &mut c_pred);
8489 } else if rect_eq(qx, qy, 2, 1) && rect_eq(qx, qy + 1, 2, 1) {
8490 mc_rect(qx, qy, 2, 1, &mut pred_y, &mut c_pred);
8491 mc_rect(qx, qy + 1, 2, 1, &mut pred_y, &mut c_pred);
8492 } else if rect_eq(qx, qy, 1, 2) && rect_eq(qx + 1, qy, 1, 2) {
8493 mc_rect(qx, qy, 1, 2, &mut pred_y, &mut c_pred);
8494 mc_rect(qx + 1, qy, 1, 2, &mut pred_y, &mut c_pred);
8495 } else {
8496 for j in 0..4usize {
8497 mc_rect(qx + (j % 2), qy + (j / 2), 1, 1, &mut pred_y, &mut c_pred);
8498 }
8499 }
8500 }
8501 }
8502 // EXPLICIT WEIGHTED PREDICTION (spec 8.4.2.3). The CAVLC inter
8503 // path weights each partition after MC; the MC-call-coalescing
8504 // rewrite of this CABAC path lost it, and nothing caught that
8505 // because the effect is invisible unless a stream actually
8506 // carries non-default weights. x264's `weightp` DUPLICATES a
8507 // reference and distinguishes the copy ONLY by its weights, so
8508 // every macroblock picking the weighted index decoded unweighted
8509 // -- a silent, accumulating luma drift.
8510 //
8511 // Applied per 4x4 block rather than per partition: the weight
8512 // depends solely on the block's reference index, so the two are
8513 // equivalent, and `gref` already holds it for every block
8514 // regardless of which rect ladder rung ran.
8515 if self.weights.is_some() {
8516 for by in 0..4usize {
8517 for bx in 0..4usize {
8518 let refi = gref[by * 4 + bx];
8519 self.weight_partition(
8520 &mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4,
8521 );
8522 }
8523 }
8524 }
8525 }
8526 // Residual add — the SAME helper the B path uses (this inline
8527 // copy was a duplicate; deduped when the zero-block fast path
8528 // landed so both paths share it).
8529 self.add_inter_residual(j.mbx, j.mby, &pred_y, &c_pred, j.luma_scan.as_ref(), j.luma8.as_ref(), &j.cdc, j.cac.as_ref(), j.cbp_chroma, &j.nnzs);
8530 }
8531
8532 /// D9b: P inter with `cbp == 0` — MC + plane copy (worker twin of FrameDecoder).
8533 fn recon_p_inter_nores(&mut self, j: &PInterNoResJob) {
8534 let mut pred_y = [0u8; 256];
8535 let mut c_pred = [[0u8; 64]; 2];
8536 // `self.refs.len() - 1` was re-loaded on every one of the sixteen
8537 // iterations; it is a per-macroblock invariant. Written once, not
8538 // zeroed-then-filled.
8539 let nrefs = self.refs.len() - 1;
8540 let gref: [usize; 16] = core::array::from_fn(|k| (j.gref[k] as usize).min(nrefs));
8541 coalesce_p_inter_mc(
8542 &self.refs,
8543 self.cw,
8544 self.ccw,
8545 self.mb_h,
8546 j.mbx,
8547 j.mby,
8548 &j.gmv,
8549 &gref,
8550 &mut pred_y,
8551 &mut c_pred,
8552 );
8553 // The identity early-out lives INSIDE `weight_partition`, so an x264
8554 // stream - which carries a pred_weight_table in EVERY P slice, identity
8555 // outside fades - still paid sixteen calls per macroblock to be told
8556 // there was nothing to do. Hoisted to one test.
8557 if self.weights.is_some() && !self.weights_l0id {
8558 for by in 0..4usize {
8559 for bx in 0..4usize {
8560 let refi = gref[by * 4 + bx];
8561 self.weight_partition(&mut pred_y, &mut c_pred, 0, refi, bx * 4, by * 4, 4, 4);
8562 }
8563 }
8564 }
8565 // ONE span per plane: the destination is a stack of contiguous runs a
8566 // fixed stride apart, so the rows share a single bounds check instead of
8567 // one each (16 + 8 + 8 of them).
8568 let (cw, ccw) = (self.cw, self.ccw);
8569 let ybase = (j.mby * 16) * cw + j.mbx * 16;
8570 let ywin = &mut self.rec_y[ybase..ybase + 15 * cw + 16];
8571 for dy in 0..16 {
8572 ywin[dy * cw..dy * cw + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
8573 }
8574 let cbase = (j.mby * 8) * ccw + j.mbx * 8;
8575 for c in 0..2 {
8576 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
8577 let cwin = &mut plane[cbase..cbase + 7 * ccw + 8];
8578 for dy in 0..8 {
8579 cwin[dy * ccw..dy * ccw + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
8580 }
8581 }
8582 }
8583
8584 fn recon_p_skip(&mut self, mb_x: usize, mb_y: usize, mv: (i32, i32)) {
8585 crate::RefFrame::set_mc_row_need(mb_y, self.mb_h * 16);
8586 let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
8587
8588 let mut pred = [0u8; 256];
8589 let Some(rf0) = self.refs.first() else { return };
8590 mc_luma_padded(&*rf0.luma_guard(rf0.ch), rf0.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16, 16, 16, mv.0, mv.1, &mut pred);
8591 if let Some(wt) = &self.weights {
8592 for p in pred.iter_mut() {
8593 *p = wt.apply_luma(*p, 0, 0);
8594 }
8595 }
8596 {
8597 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::SkipRecon);
8598 // ONE span for the sixteen rows (see `recon_p_inter_nores`).
8599 let (cw, base) = (self.cw, (mb_y * 16) * self.cw + mb_x * 16);
8600 let win = &mut self.rec_y[base..base + 15 * cw + 16];
8601 for dy in 0..16 {
8602 win[dy * cw..dy * cw + 16].copy_from_slice(&pred[dy * 16..dy * 16 + 16]);
8603 }
8604 }
8605 for c in 0..2 {
8606 let mut pc = [0u8; 64];
8607 let Some(rf0) = self.refs.first() else { return };
8608 let rc = if c == 0 { &*rf0.chroma_guard(0, rf0.ch) } else { &*rf0.chroma_guard(1, rf0.ch) };
8609 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);
8610 if let Some(wt) = &self.weights {
8611 for p in pc.iter_mut() {
8612 *p = wt.apply_chroma(*p, 0, 0, c);
8613 }
8614 }
8615 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
8616 for dy in 0..8 {
8617 let d = (mb_y * 8 + dy) * self.ccw + mb_x * 8;
8618 plane[d..d + 8].copy_from_slice(&pc[dy * 8..dy * 8 + 8]);
8619 }
8620 }
8621 }
8622
8623 fn add_inter_residual(
8624 &mut self,
8625 mb_x: usize,
8626 mb_y: usize,
8627 pred_y: &[u8; 256],
8628 c_pred: &[[u8; 64]; 2],
8629 luma_scan: Option<&[[i32; 16]; 16]>,
8630 // `Some` when the macroblock carries transform_size_8x8_flag: four 8x8
8631 // blocks in 8x8 scan order, replacing the sixteen 4x4 luma blocks.
8632 luma8: Option<&[[i32; 64]; 4]>,
8633 cdc: &[[i32; 4]; 2],
8634 cac: Option<&[[[i32; 16]; 4]; 2]>,
8635 cbp_chroma: u32,
8636 // Parsed totalCoeff per block, indexed exactly as the parse's `iz`:
8637 // [0..16] luma 4x4 z-order (for t8, the 8x8 count sits at `id8*4`),
8638 // [16..24] chroma AC as `16 + c*4 + id4`. The parser already counted
8639 // every significant coefficient; re-deriving the counts here scanned
8640 // 16-64 array elements per block (~400 loads/MB) for information the
8641 // caller was holding — the diagnosis's stage-boundary re-derivation tax.
8642 nnzs: &[u8; 24],
8643 ) {
8644 // A `None` field means the parse wrote nothing there; every read below
8645 // is guarded by a zero-count test, so the shared zero plane is
8646 // read-equivalent to the per-macroblock zeroed stack array it replaces.
8647 let luma_scan = luma_scan.unwrap_or(&ZERO_LUMA_SCAN);
8648 let cac = cac.unwrap_or(&ZERO_CAC);
8649 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecResidAdd);
8650 let qp = self.cur_qp;
8651 let qpc = self.chroma_qp_for(qp);
8652 if let Some(l8) = luma8 {
8653 // INTER 8x8 luma: same primitives the I_8x8 and CAVLC paths use.
8654 for b8 in 0..4usize {
8655 let (b8x, b8y) = (b8 % 2, b8 / 2);
8656 // Summed, not slot 0: with per-4x4 counts in the CAVLC case, slot 0
8657 // is only the first sub-block and can be 0 while the block is coded.
8658 let nnz: u32 = (0..4).map(|k| nnzs[b8 * 4 + k] as u32).sum();
8659 let (px, py) = (mb_x * 16 + b8x * 8, mb_y * 16 + b8y * 8);
8660 if nnz == 0 {
8661 // Zero residual: recon == pred — same shortcut as the
8662 // FrameDecoder twin.
8663 edcstat::bump(&edcstat::T8_ZERO, 1);
8664 for dy in 0..8 {
8665 let d = (py + dy) * self.cw + px;
8666 let po = (b8y * 8 + dy) * 16 + b8x * 8;
8667 self.rec_y[d..d + 8].copy_from_slice(&pred_y[po..po + 8]);
8668 }
8669 } else {
8670 let raster = un_scan_8x8(&l8[b8]);
8671 // list 1 = INTER 8x8 luma scaling list (0 is the intra one).
8672 let res8 = self.inv_quant8(&raster, qp, 1);
8673 let predb: [i32; 64] =
8674 std::array::from_fn(|i| pred_y[(b8y * 8 + i / 8) * 16 + (b8x * 8 + i % 8)] as i32);
8675 let recon = add_residual_8x8(&res8, &predb);
8676 // Eight row copies. This wrote SIXTY-FOUR individually
8677 // bounds-checked samples into the luma plane per coded 8x8
8678 // block — the same shape whose fix carried `recon_i8_block`.
8679 // Present in BOTH the main path and the EDC worker twin.
8680 for dy in 0..8 {
8681 let d = (py + dy) * self.cw + px;
8682 self.rec_y[d..][..8].copy_from_slice(&recon[dy * 8..][..8]);
8683 }
8684 }
8685 }
8686 }
8687 for (blk, &(lbx, lby)) in LUMA_4X4_SCAN_XY.iter().enumerate() {
8688 if luma8.is_some() {
8689 break;
8690 }
8691 let nnz = nnzs[blk];
8692 let cw = self.cw;
8693 let p_off = (lby * 4) * 16 + lbx * 4;
8694 let r_off = (mb_y * 4 + lby) * 4 * cw + (mb_x * 4 + lbx) * 4;
8695 if nnz == 0 {
8696 // Zero residual → recon == prediction EXACTLY (the integer IDCT is
8697 // linear so zeros map to zeros, and pred is already 0..=255) — copy
8698 // the pred rows straight into the plane. On real (sparse-cbp)
8699 // streams this is MOST of the 4×4 blocks.
8700 for r in 0..4 {
8701 self.rec_y[r_off + r * cw..r_off + r * cw + 4]
8702 .copy_from_slice(&pred_y[p_off + r * 16..p_off + r * 16 + 4]);
8703 }
8704 continue;
8705 }
8706 // DC-ONLY: the sole significant coefficient is scan position 0 (the
8707 // zig-zag starts at DC, and un_scan keeps it at raster 0), so the
8708 // whole dequant + IDCT collapses to one multiply and a flat add.
8709 if nnz == 1 && luma_scan[blk][0] != 0 {
8710 let f = self.dequant_dc4(luma_scan[blk][0], qp, 3);
8711 reconstruct_4x4_dc_into((f + 32) >> 6, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
8712 } else {
8713 // Fused un-scan + dequant over ONLY the significant coefficients,
8714 // then IDCT + add + clip straight into the plane — no `qb`, no
8715 // `deq`-from-dense, no `predb` gather, no `s`, no `store` call.
8716 //
8717 // HYBRID: the scatter walks scan positions with a data-dependent
8718 // branch per slot, which beats the branchless dense 16-multiply
8719 // loop only while the block is SPARSE. The DC/zero fast paths
8720 // already removed the sparsest blocks, so the population here
8721 // skews denser — above ~6 coefficients the dense loop wins.
8722 let deq = if nnz <= 6 {
8723 dequant_scatter_4x4(&luma_scan[blk], nnz, 0, qp, self.scaling.as_ref().map(|sc| &sc[3]))
8724 } else {
8725 self.dequant(&un_scan_4x4_dcac(&luma_scan[blk]), qp, 3)
8726 };
8727 reconstruct_4x4_into(&deq, pred_y, p_off, 16, &mut self.rec_y, r_off, cw);
8728 }
8729 }
8730 let mut c_dc = [[0i32; 4]; 2];
8731 if cbp_chroma != 0 {
8732 for c in 0..2 {
8733 c_dc[c] = self.dequant_chroma_dc(&cdc[c], qpc, 4 + c);
8734 }
8735 }
8736 let ccw = self.ccw;
8737 for c in 0..2 {
8738 for &(bx, by) in &CHROMA_4X4_SCAN_XY {
8739 let mut ac_nz = false;
8740 if cbp_chroma == 2 {
8741 let n = nnzs[(16 + c * 4 + by * 2 + bx).min(23)];
8742 ac_nz = n != 0;
8743 }
8744 let dc = c_dc[c & 1][(by * 2 + bx) & 3];
8745 let p_off = (by * 4) * 8 + bx * 4;
8746 let r_off = (mb_y * 2 + by) * 4 * ccw + (mb_x * 2 + bx) * 4;
8747 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
8748 if dc == 0 && !ac_nz {
8749 // Zero residual (no AC, zero DC) → recon == prediction exactly.
8750 for r in 0..4 {
8751 plane[r_off + r * ccw..r_off + r * ccw + 4]
8752 .copy_from_slice(&c_pred[c][p_off + r * 8..p_off + r * 8 + 4]);
8753 }
8754 continue;
8755 }
8756 // DC-ONLY (no coded AC — covers every cbp_chroma==1 block and the
8757 // AC-empty blocks of cbp_chroma==2): the chroma DC arrives ALREADY
8758 // dequantized, so the residual is `(dc + 32) >> 6` flat.
8759 if !ac_nz {
8760 reconstruct_4x4_dc_into((dc + 32) >> 6, &c_pred[c], p_off, 8, plane, r_off, ccw);
8761 continue;
8762 }
8763 // AC-only scan: index i is overall scan position i+1 (ac_shift=1).
8764 // Same sparse/dense hybrid as luma.
8765 let n = nnzs[(16 + c * 4 + by * 2 + bx).min(23)];
8766 let mut deq = if n <= 6 {
8767 dequant_scatter_4x4(&cac[c & 1][(by * 2 + bx) & 3], n, 1, qpc, self.scaling.as_ref().map(|sc| &sc[4 + c]))
8768 } else {
8769 let mut ac = [0i32; 16];
8770 un_scan_4x4_ac_into(&cac[c & 1][(by * 2 + bx) & 3], &mut ac);
8771 // Free-fn dequant: `self.dequant` borrows all of `self`, which
8772 // conflicts with the live `plane` (&mut self.rec_u/v) borrow.
8773 match &self.scaling {
8774 Some(sc) => dequantize_weighted(&ac, qpc, &sc[4 + c]),
8775 None => dequantize(&ac, qpc),
8776 }
8777 };
8778 deq[0] = dc;
8779 reconstruct_4x4_into(&deq, &c_pred[c], p_off, 8, plane, r_off, ccw);
8780 }
8781 }
8782 }
8783
8784 fn filter_row(&mut self, r: usize) {
8785 // The precomputed consumer path reads ONLY `bs` + `t8x8` (+ the qp grid
8786 // passed as a parameter) — verified when the path landed (WHYS Part 15).
8787 let info = rusty_h264_common::deblock::BlockInfo {
8788 inter: &[],
8789 nnz: &[],
8790 mv: &[],
8791 ref_id: &[],
8792 mv1: &[],
8793 ref_id1: &[],
8794 w4: self.mb_w * 4,
8795 t8x8: &self.t8_grid,
8796 bs: &self.bs_store,
8797 poc0: &[],
8798 poc1: &[],
8799 kind: &[],
8800 };
8801 rusty_h264_common::deblock::filter_frame_rows(
8802 &mut self.rec_y,
8803 &mut self.rec_u,
8804 &mut self.rec_v,
8805 self.mb_w,
8806 self.mb_h,
8807 r..r + 1,
8808 &self.qp_grid,
8809 self.chroma_qp_offset,
8810 self.db_oa,
8811 self.db_ob,
8812 &info,
8813 );
8814 }
8815
8816 fn save_bak(&mut self, r: usize) {
8817 let y0 = (r * 16 + 15) * self.cw;
8818 self.bak_y.copy_from_slice(&self.rec_y[y0..y0 + self.cw]);
8819 let c0 = (r * 8 + 7) * self.ccw;
8820 self.bak_u.copy_from_slice(&self.rec_u[c0..c0 + self.ccw]);
8821 self.bak_v.copy_from_slice(&self.rec_v[c0..c0 + self.ccw]);
8822 }
8823}
8824
8825/// The pixel worker: replays jobs in parse order, filters rows as their
8826/// messages arrive, and hands the whole context to the parse thread (and
8827/// back) around intra macroblocks. Returns the context at slice end.
8828/// E2 SEAM COUNTERS (D7). Deterministic — one run is the verdict, no pinning.
8829/// `RS_H264_EDC_STATS=1` prints at decode end. Counts, not clocks: the question
8830/// "does one intra macroblock drain the pipeline" is a COUNT question.
8831pub(crate) mod edcstat {
8832 use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
8833 pub static NEEDCTX: AtomicU64 = AtomicU64::new(0);
8834 pub static JOBS: AtomicU64 = AtomicU64::new(0);
8835 pub static ROWS: AtomicU64 = AtomicU64::new(0);
8836 pub static ROWBYTES: AtomicU64 = AtomicU64::new(0);
8837 pub static MBS: AtomicU64 = AtomicU64::new(0);
8838 pub static J_INTER: AtomicU64 = AtomicU64::new(0);
8839 pub static DOUBLED: AtomicU64 = AtomicU64::new(0);
8840 pub static J_NORES_SENT: AtomicU64 = AtomicU64::new(0);
8841 pub static BATCHES: AtomicU64 = AtomicU64::new(0);
8842 pub static DISPATCH_ON: AtomicU64 = AtomicU64::new(0);
8843 pub static DISPATCH_SEEN: AtomicU64 = AtomicU64::new(0);
8844 /// mb_skip_run band path (big-oppy-decoder §6 KEY glue): P_Skip MBs
8845 /// reconstructed by run-coalesced band copies, and the runs themselves.
8846 /// DETERMINISTIC WIN counters — each banded MB removed 1 luma MC call,
8847 /// 2 chroma MC calls and the 256+128-byte pred staging round-trip.
8848 pub static SKIPBAND_MBS: AtomicU64 = AtomicU64::new(0);
8849 pub static SKIPBAND_RUNS: AtomicU64 = AtomicU64::new(0);
8850 pub static SKIP_SINGLES: AtomicU64 = AtomicU64::new(0);
8851 // MEASUREMENT counters sizing the next skip-band extensions (no behavior).
8852 // P side: singles that sit in a same-row run of >=2 EQUAL nonzero MVs,
8853 // split by full-pel (band-copy-with-offset eligible) vs fractional.
8854 pub static PEQ_FP: AtomicU64 = AtomicU64::new(0);
8855 pub static PEQ_FRAC: AtomicU64 = AtomicU64::new(0);
8856 // B side: full-16x16 spatial-direct calls (B_Skip + direct-16), how many
8857 // collapse to ONE uniform rect, and of those the zero-motion bi/uni and
8858 // full-pel populations + run adjacency (previous MB same params, same row).
8859 pub static BSK_FULLMB: AtomicU64 = AtomicU64::new(0);
8860 pub static BSK_1RECT: AtomicU64 = AtomicU64::new(0);
8861 pub static BSK_ZBI: AtomicU64 = AtomicU64::new(0);
8862 pub static BSK_ZUNI: AtomicU64 = AtomicU64::new(0);
8863 pub static BSK_FP: AtomicU64 = AtomicU64::new(0);
8864 pub static BSK_RUNCONT: AtomicU64 = AtomicU64::new(0);
8865 /// DETERMINISTIC WIN counter — B_Skip MBs taken by the zero-bi fast path.
8866 pub static BSKB_FAST: AtomicU64 = AtomicU64::new(0);
8867 /// P_Skip singles taken by the full-pel direct-copy path (both planes /
8868 /// luma only, chroma still interpolating at mv%8!=0).
8869 pub static SKIP_FP_FULL: AtomicU64 = AtomicU64::new(0);
8870 pub static SKIP_FP_LUMA: AtomicU64 = AtomicU64::new(0);
8871 /// B_Skip full-pel nonzero-MV MBs taken by the offset copy/avg path.
8872 pub static BSKB_FP: AtomicU64 = AtomicU64::new(0);
8873 /// P_Skip parse side: MBs whose skip MV was FORCED (0,0) by the run
8874 /// theorem (left = just-committed (0,0) skip, or out-of-slice) vs MBs
8875 /// that ran the 3-neighbor gather + rule.
8876 pub static SKIPMV_FORCED: AtomicU64 = AtomicU64::new(0);
8877 pub static SKIPMV_DERIVED: AtomicU64 = AtomicU64::new(0);
8878 /// B_Skips whose zero-bi derivation was FORCED by the known-zero bitmap
8879 /// (left+top+topright all recorded ref0/(0,0)-both-lists) — the 6-gather
8880 /// b_direct_nbrs walk skipped entirely.
8881 pub static BSKB_FORCED: AtomicU64 = AtomicU64::new(0);
8882 /// Zero-bi fast B_Skip grid commits batched into row spans: MBs covered
8883 /// and spans flushed (fills of 4N replace N MBs x 28 per-MB fills).
8884 pub static BZ_SPAN_MBS: AtomicU64 = AtomicU64::new(0);
8885 pub static BZ_SPANS: AtomicU64 = AtomicU64::new(0);
8886 /// P_Skip (0,0) grid-commit spans (parse-side mirror of BZ_*).
8887 pub static PZ_SPAN_MBS: AtomicU64 = AtomicU64::new(0);
8888 pub static PZ_SPANS: AtomicU64 = AtomicU64::new(0);
8889 /// Census: b_mc bi-pred regions where BOTH MVs are full-pel (fusable to a
8890 /// direct offset average — sizing only, no behavior).
8891 pub static BMC_BI_FP: AtomicU64 = AtomicU64::new(0);
8892 /// weight_partition passes skipped because the whole list-0 table is
8893 /// identity (384 apply ops avoided per full-MB pass).
8894 pub static WP_SKIPPED: AtomicU64 = AtomicU64::new(0);
8895 /// Intra I_4x4 luma residual dispatch (the inter ladder, ported): how many
8896 /// blocks took the zero / DC-only / sparse-scatter / dense arms.
8897 pub static I4_ZERO: AtomicU64 = AtomicU64::new(0);
8898 pub static I4_DC: AtomicU64 = AtomicU64::new(0);
8899 pub static I4_SPARSE: AtomicU64 = AtomicU64::new(0);
8900 pub static I4_DENSE: AtomicU64 = AtomicU64::new(0);
8901 /// 8x8 residual blocks (intra + inter t8) whose residual is entirely zero:
8902 /// recon == pred, the 64-px widen + add + clip skipped.
8903 pub static T8_ZERO: AtomicU64 = AtomicU64::new(0);
8904 /// I_16x16 4x4 sub-blocks with zero AC: the residual is the (already
8905 /// Hadamard-transformed) DC alone, so the dense dequant + IDCT collapse
8906 /// to one flat add.
8907 pub static I16_DCONLY: AtomicU64 = AtomicU64::new(0);
8908 pub static J_INTER_NORES: AtomicU64 = AtomicU64::new(0);
8909 // ---- deb:derive census (derive_bs_row) — sizing the bS derivation arms.
8910 /// Rows derived, and macroblocks derived across them.
8911 pub static DBS_ROWS: AtomicU64 = AtomicU64::new(0);
8912 pub static DBS_MB: AtomicU64 = AtomicU64::new(0);
8913 /// Arm split: Intra kind-arm / Skip|InterUniform kind-arm (only under
8914 /// RS_H264_KIND_LOADS=1) / the packed `derive_mb_records` default.
8915 pub static DBS_INTRA: AtomicU64 = AtomicU64::new(0);
8916 pub static DBS_KINDARM: AtomicU64 = AtomicU64::new(0);
8917 /// Macroblocks whose kind is Skip|InterUniform: the population that used
8918 /// to evaluate the `kind_loads()` OnceLock guard once EACH.
8919 pub static DBS_KINDGUARD: AtomicU64 = AtomicU64::new(0);
8920 pub static DBS_PACKED: AtomicU64 = AtomicU64::new(0);
8921 /// Of the packed arm: macroblocks whose derivation returned `flat_inter`
8922 /// (internal strengths 0 by construction — the early-return class).
8923 pub static DBS_FLAT: AtomicU64 = AtomicU64::new(0);
8924 /// Macroblocks whose FINAL stored MbBs is all-zero: the population the
8925 /// consumer's two-16-byte-compare early-out serves, and the denominator
8926 /// for the per-MB 128-byte bs_v/bs_h zero-init that precedes it.
8927 pub static DBS_ALLZERO: AtomicU64 = AtomicU64::new(0);
8928 /// nnz_dbr maintenance: rows copied wholesale vs rows that actually
8929 /// carried a transform-8x8 macroblock needing the 8x8 OR fixup.
8930 pub static DBS_NNZ_ROWCOPY: AtomicU64 = AtomicU64::new(0);
8931 pub static DBS_T8ROW: AtomicU64 = AtomicU64::new(0);
8932 pub static DBS_T8MB: AtomicU64 = AtomicU64::new(0);
8933 /// Rows that ran the disable_deblocking_filter_idc==2 crossing-edge pass.
8934 pub static DBS_IDC2ROW: AtomicU64 = AtomicU64::new(0);
8935 #[inline]
8936 pub fn bump(c: &AtomicU64, n: u64) {
8937 if on() {
8938 c.fetch_add(n, Relaxed);
8939 }
8940 }
8941 /// Off by default, yet read on EVERY bump — up to four times per B_Skip.
8942 /// `OnceLock::get_or_init` is an acquire load plus an initialised-state
8943 /// branch behind a non-inlined call; this is the relaxed AtomicU8 tri-state
8944 /// the other knobs in this file already use, and it inlines to one load.
8945 #[inline]
8946 pub fn on() -> bool {
8947 use std::sync::atomic::AtomicU8;
8948 static V: AtomicU8 = AtomicU8::new(0);
8949 match V.load(Relaxed) {
8950 1 => true,
8951 2 => false,
8952 _ => {
8953 let b = std::env::var_os("RS_H264_EDC_STATS").is_some();
8954 V.store(if b { 1 } else { 2 }, Relaxed);
8955 b
8956 }
8957 }
8958 }
8959 pub fn report() {
8960 if !on() {
8961 return;
8962 }
8963 eprintln!(
8964 "EDCDISPATCH threaded_slices={} eligible_slices={}",
8965 DISPATCH_ON.load(Relaxed), DISPATCH_SEEN.load(Relaxed)
8966 );
8967 eprintln!(
8968 "EDCSIZE EdcMsg={} EdcJob={} PInterJob={} BJob={}",
8969 std::mem::size_of::<super::EdcMsg>(),
8970 std::mem::size_of::<super::EdcJob>(),
8971 std::mem::size_of::<super::PInterJob>(),
8972 std::mem::size_of::<super::BJob>(),
8973 );
8974 let (n, j, r, b, m) = (
8975 NEEDCTX.load(Relaxed), JOBS.load(Relaxed), ROWS.load(Relaxed),
8976 ROWBYTES.load(Relaxed), MBS.load(Relaxed),
8977 );
8978 eprintln!(
8979 "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}",
8980 BATCHES.load(Relaxed),
8981 j as f64 / BATCHES.load(Relaxed).max(1) as f64,
8982 1000.0 * n as f64 / m.max(1) as f64,
8983 j as f64 / n.max(1) as f64
8984 );
8985 eprintln!(
8986 "SKIPRUN band_mbs={} band_runs={} mbs_per_run={:.1} singles={} banded_pct={:.1}",
8987 SKIPBAND_MBS.load(Relaxed),
8988 SKIPBAND_RUNS.load(Relaxed),
8989 SKIPBAND_MBS.load(Relaxed) as f64 / SKIPBAND_RUNS.load(Relaxed).max(1) as f64,
8990 SKIP_SINGLES.load(Relaxed),
8991 100.0 * SKIPBAND_MBS.load(Relaxed) as f64
8992 / (SKIPBAND_MBS.load(Relaxed) + SKIP_SINGLES.load(Relaxed)).max(1) as f64,
8993 );
8994 eprintln!(
8995 "SKIPNEXT peq_fp={} peq_frac={} bsk_fullmb={} bsk_1rect={} bsk_zbi={} bsk_zuni={} bsk_fp={} bsk_runcont={} bskb_fast={} skip_fp_full={} skip_fp_luma={} bskb_fp={} skipmv_forced={} skipmv_derived={} bskb_forced={} bz_spans={} bz_span_mbs={} pz_spans={} pz_span_mbs={} bmc_bi_fp={} wp_skipped={} i4_zero={} i4_dc={} i4_sparse={} i4_dense={} t8_zero={} i16_dconly={}",
8996 PEQ_FP.load(Relaxed), PEQ_FRAC.load(Relaxed),
8997 BSK_FULLMB.load(Relaxed), BSK_1RECT.load(Relaxed),
8998 BSK_ZBI.load(Relaxed), BSK_ZUNI.load(Relaxed),
8999 BSK_FP.load(Relaxed), BSK_RUNCONT.load(Relaxed),
9000 BSKB_FAST.load(Relaxed),
9001 SKIP_FP_FULL.load(Relaxed), SKIP_FP_LUMA.load(Relaxed),
9002 BSKB_FP.load(Relaxed),
9003 SKIPMV_FORCED.load(Relaxed), SKIPMV_DERIVED.load(Relaxed),
9004 BSKB_FORCED.load(Relaxed),
9005 BZ_SPANS.load(Relaxed), BZ_SPAN_MBS.load(Relaxed),
9006 PZ_SPANS.load(Relaxed), PZ_SPAN_MBS.load(Relaxed),
9007 BMC_BI_FP.load(Relaxed),
9008 WP_SKIPPED.load(Relaxed),
9009 I4_ZERO.load(Relaxed), I4_DC.load(Relaxed),
9010 I4_SPARSE.load(Relaxed), I4_DENSE.load(Relaxed),
9011 T8_ZERO.load(Relaxed), I16_DCONLY.load(Relaxed),
9012 );
9013 let dmb = DBS_MB.load(Relaxed).max(1);
9014 eprintln!(
9015 "DBSDERIVE rows={} mb={} intra={} kindarm={} kindguard={} packed={} flat={} ({:.1}%) allzero={} ({:.1}%) nnz_rowcopy={} t8row={} t8mb={} idc2row={}",
9016 DBS_ROWS.load(Relaxed), DBS_MB.load(Relaxed),
9017 DBS_INTRA.load(Relaxed), DBS_KINDARM.load(Relaxed),
9018 DBS_KINDGUARD.load(Relaxed), DBS_PACKED.load(Relaxed),
9019 DBS_FLAT.load(Relaxed), 100.0 * DBS_FLAT.load(Relaxed) as f64 / dmb as f64,
9020 DBS_ALLZERO.load(Relaxed), 100.0 * DBS_ALLZERO.load(Relaxed) as f64 / dmb as f64,
9021 DBS_NNZ_ROWCOPY.load(Relaxed), DBS_T8ROW.load(Relaxed),
9022 DBS_T8MB.load(Relaxed), DBS_IDC2ROW.load(Relaxed),
9023 );
9024 let (ji, jn) = (J_INTER.load(Relaxed), J_INTER_NORES.load(Relaxed));
9025 eprintln!(
9026 "EDCMIX doubled={} nores_sent={} inter={ji} inter_no_residual={jn} ({:.1}% of inter) wasted_bytes={:.1} MB of {:.1} MB total inter payload",
9027 DOUBLED.load(Relaxed),
9028 J_NORES_SENT.load(Relaxed),
9029 100.0 * jn as f64 / ji.max(1) as f64,
9030 (jn * 2784) as f64 / 1.048576e6,
9031 (ji * 2784) as f64 / 1.048576e6,
9032 );
9033 }
9034}
9035
9036fn edc_worker(
9037 mut ctx: PixelCtx,
9038 rx: std::sync::mpsc::Receiver<EdcMsg>,
9039 ctx_tx: std::sync::mpsc::Sender<PixelCtx>,
9040 back_rx: std::sync::mpsc::Receiver<PixelCtx>,
9041) -> PixelCtx {
9042 while let Ok(msg) = rx.recv() {
9043 match msg {
9044 EdcMsg::Batch(jobs) => {
9045 for j in jobs {
9046 match j {
9047 EdcJob::Skip { mbx, mby, mv } => ctx.recon_p_skip(mbx, mby, mv),
9048 EdcJob::Inter(j) => ctx.recon_p_inter(&j),
9049 EdcJob::InterNoRes(j) => ctx.recon_p_inter_nores(&j),
9050 EdcJob::B(j) => ctx.recon_b(&j),
9051 EdcJob::BSkip { mbx, mby, regions } => ctx.recon_b_skip(mbx, mby, ®ions),
9052 }
9053 }
9054 }
9055 EdcMsg::Job(EdcJob::Skip { mbx, mby, mv }) => ctx.recon_p_skip(mbx, mby, mv),
9056 EdcMsg::Job(EdcJob::Inter(j)) => ctx.recon_p_inter(&j),
9057 EdcMsg::Job(EdcJob::InterNoRes(j)) => ctx.recon_p_inter_nores(&j),
9058 EdcMsg::Job(EdcJob::B(j)) => ctx.recon_b(&j),
9059 EdcMsg::Job(EdcJob::BSkip { mbx, mby, regions }) => ctx.recon_b_skip(mbx, mby, ®ions),
9060 EdcMsg::Row { r, bs, qp, t8 } => {
9061 let (w, base) = (ctx.mb_w, r * ctx.mb_w);
9062 ctx.bs_store[base..base + w].copy_from_slice(&bs);
9063 ctx.qp_grid[base..base + w].copy_from_slice(&qp);
9064 ctx.t8_grid[base..base + w].copy_from_slice(&t8);
9065 if ctx.db_ena {
9066 ctx.save_bak(r);
9067 ctx.filter_row(r);
9068 ctx.flt_rows = r + 1;
9069 ctx.publish_progress_rows();
9070 }
9071 }
9072 EdcMsg::NeedCtx => {
9073 ctx_tx.send(ctx).expect("parse thread alive");
9074 ctx = back_rx.recv().expect("ctx returned after intra");
9075 }
9076 }
9077 }
9078 ctx
9079}
9080
9081
9082/// One motion-compensation region of a B macroblock, recorded at parse time
9083/// (E3). Weights are the RESOLVED implicit pair — computing them needs the
9084/// ref lists' POCs, which are parse-side state.
9085pub(crate) struct BRegion {
9086 px: usize,
9087 py: usize,
9088 rw: usize,
9089 rh: usize,
9090 refi0: i32,
9091 refi1: i32,
9092 mv0: (i32, i32),
9093 mv1: (i32, i32),
9094 w: Option<(i32, i32)>,
9095}
9096
9097/// A B macroblock's deferred pixel work: replay the regions into fresh
9098/// prediction buffers, then either copy them out (skip/direct, no residual)
9099/// or run the residual add.
9100pub(crate) struct BJob {
9101 mbx: usize,
9102 mby: usize,
9103 qp: u8,
9104 cbp_chroma: u32,
9105 skip: bool,
9106 regions: Vec<BRegion>,
9107 /// `None` when no 4x4 luma block was coded — t8 macroblocks included,
9108 /// since those carry `luma8` instead.
9109 luma_scan: Option<[[i32; 16]; 16]>,
9110 /// `Some` only under transform_size_8x8_flag.
9111 luma8: Option<[[i32; 64]; 4]>,
9112 cdc: [[i32; 4]; 2],
9113 /// `None` unless cbp_chroma == 2 (chroma AC coded).
9114 cac: Option<[[[i32; 16]; 4]; 2]>,
9115 nnzs: [u8; 24],
9116}
9117
9118impl PixelCtx {
9119 fn b_mc(
9120 &self,
9121 mb_x: usize,
9122 mb_y: usize,
9123 px: usize,
9124 py: usize,
9125 rw: usize,
9126 rh: usize,
9127 refi0: i32,
9128 mv0: (i32, i32),
9129 refi1: i32,
9130 mv1: (i32, i32),
9131 pred_y: &mut [u8; 256],
9132 c_pred: &mut [[u8; 64]; 2],
9133 wparam: Option<(i32, i32)>,
9134 ) {
9135 let _gb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBMc);
9136 // Malformed-stream armor, mirroring the P path: now that B slices actually
9137 // PARSE ref_idx (they used to be hardcoded to 0), a mutated stream can hand
9138 // us an index past the end of either list. Clamp rather than panic — the
9139 // crate is `forbid(unsafe_code)` and fuzz-gated to never panic, and a
9140 // wrong picture on garbage input carries no conformance duty.
9141 let refi0 = if refi0 >= 0 { (refi0 as usize).min(self.refs.len().saturating_sub(1)) as i32 } else { -1 };
9142 let refi1 = if refi1 >= 0 { (refi1 as usize).min(self.refs1.len().saturating_sub(1)) as i32 } else { -1 };
9143 if (refi0 >= 0 && self.refs.is_empty()) || (refi1 >= 0 && self.refs1.is_empty()) {
9144 return;
9145 }
9146 let (ch, cch) = (self.mb_h * 16, self.mb_h * 8);
9147 // E3: implicit weights are PARSE-side (they read the ref lists' POCs);
9148 // the region carries the resolved pair.
9149 let weights = wparam;
9150 // Bi-prediction blend: the weights decision is LOOP-INVARIANT, so every
9151 // blend site below matches on `weights` ONCE and runs a branch-free
9152 // pixel loop — the unweighted `(p+q+1)>>1` average then autovectorizes
9153 // (the per-pixel closure this replaces hid the invariant behind a
9154 // capture, and its chroma form was a &dyn call PER PIXEL).
9155 // FULL-WIDTH regions (px == 0, rw == 16 — every 16×16/16×8 partition and
9156 // most direct regions) occupy contiguous rows of `pred_y`, so MC writes
9157 // the destination DIRECTLY: uni-pred needs no staging at all, bi-pred
9158 // stages only the second list and blends in place. The staging arrays
9159 // (512 B zeroed per call before this) now exist only on the branches
9160 // that read them. Same fusion as the P path's mc_rect (WHYS Part 8).
9161 let full = px == 0 && rw == 16;
9162 let _gl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBLuma);
9163 // One scratch borrow for the whole region — both bi-pred passes included.
9164 // The closure yields whether the arm already ran the chroma half (the
9165 // bi-pred full-width arm does, to keep its staging alive) — a plain
9166 // `return` inside would exit the CLOSURE only and chroma would run twice.
9167 let chroma_done = rusty_h264_common::inter::with_mc_scratch(|scr| match (refi0 >= 0, refi1 >= 0, full) {
9168 (true, false, true) => {
9169 let Some(rf) = self.refs.get(refi0 as usize) else { return false };
9170 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
9171 false
9172 }
9173 (false, true, true) => {
9174 let Some(rf) = self.refs1.get(refi1 as usize) else { return false };
9175 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
9176 false
9177 }
9178 (true, true, true) => {
9179 let Some(rf) = self.refs.get(refi0 as usize) else { return false };
9180 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
9181 let mut b = [0u8; 256];
9182 let Some(rf) = self.refs1.get(refi1 as usize) else { return false };
9183 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), rf.lstride(), crate::LPAD, self.cw, ch, mb_x * 16, mb_y * 16 + py, rw, rh, mv1.0, mv1.1, &mut b[..rw * rh]);
9184 drop(_gl);
9185 let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
9186 // SLICE-then-zip: proving the bounds ONCE lets rustc emit the whole
9187 // 256-byte average as 8 straight-line vpavgb ops (verified in
9188 // isolation, x86-64-v3); the indexed form kept a per-iteration
9189 // bounds check and a loop. A hand AVX2 kernel is refuted — the
9190 // compiler already emits the ideal instruction.
9191 let dst = &mut pred_y[py * 16..py * 16 + rw * rh];
9192 match weights {
9193 None => {
9194 for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
9195 *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
9196 }
9197 }
9198 Some((w0, w1)) => {
9199 for (d, s) in dst.iter_mut().zip(&b[..rw * rh]) {
9200 *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
9201 }
9202 }
9203 }
9204 let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
9205 self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
9206 true
9207 }
9208 _ => {
9209 // Narrow region — rows are strided in `pred_y`; stage and copy.
9210 let (mut a, mut b) = ([0u8; 256], [0u8; 256]);
9211 if refi0 >= 0 {
9212 let Some(rf) = self.refs.get(refi0 as usize) else { return false };
9213 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
9214 }
9215 if refi1 >= 0 {
9216 let Some(rf) = self.refs1.get(refi1 as usize) else { return false };
9217 rusty_h264_common::inter::mc_luma_padded_pre(scr, &*rf.luma_guard(rf.ch), 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]);
9218 }
9219 drop(_gl);
9220 let _gbl = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBBlend);
9221 match (refi0 >= 0, refi1 >= 0) {
9222 (true, true) => {
9223 for dy in 0..rh {
9224 let (ar, br) = (&a[dy * rw..dy * rw + rw], &b[dy * rw..dy * rw + rw]);
9225 let base = (py + dy) * 16 + px;
9226 let dst = &mut pred_y[base..base + rw];
9227 match weights {
9228 None => {
9229 for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
9230 *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
9231 }
9232 }
9233 Some((w0, w1)) => {
9234 for ((d, p), q) in dst.iter_mut().zip(ar).zip(br) {
9235 *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
9236 }
9237 }
9238 }
9239 }
9240 }
9241 (true, false) => {
9242 for dy in 0..rh {
9243 let d = (py + dy) * 16 + px;
9244 pred_y[d..d + rw].copy_from_slice(&a[dy * rw..dy * rw + rw]);
9245 }
9246 }
9247 _ => {
9248 for dy in 0..rh {
9249 let d = (py + dy) * 16 + px;
9250 pred_y[d..d + rw].copy_from_slice(&b[dy * rw..dy * rw + rw]);
9251 }
9252 }
9253 }
9254 false
9255 }
9256 });
9257 if chroma_done {
9258 return;
9259 }
9260 let _gc = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecBChroma);
9261 self.b_mc_chroma(mb_x, mb_y, px, py, rw, rh, refi0, mv0, refi1, mv1, c_pred, weights, cch);
9262 }
9263
9264 fn b_mc_chroma(
9265 &self,
9266 mb_x: usize,
9267 mb_y: usize,
9268 px: usize,
9269 py: usize,
9270 rw: usize,
9271 rh: usize,
9272 refi0: i32,
9273 mv0: (i32, i32),
9274 refi1: i32,
9275 mv1: (i32, i32),
9276 c_pred: &mut [[u8; 64]; 2],
9277 weights: Option<(i32, i32)>,
9278 cch: usize,
9279 ) {
9280 let (crx, cry, crw, crh) = (px / 2, py / 2, rw / 2, rh / 2);
9281 let full = crx == 0 && crw == 8;
9282 for c in 0..2 {
9283 match (refi0 >= 0, refi1 >= 0, full) {
9284 (true, false, true) => {
9285 let Some(rf) = self.refs.get(refi0 as usize) else { return };
9286 let pl = if c == 0 { &*rf.chroma_guard(0, rf.ch) } else { &*rf.chroma_guard(1, rf.ch) };
9287 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]);
9288 }
9289 (false, true, true) => {
9290 let Some(rf) = self.refs1.get(refi1 as usize) else { return };
9291 let pl = if c == 0 { &*rf.chroma_guard(0, rf.ch) } else { &*rf.chroma_guard(1, rf.ch) };
9292 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]);
9293 }
9294 (true, true, true) => {
9295 let Some(rf) = self.refs.get(refi0 as usize) else { return };
9296 let pl = if c == 0 { &*rf.chroma_guard(0, rf.ch) } else { &*rf.chroma_guard(1, rf.ch) };
9297 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]);
9298 let mut cb = [0u8; 64];
9299 let Some(rf) = self.refs1.get(refi1 as usize) else { return };
9300 let pl = if c == 0 { &*rf.chroma_guard(0, rf.ch) } else { &*rf.chroma_guard(1, rf.ch) };
9301 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]);
9302 let dst = &mut c_pred[c][cry * 8..cry * 8 + crw * crh];
9303 match weights {
9304 None => {
9305 for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
9306 *d = ((*d as u16 + *s as u16 + 1) >> 1) as u8;
9307 }
9308 }
9309 Some((w0, w1)) => {
9310 for (d, s) in dst.iter_mut().zip(&cb[..crw * crh]) {
9311 *d = ((*d as i32 * w0 + *s as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
9312 }
9313 }
9314 }
9315 }
9316 _ => {
9317 let (mut ca, mut cb) = ([0u8; 64], [0u8; 64]);
9318 if refi0 >= 0 {
9319 let Some(rf) = self.refs.get(refi0 as usize) else { return };
9320 let pl = if c == 0 { &*rf.chroma_guard(0, rf.ch) } else { &*rf.chroma_guard(1, rf.ch) };
9321 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]);
9322 }
9323 if refi1 >= 0 {
9324 let Some(rf) = self.refs1.get(refi1 as usize) else { return };
9325 let pl = if c == 0 { &*rf.chroma_guard(0, rf.ch) } else { &*rf.chroma_guard(1, rf.ch) };
9326 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]);
9327 }
9328 match (refi0 >= 0, refi1 >= 0) {
9329 (true, true) => {
9330 for dy in 0..crh {
9331 let (pr, qr) = (&ca[dy * crw..dy * crw + crw], &cb[dy * crw..dy * crw + crw]);
9332 let base = (cry + dy) * 8 + crx;
9333 let dst = &mut c_pred[c][base..base + crw];
9334 match weights {
9335 None => {
9336 for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
9337 *d = ((*p as u16 + *q as u16 + 1) >> 1) as u8;
9338 }
9339 }
9340 Some((w0, w1)) => {
9341 for ((d, p), q) in dst.iter_mut().zip(pr).zip(qr) {
9342 *d = ((*p as i32 * w0 + *q as i32 * w1 + 32) >> 6).clamp(0, 255) as u8;
9343 }
9344 }
9345 }
9346 }
9347 }
9348 (true, false) => {
9349 for dy in 0..crh {
9350 let d = (cry + dy) * 8 + crx;
9351 c_pred[c][d..d + crw].copy_from_slice(&ca[dy * crw..dy * crw + crw]);
9352 }
9353 }
9354 _ => {
9355 for dy in 0..crh {
9356 let d = (cry + dy) * 8 + crx;
9357 c_pred[c][d..d + crw].copy_from_slice(&cb[dy * crw..dy * crw + crw]);
9358 }
9359 }
9360 }
9361 }
9362 }
9363 }
9364 }
9365
9366 /// B_Skip replay: regions into fresh prediction buffers, then the plane
9367 /// copy — no residual, no coefficient arrays.
9368 fn recon_b_skip(&mut self, mbx: usize, mby: usize, regions: &[BRegion]) {
9369 crate::RefFrame::set_mc_row_need(mby, self.mb_h * 16);
9370 let mut pred_y = [0u8; 256];
9371 let mut c_pred = [[0u8; 64]; 2];
9372 for r in regions {
9373 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);
9374 }
9375 for dy in 0..16 {
9376 let d = (mby * 16 + dy) * self.cw + mbx * 16;
9377 self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
9378 }
9379 for c in 0..2 {
9380 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
9381 for dy in 0..8 {
9382 let d = (mby * 8 + dy) * self.ccw + mbx * 8;
9383 plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
9384 }
9385 }
9386 }
9387
9388 /// Replays one B macroblock's regions + residual (the worker half of the
9389 /// E3 seam). Mirrors the inline order exactly: MC regions in parse order
9390 /// into the prediction buffers, then the residual add (or the skip copy).
9391 fn recon_b(&mut self, j: &BJob) {
9392 crate::RefFrame::set_mc_row_need(j.mby, self.mb_h * 16);
9393 let mut pred_y = [0u8; 256];
9394 let mut c_pred = [[0u8; 64]; 2];
9395 for r in &j.regions {
9396 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);
9397 }
9398 if j.skip {
9399 for dy in 0..16 {
9400 let d = (j.mby * 16 + dy) * self.cw + j.mbx * 16;
9401 self.rec_y[d..d + 16].copy_from_slice(&pred_y[dy * 16..dy * 16 + 16]);
9402 }
9403 for c in 0..2 {
9404 let plane = if c == 0 { &mut self.rec_u } else { &mut self.rec_v };
9405 for dy in 0..8 {
9406 let d = (j.mby * 8 + dy) * self.ccw + j.mbx * 8;
9407 plane[d..d + 8].copy_from_slice(&c_pred[c][dy * 8..dy * 8 + 8]);
9408 }
9409 }
9410 } else {
9411 self.cur_qp = j.qp;
9412 self.add_inter_residual(j.mbx, j.mby, &pred_y, &c_pred, j.luma_scan.as_ref(), j.luma8.as_ref(), &j.cdc, j.cac.as_ref(), j.cbp_chroma, &j.nnzs);
9413 }
9414 }
9415}
9416
9417
9418/// D9b: MC-call coalescing for a P inter MB from committed per-block (mv, ref).
9419/// Shared by residual and no-residual recon — filters are per-output-pixel so
9420/// wider rects are bit-identical to sixteen 4×4 calls.
9421fn coalesce_p_inter_mc(
9422 refs: &[crate::Ref],
9423 cw: usize,
9424 ccw: usize,
9425 mb_h: usize,
9426 mbx: usize,
9427 mby: usize,
9428 gmv: &[(i32, i32); 16],
9429 gref: &[usize; 16],
9430 pred_y: &mut [u8; 256],
9431 c_pred: &mut [[u8; 64]; 2],
9432) {
9433 let _ms = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::DecMcStage);
9434 let (rh16, cch) = (mb_h * 16, mb_h * 8);
9435 let rect_eq = |x4: usize, y4: usize, w4: usize, h4: usize| -> bool {
9436 let t = y4 * 4 + x4;
9437 (0..h4).all(|dy| {
9438 (0..w4).all(|dx| {
9439 // 4x4 block grid: the index is always < 16, but LLVM cannot
9440 // infer it from the nested ranges. `& 15` states it.
9441 let b = ((y4 + dy) * 4 + (x4 + dx)) & 15;
9442 gmv[b] == gmv[t] && gref[b] == gref[t]
9443 })
9444 })
9445 };
9446 let mut mc_rect = |x4: usize, y4: usize, w4: usize, h4: usize| {
9447 let b = y4 * 4 + x4;
9448 let (mv, gr) = (gmv[b & 15], gref[b & 15]);
9449 let Some(reference) = refs.get(gr) else { return };
9450 let (w, h) = (w4 * 4, h4 * 4);
9451 if w == 16 {
9452 rusty_h264_common::inter::with_mc_scratch(|scr| {
9453 rusty_h264_common::inter::mc_luma_padded_pre(
9454 scr,
9455 &*reference.luma_guard(reference.ch),
9456 reference.lstride(),
9457 crate::LPAD,
9458 cw,
9459 rh16,
9460 mbx * 16,
9461 mby * 16 + y4 * 4,
9462 w,
9463 h,
9464 mv.0,
9465 mv.1,
9466 &mut pred_y[y4 * 64..y4 * 64 + w * h],
9467 )
9468 });
9469 } else {
9470 let mut t = [0u8; 256];
9471 rusty_h264_common::inter::with_mc_scratch(|scr| {
9472 rusty_h264_common::inter::mc_luma_padded_pre(
9473 scr,
9474 &*reference.luma_guard(reference.ch),
9475 reference.lstride(),
9476 crate::LPAD,
9477 cw,
9478 rh16,
9479 mbx * 16 + x4 * 4,
9480 mby * 16 + y4 * 4,
9481 w,
9482 h,
9483 mv.0,
9484 mv.1,
9485 &mut t[..w * h],
9486 )
9487 });
9488 let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
9489 for dy in 0..h {
9490 pred_y[(y4 * 4 + dy) * 16 + x4 * 4..][..w]
9491 .copy_from_slice(&t[dy * w..dy * w + w]);
9492 }
9493 }
9494 let (cw4, ch4) = (w4 * 2, h4 * 2);
9495 let nc = cw4 * ch4;
9496 let (gu, gv) = (reference.chroma_guard(0, reference.ch), reference.chroma_guard(1, reference.ch));
9497 // U+V paired: one setup serves both planes (see mc_chroma_padded_pair).
9498 if cw4 == 8 {
9499 let [cu, cv] = &mut *c_pred;
9500 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, reference.cstride(), crate::CPAD, ccw, cch, mbx * 8, mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut cu[y4 * 16..y4 * 16 + nc], &mut cv[y4 * 16..y4 * 16 + nc]);
9501 } else {
9502 let (mut tu, mut tv) = ([0u8; 64], [0u8; 64]);
9503 rusty_h264_common::inter::mc_chroma_padded_pair(&gu, &gv, reference.cstride(), crate::CPAD, ccw, cch, mbx * 8 + x4 * 2, mby * 8 + y4 * 2, cw4, ch4, mv.0, mv.1, &mut tu[..nc], &mut tv[..nc]);
9504 let _pb = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::PredBuf);
9505 for (cc, tc) in [(0usize, &tu), (1, &tv)] {
9506 for dy in 0..ch4 {
9507 c_pred[cc][(y4 * 2 + dy) * 8 + x4 * 2..][..cw4]
9508 .copy_from_slice(&tc[dy * cw4..dy * cw4 + cw4]);
9509 }
9510 }
9511 }
9512 };
9513 if rect_eq(0, 0, 4, 4) {
9514 mc_rect(0, 0, 4, 4);
9515 } else if rect_eq(0, 0, 4, 2) && rect_eq(0, 2, 4, 2) {
9516 mc_rect(0, 0, 4, 2);
9517 mc_rect(0, 2, 4, 2);
9518 } else if rect_eq(0, 0, 2, 4) && rect_eq(2, 0, 2, 4) {
9519 mc_rect(0, 0, 2, 4);
9520 mc_rect(2, 0, 2, 4);
9521 } else {
9522 for q in 0..4usize {
9523 let (qx, qy) = ((q % 2) * 2, (q / 2) * 2);
9524 if rect_eq(qx, qy, 2, 2) {
9525 mc_rect(qx, qy, 2, 2);
9526 } else if rect_eq(qx, qy, 2, 1) && rect_eq(qx, qy + 1, 2, 1) {
9527 mc_rect(qx, qy, 2, 1);
9528 mc_rect(qx, qy + 1, 2, 1);
9529 } else if rect_eq(qx, qy, 1, 2) && rect_eq(qx + 1, qy, 1, 2) {
9530 mc_rect(qx, qy, 1, 2);
9531 mc_rect(qx + 1, qy, 1, 2);
9532 } else {
9533 for j in 0..4usize {
9534 mc_rect(qx + (j % 2), qy + (j / 2), 1, 1);
9535 }
9536 }
9537 }
9538 }
9539}
9540
9541/// One deferred pixel-reconstruction job (entropy-decouple E1 seam).
9542enum EdcJob {
9543 Skip { mbx: usize, mby: usize, mv: (i32, i32) },
9544 Inter(Box<PInterJob>),
9545 B(Box<BJob>),
9546 /// B_Skip / no-residual direct: regions only. The full `BJob` carried
9547 /// 2.6 KB of ZEROED coefficient arrays for ~60% of B macroblocks — the
9548 /// wall-time regression's main CPU tax (B-heavy MT arm measured +45% CPU).
9549 BSkip { mbx: usize, mby: usize, regions: Vec<BRegion> },
9550 /// P inter with `cbp == 0` — the P-side twin of `BSkip` (D9).
9551 InterNoRes(Box<PInterNoResJob>),
9552}
9553
9554/// A P inter macroblock with NO residual (`cbp == 0`) — motion only.
9555///
9556/// D9. `PInterJob` is 2,784 bytes and **93% of that is coefficient arrays**
9557/// (`luma_scan` 1024 + `luma8` 1024 + `cac` 512 + `cdc` 32 = 2,592). When
9558/// `cbp == 0` every one of them is ZERO, and the seam was heap-allocating,
9559/// filling, channel-passing and freeing all 2,592 bytes of nothing —
9560/// 12.8-37.5% of inter jobs on the x264 corpus, 15.8-44.1 MB per 60-frame pass.
9561///
9562/// This is the same pathology `EdcJob::BSkip` was introduced to fix on the B
9563/// side ("2.6 KB of ZEROED coefficient arrays for ~60% of B macroblocks — the
9564/// wall-time regression's main CPU tax"). It was never ported to P.
9565///
9566/// Consumer uses `recon_p_inter_nores` (MC + plane copy) — bit-identical to
9567/// `recon_p_inter` on zero residuals, without memset of 2.5 KB coeff arrays.
9568/// (`RS_H264_NORES=0` keeps the old full-job path for A/B — routed at the
9569/// CONSTRUCTOR, so this job carries only what nores recon reads.)
9570struct PInterNoResJob {
9571 mbx: usize,
9572 mby: usize,
9573 t8: bool,
9574 gmv: [(i32, i32); 16],
9575 gref: [u8; 16],
9576}
9577
9578
9579/// The compact inputs of one CABAC P inter macroblock's reconstruction.
9580/// Shared all-zero residual planes. An Option-shaped residual field is `None`
9581/// exactly when the parse wrote nothing into it, and every consumer read is
9582/// already guarded by a zero-count test — so a `None` reader would have seen
9583/// zeros anyway. Pointing at one static beats zero-initialising 1 KB (luma) or
9584/// 512 B (chroma AC) of stack per macroblock.
9585static ZERO_LUMA_SCAN: [[i32; 16]; 16] = [[0; 16]; 16];
9586/// `(16384 + |td|/2) / td` for every `td` the spec can present. `td` is
9587/// `.clamp(-128, 127)` at both call sites (spec 8.4.1.2.3), so the divide has
9588/// exactly 256 possible inputs and a table replaces it EXACTLY -- this is not an
9589/// approximation, it is the same arithmetic evaluated ahead of time. `td == 0`
9590/// is guarded by the caller and maps to 0 here so the table has no hole.
9591static TX_FOR_TD: [i32; 256] = {
9592 let mut t = [0i32; 256];
9593 let mut i = 0usize;
9594 while i < 256 {
9595 let td = i as i32 - 128; // index 0 => -128 .. index 255 => 127
9596 t[i] = if td == 0 { 0 } else { (16384 + td.abs() / 2) / td };
9597 i += 1;
9598 }
9599 t
9600};
9601
9602#[inline(always)]
9603fn tx_for_td(td: i32) -> i32 {
9604 debug_assert!((-128..=127).contains(&td), "td must be clamped before this");
9605 TX_FOR_TD[(td.clamp(-128, 127) + 128) as usize]
9606}
9607
9608/// The "no coefficients" neighbour record. `mb_nzc` reads are reached only
9609/// through `if let Some(..) = top/left`, so an out-of-range index is
9610/// unreachable; this is what the unavailable branches already imply.
9611static ZERO_NZC: [u8; 24] = [0u8; 24];
9612static ZERO_CAC: [[[i32; 16]; 4]; 2] = [[[0; 16]; 4]; 2];
9613
9614struct PInterJob {
9615 mbx: usize,
9616 mby: usize,
9617 qp: u8,
9618 cbp_chroma: u32,
9619 /// The committed per-block motion, copied at parse time so the worker
9620 /// never reads the parse thread's grids (E2). Ref indices clamped by the
9621 /// consumer, kept u8 (spec max 15).
9622 gmv: [(i32, i32); 16],
9623 gref: [u8; 16],
9624 /// `None` when no 4x4 luma block was coded — t8 macroblocks included,
9625 /// since those carry `luma8` instead.
9626 luma_scan: Option<[[i32; 16]; 16]>,
9627 /// `Some` only under transform_size_8x8_flag.
9628 luma8: Option<[[i32; 64]; 4]>,
9629 cdc: [[i32; 4]; 2],
9630 /// `None` unless cbp_chroma == 2 (chroma AC coded).
9631 cac: Option<[[[i32; 16]; 4]; 2]>,
9632 nnzs: [u8; 24],
9633}
9634
9635/// Entropy-decouple master knob — DEFAULT ON since 2026-08-05 (`RS_H264_EDC=0`
9636/// opts out). E1 was expected to be cost-neutral scaffolding for the E2
9637/// thread; it BANKED on its own: 13/15 pairs, z=2.84, median +4.0% (pooled
9638/// 19/24, z=2.86). Mechanism: LOOP FISSION — batching a row's parsing and
9639/// then a row's reconstruction keeps each large code path's I-cache and
9640/// branch state hot, instead of alternating two giant bodies per macroblock.
9641fn edc_on() -> bool {
9642 use std::sync::atomic::{AtomicU8, Ordering};
9643 static ON: AtomicU8 = AtomicU8::new(0);
9644 match ON.load(Ordering::Relaxed) {
9645 0 => {
9646 let v = !std::env::var_os("RS_H264_EDC").is_some_and(|v| v == "0");
9647 ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
9648 v
9649 }
9650 n => n == 1,
9651 }
9652}
9653
9654/// E2/E3 worker-thread knob.
9655///
9656/// **Default OFF (2026-08-11).** ffmpeg's parallel unit is the PICTURE
9657/// (`decode_slice` + `hl_decode_mb` on the same thread). `edc_worker` is a
9658/// nested recon thread — the wrong function:
9659/// * 1T (`ffmpeg -threads 1`, pinvs pin): two threads thrash one core
9660/// * frame-MT (`ffmpeg -threads N`): each picture worker would spawn another
9661/// Frame-MT (`RS_H264_FRAME_THREADS=N`) is the ffmpeg-shaped pool. This
9662/// pipeline stays as an explicit oracle (`RS_H264_EDC_MT=1`) / old auto-gate
9663/// (`=auto`). `=0` forces inline.
9664/// D8: run every pixel reconstruction TWICE (the second pass is discarded work,
9665/// not discarded output -- recon is idempotent, so the bytes are unchanged).
9666/// `t(double) - t(single)` IS the pixel half's cost, which is the parallel
9667/// fraction the E2 seam can address. Byte-identity is the proof the ablation
9668/// did not change the program (unlike removing the stage, which cascades).
9669/// D9 compact no-residual P inter jobs. `RS_H264_NORES=0` restores the old
9670/// always-full-payload path for A/B (the arm must PIN the value, never inherit
9671/// a default -- an "off" arm that only omits an override measures
9672/// default-vs-default and prints all zeros).
9673/// D10 row batching — **DEFAULT ON. A DELIBERATE THROUGHPUT-OVER-LATENCY TRADE.**
9674///
9675/// Ships a row's pixel jobs in one message instead of one per macroblock:
9676/// 207,949 sends -> 3,086 (**67-70x fewer**), and **~3% less CPU**.
9677///
9678/// ⚠ IT COSTS 2-4% WALL on a single stream — 0.963x / 0.980x, **11/11 pairs on
9679/// two clips**, A/B'd against itself at a fixed queue bound. That is measured,
9680/// reproducible, and ACCEPTED, not an oversight. Do not "fix" it by flipping
9681/// the default; read this first.
9682///
9683/// WHY IT COSTS WALL: batching trades pipelining for synchronisation.
9684/// Per-macroblock sends let the worker start on job 1 immediately; a row batch
9685/// makes it idle until ~70 macroblocks are parsed, then hands it a burst.
9686///
9687/// WHY IT IS STILL THE RIGHT DEFAULT: wall time here is SINGLE-STREAM LATENCY;
9688/// CPU is THROUGHPUT. A host decoding many streams concurrently is CPU-bound,
9689/// not latency-bound, so 3% less CPU is ~3% more capacity while the 2-4% wall
9690/// cost falls on a dimension that is not the constraint. The 67x drop in
9691/// channel operations also removes a park/unpark storm that scales with the
9692/// number of runnable threads — it gets better, not worse, as the box fills.
9693///
9694/// `RS_H264_BATCH=0` restores per-macroblock sends for the latency-sensitive
9695/// single-stream case (playback, seek preview, anything where first-frame time
9696/// dominates).
9697fn batch_on() -> bool {
9698 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9699 *V.get_or_init(|| !std::env::var_os("RS_H264_BATCH").is_some_and(|v| v == "0"))
9700}
9701
9702fn nores_on() -> bool {
9703 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9704 *V.get_or_init(|| !std::env::var_os("RS_H264_NORES").is_some_and(|v| v == "0"))
9705}
9706
9707/// D13 A/B: always allocate B-only CABAC neighbour grids even on P/I slices.
9708fn fat_slice_on() -> bool {
9709 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9710 *V.get_or_init(|| std::env::var_os("RS_H264_FAT_SLICE").is_some_and(|v| v == "1"))
9711}
9712
9713/// MEASUREMENT KNOB — `RS_H264_NO_SKIPBAND=1` forces the per-MB skip path so the
9714/// mb_skip_run band coalescer can be A/B'd paired on ONE binary. Inert when unset.
9715fn no_skipband() -> bool {
9716 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9717 *V.get_or_init(|| std::env::var_os("RS_H264_NO_SKIPBAND").is_some_and(|v| v == "1"))
9718}
9719
9720/// MEASUREMENT KNOB — `RS_H264_NO_RUNMV=1` forces the full skip_mv derivation
9721/// on every P_Skip (disables the run-theorem forced-(0,0) branch) for paired A/B.
9722fn no_runmv() -> bool {
9723 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9724 *V.get_or_init(|| std::env::var_os("RS_H264_NO_RUNMV").is_some_and(|v| v == "1"))
9725}
9726
9727/// MEASUREMENT KNOB — `RS_H264_NO_SKIPFP=1` disables the P_Skip single fast
9728/// paths (full-pel direct copy + identity-weight-pass skip) for paired A/B.
9729fn no_skipfp() -> bool {
9730 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9731 *V.get_or_init(|| std::env::var_os("RS_H264_NO_SKIPFP").is_some_and(|v| v == "1"))
9732}
9733
9734/// MEASUREMENT KNOB — `RS_H264_NO_BSKIPFAST=1` forces the full decode_b_direct
9735/// path for B_Skip so the zero-bi fast path can be A/B'd paired on ONE binary.
9736fn no_bskipfast() -> bool {
9737 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9738 *V.get_or_init(|| std::env::var_os("RS_H264_NO_BSKIPFAST").is_some_and(|v| v == "1"))
9739}
9740
9741fn double_recon() -> bool {
9742 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9743 // `== "1"`, not `is_some()`: the old presence test meant even
9744 // `RS_H264_DOUBLE_RECON=0` DOUBLED the recon work — the one knob in the
9745 // inventory whose "off" spelling turned it on (2026-08-27 audit, site 8).
9746 *V.get_or_init(|| std::env::var_os("RS_H264_DOUBLE_RECON").is_some_and(|v| v == "1"))
9747}
9748
9749fn edc_bound() -> usize {
9750 static V: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9751 *V.get_or_init(|| {
9752 std::env::var("RS_H264_EDC_BOUND")
9753 .ok()
9754 .and_then(|v| v.parse().ok())
9755 .unwrap_or(256)
9756 })
9757}
9758
9759/// D12 — E2 THREADING DISPATCH. Fires on `720p-or-smaller AND bits/MB > 38.4`.
9760///
9761/// The seam threads unconditionally before this, and that shipped a REGRESSION:
9762/// 8-10% slower wall on main profile for 38-56% more CPU. It cannot pay in
9763/// general — the pixel half is only ~15.6% of decode, so Amdahl caps two
9764/// threads at 1.085x — but it DOES win on some streams, so the answer is a
9765/// dispatch, not abandonment.
9766///
9767/// Fitted with `bench/examples/gate_optimizer.rs` over **28 interleaved
9768/// configurations** (12 clips x core counts 4-8, `bench/pinmtx.ps1`):
9769/// **net +29.40 of +30.70 perfect**, train +25.10 AND holdout +4.30, worst
9770/// fired class **+2.94**, precision 0.80. It forgoes +0.40 of wins to avoid
9771/// **169.90** of losses. Calibration: depth-2 **2/300** rules passed (0.67%),
9772/// so the separation carries information.
9773///
9774/// Per clause, both load-bearing (dropping either: -20.50 / -50.60, 6-7 big
9775/// losers):
9776/// * `bits/MB > 38.4` — coefficient density is the runtime proxy for PIXEL
9777/// SHARE: more coefficients means more residual work on the far side of the
9778/// seam. Threshold sits in an open gap (highest excluded 35.4, lowest firing
9779/// 41.4). Note this is the OPPOSITE direction to an earlier hand-fitted
9780/// `bits < 65`, which was fitted to one clip and falsified.
9781/// * frame <= 720p — every 1080p configuration measured loses, including a
9782/// low-density one, so density alone is not sufficient.
9783/// * `cabac` — ADDED 2026-08-07 after the CAVLC arm made those units
9784/// measurable. bits/MB DOES NOT TRANSFER ACROSS ENTROPY CODERS: CAVLC needs
9785/// more bits for the SAME coefficients, so its density (62-65) reads deep
9786/// inside the firing region while its pixel work is unchanged. Without this
9787/// clause the rule routed CAVLC into threading, where it measured 1.29-1.49x
9788/// SLOWER — net -52.30, worst class -40.85. With it, +29.40 and worst class
9789/// +2.94. `gate_optimizer` could not find this: the rule needs THREE clauses
9790/// and the search is depth-2 (both depth-2 pairs fail, -20.50 / -50.60).
9791///
9792/// The estimate comes from ALREADY-DECODED slices, so the first slice of a
9793/// stream runs INLINE (the safe arm) until a measurement exists. Both arms are
9794/// byte-identical, so the choice can never affect output.
9795/// Invoked only by `RS_H264_EDC_MT=auto` (the pre-2026-08-11 default).
9796fn edc_dispatch(mb_w: usize, mb_h: usize, bits_per_mb: f64, cabac: bool) -> bool {
9797 const BITS_MIN: f64 = 38.4;
9798 const MAX_MBS: usize = 5000; // 720p = 3600, 1080p = 8160
9799 // The `cabac` clause is NOT cosmetic — see the header note. Without it this
9800 // rule scores net -52.30 with worst class -40.85 once CAVLC units are in the
9801 // corpus, because CAVLC's bits/MB is inflated by a less efficient entropy
9802 // coder rather than by more pixel work.
9803 cabac && bits_per_mb > BITS_MIN && mb_w * mb_h <= MAX_MBS
9804}
9805
9806fn edc_mt() -> Option<bool> {
9807 static V: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
9808 *V.get_or_init(|| match std::env::var("RS_H264_EDC_MT").ok().as_deref() {
9809 Some("0") => Some(false),
9810 Some("1") => Some(true),
9811 Some("auto") => None,
9812 _ => Some(false),
9813 })
9814}
9815
9816/// Spawn `edc_worker`? ffmpeg never does this: the picture thread owns recon.
9817/// Frame-MT workers (`FRAME_THREADS>1`) always inline. Otherwise honor the knob
9818/// (`1` / `0` / `auto`→[`edc_dispatch`]; unset = inline).
9819fn edc_spawn_worker(mb_w: usize, mb_h: usize, bits_per_mb: f64, cabac: bool) -> bool {
9820 if crate::frame_mt::frame_threads() > 1 {
9821 return false;
9822 }
9823 edc_mt().unwrap_or_else(|| edc_dispatch(mb_w, mb_h, bits_per_mb, cabac))
9824}
9825
9826
9827
9828/// Picture-end bS precompute (rowdb-off fallback). `RS_H264_BS_PRE=0` opts out.
9829fn bs_pre_on() -> bool {
9830 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9831 *V.get_or_init(|| !std::env::var_os("RS_H264_BS_PRE").is_some_and(|v| v == "0"))
9832}
9833
9834/// Row-interleaved deblocking master knob: `RS_H264_ROWDB=0` opts out,
9835/// restoring the picture-end pipeline (WHYS Part 17) as the A/B comparator.
9836/// MEASUREMENT KNOB — `RS_H264_KIND_LOADS=1` restores the Blk::load-based
9837/// kind arms in derive_bs_row (see the routing comment there) for paired A/B.
9838fn kind_loads() -> bool {
9839 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9840 *V.get_or_init(|| std::env::var_os("RS_H264_KIND_LOADS").is_some_and(|v| v == "1"))
9841}
9842
9843fn rowdb_on() -> bool {
9844 use std::sync::atomic::{AtomicU8, Ordering};
9845 static ON: AtomicU8 = AtomicU8::new(0);
9846 match ON.load(Ordering::Relaxed) {
9847 0 => {
9848 let v = !std::env::var_os("RS_H264_ROWDB").is_some_and(|v| v == "0");
9849 ON.store(if v { 1 } else { 2 }, Ordering::Relaxed);
9850 v
9851 }
9852 n => n == 1,
9853 }
9854}
9855
9856/// A/B: per-MB `row_hook` body even when no row has completed (old behaviour).
9857fn rowhook_eager() -> bool {
9858 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9859 *V.get_or_init(|| std::env::var_os("RS_H264_ROWHOOK_EAGER").is_some_and(|v| v == "1"))
9860}
9861
9862/// A/B: `RS_H264_DIRECT_MEMO=0` rewalks spatial-direct neighbours every 8×8.
9863#[inline]
9864fn direct_memo_on() -> bool {
9865 static V: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9866 *V.get_or_init(|| !std::env::var_os("RS_H264_DIRECT_MEMO").is_some_and(|v| v == "0"))
9867}
9868
9869/// 4×4-block (z-order) → 30-entry (6-stride) mv/ref/mvd cache index (openh264
9870/// g_kCache30ScanIdx). Top neighbour = cache[idx-6], left = cache[idx-1].
9871use rusty_h264_common::cabac_tables::CACHE30;
9872
9873/// z-order 4×4-block → raster index (openh264 g_kuiScan4). Per-MB mvd/ref state is
9874/// stored raster-indexed (matching how neighbour blocks 3/7/11/15 and 12..15 are read).
9875use rusty_h264_common::cabac_tables::G_SCAN4;
9876
9877/// P `sub_mb_type` CABAC (openh264 `ParseSubMBTypeCabac`, ctx 21). 0=8×8, 1=8×4, 2=4×8, 3=4×4.
9878fn parse_sub_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
9879 const S: usize = 21;
9880 if cab.decode_decision(S) != 0 {
9881 return 0;
9882 }
9883 if cab.decode_decision(S + 1) != 0 {
9884 3 - cab.decode_decision(S + 2)
9885 } else {
9886 1
9887 }
9888}
9889
9890/// Intra `mb_type` sub-parse for P/B slices (openh264 `DecodeCabacIntraMbType`, `base`=32
9891/// for B). Returns 0 = I_4x4, 1..=24 = I_16x16, 25 = I_PCM (in the intra numbering).
9892fn parse_intra_mb_type_cabac(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
9893 if cab.decode_decision(base) == 0 {
9894 return 0; // I_4x4
9895 }
9896 if cab.decode_terminate() {
9897 return 25; // I_PCM
9898 }
9899 let mut t = 1 + 12 * cab.decode_decision(base + 1) as u32; // cbp_luma != 0
9900 if cab.decode_decision(base + 2) != 0 {
9901 t += 4 + 4 * cab.decode_decision(base + 2) as u32;
9902 }
9903 t += 2 * cab.decode_decision(base + 3) as u32;
9904 t += cab.decode_decision(base + 3) as u32;
9905 t
9906}
9907
9908/// B `mb_type` CABAC (openh264 `ParseMBTypeBSliceCabac`, ctx base 27). `ctx_inc` = (left
9909/// avail & !direct) + (top avail & !direct). Returns 0 = B_Direct_16x16, 1..=21 = the
9910/// L0/L1/Bi 16×16/16×8/8×16 shapes, 22 = B_8x8, 23.. = intra (mb_type − 23).
9911/// Test-only alias so the ENCODER crate can gate `cb_mb_type_b` against this
9912/// parser directly — they are exact inverses, so a round-trip is a complete gate.
9913#[doc(hidden)]
9914pub fn parse_mb_type_b(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
9915 parse_mb_type_b_cabac(cab, ctx_inc)
9916}
9917
9918#[inline]
9919fn parse_mb_type_b_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
9920 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
9921 const B: usize = 27;
9922 if cab.decode_decision(B + ctx_inc) == 0 {
9923 return 0; // B_Direct_16x16
9924 }
9925 if cab.decode_decision(B + 3) == 0 {
9926 return 1 + cab.decode_decision(B + 5) as u32; // 16×16 L0 / L1
9927 }
9928 let mut m = (cab.decode_decision(B + 4) as u32) << 3;
9929 m |= (cab.decode_decision(B + 5) as u32) << 2;
9930 m |= (cab.decode_decision(B + 5) as u32) << 1;
9931 m |= cab.decode_decision(B + 5) as u32;
9932 if m < 8 {
9933 return m + 3;
9934 }
9935 if m == 13 {
9936 return parse_intra_mb_type_cabac(cab, 32) + 23;
9937 }
9938 if m == 14 {
9939 return 11; // B_Bi_8x16
9940 }
9941 if m == 15 {
9942 return 22; // B_8x8
9943 }
9944 m = (m << 1) | cab.decode_decision(B + 5) as u32;
9945 m - 4
9946}
9947
9948/// B `sub_mb_type` CABAC (openh264 `ParseBSubMBTypeCabac`, ctx base 36). Returns 0..=12
9949/// per spec Table 7-18 (0 = B_Direct_8x8, 1 = B_L0_8x8, …, 12 = B_Bi_4x4).
9950fn parse_sub_mb_type_b_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
9951 const B: usize = 36;
9952 if cab.decode_decision(B) == 0 {
9953 return 0; // B_Direct_8x8
9954 }
9955 if cab.decode_decision(B + 1) == 0 {
9956 return 1 + cab.decode_decision(B + 3) as u32; // B_L0_8x8 / B_L1_8x8
9957 }
9958 let mut st = 3u32;
9959 if cab.decode_decision(B + 2) != 0 {
9960 if cab.decode_decision(B + 3) != 0 {
9961 return 11 + cab.decode_decision(B + 3) as u32; // B_L1_4x4 / B_Bi_4x4
9962 }
9963 st += 4;
9964 }
9965 st += 2 * cab.decode_decision(B + 3) as u32;
9966 st += cab.decode_decision(B + 3) as u32;
9967 st
9968}
9969
9970/// Parse one motion partition's `mvd` (x,y) and splat it into the 30-entry cache + the
9971/// per-MB raster mvd/ref state. `part_idx` = the partition's top-left z-order block (for
9972/// the ctxInc neighbour lookup); `zblocks` = every z-order 4×4 block the partition covers.
9973fn parse_mvd_partition(
9974 cab: &mut crate::cabac::Cabac,
9975 part_idx: usize,
9976 zblocks: &[usize],
9977 mvdc: &mut [[i16; 2]; 30],
9978 refc: &mut [i8; 30],
9979 mmvd: &mut [[i16; 2]; 16],
9980 mref: &mut [i8; 16],
9981 ref_idx: i8,
9982) -> (i32, i32) {
9983 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
9984 // `CACHE30` is [usize; 16] and every entry lies in [7, 28]; the mask and
9985 // clamp are semantic no-ops that hand LLVM the ranges it cannot infer, so
9986 // the `refc[s - 6]` / `mvdc[s - 1]` indexes into the 30-entry caches become
9987 // provable instead of bounds-checked.
9988 let s = CACHE30[part_idx & 15].clamp(6, 29);
9989 // Neighbour AVAILABILITY is per-neighbour, not per-component: the old closure
9990 // re-tested both refc slots for each of x and y, four loads to answer two
9991 // questions. Resolve once, then sum each component.
9992 let (av_b, av_a) = (refc[s - 6] >= 0, refc[s - 1] >= 0);
9993 let (nb, na) = (mvdc[s - 6], mvdc[s - 1]);
9994 let ctx = |comp: usize| -> usize {
9995 let mut a = 0i32;
9996 if av_b {
9997 a += nb[comp].unsigned_abs() as i32;
9998 }
9999 if av_a {
10000 a += na[comp].unsigned_abs() as i32;
10001 }
10002 if a >= 3 {
10003 1 + (a > 32) as usize
10004 } else {
10005 0
10006 }
10007 };
10008 let (cx, cy) = (ctx(0), ctx(1));
10009 let mvx = parse_mvd_cabac(cab, 0, cx);
10010 let mvy = parse_mvd_cabac(cab, 1, cy);
10011 let mvd = [mvx, mvy];
10012 if zblocks.len() == 16 {
10013 // Whole-macroblock partition (the dominant B shape): every raster slot
10014 // takes the same value — one fill each instead of 16 indexed stores.
10015 mmvd.fill(mvd);
10016 mref.fill(ref_idx);
10017 for &zb in zblocks {
10018 let c = CACHE30[zb & 15];
10019 mvdc[c] = mvd;
10020 refc[c] = ref_idx;
10021 }
10022 } else {
10023 for &zb in zblocks {
10024 // Each table was being read twice per block.
10025 let (c, g) = (CACHE30[zb & 15], G_SCAN4[zb & 15]);
10026 mvdc[c] = mvd;
10027 refc[c] = ref_idx;
10028 mmvd[g] = mvd;
10029 mref[g] = ref_idx;
10030 }
10031 }
10032 (mvx as i32, mvy as i32)
10033}
10034
10035/// `ref_idx_l0` (P) CABAC — mirror of the encoder `cb_ref_idx`. Unary, ctxIdxOffset
10036/// 54: binIdx 0 → `ctx0` (condTermFlagA + 2·condTermFlagB), binIdx 1 → 4, binIdx ≥2 → 5.
10037pub fn parse_ref_idx_cabac(cab: &mut crate::cabac::Cabac, ctx0: usize) -> i8 {
10038 const B: usize = 54;
10039 let mut r = 0i8;
10040 let mut bin_idx = 0u32;
10041 // Cap the unary length: valid ref_idx ≤ 15 (16 refs max); the cap keeps a corrupt
10042 // stream from looping unboundedly. The MC clamps the index, so an over-range value
10043 // is decoded as garbage (never a panic) — the robustness contract, not correctness.
10044 while bin_idx < 32 {
10045 let ctx = match bin_idx {
10046 0 => ctx0,
10047 1 => 4,
10048 _ => 5,
10049 };
10050 if cab.decode_decision(B + ctx) == 0 {
10051 break;
10052 }
10053 r += 1;
10054 bin_idx += 1;
10055 }
10056 r
10057}
10058
10059/// UEG3 mvd suffix (openh264 `DecodeUEGMvCabac`): TU prefix at `base + {0,1,2,3,3,..}`
10060/// (≤7), then EG3 bypass.
10061fn decode_ueg_mv(cab: &mut crate::cabac::Cabac, base: usize) -> u32 {
10062 const P2C: [usize; 8] = [0, 1, 2, 3, 3, 3, 3, 3];
10063 if cab.decode_decision(base) == 0 {
10064 return 0;
10065 }
10066 let mut code = 0u32;
10067 let mut count = 1usize;
10068 let mut tmp;
10069 loop {
10070 tmp = cab.decode_decision(base + P2C[count]);
10071 code += 1;
10072 count += 1;
10073 if tmp == 0 || count == 8 {
10074 break;
10075 }
10076 }
10077 if tmp != 0 {
10078 code += cabac_exp_bypass(cab, 3) + 1;
10079 }
10080 code
10081}
10082
10083/// One `mvd` component (openh264 `ParseMvdInfoCabac`). `ctx_inc` (0/1/2) from the
10084/// neighbour |mvd| sum. ctxIdxOffset 40 (x) / 47 (y).
10085fn parse_mvd_cabac(cab: &mut crate::cabac::Cabac, comp: usize, ctx_inc: usize) -> i16 {
10086 let base = 40 + comp * 7; // NEW_CTX_OFFSET_MVD + comp*CTX_NUM_MVD
10087 if cab.decode_decision(base + ctx_inc) == 0 {
10088 return 0;
10089 }
10090 let mag = (decode_ueg_mv(cab, base + 3) + 1) as i16;
10091 if cab.decode_bypass() != 0 {
10092 -mag
10093 } else {
10094 mag
10095 }
10096}
10097
10098/// `mb_skip_flag` CABAC (openh264 `ParseSkipFlagCabac`). `ctx_inc` = base 11 (P) or 24
10099/// (B) + (left avail & not-skip) + (top avail & not-skip). Returns true if skipped.
10100#[inline]
10101fn parse_mb_skip_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> bool {
10102 cab.decode_decision(ctx_inc) != 0
10103}
10104
10105/// P-slice `mb_type` CABAC (openh264 `ParseMBTypePSliceCabac`). Returns 0..3 = inter
10106/// (P_L0_16x16 / P_16x8 / P_8x16 / P_8x8), 5 = I_4x4, 6..29 = I_16x16, 30 = I_PCM.
10107#[inline]
10108fn parse_mb_type_p_cabac(cab: &mut crate::cabac::Cabac) -> u32 {
10109 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
10110 const S: usize = 11; // NEW_CTX_OFFSET_SKIP; P mb_type contexts hang off it
10111 if cab.decode_decision(S + 3) == 0 {
10112 // inter
10113 return if cab.decode_decision(S + 4) != 0 {
10114 if cab.decode_decision(S + 6) != 0 { 1 } else { 2 }
10115 } else if cab.decode_decision(S + 5) != 0 {
10116 3
10117 } else {
10118 0
10119 };
10120 }
10121 // intra (prefix bit was 1)
10122 if cab.decode_decision(S + 6) == 0 {
10123 return 5; // I_4x4
10124 }
10125 if cab.decode_terminate() {
10126 return 30; // I_PCM
10127 }
10128 let mut t = 6 + cab.decode_decision(S + 7) * 12;
10129 if cab.decode_decision(S + 8) != 0 {
10130 t += 4;
10131 if cab.decode_decision(S + 8) != 0 {
10132 t += 4;
10133 }
10134 }
10135 t += cab.decode_decision(S + 9) << 1;
10136 t += cab.decode_decision(S + 9);
10137 t
10138}
10139
10140/// I-slice `mb_type` CABAC parse (spec §9.3.2.5 / openh264 `ParseMBTypeISliceCabac`).
10141/// `ctx_inc` = (left MB is I_16x16/non-intra) + (top MB is …), i.e. 0..2; the corner
10142/// MB has no neighbours so `ctx_inc = 0`. Returns the raw mb_type: 0 = I_NxN (I_4x4/
10143/// I_8x8), 1..24 = I_16x16 (pred-mode/cbp packed), 25 = I_PCM.
10144fn parse_mb_type_i_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
10145 const O: usize = 3; // ctxIdxOffset for I-slice mb_type
10146 if cab.decode_decision(O + ctx_inc) == 0 {
10147 return 0; // I_NxN
10148 }
10149 if cab.decode_terminate() {
10150 return 25; // I_PCM
10151 }
10152 let mut t = 1 + cab.decode_decision(O + 3) * 12; // CBP luma: 0 or 12
10153 if cab.decode_decision(O + 4) != 0 {
10154 t += 4; // CBP chroma 1 or 2
10155 if cab.decode_decision(O + 5) != 0 {
10156 t += 4;
10157 }
10158 }
10159 t += cab.decode_decision(O + 6) << 1; // I_16x16 pred mode (2 bins)
10160 t += cab.decode_decision(O + 7);
10161 t
10162}
10163
10164/// One `Intra_4x4` (or `8x8`) pred-mode CABAC parse (openh264 `ParseIntraPredModeLuma
10165/// Cabac`): `prev_intra4x4_pred_mode_flag` (ctx 68) then, if 0, `rem_intra4x4_pred_mode`
10166/// (3 bins at ctx 69). Returns `-1` for "use predicted mode", else the 0..7 remainder.
10167fn parse_intra4x4_pred_mode_cabac(cab: &mut crate::cabac::Cabac) -> i32 {
10168 const IPR: usize = 68;
10169 if cab.decode_decision(IPR) == 1 {
10170 return -1; // prev_intra4x4_pred_mode_flag = 1
10171 }
10172 let mut m = cab.decode_decision(IPR + 1) as i32;
10173 m |= (cab.decode_decision(IPR + 1) as i32) << 1;
10174 m |= (cab.decode_decision(IPR + 1) as i32) << 2;
10175 m
10176}
10177
10178/// `intra_chroma_pred_mode` CABAC parse (openh264 `ParseIntraPredModeChromaCabac`):
10179/// TU(cMax=3) — bin0 at ctx `64 + ctx_inc` (ctx_inc from neighbour chroma modes, 0 for
10180/// the corner MB), the rest at ctx 67. Returns the mode 0..3.
10181fn parse_intra_chroma_pred_mode_cabac(cab: &mut crate::cabac::Cabac, ctx_inc: usize) -> u32 {
10182 const CIPR: usize = 64;
10183 if cab.decode_decision(CIPR + ctx_inc) == 0 {
10184 return 0;
10185 }
10186 if cab.decode_decision(CIPR + 3) == 0 {
10187 return 1;
10188 }
10189 if cab.decode_decision(CIPR + 3) == 0 {
10190 return 2;
10191 }
10192 3
10193}
10194
10195/// `coded_block_pattern` CABAC parse (openh264 `ParseCbpInfoCabac`), corner-MB variant
10196/// (top/left neighbours unavailable → their terms are 0). ctxIdxOffset 73 (luma) with 4
10197/// z-order 8×8 bins whose ctxInc uses the EARLIER-decoded bits within this MB, then
10198/// chroma bits at 77/81. Returns cbp: bits 0-3 = luma 8×8, bits 4-5 = chroma pattern.
10199pub fn parse_cbp_cabac(cab: &mut crate::cabac::Cabac, top: Option<u8>, left: Option<u8>) -> u32 {
10200 let _g = rusty_h264_common::prof::scope(rusty_h264_common::prof::Stage::Syntax);
10201 const CBP: usize = 73;
10202 let t = |m: u32| top.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
10203 let l = |m: u32| left.map_or(0u32, |c| ((c as u32 & m) == 0) as u32);
10204 let nb = |x: u32| (x == 0) as u32; // earlier 8×8 bin within this MB was NOT coded
10205 // Luma, 4 8×8 blocks in z-order. Top uses cbp bits 2/3, left uses 1/3.
10206 let b0 = cab.decode_decision(CBP + (l(1 << 1) + (t(1 << 2) << 1)) as usize);
10207 let b1 = cab.decode_decision(CBP + (nb(b0) + (t(1 << 3) << 1)) as usize);
10208 let b2 = cab.decode_decision(CBP + (l(1 << 3) + (nb(b0) << 1)) as usize);
10209 let b3 = cab.decode_decision(CBP + (nb(b2) + (nb(b1) << 1)) as usize);
10210 let mut cbp = b0 | (b1 << 1) | (b2 << 2) | (b3 << 3);
10211 // Chroma (4:2:0). ctxInc from neighbour chroma cbp (>>4).
10212 let ct = top.map_or(0u32, |c| ((c >> 4) != 0) as u32);
10213 let cl = left.map_or(0u32, |c| ((c >> 4) != 0) as u32);
10214 if cab.decode_decision(CBP + 4 + (cl + (ct << 1)) as usize) != 0 {
10215 let ct2 = top.map_or(0u32, |c| ((c >> 4) == 2) as u32);
10216 let cl2 = left.map_or(0u32, |c| ((c >> 4) == 2) as u32);
10217 let c1 = cab.decode_decision(CBP + 8 + (cl2 + (ct2 << 1)) as usize);
10218 cbp |= 1 << (4 + c1);
10219 }
10220 cbp
10221}
10222
10223fn read_ref_idx(r: &mut BitReader, num_ref_active: usize) -> Result<i32, OutOfData> {
10224 if num_ref_active == 2 {
10225 Ok(if r.read_bit()? { 0 } else { 1 }) // te(v): value = !bit
10226 } else {
10227 Ok(r.read_ue()? as i32)
10228 }
10229}
10230
10231/// B-partition prediction direction.
10232#[derive(Clone, Copy, PartialEq)]
10233enum BPred {
10234 L0,
10235 L1,
10236 Bi,
10237}
10238impl BPred {
10239 /// Whether this direction uses reference list `list` (0 or 1).
10240 fn uses(self, list: usize) -> bool {
10241 matches!(
10242 (self, list),
10243 (BPred::L0, 0) | (BPred::L1, 1) | (BPred::Bi, 0) | (BPred::Bi, 1)
10244 )
10245 }
10246}
10247
10248const B16X16: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 16)];
10249const B16X8: &[(usize, usize, usize, usize)] = &[(0, 0, 16, 8), (0, 8, 16, 8)];
10250const B8X16: &[(usize, usize, usize, usize)] = &[(0, 0, 8, 16), (8, 0, 8, 16)];
10251
10252/// A partition region `(x, y, w, h)` in samples.
10253type Region = (usize, usize, usize, usize);
10254
10255/// B `mb_type` 1..=21 → (partition layout, MV-prediction mode 0/1/2 for 16×16/
10256/// 16×8/8×16, per-partition prediction direction) (spec Table 7-14).
10257/// Test-only view of [`b_inter_layout`] for the ENCODER crate: `(mvmode, p0, p1)`
10258/// with pred coded 1 = L0, 2 = L1, 3 = Bi — the encoder's `b_part_mb_type` is the
10259/// exact inverse, so a round-trip over 4..=21 gates the two tables against drift.
10260pub fn b_inter_shape(mb_type: u32) -> (u8, u8, u8) {
10261 let (_, mvmode, preds) = b_inter_layout(mb_type);
10262 let code = |p: BPred| match (p.uses(0), p.uses(1)) {
10263 (true, true) => 3,
10264 (true, false) => 1,
10265 _ => 2,
10266 };
10267 (mvmode, code(preds[0]), code(preds[1]))
10268}
10269
10270fn b_inter_layout(mb_type: u32) -> (&'static [Region], u8, [BPred; 2]) {
10271 use BPred::*;
10272 match mb_type {
10273 1 => (B16X16, 0, [L0, L0]),
10274 2 => (B16X16, 0, [L1, L1]),
10275 3 => (B16X16, 0, [Bi, Bi]),
10276 4 => (B16X8, 1, [L0, L0]),
10277 5 => (B8X16, 2, [L0, L0]),
10278 6 => (B16X8, 1, [L1, L1]),
10279 7 => (B8X16, 2, [L1, L1]),
10280 8 => (B16X8, 1, [L0, L1]),
10281 9 => (B8X16, 2, [L0, L1]),
10282 10 => (B16X8, 1, [L1, L0]),
10283 11 => (B8X16, 2, [L1, L0]),
10284 12 => (B16X8, 1, [L0, Bi]),
10285 13 => (B8X16, 2, [L0, Bi]),
10286 14 => (B16X8, 1, [L1, Bi]),
10287 15 => (B8X16, 2, [L1, Bi]),
10288 16 => (B16X8, 1, [Bi, L0]),
10289 17 => (B8X16, 2, [Bi, L0]),
10290 18 => (B16X8, 1, [Bi, L1]),
10291 19 => (B8X16, 2, [Bi, L1]),
10292 20 => (B16X8, 1, [Bi, Bi]),
10293 _ => (B8X16, 2, [Bi, Bi]), // 21
10294 }
10295}
10296
10297/// Whether a B `sub_mb_type` (1..=12) uses reference list `list`.
10298fn b_sub_uses(st: u32, list: usize) -> bool {
10299 let pred = match st {
10300 1 | 4 | 5 | 10 => 0, // L0
10301 2 | 6 | 7 | 11 => 1, // L1
10302 _ => 2, // Bi (3, 8, 9, 12)
10303 };
10304 (list == 0 && pred != 1) || (list == 1 && pred != 0)
10305}
10306
10307/// Sub-partition shapes within an 8×8 for a B `sub_mb_type` (1..=12).
10308fn b_sub_parts(st: u32) -> &'static [(usize, usize, usize, usize)] {
10309 match st {
10310 1..=3 => &[(0, 0, 8, 8)],
10311 4 | 6 | 8 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
10312 5 | 7 | 9 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
10313 _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)], // 10/11/12
10314 }
10315}
10316
10317/// Sub-macroblock partition layout `(x, y, w, h)` in samples within an 8×8, for
10318/// a P-slice `sub_mb_type` (0 = 8×8, 1 = 8×4, 2 = 4×8, 3 = 4×4).
10319fn sub_mb_partitions(sub_type: u32) -> &'static [(usize, usize, usize, usize)] {
10320 match sub_type {
10321 0 => &[(0, 0, 8, 8)],
10322 1 => &[(0, 0, 8, 4), (0, 4, 8, 4)],
10323 2 => &[(0, 0, 4, 8), (4, 0, 4, 8)],
10324 _ => &[(0, 0, 4, 4), (4, 0, 4, 4), (0, 4, 4, 4), (4, 4, 4, 4)],
10325 }
10326}
10327
10328/// Copy a contiguous `w`x`h` block into a strided destination at `(x0, y0)`.
10329///
10330/// The width is SPECIALISED. Written as a per-pixel loop bounded by a runtime `w`,
10331/// this lowers to a bounds-checked store per pixel — and where it is a row copy of
10332/// runtime length, to a variable-length `memcpy` CALL per row. Both are the same
10333/// codegen trap the ENCODER fixed long ago ("H-17"); the decoder's copy of it was
10334/// never fixed, and it costs the most on exactly the streams a real encoder emits,
10335/// because x264's sub-16x16 partitions call it far more often than our own
10336/// 16x16-dominated bitstreams ever did. Byte-identical to the scalar form.
10337#[inline]
10338fn restride(dst: &mut [u8], dst_stride: usize, x0: usize, y0: usize, src: &[u8], w: usize, h: usize) {
10339 macro_rules! rows {
10340 ($n:expr) => {{
10341 for dy in 0..h {
10342 dst[(y0 + dy) * dst_stride + x0..][..$n].copy_from_slice(&src[dy * $n..][..$n]);
10343 }
10344 }};
10345 }
10346 match w {
10347 16 => rows!(16),
10348 8 => rows!(8),
10349 4 => rows!(4),
10350 2 => rows!(2),
10351 _ => {
10352 for dy in 0..h {
10353 dst[(y0 + dy) * dst_stride + x0..][..w].copy_from_slice(&src[dy * w..][..w]);
10354 }
10355 }
10356 }
10357}
10358
10359/// Un-scans an 8×8 block from frame zig-zag scan order to raster (spec Table 8-12).
10360fn un_scan_8x8(scan: &[i32; 64]) -> [i32; 64] {
10361 const ZZ8: [usize; 64] = [
10362 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,
10363 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
10364 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
10365 ];
10366 let mut out = [0i32; 64];
10367 for k in 0..64 {
10368 out[ZZ8[k]] = scan[k];
10369 }
10370 out
10371}
10372
10373#[cfg(test)]
10374mod tests {
10375 use super::*;
10376
10377 fn fd(qp: u8, offset: i32) -> FrameDecoder {
10378 FrameDecoder::new(1, 1, qp, offset, Vec::new(), 1, false, false, true)
10379 }
10380
10381 #[test]
10382 fn mb_qp_delta_accumulates_mod_52() {
10383 let mut d = fd(26, 0);
10384 assert_eq!(d.cur_qp, 26, "QPy starts at the slice QP");
10385 d.step_qp(4).unwrap();
10386 assert_eq!(d.cur_qp, 30); // 26 + 4
10387 d.step_qp(-10).unwrap();
10388 assert_eq!(d.cur_qp, 20); // carries from the previous MB, not the slice
10389 // Wrap-around: (20 + 40 + 52) % 52 = 112 % 52 = 8.
10390 d.step_qp(40).unwrap();
10391 assert_eq!(d.cur_qp, 8);
10392 // Negative wrap: (8 - 20 + 52) % 52 = 40.
10393 d.step_qp(-20).unwrap();
10394 assert_eq!(d.cur_qp, 40);
10395 }
10396
10397 #[test]
10398 fn chroma_qp_index_offset_applied_and_clamped() {
10399 // Offset 0 reproduces the bare luma->chroma table (QP30 -> 29).
10400 assert_eq!(fd(0, 0).chroma_qp_for(30), 29);
10401 // Positive offset shifts the table lookup (QP30 + 2 -> table[2] = 31).
10402 assert_eq!(fd(0, 2).chroma_qp_for(30), 31);
10403 // The qPi index is clamped into 0..=51 before the lookup.
10404 assert_eq!(fd(0, -12).chroma_qp_for(5), chroma_qp(0));
10405 assert_eq!(fd(0, 99).chroma_qp_for(40), chroma_qp(51));
10406 }
10407}