Skip to main content

videocall_codecs/vp9/dec/
mod.rs

1/*
2 * Copyright 2025 Security Union LLC
3 *
4 * Licensed under either of
5 *
6 * * Apache License, Version 2.0
7 *   (http://www.apache.org/licenses/LICENSE-2.0)
8 * * MIT license
9 *   (http://opensource.org/licenses/MIT)
10 *
11 * at your option.
12 *
13 * Unless you explicitly state otherwise, any contribution intentionally
14 * submitted for inclusion in the work by you, as defined in the Apache-2.0
15 * license, shall be dual licensed as above, without any additional terms or
16 * conditions.
17 */
18
19//! Pure-Rust VP9 decoder for our own encoder's subset (Milestones 0 and 1).
20//!
21//! Mirrors the pure-Rust encoder in [`crate::vp9::enc`]: it inverts the exact
22//! bitstream subset the encoder emits (Profile 0, error-resilient, `ALLOW_8X8`
23//! transforms, 16x16 max partition, tile columns) and reuses the bit-exact
24//! [`crate::vp9::common`] machinery — the boolean reader, token trees, dequant
25//! tables, inverse transforms, DC intra predictor, motion-vector reference
26//! derivation, integer-pel motion compensation, and the shared partition /
27//! entropy contexts — so the reconstruction it produces is byte-identical to the
28//! encoder's own reconstruction buffer.
29//!
30//! The decoder walks each tile column's superblock tree in the identical
31//! recursive z-order as the encoder's pack walk, keeping the partition and
32//! per-plane entropy contexts in lockstep. Being pure Rust (no C libvpx), it
33//! compiles for `wasm32` and will back a future UniFFI/iOS wrapper.
34//!
35//! - **M0 (keyframe):** DC_PRED intra blocks, 8x8 luma / 4x4 chroma transforms.
36//! - **M1 (inter):** single-reference (LAST) integer-pel motion compensation with
37//!   ZEROMV / NEARESTMV / NEWMV modes, decoded via the stateful [`Vp9Decoder`],
38//!   which maintains the reference-buffer slots across a keyframe-plus-inter
39//!   sequence.
40//!
41//! Not yet covered: non-DC intra modes, sub-pel motion, compound prediction,
42//! loop filtering, larger transforms, and arbitrary-profile / non-error-resilient
43//! streams. Anything outside the subset is rejected with a [`DecodeError`] rather
44//! than decoded — this parses untrusted network input, so every path either
45//! reconstructs in-bounds pixels or returns an error; none panics.
46
47mod detokenize;
48mod header;
49
50use std::rc::Rc;
51
52use crate::vp9::common::block::{
53    mi_cols as mi_cols_of, mi_rows as mi_rows_of, tile_offset, BlockSize, Partition,
54    PredictionMode, B_WIDTH_LOG2,
55};
56use crate::vp9::common::block::{TxMode, TxSize};
57use crate::vp9::common::bool_coder::BoolReader;
58use crate::vp9::common::frame_buffer::FrameBuffer;
59use crate::vp9::common::generated::{
60    DEFAULT_INTER_MODE_PROBS, DEFAULT_INTRA_INTER_PROBS, DEFAULT_PARTITION_PROBS,
61    DEFAULT_SINGLE_REF_PROBS, DEFAULT_SKIP_PROBS, KF_PARTITION_PROBS, KF_UV_MODE_PROBS,
62    KF_Y_MODE_PROBS,
63};
64use crate::vp9::common::idct::{idct4x4_add, idct8x8_add};
65use crate::vp9::common::inter_ctx::{intra_inter_context, single_ref_p1_context, InterNeighbor};
66use crate::vp9::common::inter_pred::{predict_inter_block, Plane as McPlane};
67use crate::vp9::common::intra_pred::build_intra_dc;
68use crate::vp9::common::mvref::{
69    find_mv_refs, Mv, MvRefGeom, MvRefInfo, INTRA_FRAME, LAST_FRAME, NONE_FRAME,
70};
71use crate::vp9::common::partition::{read_partition, PartitionContext};
72use crate::vp9::common::quant::{ac_quant, dc_quant};
73use crate::vp9::common::trees::{read_tree, INTER_MODE_TREE, INTRA_MODE_TREE};
74use detokenize::{decode_coefs, PlaneType, REF_TYPE_INTER, REF_TYPE_INTRA};
75use header::{parse_frame_header, FrameHeader, FrameType, REF_FRAMES};
76
77mod readmv;
78use readmv::read_mv;
79
80/// A VP9 decode failure.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum DecodeError {
83    /// The bitstream ended before a required field/payload.
84    Truncated,
85    /// A syntactically valid stream using a feature outside the supported subset.
86    Unsupported(&'static str),
87    /// A malformed field (bad marker, sync code, or partition/leaf structure).
88    Corrupt(&'static str),
89    /// Frame dimensions exceed [`MAX_DIM`]. Rejected at the header (before any
90    /// allocation) so an untrusted ~20-byte header cannot request a multi-gigabyte
91    /// buffer or overflow the 32-bit plane stride arithmetic on wasm32.
92    TooLarge,
93}
94
95impl std::fmt::Display for DecodeError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            DecodeError::Truncated => write!(f, "truncated VP9 bitstream"),
99            DecodeError::Unsupported(what) => write!(f, "unsupported VP9 feature: {what}"),
100            DecodeError::Corrupt(what) => write!(f, "corrupt VP9 bitstream: {what}"),
101            DecodeError::TooLarge => {
102                write!(f, "frame dimensions exceed the maximum of {MAX_DIM}")
103            }
104        }
105    }
106}
107
108/// Maximum decodable frame width/height in pixels. Comfortably covers 4K/UHD and
109/// any realistic call resolution; bounds every dimension-derived allocation.
110pub const MAX_DIM: u32 = 8192;
111
112impl std::error::Error for DecodeError {}
113
114/// DC_PRED discriminant (the only intra mode this decoder supports).
115const DC_PRED: u8 = 0;
116
117/// Which plane a transform block belongs to.
118#[derive(Clone, Copy)]
119enum Plane {
120    Y,
121    U,
122    V,
123}
124
125/// Per-mi decoded mode info the neighbor-context and MV-reference derivations
126/// read. Keyframe blocks are intra DC_PRED; inter blocks carry a real
127/// mode/reference/motion. Replicated across all mi units of a 16x16 leaf, exactly
128/// as the encoder stores its grid, so later neighbor lookups agree.
129#[derive(Clone)]
130struct DecMi {
131    /// Block skip flag (no coded residual).
132    skip: bool,
133    /// True for an inter (motion-compensated) block.
134    is_inter: bool,
135    /// Prediction mode (intra `DC_PRED` for keyframe blocks; an inter mode
136    /// otherwise). Feeds `find_mv_refs`'s neighbor mode counter.
137    mode: PredictionMode,
138    /// `ref_frame[0..2]` (`[INTRA_FRAME, NONE]` for intra, `[LAST_FRAME, NONE]`
139    /// for this encoder's inter blocks).
140    ref_frame: [i8; 2],
141    /// Block motion vector in 1/8-pel units (`ZERO` for intra / ZEROMV).
142    mv: Mv,
143    /// Intra luma mode (for the keyframe neighbor-mode context); `DC_PRED` only.
144    ymode: u8,
145}
146
147impl Default for DecMi {
148    fn default() -> Self {
149        DecMi {
150            skip: false,
151            is_inter: false,
152            mode: PredictionMode::DcPred,
153            ref_frame: [INTRA_FRAME, NONE_FRAME],
154            mv: Mv::ZERO,
155            ymode: DC_PRED,
156        }
157    }
158}
159
160/// Per-plane entropy contexts (`above_context` / `left_context`) maintained
161/// across the coefficient decode, mirroring the encoder's `EntropyContext`. Luma
162/// is indexed in 4x4 units at full resolution; chroma at half resolution.
163struct EntropyContext {
164    above_y: Vec<u8>,
165    above_u: Vec<u8>,
166    above_v: Vec<u8>,
167    left_y: [u8; 16],
168    left_u: [u8; 8],
169    left_v: [u8; 8],
170}
171
172impl EntropyContext {
173    fn new(mi_cols_aligned: usize) -> Self {
174        Self {
175            above_y: vec![0u8; 2 * mi_cols_aligned],
176            above_u: vec![0u8; mi_cols_aligned],
177            above_v: vec![0u8; mi_cols_aligned],
178            left_y: [0u8; 16],
179            left_u: [0u8; 8],
180            left_v: [0u8; 8],
181        }
182    }
183
184    fn reset_left(&mut self) {
185        self.left_y = [0u8; 16];
186        self.left_u = [0u8; 8];
187        self.left_v = [0u8; 8];
188    }
189}
190
191/// `combine_entropy_contexts(a, b)` = `(a != 0) + (b != 0)`.
192#[inline]
193fn ctx_combine(a: bool, b: bool) -> usize {
194    a as usize + b as usize
195}
196
197/// A stateful VP9 decoder for the encoder's subset.
198///
199/// Maintains the eight reference-buffer slots across a frame sequence so inter
200/// frames can motion-compensate against the previous reconstruction. Decode a
201/// keyframe first (which populates every slot), then feed each subsequent inter
202/// frame to [`Vp9Decoder::decode_frame`].
203pub struct Vp9Decoder {
204    /// `REF_FRAMES` reference-buffer slots. `Rc` so a keyframe can refresh all
205    /// eight without cloning the pixels; each holds a border-extended
206    /// reconstruction ready for motion compensation.
207    refs: [Option<Rc<FrameBuffer>>; REF_FRAMES],
208}
209
210impl Default for Vp9Decoder {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl Vp9Decoder {
217    /// A decoder with no reference history. The first frame must be a keyframe.
218    pub fn new() -> Self {
219        Self {
220            refs: std::array::from_fn(|_| None),
221        }
222    }
223
224    /// Decode one frame (keyframe or inter) from `bytes`, update the reference
225    /// slots per the frame's refresh mask, and return the reconstruction (borders
226    /// already extended, so it can serve directly as a later reference).
227    ///
228    /// An inter frame decoded before any keyframe — or one referencing an empty
229    /// slot — is rejected with [`DecodeError::Corrupt`] rather than reading
230    /// uninitialized state.
231    pub fn decode_frame(&mut self, bytes: &[u8]) -> Result<Rc<FrameBuffer>, DecodeError> {
232        // Reference sizes for the header parser: an inter frame inherits its
233        // dimensions from `ref_frame_idx[0]`'s buffer rather than coding them.
234        let ref_sizes: [Option<(u32, u32)>; REF_FRAMES] =
235            std::array::from_fn(|i| self.refs[i].as_ref().map(|f| (f.crop_width, f.crop_height)));
236        let hdr = parse_frame_header(bytes, Some(&ref_sizes))?;
237
238        let reference: Option<Rc<FrameBuffer>> = match hdr.frame_type {
239            FrameType::Key => None,
240            FrameType::Inter => Some(
241                self.refs[hdr.ref_frame_idx[0] as usize]
242                    .clone()
243                    .ok_or(DecodeError::Corrupt("inter frame references an empty slot"))?,
244            ),
245        };
246
247        let mut recon = decode_frame_inner(&hdr, bytes, reference.as_deref())?;
248        // Extend borders so this reconstruction can be motion-compensated against
249        // by the next inter frame, matching the encoder's `extend_borders` on the
250        // reference before the following frame is coded.
251        recon.extend_borders();
252        let frame = Rc::new(recon);
253
254        // Install the new frame into every slot its refresh mask selects.
255        for (i, slot) in self.refs.iter_mut().enumerate() {
256            if hdr.refresh_frame_flags & (1u8 << i) != 0 {
257                *slot = Some(Rc::clone(&frame));
258            }
259        }
260        Ok(frame)
261    }
262}
263
264/// Decode a single VP9 keyframe of the encoder's subset into a reconstruction
265/// buffer whose exported I420 equals the encoder's own reconstruction.
266///
267/// A convenience wrapper over [`Vp9Decoder`] for a standalone keyframe; inter
268/// frames require the stateful decoder so it can reach the reference history.
269pub fn decode_keyframe(bytes: &[u8]) -> Result<FrameBuffer, DecodeError> {
270    let hdr = parse_frame_header(bytes, None)?;
271    if hdr.frame_type != FrameType::Key {
272        return Err(DecodeError::Unsupported("non-keyframe (use Vp9Decoder)"));
273    }
274    decode_frame_inner(&hdr, bytes, None)
275}
276
277/// Decode one frame's tile payloads into a fresh reconstruction buffer, given its
278/// already-parsed header and (for inter frames) the LAST reference.
279fn decode_frame_inner(
280    hdr: &FrameHeader,
281    bytes: &[u8],
282    reference: Option<&FrameBuffer>,
283) -> Result<FrameBuffer, DecodeError> {
284    if hdr.tx_mode != TxMode::Allow8X8 {
285        return Err(DecodeError::Unsupported("tx_mode != ALLOW_8X8"));
286    }
287    let is_keyframe = hdr.frame_type == FrameType::Key;
288    if !is_keyframe && reference.is_none() {
289        return Err(DecodeError::Corrupt("inter frame without a reference"));
290    }
291
292    let mi_rows = mi_rows_of(hdr.height);
293    let mi_cols = mi_cols_of(hdr.width);
294    let mi_cols_aligned = ((mi_cols + 7) & !7) as usize;
295    let q = hdr.base_qindex as i32;
296    let dequant = [dc_quant(q, 0), ac_quant(q, 0)];
297
298    let mut dec = FrameDecoder {
299        mi_rows,
300        mi_cols,
301        mi_cols_aligned,
302        dequant,
303        recon: FrameBuffer::new(hdr.width, hdr.height),
304        grid: vec![DecMi::default(); (mi_rows * mi_cols) as usize],
305        reference,
306        is_keyframe,
307        tile_col_start: 0,
308        tile_col_end: mi_cols,
309    };
310
311    // Tile columns: all but the last carry a 4-byte big-endian size prefix.
312    let tiles = tile_geometry(mi_cols, hdr.log2_tile_cols);
313    let tile_data = &bytes[hdr.tile_data_offset()..];
314    let n = tiles.len();
315    let mut pos = 0usize;
316    for (i, &(col_start, col_end)) in tiles.iter().enumerate() {
317        let size = if i + 1 < n {
318            if pos + 4 > tile_data.len() {
319                return Err(DecodeError::Truncated);
320            }
321            let s = u32::from_be_bytes([
322                tile_data[pos],
323                tile_data[pos + 1],
324                tile_data[pos + 2],
325                tile_data[pos + 3],
326            ]) as usize;
327            pos += 4;
328            s
329        } else {
330            tile_data.len() - pos
331        };
332        if pos + size > tile_data.len() {
333            return Err(DecodeError::Truncated);
334        }
335        let tile_bytes = &tile_data[pos..pos + size];
336        pos += size;
337        dec.decode_tile(tile_bytes, col_start, col_end)?;
338    }
339
340    Ok(dec.recon)
341}
342
343/// The `1 << log2` tile-column mi spans `[col_start, col_end)` for `mi_cols`.
344fn tile_geometry(mi_cols: u32, log2: u32) -> Vec<(u32, u32)> {
345    (0..(1u32 << log2))
346        .map(|i| {
347            (
348                tile_offset(i, mi_cols, log2),
349                tile_offset(i + 1, mi_cols, log2),
350            )
351        })
352        .collect()
353}
354
355/// The block one partition level below `bsize` for the fixed full split.
356fn split_child(bsize: BlockSize) -> BlockSize {
357    match bsize {
358        BlockSize::B64X64 => BlockSize::B32X32,
359        BlockSize::B32X32 => BlockSize::B16X16,
360        BlockSize::B16X16 => BlockSize::B8X8,
361        _ => unreachable!("split_child only descends 64→32→16→8"),
362    }
363}
364
365/// One frame's decode state: geometry, quantizer, the reconstruction buffer, the
366/// per-mi decoded mode-info grid the neighbor-context derivations read, and (for
367/// inter frames) the LAST reference the motion compensation reads.
368struct FrameDecoder<'a> {
369    mi_rows: u32,
370    mi_cols: u32,
371    mi_cols_aligned: usize,
372    dequant: [i16; 2],
373    recon: FrameBuffer,
374    grid: Vec<DecMi>,
375    /// The LAST reference (border-extended) for inter motion compensation, or
376    /// `None` on a keyframe.
377    reference: Option<&'a FrameBuffer>,
378    is_keyframe: bool,
379    /// Left mi-column bound of the tile being decoded (inclusive): its left edge
380    /// is a frame edge for intra prediction, entropy/partition contexts, and the
381    /// MV-reference column clamp.
382    tile_col_start: u32,
383    /// Right mi-column bound of the tile (exclusive): caps the MV-reference scan.
384    tile_col_end: u32,
385}
386
387impl FrameDecoder<'_> {
388    #[inline]
389    fn idx(&self, mi_row: u32, mi_col: u32) -> usize {
390        (mi_row * self.mi_cols + mi_col) as usize
391    }
392
393    fn plane_ro(&self, p: Plane) -> (usize, usize, i32, i32) {
394        let (_d, o, s, w, h) = match p {
395            Plane::Y => self.recon.y(),
396            Plane::U => self.recon.u(),
397            Plane::V => self.recon.v(),
398        };
399        (o, s, w as i32, h as i32)
400    }
401
402    fn plane_mut(&mut self, p: Plane) -> &mut [u8] {
403        match p {
404            Plane::Y => self.recon.y_mut().0,
405            Plane::U => self.recon.u_mut().0,
406            Plane::V => self.recon.v_mut().0,
407        }
408    }
409
410    /// Decode one tile column into the shared reconstruction buffer.
411    fn decode_tile(
412        &mut self,
413        bytes: &[u8],
414        col_start: u32,
415        col_end: u32,
416    ) -> Result<(), DecodeError> {
417        self.tile_col_start = col_start;
418        self.tile_col_end = col_end;
419
420        let mut r = BoolReader::new(bytes);
421        let mut pc = PartitionContext::new(self.mi_cols_aligned);
422        let mut ec = EntropyContext::new(self.mi_cols_aligned);
423
424        let mut mi_row = 0;
425        while mi_row < self.mi_rows {
426            pc.reset_left();
427            ec.reset_left();
428            let mut mi_col = col_start;
429            while mi_col < col_end {
430                self.decode_sb(&mut r, &mut pc, &mut ec, mi_row, mi_col, BlockSize::B64X64)?;
431                mi_col += 8;
432            }
433            mi_row += 8;
434        }
435        Ok(())
436    }
437
438    /// Recursive superblock decode (inverse of `pack_sb`): read the partition,
439    /// then either decode a leaf or recurse into four children in z-order.
440    fn decode_sb(
441        &mut self,
442        r: &mut BoolReader,
443        pc: &mut PartitionContext,
444        ec: &mut EntropyContext,
445        mi_row: u32,
446        mi_col: u32,
447        bsize: BlockSize,
448    ) -> Result<(), DecodeError> {
449        if mi_row >= self.mi_rows || mi_col >= self.mi_cols {
450            return Ok(());
451        }
452        let bs = (1u32 << B_WIDTH_LOG2[bsize as usize]) / 4;
453        // Keyframes use the fixed KF partition probabilities; inter frames use the
454        // (default, no-update) frame-context partition probabilities.
455        let partition_probs = if self.is_keyframe {
456            &KF_PARTITION_PROBS
457        } else {
458            &DEFAULT_PARTITION_PROBS
459        };
460        let partition = read_partition(
461            r,
462            pc,
463            bs,
464            mi_row,
465            mi_col,
466            bsize,
467            self.mi_rows,
468            self.mi_cols,
469            partition_probs,
470        );
471
472        match partition {
473            Partition::None => {
474                match bsize {
475                    BlockSize::B8X8 => {
476                        if self.is_keyframe {
477                            self.decode_leaf_8x8(r, ec, mi_row, mi_col)?
478                        } else {
479                            self.decode_inter_leaf_8x8(r, ec, mi_row, mi_col)?
480                        }
481                    }
482                    BlockSize::B16X16 => {
483                        if self.is_keyframe {
484                            self.decode_leaf16(r, ec, mi_row, mi_col)?
485                        } else {
486                            self.decode_inter_leaf16(r, ec, mi_row, mi_col)?
487                        }
488                    }
489                    // Our encoder never codes a 32x32/64x64 leaf.
490                    _ => return Err(DecodeError::Corrupt("unexpected leaf block size")),
491                }
492                // subsize == bsize for PARTITION_NONE.
493                pc.update(mi_row, mi_col, bsize, bsize);
494            }
495            Partition::Split => {
496                if bsize == BlockSize::B8X8 {
497                    return Err(DecodeError::Corrupt("split of an 8x8 block"));
498                }
499                let sub = split_child(bsize);
500                self.decode_sb(r, pc, ec, mi_row, mi_col, sub)?;
501                self.decode_sb(r, pc, ec, mi_row, mi_col + bs, sub)?;
502                self.decode_sb(r, pc, ec, mi_row + bs, mi_col, sub)?;
503                self.decode_sb(r, pc, ec, mi_row + bs, mi_col + bs, sub)?;
504            }
505            // The encoder emits only NONE or SPLIT.
506            Partition::Horz | Partition::Vert => {
507                return Err(DecodeError::Unsupported("HORZ/VERT partition (M2)"))
508            }
509        }
510        Ok(())
511    }
512
513    // --- Keyframe (intra) leaves -------------------------------------------
514
515    /// Read a keyframe leaf's skip flag and DC intra modes, storing them into the
516    /// given mi units. Returns the skip flag. Supports DC_PRED only.
517    fn read_leaf_modes(
518        &mut self,
519        r: &mut BoolReader,
520        mi_row: u32,
521        mi_col: u32,
522        units: &[(u32, u32)],
523    ) -> Result<bool, DecodeError> {
524        let above_skip = mi_row > 0 && self.grid[self.idx(mi_row - 1, mi_col)].skip;
525        let left_skip =
526            mi_col > self.tile_col_start && self.grid[self.idx(mi_row, mi_col - 1)].skip;
527        let ctx = above_skip as usize + left_skip as usize;
528        let skip = r.read(DEFAULT_SKIP_PROBS[ctx]) != 0;
529
530        let a_mode = if mi_row > 0 {
531            self.grid[self.idx(mi_row - 1, mi_col)].ymode
532        } else {
533            DC_PRED
534        };
535        let l_mode = if mi_col > self.tile_col_start {
536            self.grid[self.idx(mi_row, mi_col - 1)].ymode
537        } else {
538            DC_PRED
539        };
540        let y_mode = read_tree(
541            r,
542            &INTRA_MODE_TREE,
543            &KF_Y_MODE_PROBS[a_mode as usize][l_mode as usize],
544        ) as u8;
545        let _uv_mode = read_tree(r, &INTRA_MODE_TREE, &KF_UV_MODE_PROBS[y_mode as usize]) as u8;
546        if y_mode != DC_PRED {
547            return Err(DecodeError::Unsupported("non-DC intra mode"));
548        }
549
550        let mi = DecMi {
551            skip,
552            ymode: y_mode,
553            ..DecMi::default()
554        };
555        for &(dr, dc) in units {
556            let i = self.idx(mi_row + dr, mi_col + dc);
557            self.grid[i] = mi.clone();
558        }
559        Ok(skip)
560    }
561
562    /// Decode one intra 8x8 mode-info block (luma 8x8, chroma 4x4).
563    fn decode_leaf_8x8(
564        &mut self,
565        r: &mut BoolReader,
566        ec: &mut EntropyContext,
567        mi_row: u32,
568        mi_col: u32,
569    ) -> Result<(), DecodeError> {
570        let skip = self.read_leaf_modes(r, mi_row, mi_col, &[(0, 0)])?;
571        let up = mi_row > 0;
572        let left = mi_col > self.tile_col_start;
573
574        // Luma 8x8.
575        let ay = (mi_col * 2) as usize;
576        let ly = ((mi_row & 7) * 2) as usize;
577        let pt_y = ctx_combine(
578            ec.above_y[ay] != 0 || ec.above_y[ay + 1] != 0,
579            ec.left_y[ly] != 0 || ec.left_y[ly + 1] != 0,
580        );
581        let he_y = self.intra_recon_tx(
582            r,
583            Plane::Y,
584            TxSize::Tx8X8,
585            (mi_row * 8) as usize,
586            (mi_col * 8) as usize,
587            up,
588            left,
589            skip,
590            pt_y,
591            PlaneType::Y,
592        );
593        ec.above_y[ay] = he_y;
594        ec.above_y[ay + 1] = he_y;
595        ec.left_y[ly] = he_y;
596        ec.left_y[ly + 1] = he_y;
597
598        // Chroma U/V 4x4 (single 4x4 unit → single entropy entry).
599        let au = mi_col as usize;
600        let lu = (mi_row & 7) as usize;
601        let cy = (mi_row * 4) as usize;
602        let cx = (mi_col * 4) as usize;
603
604        let pt_u = ctx_combine(ec.above_u[au] != 0, ec.left_u[lu] != 0);
605        let he_u = self.intra_recon_tx(
606            r,
607            Plane::U,
608            TxSize::Tx4X4,
609            cy,
610            cx,
611            up,
612            left,
613            skip,
614            pt_u,
615            PlaneType::Uv,
616        );
617        ec.above_u[au] = he_u;
618        ec.left_u[lu] = he_u;
619
620        let pt_v = ctx_combine(ec.above_v[au] != 0, ec.left_v[lu] != 0);
621        let he_v = self.intra_recon_tx(
622            r,
623            Plane::V,
624            TxSize::Tx4X4,
625            cy,
626            cx,
627            up,
628            left,
629            skip,
630            pt_v,
631            PlaneType::Uv,
632        );
633        ec.above_v[au] = he_v;
634        ec.left_v[lu] = he_v;
635
636        Ok(())
637    }
638
639    /// Decode one intra 16x16 leaf: four 8x8 luma transforms in raster order plus
640    /// one 8x8 transform per chroma plane, replicated across its 2x2 mi units.
641    fn decode_leaf16(
642        &mut self,
643        r: &mut BoolReader,
644        ec: &mut EntropyContext,
645        mi_row: u32,
646        mi_col: u32,
647    ) -> Result<(), DecodeError> {
648        let units = [(0, 0), (0, 1), (1, 0), (1, 1)];
649        let skip = self.read_leaf_modes(r, mi_row, mi_col, &units)?;
650        let up_blk = mi_row > 0;
651        let left_blk = mi_col > self.tile_col_start;
652
653        // Luma: four 8x8 transforms in raster order (TL, TR, BL, BR).
654        let base_ay = (mi_col * 2) as usize;
655        let base_ly = ((mi_row & 7) * 2) as usize;
656        for sr in 0..2u32 {
657            for sc in 0..2u32 {
658                let ay = base_ay + (sc * 2) as usize;
659                let ly = base_ly + (sr * 2) as usize;
660                let pt = ctx_combine(
661                    ec.above_y[ay] != 0 || ec.above_y[ay + 1] != 0,
662                    ec.left_y[ly] != 0 || ec.left_y[ly + 1] != 0,
663                );
664                let he = self.intra_recon_tx(
665                    r,
666                    Plane::Y,
667                    TxSize::Tx8X8,
668                    (mi_row * 8 + sr * 8) as usize,
669                    (mi_col * 8 + sc * 8) as usize,
670                    sr > 0 || up_blk,
671                    sc > 0 || left_blk,
672                    skip,
673                    pt,
674                    PlaneType::Y,
675                );
676                ec.above_y[ay] = he;
677                ec.above_y[ay + 1] = he;
678                ec.left_y[ly] = he;
679                ec.left_y[ly + 1] = he;
680            }
681        }
682
683        self.decode_leaf16_chroma(
684            r,
685            ec,
686            mi_row,
687            mi_col,
688            up_blk,
689            left_blk,
690            skip,
691            REF_TYPE_INTRA,
692        );
693        Ok(())
694    }
695
696    /// DC-predict one transform block into `recon`, then (unless the block is
697    /// skipped) decode its coefficients and add the inverse transform. Returns the
698    /// entropy-context flag (`1` if the block has any non-zero coefficient).
699    #[allow(clippy::too_many_arguments)]
700    fn intra_recon_tx(
701        &mut self,
702        r: &mut BoolReader,
703        plane: Plane,
704        tx_size: TxSize,
705        y_px: usize,
706        x_px: usize,
707        up: bool,
708        left: bool,
709        skip: bool,
710        pt: usize,
711        ptype: PlaneType,
712    ) -> u8 {
713        let dequant = self.dequant;
714        let (org, stride, fw, fh) = self.plane_ro(plane);
715        let off = org + y_px * stride + x_px;
716        let rdata = self.plane_mut(plane);
717
718        build_intra_dc(
719            rdata,
720            off,
721            stride,
722            tx_size,
723            up,
724            left,
725            x_px as i32,
726            y_px as i32,
727            fw,
728            fh,
729        );
730
731        if skip {
732            return 0;
733        }
734
735        let (eob, dq) = decode_coefs(r, tx_size, ptype, REF_TYPE_INTRA, pt, dequant);
736        add_residual(tx_size, eob, &dq, &mut rdata[off..], stride);
737        (eob > 0) as u8
738    }
739
740    // --- Inter leaves ------------------------------------------------------
741
742    /// Read one inter block's mode info (`pack_leaf_inter` inverse): skip,
743    /// `is_inter`, single-ref, the inter mode, and the NEWMV difference. `bsize`
744    /// selects the `mv_ref_blocks` neighborhood. Returns `(skip, mode, mv)`.
745    ///
746    /// Only this encoder's subset is accepted: an intra block, a non-LAST
747    /// reference, or a non-integer (non-multiple-of-16) motion vector is rejected.
748    /// Rejecting non-integer MVs both matches the encoder (whose emitted MVs are
749    /// always integer-pel) and keeps motion compensation a pure block copy whose
750    /// reads the umv-border clamp proves in-bounds — so a hostile MV can never
751    /// index out of the reference's 64-pixel border.
752    fn read_inter_mode_info(
753        &self,
754        r: &mut BoolReader,
755        mi_row: u32,
756        mi_col: u32,
757        bsize: BlockSize,
758    ) -> Result<(bool, PredictionMode, Mv), DecodeError> {
759        let above = self.inter_neighbor(mi_row.checked_sub(1), Some(mi_col));
760        let left = if mi_col > self.tile_col_start {
761            self.inter_neighbor(Some(mi_row), Some(mi_col - 1))
762        } else {
763            None
764        };
765
766        // skip.
767        let above_skip = above.is_some() && self.grid[self.idx(mi_row - 1, mi_col)].skip;
768        let left_skip = left.is_some() && self.grid[self.idx(mi_row, mi_col - 1)].skip;
769        let skip = r.read(DEFAULT_SKIP_PROBS[above_skip as usize + left_skip as usize]) != 0;
770
771        // is_inter — always 1 for this encoder's inter frames.
772        let is_inter = r.read(DEFAULT_INTRA_INTER_PROBS[intra_inter_context(above, left)]) != 0;
773        if !is_inter {
774            return Err(DecodeError::Unsupported("intra block in inter frame"));
775        }
776
777        // tx_size: ALLOW_8X8 is not TX_MODE_SELECT → no bits.
778
779        // reference frame: single_ref_p1 = 0 selects LAST; a 1 bit would select a
780        // reference this encoder never emits.
781        if r.read(DEFAULT_SINGLE_REF_PROBS[single_ref_p1_context(above, left)][0]) != 0 {
782            return Err(DecodeError::Unsupported("non-LAST reference"));
783        }
784
785        // inter mode, using the MV-reference-derived mode context.
786        let (mode_context, refs) = self.mv_refs_at(mi_row, mi_col, bsize);
787        let inter_offset = read_tree(
788            r,
789            &INTER_MODE_TREE,
790            &DEFAULT_INTER_MODE_PROBS[mode_context as usize],
791        );
792        // INTER_OFFSET: NEARESTMV→0, NEARMV→1, ZEROMV→2, NEWMV→3.
793        let mode = match inter_offset {
794            0 => PredictionMode::NearestMv,
795            1 => PredictionMode::NearMv,
796            2 => PredictionMode::ZeroMv,
797            _ => PredictionMode::NewMv,
798        };
799
800        // interp filter: fixed EIGHTTAP (not SWITCHABLE) → no bits.
801
802        let mv = match mode {
803            PredictionMode::ZeroMv => Mv::ZERO,
804            PredictionMode::NearestMv => refs[0],
805            PredictionMode::NearMv => refs[1],
806            // NEWMV codes the MV difference against the nearest reference MV.
807            _ => read_mv(r, refs[0]),
808        };
809
810        // Integer-pel invariant: every MV this encoder emits (and every reference
811        // candidate it selects for NEAREST/ZERO) is a multiple of 16 in 1/8-pel
812        // units. Anything else is a malformed stream; reject it before motion
813        // compensation so the block copy stays on integer sample positions.
814        if mv.row % 16 != 0 || mv.col % 16 != 0 {
815            return Err(DecodeError::Corrupt("non-integer motion vector"));
816        }
817
818        Ok((skip, mode, mv))
819    }
820
821    /// Neighbor descriptor for the inter prediction contexts, or `None` at a
822    /// frame edge (mirrors `above_mi` / `left_mi == NULL`).
823    fn inter_neighbor(&self, mi_row: Option<u32>, mi_col: Option<u32>) -> Option<InterNeighbor> {
824        let (r, c) = (mi_row?, mi_col?);
825        if r >= self.mi_rows || c >= self.mi_cols {
826            return None;
827        }
828        let mi = &self.grid[self.idx(r, c)];
829        Some(InterNeighbor {
830            is_inter: mi.is_inter,
831            ref0: mi.ref_frame[0],
832            ref1: mi.ref_frame[1],
833        })
834    }
835
836    /// `find_mv_refs` at `(mi_row, mi_col)` for block size `bsize`, reading the
837    /// decoded neighbor grid exactly as the encoder does (rows span the frame,
838    /// columns the current tile). Returns `(mode_context, [nearest, near])`.
839    fn mv_refs_at(&self, mi_row: u32, mi_col: u32, bsize: BlockSize) -> (u8, [Mv; 2]) {
840        let (mi_rows, mi_cols) = (self.mi_rows as i32, self.mi_cols as i32);
841        let cols = self.mi_cols;
842        let (tcs, tce) = (self.tile_col_start as i32, self.tile_col_end as i32);
843        let grid = &self.grid;
844        let geom = MvRefGeom {
845            mi_rows,
846            mi_cols,
847            ref_frame: LAST_FRAME,
848            ref_sign_bias: [0; 4],
849            allow_hp: false,
850        };
851        find_mv_refs(
852            |r, c| {
853                if r < 0 || c < tcs || r >= mi_rows || c >= tce {
854                    None
855                } else {
856                    let mi = &grid[(r as u32 * cols + c as u32) as usize];
857                    Some(MvRefInfo {
858                        mode: mi.mode as u8,
859                        ref_frame: mi.ref_frame,
860                        mv: [mi.mv, Mv::ZERO],
861                    })
862                }
863            },
864            mi_row as i32,
865            mi_col as i32,
866            bsize,
867            &geom,
868        )
869    }
870
871    /// Store one inter leaf's decoded mode info into its mi unit(s). A 16x16 leaf
872    /// replicates the info across all four units (as the encoder does) so later
873    /// neighbor lookups agree.
874    fn store_inter_mi(
875        &mut self,
876        mi_row: u32,
877        mi_col: u32,
878        units: &[(u32, u32)],
879        skip: bool,
880        mode: PredictionMode,
881        mv: Mv,
882    ) {
883        let mi = DecMi {
884            skip,
885            is_inter: true,
886            mode,
887            ref_frame: [LAST_FRAME, NONE_FRAME],
888            mv,
889            ymode: DC_PRED,
890        };
891        for &(dr, dc) in units {
892            let i = self.idx(mi_row + dr, mi_col + dc);
893            self.grid[i] = mi.clone();
894        }
895    }
896
897    /// Motion-compensate every plane of a leaf (`bsize_mi` mi units square) from
898    /// the reference into `recon`, a pure integer-pel block copy.
899    fn motion_compensate(&mut self, mi_row: u32, mi_col: u32, mv: Mv, bsize_mi: i32) {
900        // `reference` is a shared borrow independent of `self.recon`, so copy it
901        // out before mutably borrowing the reconstruction buffer.
902        let reference = self
903            .reference
904            .expect("inter leaf requires a reference (validated at frame entry)");
905        let (mi_rows, mi_cols) = (self.mi_rows as i32, self.mi_cols as i32);
906        for plane in [McPlane::Y, McPlane::U, McPlane::V] {
907            predict_inter_block(
908                reference,
909                &mut self.recon,
910                plane,
911                mi_row as i32,
912                mi_col as i32,
913                mv,
914                bsize_mi,
915                mi_rows,
916                mi_cols,
917            );
918        }
919    }
920
921    /// Decode one inter 8x8 leaf: single-MV block copy (8x8 luma / 4x4 chroma)
922    /// then one 8x8 luma and one 4x4 residual per chroma plane.
923    fn decode_inter_leaf_8x8(
924        &mut self,
925        r: &mut BoolReader,
926        ec: &mut EntropyContext,
927        mi_row: u32,
928        mi_col: u32,
929    ) -> Result<(), DecodeError> {
930        let (skip, mode, mv) = self.read_inter_mode_info(r, mi_row, mi_col, BlockSize::B8X8)?;
931        self.store_inter_mi(mi_row, mi_col, &[(0, 0)], skip, mode, mv);
932        self.motion_compensate(mi_row, mi_col, mv, 1);
933
934        // Luma 8x8.
935        let ay = (mi_col * 2) as usize;
936        let ly = ((mi_row & 7) * 2) as usize;
937        let pt_y = ctx_combine(
938            ec.above_y[ay] != 0 || ec.above_y[ay + 1] != 0,
939            ec.left_y[ly] != 0 || ec.left_y[ly + 1] != 0,
940        );
941        let he_y = self.inter_recon_tx(
942            r,
943            Plane::Y,
944            TxSize::Tx8X8,
945            (mi_row * 8) as usize,
946            (mi_col * 8) as usize,
947            skip,
948            pt_y,
949            PlaneType::Y,
950        );
951        ec.above_y[ay] = he_y;
952        ec.above_y[ay + 1] = he_y;
953        ec.left_y[ly] = he_y;
954        ec.left_y[ly + 1] = he_y;
955
956        // Chroma U/V 4x4.
957        let au = mi_col as usize;
958        let lu = (mi_row & 7) as usize;
959        let cy = (mi_row * 4) as usize;
960        let cx = (mi_col * 4) as usize;
961
962        let pt_u = ctx_combine(ec.above_u[au] != 0, ec.left_u[lu] != 0);
963        let he_u = self.inter_recon_tx(
964            r,
965            Plane::U,
966            TxSize::Tx4X4,
967            cy,
968            cx,
969            skip,
970            pt_u,
971            PlaneType::Uv,
972        );
973        ec.above_u[au] = he_u;
974        ec.left_u[lu] = he_u;
975
976        let pt_v = ctx_combine(ec.above_v[au] != 0, ec.left_v[lu] != 0);
977        let he_v = self.inter_recon_tx(
978            r,
979            Plane::V,
980            TxSize::Tx4X4,
981            cy,
982            cx,
983            skip,
984            pt_v,
985            PlaneType::Uv,
986        );
987        ec.above_v[au] = he_v;
988        ec.left_v[lu] = he_v;
989
990        Ok(())
991    }
992
993    /// Decode one inter 16x16 leaf: one 16x16-luma / 8x8-chroma block copy, then
994    /// four 8x8 luma residual transforms and one 8x8 transform per chroma plane.
995    fn decode_inter_leaf16(
996        &mut self,
997        r: &mut BoolReader,
998        ec: &mut EntropyContext,
999        mi_row: u32,
1000        mi_col: u32,
1001    ) -> Result<(), DecodeError> {
1002        let (skip, mode, mv) = self.read_inter_mode_info(r, mi_row, mi_col, BlockSize::B16X16)?;
1003        let units = [(0, 0), (0, 1), (1, 0), (1, 1)];
1004        self.store_inter_mi(mi_row, mi_col, &units, skip, mode, mv);
1005        self.motion_compensate(mi_row, mi_col, mv, 2);
1006
1007        // Luma: four 8x8 residual transforms in raster order (TL, TR, BL, BR).
1008        let base_ay = (mi_col * 2) as usize;
1009        let base_ly = ((mi_row & 7) * 2) as usize;
1010        for sr in 0..2u32 {
1011            for sc in 0..2u32 {
1012                let ay = base_ay + (sc * 2) as usize;
1013                let ly = base_ly + (sr * 2) as usize;
1014                let pt = ctx_combine(
1015                    ec.above_y[ay] != 0 || ec.above_y[ay + 1] != 0,
1016                    ec.left_y[ly] != 0 || ec.left_y[ly + 1] != 0,
1017                );
1018                let he = self.inter_recon_tx(
1019                    r,
1020                    Plane::Y,
1021                    TxSize::Tx8X8,
1022                    (mi_row * 8 + sr * 8) as usize,
1023                    (mi_col * 8 + sc * 8) as usize,
1024                    skip,
1025                    pt,
1026                    PlaneType::Y,
1027                );
1028                ec.above_y[ay] = he;
1029                ec.above_y[ay + 1] = he;
1030                ec.left_y[ly] = he;
1031                ec.left_y[ly + 1] = he;
1032            }
1033        }
1034
1035        self.decode_leaf16_chroma(r, ec, mi_row, mi_col, false, false, skip, REF_TYPE_INTER);
1036        Ok(())
1037    }
1038
1039    /// Decode the two chroma planes of a 16x16 leaf: one 8x8 transform each,
1040    /// covering the two 4x4 entropy entries. Shared by the intra and inter 16x16
1041    /// paths, which differ only in the coefficient reference type and whether the
1042    /// prediction already sits in `recon` (inter, `up`/`left` unused) or is
1043    /// DC-predicted here (intra).
1044    #[allow(clippy::too_many_arguments)]
1045    fn decode_leaf16_chroma(
1046        &mut self,
1047        r: &mut BoolReader,
1048        ec: &mut EntropyContext,
1049        mi_row: u32,
1050        mi_col: u32,
1051        up_blk: bool,
1052        left_blk: bool,
1053        skip: bool,
1054        ref_type: usize,
1055    ) {
1056        let au = mi_col as usize;
1057        let lu = (mi_row & 7) as usize;
1058        let cy = (mi_row * 4) as usize;
1059        let cx = (mi_col * 4) as usize;
1060
1061        for (plane, above, left) in [
1062            (Plane::U, &mut ec.above_u, &mut ec.left_u),
1063            (Plane::V, &mut ec.above_v, &mut ec.left_v),
1064        ] {
1065            let pt = ctx_combine(
1066                above[au] != 0 || above[au + 1] != 0,
1067                left[lu] != 0 || left[lu + 1] != 0,
1068            );
1069            let he = if ref_type == REF_TYPE_INTRA {
1070                self.intra_recon_tx(
1071                    r,
1072                    plane,
1073                    TxSize::Tx8X8,
1074                    cy,
1075                    cx,
1076                    up_blk,
1077                    left_blk,
1078                    skip,
1079                    pt,
1080                    PlaneType::Uv,
1081                )
1082            } else {
1083                self.inter_recon_tx(r, plane, TxSize::Tx8X8, cy, cx, skip, pt, PlaneType::Uv)
1084            };
1085            above[au] = he;
1086            above[au + 1] = he;
1087            left[lu] = he;
1088            left[lu + 1] = he;
1089        }
1090    }
1091
1092    /// Add one inter transform block's residual onto its already-placed
1093    /// motion-compensated prediction. Returns the entropy-context flag.
1094    #[allow(clippy::too_many_arguments)]
1095    fn inter_recon_tx(
1096        &mut self,
1097        r: &mut BoolReader,
1098        plane: Plane,
1099        tx_size: TxSize,
1100        y_px: usize,
1101        x_px: usize,
1102        skip: bool,
1103        pt: usize,
1104        ptype: PlaneType,
1105    ) -> u8 {
1106        if skip {
1107            return 0;
1108        }
1109        let dequant = self.dequant;
1110        let (org, stride, _, _) = self.plane_ro(plane);
1111        let off = org + y_px * stride + x_px;
1112        let (eob, dq) = decode_coefs(r, tx_size, ptype, REF_TYPE_INTER, pt, dequant);
1113        let rdata = self.plane_mut(plane);
1114        add_residual(tx_size, eob, &dq, &mut rdata[off..], stride);
1115        (eob > 0) as u8
1116    }
1117}
1118
1119/// Inverse-transform a decoded coefficient block onto `dest` (at the block's
1120/// top-left) when it has any non-zero coefficient. A no-op for an empty block.
1121fn add_residual(tx_size: TxSize, eob: usize, dq: &[i16; 64], dest: &mut [u8], stride: usize) {
1122    if eob == 0 {
1123        return;
1124    }
1125    match tx_size {
1126        TxSize::Tx4X4 => {
1127            let block: [i16; 16] = dq[..16].try_into().expect("4x4 block is 16 coeffs");
1128            idct4x4_add(&block, dest, stride);
1129        }
1130        TxSize::Tx8X8 => idct8x8_add(dq, dest, stride),
1131        _ => unreachable!("decoder only supports 4x4 and 8x8 transforms"),
1132    }
1133}
1134
1135// The oracle round-trip tests build synthetic sources from `crate::testing`,
1136// which only exists under `test-utils`.
1137#[cfg(all(test, feature = "test-utils"))]
1138mod tests;