Skip to main content

otf_pixels_codec_avif/av1/
tile.rs

1//! The tile decode driver for the intra still-picture path (spec §5.11).
2//!
3//! This is where the pieces meet: the arithmetic decoder walks the partition
4//! tree, reads each block's intra mode and transform size, predicts every
5//! transform block from the already-reconstructed neighbours, decodes its
6//! coefficients, inverts the transform, and writes the samples back. The
7//! neighbour-context arrays it threads between blocks are what make the entropy
8//! contexts match the encoder.
9//!
10//! Scope is the intra still image in YUV 4:4:4, 4:2:2 or 4:2:0, in any tiling.
11//! Each tile decodes from fresh CDFs and cleared above contexts, and treats a
12//! neighbour across its edge as unavailable (`is_inside`); the in-loop
13//! filters then run over the whole frame.
14//! Subsampled chroma is decoded in its own sample grid: a block one unit wide
15//! or high shares its chroma with its neighbour, the odd one of the pair coding
16//! it (`HasChroma`). Both `CodedLossless`
17//! (every transform is 4x4 WHT) and lossy frames decode here — the lossy path
18//! runs the full DCT/ADST/identity inverse transforms at every transform size,
19//! followed by the in-loop filters: deblocking (§7.14), CDEF (§7.15) and loop
20//! restoration (§7.17), with the super-resolution upscale (§7.16) between CDEF
21//! and restoration. Film grain is not implemented, so a lossy frame is
22//! reproduced exactly only when it codes grain off (`unimplemented_filters_off`);
23//! any other lossy frame is refused. Every intra
24//! prediction mode is handled —
25//! DC, Paeth, the smooth family, the slanted directional modes (with their
26//! edge-filter and upsample machinery), recursive filter-intra, palette, and
27//! chroma-from-luma. Intra block copy is detected and reported as
28//! [`PixelsError::unsupported`] rather than decoded wrong, so a stream that uses
29//! it fails cleanly instead of desynchronising.
30
31use super::bits::BitReader;
32use super::cdef::CdefFilter;
33use super::cdf;
34use super::coder::{CoeffJob, Site, TileCoder};
35use super::coeff::{CoeffCdfs, TxTypeCtx};
36use super::deblock::Deblock;
37use super::direction::{
38    ANGLE_STEP, Edge, mode_base_angle, predict_directional, predict_filter_intra,
39};
40use super::frame::{Cdef, FrameHeader, LoopFilter, Segmentation, TileInfo, TxMode};
41use super::palette::{PALETTE_COLORS, color_context, palette_cache};
42use super::plane::Plane;
43use super::predict::{IntraMode, PredBlock, predict_intra_block};
44use super::restoration::{
45    LoopRestore, PlaneLr, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_SWITCHABLE, RESTORE_WIENER,
46    SGRPROJ_XQD_MID, WIENER_TAPS_MID, count_units_in_frame, read_sgrproj_unit, read_wiener_unit,
47};
48use super::seq::SequenceHeader;
49use super::superres::{SUPERRES_NUM, Superres};
50use super::symbol::SymbolDecoder;
51use super::transform::{
52    TxSize, TxType, ac_q, add_residual, dc_q, dequantize_with_matrix, inverse_transform_2d,
53    quantizer_matrix,
54};
55use super::transform_type::{IntraTxTypeCdfs, intra_dir, intra_tx_set};
56use super::tx_size::{BLOCK_4X4, TxDepthCdfs, TxSizeParams, code_tx_size, max_tx_size_rect};
57use otf_pixels_core::{PixelsError, Result};
58
59/// `MI_SIZE` (§3): the side of the smallest coded block, in samples.
60const MI_SIZE: usize = 4;
61/// `FRAME_LF_COUNT` (§3): loop-filter levels — two luma directions, U, V.
62pub const FRAME_LF_COUNT: usize = 4;
63/// `DELTA_Q_SMALL` and `DELTA_LF_SMALL` (§3), which are equal: the symbol
64/// value that escapes to an explicitly sized literal.
65const DELTA_SMALL: i32 = 3;
66/// `MAX_LOOP_FILTER` (§3).
67const MAX_LOOP_FILTER: i32 = 63;
68/// `MAX_SEGMENTS` (§3).
69pub const MAX_SEGMENTS: usize = 8;
70/// `SEG_LVL_ALT_Q` (§3): the per-segment quantizer offset.
71const SEG_LVL_ALT_Q: usize = 0;
72/// `SEG_LVL_ALT_LF_Y_V` (§3): the first of the four per-segment loop-filter
73/// offsets, in `loop_filter_level` order.
74const SEG_LVL_ALT_LF_Y_V: usize = 1;
75/// `SEG_LVL_SKIP` (§3): every block of the segment is skipped.
76const SEG_LVL_SKIP: usize = 6;
77/// `DC_PRED` mode index.
78const DC_PRED: usize = 0;
79/// `UV_CFL_PRED`: the chroma-from-luma UV mode, one past the intra modes.
80const UV_CFL_PRED: usize = 13;
81/// `MAX_ANGLE_DELTA` (§3).
82const MAX_ANGLE_DELTA: i32 = 3;
83
84/// `Intra_Mode_Context` (§8.3.2): folds an intra mode into the small context
85/// used to select the key-frame Y-mode CDF.
86const INTRA_MODE_CONTEXT: [usize; 13] = [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0];
87
88/// The partition types (§6.10.4), in coded order.
89const PARTITION_NONE: usize = 0;
90const PARTITION_HORZ: usize = 1;
91const PARTITION_VERT: usize = 2;
92const PARTITION_SPLIT: usize = 3;
93const PARTITION_HORZ_A: usize = 4;
94const PARTITION_HORZ_B: usize = 5;
95const PARTITION_VERT_A: usize = 6;
96const PARTITION_VERT_B: usize = 7;
97const PARTITION_HORZ_4: usize = 8;
98const PARTITION_VERT_4: usize = 9;
99
100/// A decoded frame's sample planes, in coded order (Y, U, V). Each plane covers
101/// whole superblocks (in its own subsampled grid); only the top-left
102/// display-sized region is the picture.
103pub struct DecodedFrame {
104    /// The reconstructed planes.
105    pub planes: Vec<Plane>,
106}
107
108/// Decode an intra still frame into its sample planes, from the payloads of its
109/// tile groups in order. Handles both lossless and lossy frames, the latter
110/// only when film grain is disabled (see `unimplemented_filters_off`).
111///
112/// # Errors
113///
114/// Returns [`PixelsError::unsupported`] for anything outside the intra subset
115/// this decodes — intra block copy, or a lossy frame using film grain — and [`PixelsError::malformed`] for a stream
116/// that ends early, violates the syntax, or does not code every tile exactly
117/// once.
118pub fn decode_still(
119    seq: &SequenceHeader,
120    frame: &FrameHeader,
121    tile_groups: &[&[u8]],
122) -> Result<DecodedFrame> {
123    // We reconstruct the residual, apply every in-loop filter — deblocking
124    // (§7.14), CDEF (§7.15) and loop restoration (§7.17) — and the
125    // super-resolution upscale (§7.16), but not film grain. A lossy frame is
126    // reproduced exactly only when grain is disabled; any other would decode to
127    // a visibly wrong image, so it is refused. A coded-lossless frame turns the
128    // in-loop filters off by definition.
129    if !frame.coded_lossless && !unimplemented_filters_off(frame) {
130        return Err(PixelsError::unsupported(
131            "avif: lossy frames are decoded only with film grain disabled; film \
132             grain synthesis is not implemented yet",
133        ));
134    }
135    if frame.allow_intrabc {
136        // Intra block copy is not implemented; decoding its blocks would
137        // desynchronise the symbol stream.
138        return Err(PixelsError::unsupported(
139            "avif: intra block copy is not implemented yet",
140        ));
141    }
142
143    let info = &frame.tile_info;
144    let sb_shift = if seq.use_128x128_superblock { 5 } else { 4 };
145    let mut state = TileState::new(seq, frame)?;
146    let mut next_tile = 0;
147    for group in tile_groups {
148        for tile in split_tile_group(group, info)? {
149            // Tiles must arrive in order, each exactly once; tg_start says
150            // where a group begins, so a gap or a repeat is a broken stream.
151            if tile.number != next_tile {
152                return Err(PixelsError::malformed(
153                    "avif",
154                    format!(
155                        "tile {} arrived where tile {next_tile} was due",
156                        tile.number
157                    ),
158                ));
159            }
160            next_tile += 1;
161            let bounds = tile_bounds(info, sb_shift, state.mi_rows, state.mi_cols, tile.number);
162            state.decode_tile(tile.data, bounds)?;
163        }
164    }
165    if next_tile != info.count() {
166        return Err(PixelsError::malformed(
167            "avif",
168            format!("the frame codes {next_tile} of its {} tiles", info.count()),
169        ));
170    }
171    state.post_filter();
172    Ok(DecodedFrame {
173        planes: state.planes,
174    })
175}
176
177/// One tile's coded bytes and its index in raster order.
178#[derive(Debug)]
179struct Tile<'a> {
180    number: u32,
181    data: &'a [u8],
182}
183
184/// Split a tile group (`tile_group_obu`, §5.11.1) into its tiles.
185///
186/// A group with several tiles may name the range it carries; every tile but
187/// its last is preceded by its size, `TileSizeBytes` little-endian bytes
188/// holding the size minus one, and the last runs to the end of the group.
189fn split_tile_group<'a>(group: &'a [u8], info: &TileInfo) -> Result<Vec<Tile<'a>>> {
190    let count = info.count();
191    let mut reader = BitReader::new(group);
192    let (start, end) = if count > 1 && reader.flag()? {
193        let bits = info.cols_log2 + info.rows_log2;
194        (reader.f(bits)?, reader.f(bits)?)
195    } else {
196        (0, count.saturating_sub(1))
197    };
198    if start > end || end >= count {
199        return Err(PixelsError::malformed(
200            "avif",
201            format!("tile group spans tiles {start}..={end} of {count}"),
202        ));
203    }
204    reader.byte_alignment()?;
205    let mut rest = group.get(reader.byte_position()..).unwrap_or(&[]);
206
207    let mut tiles = Vec::new();
208    for number in start..=end {
209        let data = if number == end {
210            core::mem::take(&mut rest)
211        } else {
212            let width = info.tile_size_bytes as usize;
213            let size_bytes = rest.get(..width).ok_or_else(|| tile_overrun(number))?;
214            let size = size_bytes
215                .iter()
216                .rev()
217                .fold(0_usize, |acc, &b| (acc << 8) | usize::from(b))
218                + 1;
219            let body = rest.get(width..).ok_or_else(|| tile_overrun(number))?;
220            let (data, after) = (body.get(..size), body.get(size..));
221            rest = after.ok_or_else(|| tile_overrun(number))?;
222            data.ok_or_else(|| tile_overrun(number))?
223        };
224        tiles.push(Tile { number, data });
225    }
226    Ok(tiles)
227}
228
229fn tile_overrun(number: u32) -> PixelsError {
230    PixelsError::malformed(
231        "avif",
232        format!("tile {number} claims more bytes than its tile group holds"),
233    )
234}
235
236/// Tile `number`'s extent in 4x4 units, from the frame's tile layout.
237pub(crate) fn tile_bounds(
238    info: &TileInfo,
239    sb_shift: u32,
240    mi_rows: usize,
241    mi_cols: usize,
242    number: u32,
243) -> TileBounds {
244    let (row, col) = (number / info.cols.max(1), number % info.cols.max(1));
245    let start = |starts: &[u32], i: u32, limit: usize| {
246        starts
247            .get(i as usize)
248            .map_or(limit, |&sb| ((sb as usize) << sb_shift).min(limit))
249    };
250    TileBounds {
251        row_start: start(&info.row_starts_sb, row, mi_rows),
252        row_end: start(&info.row_starts_sb, row + 1, mi_rows),
253        col_start: start(&info.col_starts_sb, col, mi_cols),
254        col_end: start(&info.col_starts_sb, col + 1, mi_cols),
255    }
256}
257
258/// One tile's extent in 4x4 units (`MiRowStart`..`MiRowEnd`,
259/// `MiColStart`..`MiColEnd`).
260#[derive(Debug, Clone, Copy)]
261pub(crate) struct TileBounds {
262    pub(crate) row_start: usize,
263    pub(crate) row_end: usize,
264    pub(crate) col_start: usize,
265    pub(crate) col_end: usize,
266}
267
268/// Whether every post-filter this decoder does *not* implement is disabled, so
269/// the reconstruct plus the implemented in-loop filters reproduces the frame
270/// exactly. Only film grain remains: deblocking (§7.14), CDEF (§7.15),
271/// super-resolution (§7.16) and loop restoration (§7.17) are all implemented.
272fn unimplemented_filters_off(frame: &FrameHeader) -> bool {
273    !frame.film_grain.apply_grain
274}
275
276/// `MiSize >= BLOCK_8X8` for a block of `bw4 x bh4` 4x4 units. The spec compares
277/// block-size *enum* values, and 4x16/16x4 sort after 8x8, so this excludes only
278/// 4x4, 4x8 and 8x4 — an area of at least four units — rather than requiring
279/// both dimensions to reach 8. Gates `angle_delta` and `palette_mode_info`.
280fn at_least_block_8x8(bw4: usize, bh4: usize) -> bool {
281    bw4 * bh4 >= 4
282}
283
284/// Build the per-plane loop-restoration records (§7.17): the frame filter type,
285/// the unit size, and the unit-grid dimensions (`count_units_in_frame` over the
286/// plane's rounded dimensions). Per-unit data is filled later by `read_lr`.
287fn build_plane_lr(seq: &SequenceHeader, frame: &FrameHeader) -> Vec<PlaneLr> {
288    let num_planes = seq.color.num_planes as usize;
289    let lr = &frame.loop_restoration;
290    let upscaled_width = frame.upscaled_width as usize;
291    let frame_height = frame.frame_height as usize;
292    // Round2(x, n) for n in {0, 1}: the only subsampling shifts.
293    let round2 = |x: usize, n: usize| if n == 0 { x } else { (x + 1) >> 1 };
294    (0..num_planes)
295        .map(|plane| {
296            let frt = lr
297                .frame_restoration_type
298                .get(plane)
299                .copied()
300                .unwrap_or(RESTORE_NONE);
301            let unit_size = lr.unit_size.get(plane).copied().unwrap_or(256) as usize;
302            let (sub_x, sub_y) = if plane == 0 {
303                (0, 0)
304            } else {
305                (
306                    seq.color.subsampling_x as usize,
307                    seq.color.subsampling_y as usize,
308                )
309            };
310            let rows = count_units_in_frame(unit_size, round2(frame_height, sub_y));
311            let cols = count_units_in_frame(unit_size, round2(upscaled_width, sub_x));
312            PlaneLr::new(frt, unit_size, rows, cols)
313        })
314        .collect()
315}
316
317/// Mutable CDFs for the frame, cloned from the defaults and adapted as symbols
318/// are read. Only the tables the lossless intra path exercises are held.
319struct FrameCdfs {
320    partition_w8: [[u16; 5]; 4],
321    partition_w16: [[u16; 11]; 4],
322    partition_w32: [[u16; 11]; 4],
323    partition_w64: [[u16; 11]; 4],
324    partition_w128: [[u16; 9]; 4],
325    skip: [[u16; 3]; 3],
326    intra_frame_y_mode: [[[u16; 14]; 5]; 5],
327    uv_cfl_allowed: [[u16; 15]; 13],
328    uv_cfl_not_allowed: [[u16; 14]; 13],
329    angle_delta: [[u16; 8]; 8],
330    filter_intra: [[u16; 3]; 22],
331    filter_intra_mode: [u16; 6],
332    palette_y_mode: [[[u16; 3]; 3]; 7],
333    palette_uv_mode: [[u16; 3]; 2],
334    palette_y_size: [[u16; 8]; 7],
335    palette_uv_size: [[u16; 8]; 7],
336    palette_y_color: PaletteColorCdfs,
337    palette_uv_color: PaletteColorCdfs,
338    cfl_sign: [u16; 9],
339    cfl_alpha: [[u16; 17]; 6],
340    coeff: CoeffCdfs,
341    intra_tx_type: IntraTxTypeCdfs,
342    tx_depth: TxDepthCdfs,
343    restoration_type: [u16; 4],
344    use_wiener: [u16; 3],
345    use_sgrproj: [u16; 3],
346    delta_q: [u16; 5],
347    delta_lf: [u16; 5],
348    segment_id: [[u16; 9]; 3],
349    /// `DeltaLFMultiCdf[i]`, one per loop-filter level when `delta_lf_multi`.
350    delta_lf_multi: [[u16; 5]; FRAME_LF_COUNT],
351}
352
353/// The seven palette colour-index CDFs, one per palette size 2..=8.
354struct PaletteColorCdfs {
355    size2: [[u16; 3]; 5],
356    size3: [[u16; 4]; 5],
357    size4: [[u16; 5]; 5],
358    size5: [[u16; 6]; 5],
359    size6: [[u16; 7]; 5],
360    size7: [[u16; 8]; 5],
361    size8: [[u16; 9]; 5],
362}
363
364impl PaletteColorCdfs {
365    /// The colour-index CDF row for `palette_size` (2..=8) and context `ctx`.
366    fn row(&mut self, palette_size: usize, ctx: usize) -> Result<&mut [u16]> {
367        Ok(match palette_size {
368            2 => get_mut(&mut self.size2, ctx)?,
369            3 => get_mut(&mut self.size3, ctx)?,
370            4 => get_mut(&mut self.size4, ctx)?,
371            5 => get_mut(&mut self.size5, ctx)?,
372            6 => get_mut(&mut self.size6, ctx)?,
373            7 => get_mut(&mut self.size7, ctx)?,
374            _ => get_mut(&mut self.size8, ctx)?,
375        })
376    }
377}
378
379impl FrameCdfs {
380    fn new(qctx: usize) -> Self {
381        Self {
382            partition_w8: cdf::DEFAULT_PARTITION_W8_CDF,
383            partition_w16: cdf::DEFAULT_PARTITION_W16_CDF,
384            partition_w32: cdf::DEFAULT_PARTITION_W32_CDF,
385            partition_w64: cdf::DEFAULT_PARTITION_W64_CDF,
386            partition_w128: cdf::DEFAULT_PARTITION_W128_CDF,
387            skip: cdf::DEFAULT_SKIP_CDF,
388            intra_frame_y_mode: cdf::DEFAULT_INTRA_FRAME_Y_MODE_CDF,
389            uv_cfl_allowed: cdf::DEFAULT_UV_MODE_CFL_ALLOWED_CDF,
390            uv_cfl_not_allowed: cdf::DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF,
391            angle_delta: cdf::DEFAULT_ANGLE_DELTA_CDF,
392            filter_intra: cdf::DEFAULT_FILTER_INTRA_CDF,
393            filter_intra_mode: cdf::DEFAULT_FILTER_INTRA_MODE_CDF,
394            palette_y_mode: cdf::DEFAULT_PALETTE_Y_MODE_CDF,
395            palette_uv_mode: cdf::DEFAULT_PALETTE_UV_MODE_CDF,
396            palette_y_size: cdf::DEFAULT_PALETTE_Y_SIZE_CDF,
397            palette_uv_size: cdf::DEFAULT_PALETTE_UV_SIZE_CDF,
398            palette_y_color: PaletteColorCdfs {
399                size2: cdf::DEFAULT_PALETTE_SIZE_2_Y_COLOR_CDF,
400                size3: cdf::DEFAULT_PALETTE_SIZE_3_Y_COLOR_CDF,
401                size4: cdf::DEFAULT_PALETTE_SIZE_4_Y_COLOR_CDF,
402                size5: cdf::DEFAULT_PALETTE_SIZE_5_Y_COLOR_CDF,
403                size6: cdf::DEFAULT_PALETTE_SIZE_6_Y_COLOR_CDF,
404                size7: cdf::DEFAULT_PALETTE_SIZE_7_Y_COLOR_CDF,
405                size8: cdf::DEFAULT_PALETTE_SIZE_8_Y_COLOR_CDF,
406            },
407            palette_uv_color: PaletteColorCdfs {
408                size2: cdf::DEFAULT_PALETTE_SIZE_2_UV_COLOR_CDF,
409                size3: cdf::DEFAULT_PALETTE_SIZE_3_UV_COLOR_CDF,
410                size4: cdf::DEFAULT_PALETTE_SIZE_4_UV_COLOR_CDF,
411                size5: cdf::DEFAULT_PALETTE_SIZE_5_UV_COLOR_CDF,
412                size6: cdf::DEFAULT_PALETTE_SIZE_6_UV_COLOR_CDF,
413                size7: cdf::DEFAULT_PALETTE_SIZE_7_UV_COLOR_CDF,
414                size8: cdf::DEFAULT_PALETTE_SIZE_8_UV_COLOR_CDF,
415            },
416            cfl_sign: cdf::DEFAULT_CFL_SIGN_CDF,
417            cfl_alpha: cdf::DEFAULT_CFL_ALPHA_CDF,
418            coeff: CoeffCdfs::new(qctx),
419            intra_tx_type: IntraTxTypeCdfs::new(),
420            tx_depth: TxDepthCdfs::new(),
421            restoration_type: cdf::DEFAULT_RESTORATION_TYPE_CDF,
422            use_wiener: cdf::DEFAULT_USE_WIENER_CDF,
423            use_sgrproj: cdf::DEFAULT_USE_SGRPROJ_CDF,
424            delta_q: cdf::DEFAULT_DELTA_Q_CDF,
425            delta_lf: cdf::DEFAULT_DELTA_LF_CDF,
426            segment_id: cdf::DEFAULT_SEGMENT_ID_CDF,
427            delta_lf_multi: [cdf::DEFAULT_DELTA_LF_CDF; FRAME_LF_COUNT],
428        }
429    }
430}
431
432/// Per-plane neighbour level and DC-sign context arrays (`AboveLevelContext`
433/// and friends), one entry per 4-sample column or row.
434struct LevelContext {
435    above_level: Vec<u8>,
436    above_dc: Vec<u8>,
437    left_level: Vec<u8>,
438    left_dc: Vec<u8>,
439}
440
441/// The whole mutable state of a tile decode.
442pub(crate) struct TileState {
443    pub(crate) planes: Vec<Plane>,
444    cdfs: FrameCdfs,
445    bit_depth: u8,
446    num_planes: usize,
447    mi_cols: usize,
448    mi_rows: usize,
449    enable_filter_intra: bool,
450    enable_edge_filter: bool,
451    allow_screen_content: bool,
452    /// `reduced_tx_set` (frame header): shrinks the intra transform set.
453    reduced_tx_set: bool,
454    /// `CodedLossless`: every segment is lossless. Gates `cdef_idx`.
455    coded_lossless: bool,
456    /// The current block's `Lossless` (`LosslessArray[segment_id]`): a 4x4
457    /// WHT, no transform-type choice, and no quantizer matrix.
458    lossless: bool,
459    /// `LosslessArray[segment]`.
460    lossless_array: [bool; MAX_SEGMENTS],
461    /// The frame's segmentation parameters; disabled means one segment, 0.
462    segmentation: Segmentation,
463    /// `SegIdPreSkip`: `segment_id` is read before `skip`.
464    seg_pre_skip: bool,
465    /// `LastActiveSegId`: the coded `segment_id` range is `0..=` this.
466    last_active_segment: usize,
467    /// The current block's `segment_id`.
468    segment_id: usize,
469    /// `SegmentIds[r][c]`, which predicts the next block's `segment_id` and
470    /// selects the deblocking strength's segment offsets.
471    segment_ids: Vec<u8>,
472    /// `using_qmatrix` and `qm_y`/`qm_u`/`qm_v`, for `SegQMLevel`.
473    using_qmatrix: bool,
474    qm: [u8; 3],
475    /// The frame transform mode (`ONLY_4X4` / `LARGEST` / `SELECT`).
476    tx_mode: TxMode,
477    /// Per-plane DC quantiser offset from the block's qindex (`DeltaQYDc`,
478    /// `DeltaQUDc`, `DeltaQVDc`).
479    q_dc: [i32; 3],
480    /// Per-plane AC quantiser offset (zero for luma).
481    q_ac: [i32; 3],
482    /// `CurrentQIndex` (§5.11.12): `base_q_idx` at each tile's start, moved by
483    /// each `delta_qindex` when `delta_q_present`. Never 0 once moved, so never
484    /// lossless.
485    current_qindex: i32,
486    /// `base_q_idx`, which `CurrentQIndex` restarts from in every tile.
487    base_q: i32,
488    /// The coefficient-CDF quantiser context, for each tile's fresh CDFs.
489    qctx: usize,
490    /// The tile being decoded. Neighbours outside it are unavailable
491    /// (`is_inside`, §5.11.51) even when they lie inside the frame.
492    tile: TileBounds,
493    /// `delta_q_present`, `delta_q_res` (§5.9.17).
494    delta_q_present: bool,
495    delta_q_res: u32,
496    /// `delta_lf_present`, `delta_lf_res`, `delta_lf_multi` (§5.9.18).
497    delta_lf_present: bool,
498    delta_lf_res: u32,
499    delta_lf_multi: bool,
500    /// `ReadDeltas`: set at each superblock, cleared by its first block.
501    read_deltas: bool,
502    /// `DeltaLF[i]`: the running loop-filter deltas, reset per tile.
503    delta_lf: [i8; FRAME_LF_COUNT],
504    /// `DeltaLFs[r][c]`: each 4x4 unit's `DeltaLF` when its block was decoded,
505    /// which the deblocking filter reads for the strength (§7.14.4).
506    delta_lfs: Vec<[i8; FRAME_LF_COUNT]>,
507    /// `InterTxSizes[r][c]`: the luma transform size (a `TxSize` index) chosen
508    /// for each 4x4 unit, for the `tx_depth` neighbour context under `SELECT`.
509    tx_sizes: Vec<u8>,
510    /// `LoopfilterTxSizes[plane][r][c]`: the transform size actually applied at
511    /// each 4x4 unit, per plane, filled as transform blocks reconstruct. The
512    /// deblocking loop filter reads it to find transform edges (§7.14.2).
513    lf_tx_sizes: [Vec<u8>; 3],
514    /// Coded frame dimensions in luma samples (`FrameWidth`/`FrameHeight`), for
515    /// the loop filter's on-screen test. With super-resolution `FrameWidth` is
516    /// the reduced width every tile-level step runs at.
517    frame_width: usize,
518    frame_height: usize,
519    /// `UpscaledWidth`: the display width, which loop restoration runs at.
520    upscaled_width: usize,
521    /// `SuperresDenom` when `use_superres`, else `None`.
522    superres_denom: Option<usize>,
523    /// The frame loop-filter parameters, for deblocking after reconstruct.
524    loop_filter: LoopFilter,
525    /// The frame CDEF parameters, for the CDEF pass after deblocking (§7.15).
526    cdef: Cdef,
527    /// Whether CDEF is enabled at all (`enable_cdef`); when false no `cdef_idx`
528    /// is coded and the grid stays all -1.
529    enable_cdef: bool,
530    /// `cdef_idx[row][col]` (§5.11.56): the CDEF strength index per 64x64 block,
531    /// -1 until read. Only the 64x64-aligned entries are meaningful.
532    cdef_idx: Vec<i16>,
533    /// Chroma subsampling (0 for 4:4:4), for the CDEF filter's plane geometry.
534    subsampling_x: usize,
535    subsampling_y: usize,
536    /// Whether any plane uses loop restoration (`UsesLr`).
537    uses_lr: bool,
538    /// Per-plane loop-restoration parameters and per-unit filter data, filled by
539    /// `read_lr` during tile decode. Empty when `uses_lr` is false.
540    lr: Vec<PlaneLr>,
541    /// `RefLrWiener[plane][pass][coeff]`: the running Wiener reference, reset per
542    /// tile and updated by each coded unit.
543    ref_lr_wiener: [[[i32; 3]; 2]; 3],
544    /// `RefSgrXqd[plane][i]`: the running self-guided projection reference.
545    ref_sgr_xqd: [[i32; 2]; 3],
546    sb_size4: usize,
547    /// `BlockDecoded[plane]`, one flat `(sb+2) x (sb+2)` grid per plane, reset
548    /// per superblock; addressed with a one-unit border so index -1 is valid.
549    block_decoded: Vec<Vec<u8>>,
550    /// `YModes[r][c]` flattened row-major, one entry per 4x4 unit.
551    y_modes: Vec<u8>,
552    /// `UVModes[r][c]` flattened, for the intra filter-type decision.
553    uv_modes: Vec<u8>,
554    /// `PaletteSizes[plane][r][c]` flattened, for the neighbour palette cache
555    /// and `has_palette` contexts.
556    palette_sizes: [Vec<u8>; 2],
557    /// `PaletteColors[plane][r][c][0..8]` flattened (8 colours per unit).
558    palette_colors: [Vec<[u16; PALETTE_COLORS]>; 2],
559    /// `Skips[r][c]` flattened.
560    skips: Vec<u8>,
561    /// `Mi_Width_Log2` of the block owning each 4x4 unit (for partition ctx).
562    mi_wide_log2: Vec<u8>,
563    /// `Mi_Height_Log2` of the block owning each 4x4 unit.
564    mi_high_log2: Vec<u8>,
565    /// Level contexts, one per plane.
566    ctx: Vec<LevelContext>,
567    /// `MaxLumaW`/`MaxLumaH`: the right and bottom edge of the latest luma
568    /// transform block, which bounds chroma-from-luma's luma reads.
569    max_luma_w: usize,
570    max_luma_h: usize,
571}
572
573impl TileState {
574    fn new(seq: &SequenceHeader, frame: &FrameHeader) -> Result<Self> {
575        let mi_cols = frame.mi_cols as usize;
576        let mi_rows = frame.mi_rows as usize;
577        let num_planes = seq.color.num_planes as usize;
578        // `CurrFrame` must hold every sample a transform block writes, and a
579        // block overhanging the right or bottom edge writes its whole prediction
580        // and residual past `MiCols * MI_SIZE` — samples chroma-from-luma then
581        // reads back. So the planes cover whole superblocks; the decoded area
582        // (`frame_bounds`) is tracked separately. Each chroma plane is the luma
583        // grid shifted by its subsampling (superblocks are even, so the shift
584        // is exact).
585        let sb_size4 = if seq.use_128x128_superblock { 32 } else { 16 };
586        let padded_w = mi_cols.div_ceil(sb_size4) * sb_size4 * MI_SIZE;
587        let padded_h = mi_rows.div_ceil(sb_size4) * sb_size4 * MI_SIZE;
588        let (sub_x, sub_y) = (
589            seq.color.subsampling_x as usize,
590            seq.color.subsampling_y as usize,
591        );
592        let planes = (0..num_planes)
593            .map(|p| {
594                if p == 0 {
595                    Plane::new(padded_w, padded_h)
596                } else {
597                    Plane::new(padded_w >> sub_x, padded_h >> sub_y)
598                }
599            })
600            .collect();
601        let ctx = (0..num_planes)
602            .map(|_| LevelContext {
603                above_level: vec![0; mi_cols],
604                above_dc: vec![0; mi_cols],
605                left_level: vec![0; mi_rows],
606                left_dc: vec![0; mi_rows],
607            })
608            .collect();
609        let bd_stride = sb_size4 + 2;
610        let block_decoded = (0..num_planes)
611            .map(|_| vec![0; bd_stride * bd_stride])
612            .collect();
613        // The coefficient-CDF quantiser context (`get_qctx`, §8.3.2): base_q_idx
614        // <=20 -> 0, <=60 -> 1, <=120 -> 2, else 3. Lossless (0) is 0.
615        let base_q = i32::from(frame.quantization.base_q_idx);
616        let qctx = match base_q {
617            0..=20 => 0,
618            21..=60 => 1,
619            61..=120 => 2,
620            _ => 3,
621        };
622        let q = &frame.quantization;
623        Ok(Self {
624            planes,
625            cdfs: FrameCdfs::new(qctx),
626            bit_depth: seq.color.bit_depth,
627            num_planes,
628            mi_cols,
629            mi_rows,
630            enable_filter_intra: seq.enable_filter_intra,
631            enable_edge_filter: seq.enable_intra_edge_filter,
632            allow_screen_content: frame.allow_screen_content_tools,
633            reduced_tx_set: frame.reduced_tx_set,
634            coded_lossless: frame.coded_lossless,
635            lossless: frame.coded_lossless,
636            lossless_array: frame.lossless,
637            segmentation: frame.segmentation.clone(),
638            seg_pre_skip: frame.segmentation.pre_skip(),
639            last_active_segment: frame.segmentation.last_active_segment(),
640            segment_id: 0,
641            segment_ids: vec![0; mi_cols * mi_rows],
642            using_qmatrix: q.using_qmatrix,
643            qm: [q.qm_y, q.qm_u, q.qm_v],
644            tx_mode: frame.tx_mode,
645            q_dc: [q.delta_q_y_dc, q.delta_q_u_dc, q.delta_q_v_dc],
646            q_ac: [0, q.delta_q_u_ac, q.delta_q_v_ac],
647            current_qindex: base_q,
648            base_q,
649            qctx,
650            tile: TileBounds {
651                row_start: 0,
652                row_end: mi_rows,
653                col_start: 0,
654                col_end: mi_cols,
655            },
656            delta_q_present: frame.delta_q_present,
657            delta_q_res: frame.delta_q_res,
658            delta_lf_present: frame.delta_lf_present,
659            delta_lf_res: frame.delta_lf_res,
660            delta_lf_multi: frame.delta_lf_multi,
661            read_deltas: false,
662            delta_lf: [0; FRAME_LF_COUNT],
663            delta_lfs: vec![[0; FRAME_LF_COUNT]; mi_cols * mi_rows],
664            tx_sizes: vec![0; mi_cols * mi_rows],
665            lf_tx_sizes: [
666                vec![0; mi_cols * mi_rows],
667                vec![0; mi_cols * mi_rows],
668                vec![0; mi_cols * mi_rows],
669            ],
670            frame_width: frame.frame_width as usize,
671            frame_height: frame.frame_height as usize,
672            upscaled_width: frame.upscaled_width as usize,
673            // `SuperresDenom` is SUPERRES_NUM exactly when use_superres is 0. (The
674            // widths are no test: a 1-sample frame codes at its full width even
675            // with super-resolution on.)
676            superres_denom: (frame.superres_denom as usize != SUPERRES_NUM)
677                .then_some(frame.superres_denom as usize),
678            loop_filter: frame.loop_filter.clone(),
679            cdef: frame.cdef.clone(),
680            enable_cdef: seq.enable_cdef,
681            cdef_idx: vec![-1; mi_cols * mi_rows],
682            subsampling_x: seq.color.subsampling_x as usize,
683            subsampling_y: seq.color.subsampling_y as usize,
684            uses_lr: frame.loop_restoration.uses_lr,
685            lr: build_plane_lr(seq, frame),
686            ref_lr_wiener: [[WIENER_TAPS_MID; 2]; 3],
687            ref_sgr_xqd: [SGRPROJ_XQD_MID; 3],
688            sb_size4,
689            block_decoded,
690            y_modes: vec![0; mi_cols * mi_rows],
691            uv_modes: vec![0; mi_cols * mi_rows],
692            palette_sizes: [vec![0; mi_cols * mi_rows], vec![0; mi_cols * mi_rows]],
693            palette_colors: [
694                vec![[0; PALETTE_COLORS]; mi_cols * mi_rows],
695                vec![[0; PALETTE_COLORS]; mi_cols * mi_rows],
696            ],
697            skips: vec![0; mi_cols * mi_rows],
698            mi_wide_log2: vec![0; mi_cols * mi_rows],
699            mi_high_log2: vec![0; mi_cols * mi_rows],
700            ctx,
701            max_luma_w: 0,
702            max_luma_h: 0,
703        })
704    }
705
706    /// `decode_tile` (§5.11.2): one tile, from fresh CDFs and cleared above
707    /// contexts, its superblocks in raster order within its bounds.
708    fn decode_tile(&mut self, tile_data: &[u8], tile: TileBounds) -> Result<()> {
709        let mut dec = SymbolDecoder::new(tile_data, false)?;
710        self.code_tile(&mut dec, tile)
711    }
712
713    /// `decode_tile` (§5.11.2) over any [`TileCoder`]: the decoder reading a
714    /// stream, or the encoder deciding and writing one.
715    pub(crate) fn code_tile(&mut self, dec: &mut impl TileCoder, tile: TileBounds) -> Result<()> {
716        self.tile = tile;
717        self.cdfs = FrameCdfs::new(self.qctx);
718        self.current_qindex = self.base_q;
719        self.delta_lf = [0; FRAME_LF_COUNT];
720        self.ref_lr_wiener = [[WIENER_TAPS_MID; 2]; 3];
721        self.ref_sgr_xqd = [SGRPROJ_XQD_MID; 3];
722        for c in &mut self.ctx {
723            c.above_level.fill(0);
724            c.above_dc.fill(0);
725        }
726        let sb_size4 = self.sb_size4;
727        // Superblocks are decoded in raster order; each seeds the partition
728        // recursion. The left contexts reset at the start of each SB row.
729        let mut sb_row = tile.row_start;
730        while sb_row < tile.row_end {
731            self.reset_left_context();
732            let mut sb_col = tile.col_start;
733            while sb_col < tile.col_end {
734                self.read_deltas = self.delta_q_present;
735                self.clear_block_decoded(sb_row, sb_col);
736                self.read_lr(dec, sb_row, sb_col)?;
737                self.decode_partition(dec, sb_row, sb_col, sb_size4)?;
738                sb_col += sb_size4;
739            }
740            sb_row += sb_size4;
741        }
742        Ok(())
743    }
744
745    /// The in-loop filters and upscale, over the whole frame once every tile
746    /// is reconstructed: they cross tile boundaries (§5.11.52).
747    pub(crate) fn post_filter(&mut self) {
748        self.deblock();
749        // Loop restoration reads both the pre-CDEF (deblocked) and post-CDEF
750        // frames, so when it runs, snapshot the deblocked frame before CDEF. Both
751        // are then upscaled (§7.4 steps 3–4; a no-op without super-resolution)
752        // and restoration filters the upscaled CDEF output in place.
753        let curr = self.uses_lr.then(|| self.planes.clone());
754        self.cdef();
755        if let Some(superres) = self.superres() {
756            self.planes = superres.upscale(&self.planes);
757            if let Some(curr) = curr {
758                let curr = superres.upscale(&curr);
759                self.loop_restore(&curr);
760            }
761        } else if let Some(curr) = curr {
762            self.loop_restore(&curr);
763        }
764    }
765
766    /// The upscaling geometry when the frame uses super-resolution.
767    fn superres(&self) -> Option<Superres> {
768        self.superres_denom.map(|_| Superres {
769            frame_width: self.frame_width,
770            upscaled_width: self.upscaled_width,
771            frame_height: self.frame_height,
772            mi_cols: self.mi_cols,
773            subsampling_x: self.subsampling_x,
774            subsampling_y: self.subsampling_y,
775            bit_depth: self.bit_depth,
776        })
777    }
778
779    /// Apply the deblocking loop filter to the reconstructed planes (§7.14). A
780    /// no-op when every filter level is zero, which is always so for a
781    /// coded-lossless frame.
782    fn deblock(&mut self) {
783        Deblock {
784            planes: &mut self.planes,
785            loop_filter: &self.loop_filter,
786            bit_depth: self.bit_depth,
787            num_planes: self.num_planes,
788            subsampling_x: self.subsampling_x,
789            subsampling_y: self.subsampling_y,
790            mi_rows: self.mi_rows,
791            mi_cols: self.mi_cols,
792            frame_width: self.frame_width,
793            frame_height: self.frame_height,
794            lf_tx_sizes: &self.lf_tx_sizes,
795            delta_lfs: &self.delta_lfs,
796            delta_lf_multi: self.delta_lf_multi,
797            segment_ids: &self.segment_ids,
798            segment_lf: core::array::from_fn(|segment| {
799                core::array::from_fn(|i| {
800                    self.segmentation
801                        .feature_value(segment, SEG_LVL_ALT_LF_Y_V + i)
802                })
803            }),
804        }
805        .run();
806    }
807
808    /// Apply the constrained directional enhancement filter (§7.15) to the
809    /// deblocked planes. A no-op when CDEF is disabled: the `cdef_idx` grid is
810    /// then all -1, so every 8x8 block is left as it is.
811    fn cdef(&mut self) {
812        CdefFilter {
813            planes: &mut self.planes,
814            cdef: &self.cdef,
815            cdef_idx: &self.cdef_idx,
816            skips: &self.skips,
817            bit_depth: self.bit_depth,
818            num_planes: self.num_planes,
819            mi_rows: self.mi_rows,
820            mi_cols: self.mi_cols,
821            subsampling_x: self.subsampling_x,
822            subsampling_y: self.subsampling_y,
823        }
824        .run();
825    }
826
827    /// Apply loop restoration (§7.17) to the CDEF output. `curr` is the pre-CDEF
828    /// (deblocked) frame; the post-CDEF frame is the current `planes`, which also
829    /// seed `LrFrame` — restoration overwrites only the blocks that need it,
830    /// reading throughout from the two snapshots so it never sees its own output.
831    fn loop_restore(&mut self, curr: &[Plane]) {
832        let cdef = self.planes.clone();
833        LoopRestore {
834            planes: &mut self.planes,
835            curr,
836            cdef: &cdef,
837            lr: &self.lr,
838            bit_depth: self.bit_depth,
839            num_planes: self.num_planes,
840            subsampling_x: self.subsampling_x,
841            subsampling_y: self.subsampling_y,
842            upscaled_width: self.upscaled_width,
843            frame_height: self.frame_height,
844        }
845        .run();
846    }
847
848    /// `clear_block_decoded_flags` (§5.11.3) for one superblock, each plane in
849    /// its own (subsampled) 4x4 units.
850    fn clear_block_decoded(&mut self, r: usize, c: usize) {
851        let sb = self.sb_size4;
852        let stride = sb + 2;
853        for plane in 0..self.num_planes {
854            let (sub_x, sub_y) = self.plane_subsampling(plane);
855            let sb_width4 = ((self.tile.col_end - c) >> sub_x) as isize;
856            let sb_height4 = ((self.tile.row_end - r) >> sub_y) as isize;
857            let (sb_w, sb_h) = ((sb >> sub_x) as isize, (sb >> sub_y) as isize);
858            let Some(grid) = self.block_decoded.get_mut(plane) else {
859                continue;
860            };
861            for v in grid.iter_mut() {
862                *v = 0;
863            }
864            // Row above (y == -1) valid where x < sbWidth4; column left (x == -1)
865            // valid where y < sbHeight4. Indices carry a +1 border.
866            for x in -1_isize..=sb_w {
867                if x < sb_width4 {
868                    if let Some(slot) = grid.get_mut(bd_index(stride, -1, x)) {
869                        *slot = 1;
870                    }
871                }
872            }
873            for y in -1_isize..=sb_h {
874                if y < sb_height4 {
875                    if let Some(slot) = grid.get_mut(bd_index(stride, y, -1)) {
876                        *slot = 1;
877                    }
878                }
879            }
880            if let Some(slot) = grid.get_mut(bd_index(stride, sb_h, -1)) {
881                *slot = 0;
882            }
883        }
884    }
885
886    fn block_decoded_at(&self, plane: usize, sub_row: isize, sub_col: isize) -> bool {
887        let stride = self.sb_size4 + 2;
888        self.block_decoded
889            .get(plane)
890            .and_then(|g| g.get(bd_index(stride, sub_row, sub_col)))
891            .is_some_and(|&v| v != 0)
892    }
893
894    fn set_block_decoded(&mut self, plane: usize, sub_row: isize, sub_col: isize) {
895        let stride = self.sb_size4 + 2;
896        if let Some(slot) = self
897            .block_decoded
898            .get_mut(plane)
899            .and_then(|g| g.get_mut(bd_index(stride, sub_row, sub_col)))
900        {
901            *slot = 1;
902        }
903    }
904
905    /// `AvailU`: `is_inside(r - 1, c)` for a block at `(r, c)` in this tile.
906    const fn avail_u(&self, r: usize) -> bool {
907        r > self.tile.row_start
908    }
909
910    /// `AvailL`: `is_inside(r, c - 1)`.
911    const fn avail_l(&self, c: usize) -> bool {
912        c > self.tile.col_start
913    }
914
915    fn reset_left_context(&mut self) {
916        for c in &mut self.ctx {
917            c.left_level.fill(0);
918            c.left_dc.fill(0);
919        }
920    }
921
922    /// `decode_partition` (§5.11.4), restricted to what the lossless subset
923    /// produces. `bsize4` is the block side in 4-sample units (a power of two).
924    fn decode_partition(
925        &mut self,
926        dec: &mut impl TileCoder,
927        r: usize,
928        c: usize,
929        bsize4: usize,
930    ) -> Result<()> {
931        if r >= self.mi_rows || c >= self.mi_cols {
932            return Ok(());
933        }
934        let avail_u = self.avail_u(r);
935        let avail_l = self.avail_l(c);
936        let half = bsize4 >> 1;
937        let has_rows = r + half < self.mi_rows;
938        let has_cols = c + half < self.mi_cols;
939
940        let partition = if bsize4 < 2 {
941            PARTITION_NONE
942        } else if has_rows && has_cols {
943            self.read_partition(dec, r, c, bsize4, avail_u, avail_l)?
944        } else if has_cols {
945            if self.read_split_or(dec, r, c, bsize4, avail_u, avail_l, true)? {
946                PARTITION_SPLIT
947            } else {
948                PARTITION_HORZ
949            }
950        } else if has_rows {
951            if self.read_split_or(dec, r, c, bsize4, avail_u, avail_l, false)? {
952                PARTITION_SPLIT
953            } else {
954                PARTITION_VERT
955            }
956        } else {
957            PARTITION_SPLIT
958        };
959
960        let quarter = bsize4 >> 2;
961        match partition {
962            PARTITION_NONE => self.decode_block(dec, r, c, bsize4, bsize4)?,
963            PARTITION_HORZ => {
964                self.decode_block(dec, r, c, bsize4, half)?;
965                if has_rows {
966                    self.decode_block(dec, r + half, c, bsize4, half)?;
967                }
968            }
969            PARTITION_VERT => {
970                self.decode_block(dec, r, c, half, bsize4)?;
971                if has_cols {
972                    self.decode_block(dec, r, c + half, half, bsize4)?;
973                }
974            }
975            PARTITION_SPLIT => {
976                self.decode_partition(dec, r, c, half)?;
977                self.decode_partition(dec, r, c + half, half)?;
978                self.decode_partition(dec, r + half, c, half)?;
979                self.decode_partition(dec, r + half, c + half, half)?;
980            }
981            PARTITION_HORZ_A => {
982                self.decode_block(dec, r, c, half, half)?;
983                self.decode_block(dec, r, c + half, half, half)?;
984                self.decode_block(dec, r + half, c, bsize4, half)?;
985            }
986            PARTITION_HORZ_B => {
987                self.decode_block(dec, r, c, bsize4, half)?;
988                self.decode_block(dec, r + half, c, half, half)?;
989                self.decode_block(dec, r + half, c + half, half, half)?;
990            }
991            PARTITION_VERT_A => {
992                self.decode_block(dec, r, c, half, half)?;
993                self.decode_block(dec, r + half, c, half, half)?;
994                self.decode_block(dec, r, c + half, half, bsize4)?;
995            }
996            PARTITION_VERT_B => {
997                self.decode_block(dec, r, c, half, bsize4)?;
998                self.decode_block(dec, r, c + half, half, half)?;
999                self.decode_block(dec, r + half, c + half, half, half)?;
1000            }
1001            PARTITION_HORZ_4 => {
1002                for k in 0..4 {
1003                    let rr = r + quarter * k;
1004                    if k == 3 && rr >= self.mi_rows {
1005                        break;
1006                    }
1007                    self.decode_block(dec, rr, c, bsize4, quarter)?;
1008                }
1009            }
1010            PARTITION_VERT_4 => {
1011                for k in 0..4 {
1012                    let cc = c + quarter * k;
1013                    if k == 3 && cc >= self.mi_cols {
1014                        break;
1015                    }
1016                    self.decode_block(dec, r, cc, quarter, bsize4)?;
1017                }
1018            }
1019            _ => {
1020                return Err(PixelsError::malformed("avif", "invalid partition type"));
1021            }
1022        }
1023        Ok(())
1024    }
1025
1026    /// Read the `partition` symbol and return the partition type.
1027    fn read_partition(
1028        &mut self,
1029        dec: &mut impl TileCoder,
1030        r: usize,
1031        c: usize,
1032        bsize4: usize,
1033        avail_u: bool,
1034        avail_l: bool,
1035    ) -> Result<usize> {
1036        let ctx = self.partition_ctx(r, c, bsize4, avail_u, avail_l);
1037        let bsl = floor_log2_usize(bsize4);
1038        let cdf_row = self.partition_cdf(bsl, ctx)?;
1039        dec.symbol(cdf_row, Site::Partition { r, c, bsize4 })
1040    }
1041
1042    /// The context for the `partition` and `split_or_*` symbols (§8.3.2).
1043    fn partition_ctx(
1044        &self,
1045        r: usize,
1046        c: usize,
1047        bsize4: usize,
1048        avail_u: bool,
1049        avail_l: bool,
1050    ) -> usize {
1051        let bsl = floor_log2_usize(bsize4) as u8;
1052        let above = avail_u
1053            && r.checked_sub(1)
1054                .and_then(|ru| self.mi_wide_log2.get(ru * self.mi_cols + c))
1055                .is_some_and(|&w| w < bsl);
1056        let left = avail_l
1057            && c.checked_sub(1)
1058                .and_then(|cl| self.mi_high_log2.get(r * self.mi_cols + cl))
1059                .is_some_and(|&h| h < bsl);
1060        usize::from(left) * 2 + usize::from(above)
1061    }
1062
1063    /// The mutable `partition` CDF row for `bsl` and `ctx`.
1064    fn partition_cdf(&mut self, bsl: u32, ctx: usize) -> Result<&mut [u16]> {
1065        let row: &mut [u16] = match bsl {
1066            1 => get_mut(&mut self.cdfs.partition_w8, ctx)?,
1067            2 => get_mut(&mut self.cdfs.partition_w16, ctx)?,
1068            3 => get_mut(&mut self.cdfs.partition_w32, ctx)?,
1069            4 => get_mut(&mut self.cdfs.partition_w64, ctx)?,
1070            _ => get_mut(&mut self.cdfs.partition_w128, ctx)?,
1071        };
1072        Ok(row)
1073    }
1074
1075    /// Read `split_or_horz` / `split_or_vert` (§8.3.2): a binary decision built
1076    /// from the full partition CDF. Returns whether the partition is a split.
1077    #[allow(
1078        clippy::too_many_arguments,
1079        reason = "mirrors the split_or_* context inputs"
1080    )]
1081    fn read_split_or(
1082        &mut self,
1083        dec: &mut impl TileCoder,
1084        r: usize,
1085        c: usize,
1086        bsize4: usize,
1087        avail_u: bool,
1088        avail_l: bool,
1089        horz: bool,
1090    ) -> Result<bool> {
1091        let ctx = self.partition_ctx(r, c, bsize4, avail_u, avail_l);
1092        let bsl = floor_log2_usize(bsize4);
1093        let is_128 = bsize4 == 32;
1094        // Copy the partition CDF so the derived binary read does not adapt it.
1095        let src = self.partition_cdf(bsl, ctx)?;
1096        let partition_cdf: Vec<u16> = src.to_vec();
1097        let prob = |k: usize| -> i32 {
1098            let hi = partition_cdf.get(k).copied().unwrap_or(0);
1099            let lo = k
1100                .checked_sub(1)
1101                .and_then(|i| partition_cdf.get(i))
1102                .copied()
1103                .unwrap_or(0);
1104            i32::from(hi) - i32::from(lo)
1105        };
1106        // split_or_horz cannot return VERT, split_or_vert cannot return HORZ:
1107        // the excluded direction's mass is folded into the split probability.
1108        let mut psum = if horz {
1109            prob(PARTITION_VERT) + prob(PARTITION_SPLIT) + prob(4) + prob(6) + prob(7)
1110        } else {
1111            prob(PARTITION_HORZ) + prob(PARTITION_SPLIT) + prob(4) + prob(5) + prob(6)
1112        };
1113        if !is_128 {
1114            psum += if horz { prob(9) } else { prob(8) };
1115        }
1116        let mut derived = [((1 << 15) - psum) as u16, 1 << 15, 0];
1117        Ok(dec.symbol(&mut derived, Site::SplitOr { r, c, bsize4 })? != 0)
1118    }
1119
1120    /// `decode_block` (§5.11.5) plus mode info and residual, for one block.
1121    fn decode_block(
1122        &mut self,
1123        dec: &mut impl TileCoder,
1124        r: usize,
1125        c: usize,
1126        bw4: usize,
1127        bh4: usize,
1128    ) -> Result<()> {
1129        let avail_u = self.avail_u(r);
1130        let avail_l = self.avail_l(c);
1131        // HasChroma (§5.11.5): with subsampling, a block one unit wide (high)
1132        // at an even column (row) shares its chroma with the next block, which
1133        // codes it; only that odd-positioned block carries chroma.
1134        let (sub_x, sub_y) = (self.subsampling_x, self.subsampling_y);
1135        let shares_chroma =
1136            (bh4 == 1 && sub_y == 1 && r & 1 == 0) || (bw4 == 1 && sub_x == 1 && c & 1 == 0);
1137        let has_chroma = self.num_planes > 1 && !shares_chroma;
1138        // AvailUChroma/AvailLChroma: such a chroma-owning block's chroma extends
1139        // one unit further up (left), so its neighbour is two units away.
1140        let (avail_u_chroma, avail_l_chroma) = if has_chroma {
1141            (
1142                if sub_y == 1 && bh4 == 1 {
1143                    r >= self.tile.row_start + 2
1144                } else {
1145                    avail_u
1146                },
1147                if sub_x == 1 && bw4 == 1 {
1148                    c >= self.tile.col_start + 2
1149                } else {
1150                    avail_l
1151                },
1152            )
1153        } else {
1154            (false, false)
1155        };
1156
1157        // --- intra_frame_mode_info (§5.11.7) ---
1158        // segment_id comes before skip when a segment forces skipping, and
1159        // after it (where a skipped block inherits its prediction) otherwise.
1160        dec.plan_block(self, r, c, bw4, bh4);
1161        self.segment_id = 0;
1162        if self.seg_pre_skip {
1163            self.read_segment_id(dec, r, c, avail_u, avail_l, false)?;
1164        }
1165        let skip = if self.seg_pre_skip
1166            && self
1167                .segmentation
1168                .feature_active(self.segment_id, SEG_LVL_SKIP)
1169        {
1170            true
1171        } else {
1172            self.read_skip(dec, r, c, avail_u, avail_l)?
1173        };
1174        if !self.seg_pre_skip {
1175            self.read_segment_id(dec, r, c, avail_u, avail_l, skip)?;
1176        }
1177        self.lossless = self
1178            .lossless_array
1179            .get(self.segment_id)
1180            .copied()
1181            .unwrap_or(false);
1182        let segment = self.segment_id as u8;
1183        for row in r..(r + bh4).min(self.mi_rows) {
1184            for col in c..(c + bw4).min(self.mi_cols) {
1185                if let Some(slot) = self.segment_ids.get_mut(row * self.mi_cols + col) {
1186                    *slot = segment;
1187                }
1188            }
1189        }
1190
1191        // read_cdef (§5.11.56) sits right after the segment id, then the
1192        // superblock's quantizer and loop-filter deltas.
1193        self.read_cdef(dec, r, c, bw4, bh4, skip)?;
1194        self.read_delta_qindex(dec, bw4, bh4, skip)?;
1195        self.read_delta_lf(dec, bw4, bh4, skip)?;
1196        self.read_deltas = false;
1197        for row in r..(r + bh4).min(self.mi_rows) {
1198            for col in c..(c + bw4).min(self.mi_cols) {
1199                if let Some(slot) = self.delta_lfs.get_mut(row * self.mi_cols + col) {
1200                    *slot = self.delta_lf;
1201                }
1202            }
1203        }
1204
1205        let y_mode = self.read_intra_frame_y_mode(dec, r, c, avail_u, avail_l)?;
1206        let y_delta = self.read_angle_delta(dec, y_mode, bw4, bh4, Site::AngleDeltaY)?;
1207
1208        let (uv_mode, uv_delta, cfl) = if has_chroma {
1209            let (uv, cfl) = self.read_uv_mode(dec, y_mode, bw4, bh4)?;
1210            let d = self.read_angle_delta(dec, uv, bw4, bh4, Site::AngleDeltaUv)?;
1211            (uv, d, cfl)
1212        } else {
1213            (DC_PRED, 0, None)
1214        };
1215
1216        // palette_mode_info (§5.11.46): only when screen-content tools are
1217        // enabled, for `MiSize >= BLOCK_8X8` up to 64x64.
1218        let mut palette = Palette {
1219            block_w: bw4 * MI_SIZE,
1220            block_h: bh4 * MI_SIZE,
1221            ..Palette::default()
1222        };
1223        let palette_ok =
1224            self.allow_screen_content && at_least_block_8x8(bw4, bh4) && bw4 <= 16 && bh4 <= 16;
1225        if palette_ok {
1226            self.read_palette_mode_info(
1227                dec,
1228                r,
1229                c,
1230                bw4,
1231                bh4,
1232                y_mode,
1233                uv_mode,
1234                has_chroma,
1235                &mut palette,
1236            )?;
1237        }
1238
1239        // filter-intra is not coded for a palette-Y block.
1240        let filter_intra = if palette.size_y > 0 {
1241            None
1242        } else {
1243            self.read_filter_intra(dec, y_mode, bw4, bh4)?
1244        };
1245
1246        // Record the block's mode, geometry, and palette across its 4x4 units.
1247        self.record_block(r, c, bw4, bh4, y_mode, uv_mode, has_chroma, skip, &palette);
1248
1249        // palette_tokens (§5.11.49): the colour-index maps.
1250        if palette.size_y > 0 || palette.size_uv > 0 {
1251            self.read_palette_tokens(dec, r, c, &mut palette)?;
1252        }
1253
1254        // read_block_tx_size (§5.11.16): the luma transform size for the block.
1255        // Under TX_MODE_SELECT this reads a `tx_depth` symbol, so it must run
1256        // before residual and after mode info.
1257        let luma_tx_size = self.read_block_tx_size(dec, r, c, bw4, bh4, skip)?;
1258
1259        if skip {
1260            self.reset_block_context(r, c, bw4, bh4, has_chroma);
1261        }
1262
1263        // --- residual: every plane, every transform block ---
1264        let modes = BlockModes {
1265            r,
1266            c,
1267            avail_u,
1268            avail_l,
1269            avail_u_chroma,
1270            avail_l_chroma,
1271            y_mode,
1272            uv_mode,
1273            y_delta,
1274            uv_delta,
1275            filter_intra,
1276            cfl,
1277            palette,
1278            luma_tx_size,
1279        };
1280        self.residual(dec, &modes, bw4, bh4, skip, has_chroma)?;
1281        Ok(())
1282    }
1283
1284    fn read_skip(
1285        &mut self,
1286        dec: &mut impl TileCoder,
1287        r: usize,
1288        c: usize,
1289        avail_u: bool,
1290        avail_l: bool,
1291    ) -> Result<bool> {
1292        let mut ctx = 0;
1293        if avail_u {
1294            ctx += usize::from(self.skip_at(r.wrapping_sub(1), c));
1295        }
1296        if avail_l {
1297            ctx += usize::from(self.skip_at(r, c.wrapping_sub(1)));
1298        }
1299        let cdf_row = get_mut(&mut self.cdfs.skip, ctx)?;
1300        Ok(dec.symbol(cdf_row, Site::Skip)? != 0)
1301    }
1302
1303    /// `read_cdef` (§5.11.56): read the `cdef_idx` literal for the 64x64 block
1304    /// containing `(r, c)`, the first time that block is reached. A skip block,
1305    /// a coded-lossless frame, or CDEF being disabled reads nothing (`allow_intrabc`
1306    /// is always false in this subset). `cdef_bits` is often zero, in which case
1307    /// the literal is empty and the index is simply 0 (filtering with the single
1308    /// coded strength).
1309    fn read_cdef(
1310        &mut self,
1311        dec: &mut impl TileCoder,
1312        r: usize,
1313        c: usize,
1314        bw4: usize,
1315        bh4: usize,
1316        skip: bool,
1317    ) -> Result<()> {
1318        if skip || self.coded_lossless || !self.enable_cdef {
1319            return Ok(());
1320        }
1321        // CDEF parameters are stored per 64x64 luma block (16 units).
1322        let cdef_size4 = 16;
1323        let mask = !(cdef_size4 - 1);
1324        let base_r = r & mask;
1325        let base_c = c & mask;
1326        if self.cdef_idx.get(base_r * self.mi_cols + base_c).copied() != Some(-1) {
1327            return Ok(());
1328        }
1329        let value = dec.literal(self.cdef.bits, Site::CdefIdx)? as i16;
1330        let mut i = base_r;
1331        while i < base_r + bh4 {
1332            let mut j = base_c;
1333            while j < base_c + bw4 {
1334                if i < self.mi_rows && j < self.mi_cols {
1335                    if let Some(slot) = self.cdef_idx.get_mut(i * self.mi_cols + j) {
1336                        *slot = value;
1337                    }
1338                }
1339                j += cdef_size4;
1340            }
1341            i += cdef_size4;
1342        }
1343        Ok(())
1344    }
1345
1346    /// `intra_segment_id` / `read_segment_id` (§5.11.8, §5.11.9): predict the
1347    /// block's segment from its neighbours, then — unless it is skipped, when
1348    /// the prediction stands — read the difference from it.
1349    fn read_segment_id(
1350        &mut self,
1351        dec: &mut impl TileCoder,
1352        r: usize,
1353        c: usize,
1354        avail_u: bool,
1355        avail_l: bool,
1356        skip: bool,
1357    ) -> Result<()> {
1358        if !self.segmentation.enabled {
1359            self.segment_id = 0;
1360            return Ok(());
1361        }
1362        let at = |row: usize, col: usize| -> i32 {
1363            self.segment_ids
1364                .get(row * self.mi_cols + col)
1365                .map_or(-1, |&s| i32::from(s))
1366        };
1367        let prev_ul = if avail_u && avail_l {
1368            at(r - 1, c - 1)
1369        } else {
1370            -1
1371        };
1372        let prev_u = if avail_u { at(r - 1, c) } else { -1 };
1373        let prev_l = if avail_l { at(r, c - 1) } else { -1 };
1374        let pred = if prev_u == -1 {
1375            prev_l.max(0)
1376        } else if prev_l == -1 || prev_ul == prev_u {
1377            prev_u
1378        } else {
1379            prev_l
1380        };
1381        let segment = if skip {
1382            pred
1383        } else {
1384            let ctx = if prev_ul < 0 {
1385                0
1386            } else if prev_ul == prev_u && prev_ul == prev_l {
1387                2
1388            } else if prev_ul == prev_u || prev_ul == prev_l || prev_u == prev_l {
1389                1
1390            } else {
1391                0
1392            };
1393            let diff = dec.symbol(get_mut(&mut self.cdfs.segment_id, ctx)?, Site::Other)? as i32;
1394            neg_deinterleave(diff, pred, self.last_active_segment as i32 + 1)
1395        };
1396        // A conformant stream stays within 0..=LastActiveSegId.
1397        self.segment_id = usize::try_from(segment)
1398            .ok()
1399            .filter(|&s| s <= self.last_active_segment)
1400            .ok_or_else(|| {
1401                PixelsError::malformed(
1402                    "avif",
1403                    format!(
1404                        "segment_id {segment} is outside 0..={}",
1405                        self.last_active_segment
1406                    ),
1407                )
1408            })?;
1409        Ok(())
1410    }
1411
1412    /// `get_qindex(ignoreDeltaQ, segment_id)` (§7.12.2) for the current block.
1413    fn segment_qindex(&self, ignore_delta_q: bool) -> i32 {
1414        let base = if !ignore_delta_q && self.delta_q_present {
1415            self.current_qindex
1416        } else {
1417            self.base_q
1418        };
1419        if self
1420            .segmentation
1421            .feature_active(self.segment_id, SEG_LVL_ALT_Q)
1422        {
1423            (base
1424                + self
1425                    .segmentation
1426                    .feature_value(self.segment_id, SEG_LVL_ALT_Q))
1427            .clamp(0, 255)
1428        } else {
1429            base
1430        }
1431    }
1432
1433    /// `read_delta_qindex` (§5.11.12): the first block of a superblock may move
1434    /// `CurrentQIndex`, unless it covers the whole superblock and is skipped
1435    /// (it then has no coefficients for a quantizer to matter to).
1436    fn read_delta_qindex(
1437        &mut self,
1438        dec: &mut impl TileCoder,
1439        bw4: usize,
1440        bh4: usize,
1441        skip: bool,
1442    ) -> Result<()> {
1443        if (bw4 == self.sb_size4 && bh4 == self.sb_size4 && skip) || !self.read_deltas {
1444            return Ok(());
1445        }
1446        let delta = read_delta(dec, &mut self.cdfs.delta_q)?;
1447        if delta != 0 {
1448            self.current_qindex = (self.current_qindex + (delta << self.delta_q_res)).clamp(1, 255);
1449        }
1450        Ok(())
1451    }
1452
1453    /// `read_delta_lf` (§5.11.13): likewise for the loop-filter deltas — one,
1454    /// or with `delta_lf_multi` one per filter level (two luma directions and,
1455    /// with chroma, U and V).
1456    fn read_delta_lf(
1457        &mut self,
1458        dec: &mut impl TileCoder,
1459        bw4: usize,
1460        bh4: usize,
1461        skip: bool,
1462    ) -> Result<()> {
1463        if (bw4 == self.sb_size4 && bh4 == self.sb_size4 && skip)
1464            || !self.read_deltas
1465            || !self.delta_lf_present
1466        {
1467            return Ok(());
1468        }
1469        let count = if !self.delta_lf_multi {
1470            1
1471        } else if self.num_planes > 1 {
1472            FRAME_LF_COUNT
1473        } else {
1474            FRAME_LF_COUNT - 2
1475        };
1476        for i in 0..count {
1477            let cdf = if self.delta_lf_multi {
1478                get_mut(&mut self.cdfs.delta_lf_multi, i)?
1479            } else {
1480                &mut self.cdfs.delta_lf
1481            };
1482            let delta = read_delta(dec, cdf)?;
1483            if delta != 0 {
1484                let slot = get_mut(&mut self.delta_lf, i)?;
1485                let level = (i32::from(*slot) + (delta << self.delta_lf_res))
1486                    .clamp(-MAX_LOOP_FILTER, MAX_LOOP_FILTER);
1487                *slot = level as i8;
1488            }
1489        }
1490        Ok(())
1491    }
1492
1493    /// `read_lr` (§5.11.57): read the loop-restoration units this superblock
1494    /// covers, once per plane that uses restoration. `allow_intrabc` is always
1495    /// false in this subset. Units are laid out over the upscaled frame, so with
1496    /// super-resolution a superblock's coded columns are scaled by
1497    /// `SuperresDenom / SUPERRES_NUM` to find the units it covers.
1498    fn read_lr(&mut self, dec: &mut impl TileCoder, r: usize, c: usize) -> Result<()> {
1499        if !self.uses_lr {
1500            return Ok(());
1501        }
1502        let (w4, h4) = (self.sb_size4, self.sb_size4);
1503        for plane in 0..self.num_planes {
1504            let Some(info) = self.lr.get(plane) else {
1505                continue;
1506            };
1507            if info.frame_restoration_type == RESTORE_NONE {
1508                continue;
1509            }
1510            let (sub_x, sub_y) = if plane == 0 {
1511                (0, 0)
1512            } else {
1513                (self.subsampling_x, self.subsampling_y)
1514            };
1515            let unit_size = info.unit_size;
1516            let unit_rows = info.unit_rows;
1517            let unit_cols = info.unit_cols;
1518            let unit_row_start = (r * (MI_SIZE >> sub_y)).div_ceil(unit_size);
1519            let unit_row_end = unit_rows.min(((r + h4) * (MI_SIZE >> sub_y)).div_ceil(unit_size));
1520            let (numerator, denominator) = match self.superres_denom {
1521                Some(denom) => ((MI_SIZE >> sub_x) * denom, unit_size * SUPERRES_NUM),
1522                None => (MI_SIZE >> sub_x, unit_size),
1523            };
1524            let unit_col_start = (c * numerator).div_ceil(denominator);
1525            let unit_col_end = unit_cols.min(((c + w4) * numerator).div_ceil(denominator));
1526            for unit_row in unit_row_start..unit_row_end {
1527                for unit_col in unit_col_start..unit_col_end {
1528                    self.read_lr_unit(dec, plane, unit_row, unit_col)?;
1529                }
1530            }
1531        }
1532        Ok(())
1533    }
1534
1535    /// `read_lr_unit` (§5.11.58): read one restoration unit's type and, for a
1536    /// Wiener or self-guided unit, its coefficients (which update the running
1537    /// per-plane reference used by the sub-exponential coding).
1538    fn read_lr_unit(
1539        &mut self,
1540        dec: &mut impl TileCoder,
1541        plane: usize,
1542        unit_row: usize,
1543        unit_col: usize,
1544    ) -> Result<()> {
1545        let frame_type = self
1546            .lr
1547            .get(plane)
1548            .map_or(RESTORE_NONE, |p| p.frame_restoration_type);
1549        let restoration_type = match frame_type {
1550            RESTORE_WIENER => {
1551                if dec.symbol(&mut self.cdfs.use_wiener, Site::Other)? != 0 {
1552                    RESTORE_WIENER
1553                } else {
1554                    RESTORE_NONE
1555                }
1556            }
1557            RESTORE_SGRPROJ => {
1558                if dec.symbol(&mut self.cdfs.use_sgrproj, Site::Other)? != 0 {
1559                    RESTORE_SGRPROJ
1560                } else {
1561                    RESTORE_NONE
1562                }
1563            }
1564            RESTORE_SWITCHABLE => dec.symbol(&mut self.cdfs.restoration_type, Site::Other)? as u8,
1565            _ => RESTORE_NONE,
1566        };
1567
1568        let unit_cols = self.lr.get(plane).map_or(0, |p| p.unit_cols);
1569        let idx = unit_row * unit_cols + unit_col;
1570        if let Some(p) = self.lr.get_mut(plane) {
1571            if let Some(t) = p.lr_type.get_mut(idx) {
1572                *t = restoration_type;
1573            }
1574        }
1575
1576        if restoration_type == RESTORE_WIENER {
1577            let coeffs = match self.ref_lr_wiener.get_mut(plane) {
1578                Some(reference) => read_wiener_unit(dec, reference, plane != 0)?,
1579                None => return Ok(()),
1580            };
1581            if let Some(p) = self.lr.get_mut(plane) {
1582                if let Some(slot) = p.wiener.get_mut(idx) {
1583                    *slot = coeffs;
1584                }
1585            }
1586        } else if restoration_type == RESTORE_SGRPROJ {
1587            let (set, xqd) = match self.ref_sgr_xqd.get_mut(plane) {
1588                Some(reference) => read_sgrproj_unit(dec, reference)?,
1589                None => return Ok(()),
1590            };
1591            if let Some(p) = self.lr.get_mut(plane) {
1592                if let Some(s) = p.sgr_set.get_mut(idx) {
1593                    *s = set;
1594                }
1595                if let Some(x) = p.sgr_xqd.get_mut(idx) {
1596                    *x = xqd;
1597                }
1598            }
1599        }
1600        Ok(())
1601    }
1602
1603    fn read_intra_frame_y_mode(
1604        &mut self,
1605        dec: &mut impl TileCoder,
1606        r: usize,
1607        c: usize,
1608        avail_u: bool,
1609        avail_l: bool,
1610    ) -> Result<usize> {
1611        let above = if avail_u {
1612            self.y_mode_at(r.wrapping_sub(1), c)
1613        } else {
1614            DC_PRED
1615        };
1616        let left = if avail_l {
1617            self.y_mode_at(r, c.wrapping_sub(1))
1618        } else {
1619            DC_PRED
1620        };
1621        let a = INTRA_MODE_CONTEXT.get(above).copied().unwrap_or(0);
1622        let l = INTRA_MODE_CONTEXT.get(left).copied().unwrap_or(0);
1623        let cdf_row = get_mut(get_mut(&mut self.cdfs.intra_frame_y_mode, a)?, l)?;
1624        dec.symbol(cdf_row, Site::YMode)
1625    }
1626
1627    /// Read `uv_mode` and, for a chroma-from-luma block, its alphas. Returns the
1628    /// UV mode and `Some((alphaU, alphaV))` when the block is CfL.
1629    fn read_uv_mode(
1630        &mut self,
1631        dec: &mut impl TileCoder,
1632        y_mode: usize,
1633        bw4: usize,
1634        bh4: usize,
1635    ) -> Result<(usize, Option<(i32, i32)>)> {
1636        // `is_cfl_allowed` (§5.11.5). Lossless allows chroma-from-luma only when
1637        // the chroma residual is 4x4 (for 4:4:4 a 4x4 block; for 4:2:0 anything
1638        // up to 8x8). Otherwise CfL is allowed for any block up to
1639        // 32x32 (Block_Width/Height <= 32, i.e. <= 8 mode-info units). Getting
1640        // this wrong picks the other uv_mode CDF — one has the extra CfL symbol,
1641        // the other does not — which desynchronises the whole tile.
1642        let cfl_allowed = if self.lossless {
1643            let block = block_size_index(bw4, bh4);
1644            plane_residual_size(block, self.subsampling_x, self.subsampling_y) == BLOCK_4X4
1645        } else {
1646            bw4 <= 8 && bh4 <= 8
1647        };
1648        let uv = if cfl_allowed {
1649            let cdf_row = get_mut(&mut self.cdfs.uv_cfl_allowed, y_mode)?;
1650            dec.symbol(cdf_row, Site::UvMode)?
1651        } else {
1652            let cdf_row = get_mut(&mut self.cdfs.uv_cfl_not_allowed, y_mode)?;
1653            dec.symbol(cdf_row, Site::UvMode)?
1654        };
1655        let cfl = if uv == UV_CFL_PRED {
1656            Some(self.read_cfl_alphas(dec)?)
1657        } else {
1658            None
1659        };
1660        Ok((uv, cfl))
1661    }
1662
1663    /// `read_cfl_alphas` (§5.11.45): the signed U and V scaling factors.
1664    fn read_cfl_alphas(&mut self, dec: &mut impl TileCoder) -> Result<(i32, i32)> {
1665        let signs = dec.symbol(&mut self.cdfs.cfl_sign, Site::CflSign)? as i32;
1666        let sign_u = (signs + 1) / 3;
1667        let sign_v = (signs + 1) % 3;
1668        // CFL_SIGN_ZERO = 0, CFL_SIGN_NEG = 1, CFL_SIGN_POS = 2.
1669        let alpha_u = if sign_u != 0 {
1670            let ctx = ((sign_u - 1) * 3 + sign_v) as usize;
1671            let mag =
1672                dec.symbol(get_mut(&mut self.cdfs.cfl_alpha, ctx)?, Site::CflAlpha)? as i32 + 1;
1673            if sign_u == 1 { -mag } else { mag }
1674        } else {
1675            0
1676        };
1677        let alpha_v = if sign_v != 0 {
1678            let ctx = ((sign_v - 1) * 3 + sign_u) as usize;
1679            let mag =
1680                dec.symbol(get_mut(&mut self.cdfs.cfl_alpha, ctx)?, Site::CflAlpha)? as i32 + 1;
1681            if sign_v == 1 { -mag } else { mag }
1682        } else {
1683            0
1684        };
1685        Ok((alpha_u, alpha_v))
1686    }
1687
1688    /// `palette_mode_info` (§5.11.46): read the luma and chroma palettes into
1689    /// `palette` (their colours and sizes).
1690    #[allow(
1691        clippy::too_many_arguments,
1692        reason = "mirrors palette_mode_info inputs"
1693    )]
1694    fn read_palette_mode_info(
1695        &mut self,
1696        dec: &mut impl TileCoder,
1697        r: usize,
1698        c: usize,
1699        bw4: usize,
1700        bh4: usize,
1701        y_mode: usize,
1702        uv_mode: usize,
1703        has_chroma: bool,
1704        palette: &mut Palette,
1705    ) -> Result<()> {
1706        // bsizeCtx = Mi_Width_Log2 + Mi_Height_Log2 - 2 (spec §5.11.46); palette
1707        // is only reached for blocks of at least four 4x4 units (log2 sum >= 2),
1708        // so the subtraction never underflows.
1709        let bsize_ctx = (floor_log2_usize(bw4) + floor_log2_usize(bh4)).saturating_sub(2) as usize;
1710        let avail_u = self.avail_u(r);
1711        let avail_l = self.avail_l(c);
1712        if y_mode == DC_PRED {
1713            let ctx = usize::from(avail_u && self.palette_size_at(0, r.wrapping_sub(1), c) > 0)
1714                + usize::from(avail_l && self.palette_size_at(0, r, c.wrapping_sub(1)) > 0);
1715            let cdf_row = get_mut(get_mut(&mut self.cdfs.palette_y_mode, bsize_ctx)?, ctx)?;
1716            if dec.symbol(cdf_row, Site::Other)? != 0 {
1717                let size_cdf = get_mut(&mut self.cdfs.palette_y_size, bsize_ctx)?;
1718                let size = dec.symbol(size_cdf, Site::Other)? + 2;
1719                let cache = self.palette_cache_for(0, r, c);
1720                palette.size_y = size;
1721                palette.colors_y = self.read_palette_colors(dec, size, &cache, true)?;
1722            }
1723        }
1724        if has_chroma && uv_mode == DC_PRED {
1725            let ctx = usize::from(palette.size_y > 0);
1726            let cdf_row = get_mut(&mut self.cdfs.palette_uv_mode, ctx)?;
1727            if dec.symbol(cdf_row, Site::Other)? != 0 {
1728                let size_cdf = get_mut(&mut self.cdfs.palette_uv_size, bsize_ctx)?;
1729                let size = dec.symbol(size_cdf, Site::Other)? + 2;
1730                let cache = self.palette_cache_for(1, r, c);
1731                palette.size_uv = size;
1732                palette.colors_u = self.read_palette_colors(dec, size, &cache, false)?;
1733                palette.colors_v = self.read_palette_colors_v(dec, size)?;
1734            }
1735        }
1736        Ok(())
1737    }
1738
1739    /// The neighbour palette cache for `plane` at `(r, c)` (`get_palette_cache`).
1740    fn palette_cache_for(&self, plane: usize, r: usize, c: usize) -> Vec<u16> {
1741        let above = if self.avail_u(r) && (r * MI_SIZE) % 64 != 0 {
1742            let n = self.palette_size_at(plane, r - 1, c) as usize;
1743            self.palette_colors_at(plane, r - 1, c, n)
1744        } else {
1745            Vec::new()
1746        };
1747        let left = if self.avail_l(c) {
1748            let n = self.palette_size_at(plane, r, c - 1) as usize;
1749            self.palette_colors_at(plane, r, c - 1, n)
1750        } else {
1751            Vec::new()
1752        };
1753        palette_cache(&above, &left)
1754    }
1755
1756    /// Read a palette's colours (`palette_colors_y`/`_u`): cache hits first, then
1757    /// a base colour, then Clip1-accumulated deltas, sorted ascending.
1758    fn read_palette_colors(
1759        &self,
1760        dec: &mut impl TileCoder,
1761        size: usize,
1762        cache: &[u16],
1763        is_luma: bool,
1764    ) -> Result<[u16; PALETTE_COLORS]> {
1765        let bd = u32::from(self.bit_depth);
1766        let max = (1_i32 << bd) - 1;
1767        let clip1 = |v: i32| v.clamp(0, max) as u16;
1768        let mut colors = [0_u16; PALETTE_COLORS];
1769        let mut idx = 0;
1770        for &cached in cache.iter() {
1771            if idx >= size {
1772                break;
1773            }
1774            if dec.literal(1, Site::Other)? != 0 {
1775                set_at(&mut colors, idx, cached);
1776                idx += 1;
1777            }
1778        }
1779        if idx < size {
1780            set_at(&mut colors, idx, dec.literal(bd, Site::Other)? as u16);
1781            idx += 1;
1782        }
1783        if idx < size {
1784            let min_bits = bd.saturating_sub(3);
1785            let mut palette_bits = min_bits + dec.literal(2, Site::Other)?;
1786            while idx < size {
1787                // The luma delta is coded one less than its value; the chroma
1788                // delta is coded directly (spec §5.11.47). The range that bounds
1789                // the next `paletteBits` likewise drops one only for luma.
1790                let delta = dec.literal(palette_bits, Site::Other)? + u32::from(is_luma);
1791                let prev = i32::from(at(&colors, idx - 1));
1792                let color = clip1(prev + delta as i32);
1793                set_at(&mut colors, idx, color);
1794                let range = (1_i32 << bd) - i32::from(color) - i32::from(is_luma);
1795                palette_bits = palette_bits.min(ceil_log2(range.max(0) as u32));
1796                idx += 1;
1797            }
1798        }
1799        let slice = colors.get_mut(..size).unwrap_or(&mut []);
1800        slice.sort_unstable();
1801        Ok(colors)
1802    }
1803
1804    /// Read the V-plane palette colours (`palette_colors_v`), which are coded
1805    /// either as wrapping deltas or as raw literals.
1806    fn read_palette_colors_v(
1807        &self,
1808        dec: &mut impl TileCoder,
1809        size: usize,
1810    ) -> Result<[u16; PALETTE_COLORS]> {
1811        let bd = u32::from(self.bit_depth);
1812        let max = (1_i32 << bd) - 1;
1813        let max_val = 1_i32 << bd;
1814        let mut colors = [0_u16; PALETTE_COLORS];
1815        if dec.literal(1, Site::Other)? != 0 {
1816            let mut palette_bits = bd.saturating_sub(4) + dec.literal(2, Site::Other)?;
1817            set_at(&mut colors, 0, dec.literal(bd, Site::Other)? as u16);
1818            for idx in 1..size {
1819                let mut delta = dec.literal(palette_bits, Site::Other)? as i32;
1820                if delta != 0 && dec.literal(1, Site::Other)? != 0 {
1821                    delta = -delta;
1822                }
1823                let mut val = i32::from(at(&colors, idx - 1)) + delta;
1824                if val < 0 {
1825                    val += max_val;
1826                }
1827                if val >= max_val {
1828                    val -= max_val;
1829                }
1830                set_at(&mut colors, idx, val.clamp(0, max) as u16);
1831                let _ = &mut palette_bits;
1832            }
1833        } else {
1834            for idx in 0..size {
1835                set_at(&mut colors, idx, dec.literal(bd, Site::Other)? as u16);
1836            }
1837        }
1838        Ok(colors)
1839    }
1840
1841    /// `palette_tokens` (§5.11.49): decode the colour-index maps by the
1842    /// wavefront traversal. Only the on-screen part of a block that overhangs the
1843    /// frame edge is coded; the rest of the map replicates its last column/row.
1844    fn read_palette_tokens(
1845        &mut self,
1846        dec: &mut impl TileCoder,
1847        r: usize,
1848        c: usize,
1849        palette: &mut Palette,
1850    ) -> Result<()> {
1851        let (bw, bh) = (palette.block_w, palette.block_h);
1852        let onscreen_w = bw.min((self.mi_cols - c) * MI_SIZE);
1853        let onscreen_h = bh.min((self.mi_rows - r) * MI_SIZE);
1854        if palette.size_y > 0 {
1855            let dims = MapDims {
1856                w: bw,
1857                h: bh,
1858                onscreen_w,
1859                onscreen_h,
1860            };
1861            palette.map_y = self.read_color_map(dec, palette.size_y, dims, false)?;
1862        }
1863        if palette.size_uv > 0 {
1864            // The chroma map is the subsampled block, widened by 2 where that
1865            // leaves it under 4 samples (a 4xN luma block in 4:2:0).
1866            let (sub_x, sub_y) = (self.subsampling_x, self.subsampling_y);
1867            let mut dims = MapDims {
1868                w: bw >> sub_x,
1869                h: bh >> sub_y,
1870                onscreen_w: onscreen_w >> sub_x,
1871                onscreen_h: onscreen_h >> sub_y,
1872            };
1873            if dims.w < 4 {
1874                dims.w += 2;
1875                dims.onscreen_w += 2;
1876            }
1877            if dims.h < 4 {
1878                dims.h += 2;
1879                dims.onscreen_h += 2;
1880            }
1881            palette.uv_w = dims.w;
1882            palette.map_uv = self.read_color_map(dec, palette.size_uv, dims, true)?;
1883        }
1884        Ok(())
1885    }
1886
1887    /// Decode one colour-index map (`ColorMapY`/`ColorMapUV`) of `dims`.
1888    fn read_color_map(
1889        &mut self,
1890        dec: &mut impl TileCoder,
1891        size: usize,
1892        dims: MapDims,
1893        chroma: bool,
1894    ) -> Result<Vec<u8>> {
1895        let (bw, bh) = (dims.onscreen_w, dims.onscreen_h);
1896        let stride = dims.w;
1897        let mut map = vec![0_u8; dims.w * dims.h];
1898        let first = dec.ns(size as u32)? as u8;
1899        if let Some(m) = map.first_mut() {
1900            *m = first;
1901        }
1902        let get =
1903            |map: &[u8], i: usize, j: usize| -> Option<u8> { map.get(i * stride + j).copied() };
1904        for i in 1..(bh + bw - 1) {
1905            let j_hi = i.min(bw - 1);
1906            let j_lo = i.saturating_sub(bh - 1);
1907            let mut j = j_hi as isize;
1908            while j >= j_lo as isize {
1909                let jj = j as usize;
1910                let row = i - jj;
1911                let left = if jj > 0 { get(&map, row, jj - 1) } else { None };
1912                let above_left = if row > 0 && jj > 0 {
1913                    get(&map, row - 1, jj - 1)
1914                } else {
1915                    None
1916                };
1917                let above = if row > 0 {
1918                    get(&map, row - 1, jj)
1919                } else {
1920                    None
1921                };
1922                let (order, ctx) = color_context(left, above_left, above, size);
1923                let cdf = if chroma {
1924                    self.cdfs.palette_uv_color.row(size, ctx)?
1925                } else {
1926                    self.cdfs.palette_y_color.row(size, ctx)?
1927                };
1928                let sym = dec.symbol(cdf, Site::Other)?;
1929                let color = order.get(sym).copied().unwrap_or(0);
1930                if let Some(slot) = map.get_mut(row * stride + jj) {
1931                    *slot = color;
1932                }
1933                j -= 1;
1934            }
1935        }
1936        // Replicate the last on-screen column rightward, then the last on-screen
1937        // row downward, over the part of the block past the frame edge.
1938        for i in 0..bh {
1939            let last = get(&map, i, bw - 1).unwrap_or(0);
1940            for j in bw..dims.w {
1941                if let Some(slot) = map.get_mut(i * stride + j) {
1942                    *slot = last;
1943                }
1944            }
1945        }
1946        for i in bh..dims.h {
1947            for j in 0..dims.w {
1948                let v = get(&map, bh - 1, j).unwrap_or(0);
1949                if let Some(slot) = map.get_mut(i * stride + j) {
1950                    *slot = v;
1951                }
1952            }
1953        }
1954        Ok(map)
1955    }
1956
1957    /// Read `angle_delta` for a directional mode on a `MiSize >= BLOCK_8X8`
1958    /// block, returning the signed delta (`angle_delta - MAX_ANGLE_DELTA`). Zero
1959    /// for non-directional modes and small blocks, which read nothing.
1960    fn read_angle_delta(
1961        &mut self,
1962        dec: &mut impl TileCoder,
1963        mode: usize,
1964        bw4: usize,
1965        bh4: usize,
1966        site: Site,
1967    ) -> Result<i32> {
1968        let directional = (1..=8).contains(&mode);
1969        if directional && at_least_block_8x8(bw4, bh4) {
1970            let index = mode - 1;
1971            let cdf_row = get_mut(&mut self.cdfs.angle_delta, index)?;
1972            let symbol = dec.symbol(cdf_row, site)? as i32;
1973            return Ok(symbol - MAX_ANGLE_DELTA);
1974        }
1975        Ok(0)
1976    }
1977
1978    /// `filter_intra_mode_info` (§5.11.10): whether luma uses recursive
1979    /// filter-intra, and if so which of the five kernels.
1980    fn read_filter_intra(
1981        &mut self,
1982        dec: &mut impl TileCoder,
1983        y_mode: usize,
1984        bw4: usize,
1985        bh4: usize,
1986    ) -> Result<Option<usize>> {
1987        let max_dim = bw4.max(bh4) * MI_SIZE;
1988        if self.enable_filter_intra && y_mode == DC_PRED && max_dim <= 32 {
1989            let size = block_size_index(bw4, bh4);
1990            let cdf_row = get_mut(&mut self.cdfs.filter_intra, size)?;
1991            if dec.symbol(cdf_row, Site::Other)? != 0 {
1992                let mode = dec.symbol(&mut self.cdfs.filter_intra_mode, Site::Other)?;
1993                return Ok(Some(mode));
1994            }
1995        }
1996        Ok(None)
1997    }
1998
1999    /// `read_block_tx_size` (§5.11.16) for an intra block: resolve the luma
2000    /// transform size and record it across the block's 4x4 units for the
2001    /// `tx_depth` neighbour context. Intra frames have `is_inter == 0`, so the
2002    /// selection gate is always open.
2003    fn read_block_tx_size(
2004        &mut self,
2005        dec: &mut impl TileCoder,
2006        r: usize,
2007        c: usize,
2008        bw4: usize,
2009        bh4: usize,
2010        _skip: bool,
2011    ) -> Result<TxSize> {
2012        let above_w = if self.avail_u(r) {
2013            self.tx_width_at(r - 1, c)
2014        } else {
2015            0
2016        };
2017        let left_h = if self.avail_l(c) {
2018            self.tx_height_at(r, c - 1)
2019        } else {
2020            0
2021        };
2022        let params = TxSizeParams {
2023            block: block_size_index(bw4, bh4),
2024            tx_mode_select: self.tx_mode == TxMode::Select,
2025            lossless: self.lossless,
2026            allow_select: true,
2027            above_w,
2028            left_h,
2029        };
2030        let tx = code_tx_size(dec, &mut self.cdfs.tx_depth, &params)?;
2031        for y in r..(r + bh4).min(self.mi_rows) {
2032            for x in c..(c + bw4).min(self.mi_cols) {
2033                if let Some(v) = self.tx_sizes.get_mut(y * self.mi_cols + x) {
2034                    *v = tx as u8;
2035                }
2036            }
2037        }
2038        Ok(tx)
2039    }
2040
2041    /// `Tx_Width[InterTxSizes[r][c]]`: the stored luma transform width at a unit.
2042    fn tx_width_at(&self, r: usize, c: usize) -> usize {
2043        self.tx_sizes
2044            .get(r * self.mi_cols + c)
2045            .map_or(0, |&i| TxSize::from_index(usize::from(i)).width())
2046    }
2047
2048    /// `Tx_Height[InterTxSizes[r][c]]`: the stored luma transform height.
2049    fn tx_height_at(&self, r: usize, c: usize) -> usize {
2050        self.tx_sizes
2051            .get(r * self.mi_cols + c)
2052            .map_or(0, |&i| TxSize::from_index(usize::from(i)).height())
2053    }
2054
2055    /// `residual` (§5.11.34): every transform block of every plane the block
2056    /// codes. A block wider or taller than 64 is walked in 64x64 chunks, each
2057    /// chunk doing all its planes before the next; each plane steps its own
2058    /// (subsampled) residual block in that plane's transform size.
2059    fn residual(
2060        &mut self,
2061        dec: &mut impl TileCoder,
2062        modes: &BlockModes,
2063        bw4: usize,
2064        bh4: usize,
2065        skip: bool,
2066        has_chroma: bool,
2067    ) -> Result<()> {
2068        let planes = if has_chroma { self.num_planes } else { 1 };
2069        let block = block_size_index(bw4, bh4);
2070        let width_chunks = (bw4 / 16).max(1);
2071        let height_chunks = (bh4 / 16).max(1);
2072        let chunk_size = if width_chunks > 1 || height_chunks > 1 {
2073            BLOCK_64X64
2074        } else {
2075            block
2076        };
2077        for chunk_y in 0..height_chunks {
2078            for chunk_x in 0..width_chunks {
2079                for plane in 0..planes {
2080                    self.residual_plane(
2081                        dec,
2082                        modes,
2083                        block,
2084                        chunk_size,
2085                        (chunk_x, chunk_y),
2086                        plane,
2087                        skip,
2088                    )?;
2089                }
2090            }
2091        }
2092        Ok(())
2093    }
2094
2095    /// One plane of one `residual` chunk (§5.11.34).
2096    #[allow(clippy::too_many_arguments, reason = "mirrors the residual loop state")]
2097    fn residual_plane(
2098        &mut self,
2099        dec: &mut impl TileCoder,
2100        modes: &BlockModes,
2101        block: usize,
2102        chunk_size: usize,
2103        (chunk_x, chunk_y): (usize, usize),
2104        plane: usize,
2105        skip: bool,
2106    ) -> Result<()> {
2107        let (sub_x, sub_y) = self.plane_subsampling(plane);
2108        // A CfL chroma block predicts from DC and then adds the scaled luma.
2109        let cfl_alpha = match plane {
2110            1 => modes.cfl.map(|(u, _)| u),
2111            2 => modes.cfl.map(|(_, v)| v),
2112            _ => None,
2113        };
2114        let mode = if plane == 0 {
2115            modes.y_mode
2116        } else if modes.cfl.is_some() {
2117            DC_PRED
2118        } else {
2119            modes.uv_mode
2120        };
2121        let delta = if plane == 0 {
2122            modes.y_delta
2123        } else {
2124            modes.uv_delta
2125        };
2126        let intra = IntraMode::from_index(mode as u8)
2127            .ok_or_else(|| PixelsError::malformed("avif", "intra mode index out of range"))?;
2128        let filter_type = self.filter_type(modes, plane);
2129        // The block's residual size on this plane (for the coefficient
2130        // contexts), and the chunk's (for the loop bounds).
2131        let plane_block = plane_residual_size(block, sub_x, sub_y);
2132        if plane_block == BLOCK_INVALID {
2133            return Err(PixelsError::malformed(
2134                "avif",
2135                "a block shape the chroma subsampling cannot represent",
2136            ));
2137        }
2138        let (plane_bw4, plane_bh4) = block_4x4_dims(plane_block);
2139        let (num_w, num_h) = block_4x4_dims(plane_residual_size(chunk_size, sub_x, sub_y));
2140        // The plane's transform size: lossless forces TX_4X4 for every plane (the
2141        // WHT is 4x4 only); otherwise luma is the read block size and chroma is
2142        // its residual block's largest rectangular transform (`get_tx_size`).
2143        let tx_size = if self.lossless {
2144            TxSize::Tx4x4
2145        } else if plane == 0 {
2146            modes.luma_tx_size
2147        } else {
2148            chroma_tx_size(plane_block)
2149        };
2150        let step_x = (tx_size.width() / MI_SIZE).max(1);
2151        let step_y = (tx_size.height() / MI_SIZE).max(1);
2152        // Everything below is in this plane's sample grid.
2153        let base_x = (modes.c >> sub_x) * MI_SIZE;
2154        let base_y = (modes.r >> sub_y) * MI_SIZE;
2155        let palette = self.palette_view_for(&modes.palette, plane, base_x, base_y);
2156        let (avail_l, avail_u) = if plane == 0 {
2157            (modes.avail_l, modes.avail_u)
2158        } else {
2159            (modes.avail_l_chroma, modes.avail_u_chroma)
2160        };
2161        let mut y = 0;
2162        while y < num_h {
2163            let mut x = 0;
2164            while x < num_w {
2165                // Offset of this transform block within the whole block, in the
2166                // plane's 4x4 units.
2167                let bx = x + ((chunk_x << 4) >> sub_x);
2168                let by = y + ((chunk_y << 4) >> sub_y);
2169                let tb = TxBlock {
2170                    plane,
2171                    x: base_x + bx * MI_SIZE,
2172                    y: base_y + by * MI_SIZE,
2173                    tx_size,
2174                    mode: intra,
2175                    mode_index: mode,
2176                    angle_delta: delta,
2177                    have_left: avail_l || bx > 0,
2178                    have_above: avail_u || by > 0,
2179                    filter_type,
2180                    // Filter-intra is a luma-only tool.
2181                    filter_intra: if plane == 0 { modes.filter_intra } else { None },
2182                    cfl_alpha,
2183                    palette,
2184                    skip,
2185                    plane_bw4,
2186                    plane_bh4,
2187                };
2188                self.transform_block(dec, &tb)?;
2189                x += step_x;
2190            }
2191            y += step_y;
2192        }
2193        Ok(())
2194    }
2195
2196    /// `(maxX, maxY)` for intra edge fetches on `plane` (§7.11.2): the last
2197    /// sample column and row of the decoded (mode-info) area in that plane. The
2198    /// plane buffers extend further, to whole superblocks.
2199    fn frame_bounds(&self, plane: usize) -> (usize, usize) {
2200        let (sub_x, sub_y) = self.plane_subsampling(plane);
2201        (
2202            ((self.mi_cols * MI_SIZE) >> sub_x) - 1,
2203            ((self.mi_rows * MI_SIZE) >> sub_y) - 1,
2204        )
2205    }
2206
2207    /// `(sbMask >> subX, sbMask >> subY)`: the superblock masks in `plane`'s
2208    /// 4x4 units along each axis, which index `BlockDecoded[plane]`.
2209    fn plane_sb_mask_xy(&self, plane: usize) -> (isize, isize) {
2210        let (sub_x, sub_y) = self.plane_subsampling(plane);
2211        let mask = self.sb_size4 - 1;
2212        ((mask >> sub_x) as isize, (mask >> sub_y) as isize)
2213    }
2214
2215    /// `(subsampling_x, subsampling_y)` for `plane`: zero for luma.
2216    fn plane_subsampling(&self, plane: usize) -> (usize, usize) {
2217        if plane == 0 {
2218            (0, 0)
2219        } else {
2220            (self.subsampling_x, self.subsampling_y)
2221        }
2222    }
2223
2224    /// Build the palette view for `plane` if that plane is palette-coded.
2225    fn palette_view_for<'a>(
2226        &self,
2227        palette: &'a Palette,
2228        plane: usize,
2229        base_x: usize,
2230        base_y: usize,
2231    ) -> Option<PaletteView<'a>> {
2232        if plane == 0 && palette.size_y > 0 {
2233            Some(PaletteView {
2234                map: &palette.map_y,
2235                colors: &palette.colors_y,
2236                block_w: palette.block_w,
2237                base_x,
2238                base_y,
2239            })
2240        } else if plane == 1 && palette.size_uv > 0 {
2241            Some(PaletteView {
2242                map: &palette.map_uv,
2243                colors: &palette.colors_u,
2244                block_w: palette.uv_w,
2245                base_x,
2246                base_y,
2247            })
2248        } else if plane == 2 && palette.size_uv > 0 {
2249            Some(PaletteView {
2250                map: &palette.map_uv,
2251                colors: &palette.colors_v,
2252                block_w: palette.uv_w,
2253                base_x,
2254                base_y,
2255            })
2256        } else {
2257            None
2258        }
2259    }
2260
2261    /// `get_filter_type` (§7.11.2.8): whether the above or left neighbour block
2262    /// used a smooth mode, which softens the directional edge filter. On a
2263    /// subsampled chroma plane the neighbour is looked up at the unit that owns
2264    /// the co-located chroma (the odd column/row of each pair).
2265    /// What intra `mode` would predict for the whole of plane `plane` of the
2266    /// block at `(r, c)`, as the decoder will predict its first transform
2267    /// block — exact for an encoder that codes every block of 8x8 or more
2268    /// with one transform. `None` for a mode or shape this cannot preview.
2269    pub(crate) fn preview_prediction(
2270        &self,
2271        r: usize,
2272        c: usize,
2273        bw4: usize,
2274        bh4: usize,
2275        plane: usize,
2276        mode: usize,
2277    ) -> Option<Vec<u16>> {
2278        let (avail_u, avail_l) = (self.avail_u(r), self.avail_l(c));
2279        let modes = BlockModes {
2280            r,
2281            c,
2282            avail_u,
2283            avail_l,
2284            avail_u_chroma: avail_u,
2285            avail_l_chroma: avail_l,
2286            y_mode: mode,
2287            uv_mode: mode,
2288            y_delta: 0,
2289            uv_delta: 0,
2290            filter_intra: None,
2291            cfl: None,
2292            palette: Palette::default(),
2293            luma_tx_size: TxSize::Tx4x4,
2294        };
2295        let (sub_x, sub_y) = self.plane_subsampling(plane);
2296        let block = block_size_index(bw4, bh4);
2297        let plane_block = plane_residual_size(block, sub_x, sub_y);
2298        if plane_block == BLOCK_INVALID {
2299            return None;
2300        }
2301        let (plane_bw4, plane_bh4) = block_4x4_dims(plane_block);
2302        let tx_size = if plane == 0 {
2303            max_tx_size_rect(block)
2304        } else {
2305            chroma_tx_size(plane_block)
2306        };
2307        let tb = TxBlock {
2308            plane,
2309            x: (c >> sub_x) * MI_SIZE,
2310            y: (r >> sub_y) * MI_SIZE,
2311            tx_size,
2312            mode: IntraMode::from_index(mode as u8)?,
2313            mode_index: mode,
2314            angle_delta: 0,
2315            have_left: avail_l,
2316            have_above: avail_u,
2317            filter_type: self.filter_type(&modes, plane),
2318            filter_intra: None,
2319            cfl_alpha: None,
2320            palette: None,
2321            skip: false,
2322            plane_bw4,
2323            plane_bh4,
2324        };
2325        self.predict(&tb, tx_size.width(), tx_size.height()).ok()
2326    }
2327
2328    /// The decoder state for `seq` and `frame`, ready to code tiles into.
2329    pub(crate) fn for_frame(seq: &SequenceHeader, frame: &FrameHeader) -> Result<Self> {
2330        Self::new(seq, frame)
2331    }
2332
2333    /// `MiRows` and `MiCols`.
2334    pub(crate) const fn mi_dims(&self) -> (usize, usize) {
2335        (self.mi_rows, self.mi_cols)
2336    }
2337
2338    fn filter_type(&self, modes: &BlockModes, plane: usize) -> bool {
2339        let is_smooth = |mode: usize| (9..=11).contains(&mode);
2340        let smooth_at = |r: usize, c: usize| {
2341            is_smooth(if plane == 0 {
2342                self.y_mode_at(r, c)
2343            } else {
2344                self.uv_mode_at(r, c)
2345            })
2346        };
2347        let (sub_x, sub_y) = self.plane_subsampling(plane);
2348        let (r, c) = (modes.r, modes.c);
2349        let (avail_u, avail_l) = if plane == 0 {
2350            (modes.avail_u, modes.avail_l)
2351        } else {
2352            (modes.avail_u_chroma, modes.avail_l_chroma)
2353        };
2354        let above = avail_u && {
2355            let row = r - 1 - usize::from(sub_y == 1 && r & 1 == 1);
2356            let col = c + usize::from(sub_x == 1 && c & 1 == 0);
2357            smooth_at(row, col)
2358        };
2359        let left = avail_l && {
2360            let row = r + usize::from(sub_y == 1 && r & 1 == 0);
2361            let col = c - 1 - usize::from(sub_x == 1 && c & 1 == 1);
2362            smooth_at(row, col)
2363        };
2364        above || left
2365    }
2366
2367    fn transform_block(&mut self, dec: &mut impl TileCoder, tb: &TxBlock) -> Result<()> {
2368        let (plane, x, y, tx_size, skip) = (tb.plane, tb.x, tb.y, tb.tx_size, tb.skip);
2369        let w = tx_size.width();
2370        let h = tx_size.height();
2371        let w4 = (w / MI_SIZE).max(1);
2372        let h4 = (h / MI_SIZE).max(1);
2373
2374        // `x`/`y` are in this plane's sample grid. A transform block whose
2375        // top-left lies outside the (plane's) frame is not coded: the block may
2376        // extend past the right or bottom edge, but only the tx blocks that
2377        // start inside it read symbols (spec §5.11.35). Skipping this
2378        // desynchronises every symbol after the edge — for a last-region block
2379        // that surfaces as wrong chroma while the luma before it stays correct.
2380        let (sub_x, sub_y) = self.plane_subsampling(plane);
2381        let max_x = (self.mi_cols * MI_SIZE) >> sub_x;
2382        let max_y = (self.mi_rows * MI_SIZE) >> sub_y;
2383        if x >= max_x || y >= max_y {
2384            return Ok(());
2385        }
2386
2387        // Predict from the reconstructed neighbours.
2388        let prediction = self.predict(tb, w, h)?;
2389        if plane == 0 {
2390            // MaxLumaW/MaxLumaH: how far this block's luma reaches, which bounds
2391            // the luma a chroma-from-luma prediction may read.
2392            self.max_luma_w = x + w;
2393            self.max_luma_h = y + h;
2394        }
2395
2396        let x4 = x / MI_SIZE;
2397        let y4 = y / MI_SIZE;
2398        let final_block = if skip {
2399            prediction
2400        } else {
2401            let all_zero_ctx =
2402                self.all_zero_ctx(plane, x4, y4, w4, h4, tx_size, tb.plane_bw4, tb.plane_bh4);
2403            let dc_sign_ctx = self.dc_sign_ctx(plane, x4, y4, w4, h4);
2404            let ptype = usize::from(plane > 0);
2405            // Resolve the block's PlaneTxType inside decode_coeffs, at the spec
2406            // position between all_zero and eob_pt. Luma reads the intra_tx_type
2407            // symbol against these contexts; chroma derives from its mode. When
2408            // qindex is 0 (lossless) no symbol is read and the type is DCT_DCT.
2409            let tx_set = intra_tx_set(tx_size, self.reduced_tx_set);
2410            let dir = if plane == 0 {
2411                intra_dir(tb.mode_index, tb.filter_intra)
2412            } else {
2413                0
2414            };
2415            // transform_type (§5.11.47) gates the luma symbol on the segment's
2416            // quantizer before any delta_q.
2417            let qindex_positive = self.segment_qindex(true) > 0;
2418            // The block's quantizers, which an encoder needs before it codes
2419            // the coefficients and the decoder after.
2420            let qindex = self.segment_qindex(false);
2421            let dc = dc_q(
2422                self.bit_depth,
2423                qindex + self.q_dc.get(plane).copied().unwrap_or(0),
2424            );
2425            let ac = ac_q(
2426                self.bit_depth,
2427                qindex + self.q_ac.get(plane).copied().unwrap_or(0),
2428            );
2429            let tx_ctx = TxTypeCtx {
2430                set: tx_set,
2431                intra_cdfs: &mut self.cdfs.intra_tx_type,
2432                intra_dir: dir,
2433                uv_mode: tb.mode_index,
2434                qindex_positive,
2435                lossless: self.lossless,
2436            };
2437            let job = CoeffJob {
2438                plane,
2439                x,
2440                y,
2441                prediction: &prediction,
2442                dc_q: dc,
2443                ac_q: ac,
2444            };
2445            let block = dec.coeffs(
2446                &mut self.cdfs.coeff,
2447                tx_size,
2448                tx_ctx,
2449                ptype,
2450                all_zero_ctx,
2451                dc_sign_ctx,
2452                &job,
2453            )?;
2454            self.update_level_context(plane, x4, y4, w4, h4, block.cul_level, block.dc_category);
2455            if block.eob > 0 {
2456                // §7.12.3 step 1b: a matrix weights only the 2D transforms
2457                // (types before IDTX), and level 15 means none.
2458                // SegQMLevel (§5.9.12): 15, meaning none, for a lossless segment.
2459                let level = if self.using_qmatrix && !self.lossless {
2460                    self.qm.get(plane).copied().unwrap_or(15)
2461                } else {
2462                    15
2463                };
2464                let matrix = ((block.tx_type as usize) < TxType::Idtx as usize)
2465                    .then(|| quantizer_matrix(level, plane > 0, tx_size))
2466                    .flatten();
2467                let dequant =
2468                    dequantize_with_matrix(&block.quant, tx_size, dc, ac, matrix, self.bit_depth);
2469                let residual = inverse_transform_2d(
2470                    &dequant,
2471                    tx_size,
2472                    block.tx_type,
2473                    self.lossless,
2474                    self.bit_depth,
2475                );
2476                add_residual(&prediction, &residual, block.tx_type, self.bit_depth)
2477            } else {
2478                prediction
2479            }
2480        };
2481
2482        if let Some(p) = self.planes.get_mut(plane) {
2483            for i in 0..h {
2484                for j in 0..w {
2485                    let value = final_block.get(i * w + j).copied().unwrap_or(0);
2486                    p.set(x + j, y + i, value);
2487                }
2488            }
2489        }
2490
2491        // Mark the tx block's 4x4 units decoded for the neighbour tests, and
2492        // record its transform size per unit for the deblocking loop filter —
2493        // both in the plane's 4x4 units.
2494        let (mask_x, mask_y) = self.plane_sb_mask_xy(plane);
2495        let tx_index = tx_size as u8;
2496        for dy in 0..h4 {
2497            for dx in 0..w4 {
2498                let sub_row = ((y4 + dy) as isize) & mask_y;
2499                let sub_col = ((x4 + dx) as isize) & mask_x;
2500                self.set_block_decoded(plane, sub_row, sub_col);
2501                if let Some(grid) = self.lf_tx_sizes.get_mut(plane) {
2502                    if let Some(cell) = grid.get_mut((y4 + dy) * self.mi_cols + (x4 + dx)) {
2503                        *cell = tx_index;
2504                    }
2505                }
2506            }
2507        }
2508        Ok(())
2509    }
2510
2511    /// Predict a `w` by `h` transform block: directional modes go through the
2512    /// edge machinery, the rest through the pure non-directional predictors. The
2513    /// result is `w * h` samples in row-major order.
2514    fn predict(&self, tb: &TxBlock, w: usize, h: usize) -> Result<Vec<u16>> {
2515        if let Some(pv) = tb.palette {
2516            // predict_palette (§7.11.4): each sample is the palette colour its
2517            // index map selects. The map is block-relative.
2518            let mut pred = vec![0_u16; w * h];
2519            for i in 0..h {
2520                for j in 0..w {
2521                    let my = (tb.y + i).saturating_sub(pv.base_y);
2522                    let mx = (tb.x + j).saturating_sub(pv.base_x);
2523                    let idx = pv.map.get(my * pv.block_w + mx).copied().unwrap_or(0);
2524                    if let Some(cell) = pred.get_mut(i * w + j) {
2525                        *cell = pv.colors.get(usize::from(idx)).copied().unwrap_or(0);
2526                    }
2527                }
2528            }
2529            return Ok(pred);
2530        }
2531        if let Some(filter_mode) = tb.filter_intra {
2532            let (above, left) = self.gather_edges(tb, w, h);
2533            return Ok(predict_filter_intra(
2534                filter_mode,
2535                &above,
2536                &left,
2537                w,
2538                h,
2539                self.bit_depth,
2540            ));
2541        }
2542        if let Some(base_angle) = mode_base_angle(tb.mode_index) {
2543            let p_angle = base_angle + tb.angle_delta * ANGLE_STEP;
2544            if p_angle != 90 && p_angle != 180 {
2545                return Ok(self.predict_directional(tb, p_angle, w, h));
2546            }
2547        }
2548        let (above, left, corner, have_above, have_left) =
2549            self.gather_neighbours(tb.plane, tb.x, tb.y, tb.have_left, tb.have_above, w, h);
2550        let block = PredBlock {
2551            above: &above,
2552            left: &left,
2553            corner,
2554            have_above,
2555            have_left,
2556            w,
2557            h,
2558        };
2559        let mut pred = predict_intra_block(tb.mode, &block, self.bit_depth)?;
2560        if let Some(alpha) = tb.cfl_alpha {
2561            self.apply_cfl(&mut pred, tb.x, tb.y, w, h, alpha);
2562        }
2563        Ok(pred)
2564    }
2565
2566    /// `predict_chroma_from_luma` (§7.11.5) for a `w` by `h` chroma block at
2567    /// chroma `(x, y)`: add the alpha-scaled, DC-removed reconstructed luma to
2568    /// the DC chroma prediction. With subsampling each chroma sample averages
2569    /// its 2 (4:2:2) or 4 (4:2:0) co-located luma samples, clamped to the luma
2570    /// the block actually reconstructed (`MaxLumaW`/`MaxLumaH`).
2571    fn apply_cfl(&self, pred: &mut [u16], x: usize, y: usize, w: usize, h: usize, alpha: i32) {
2572        let max = (1_i32 << self.bit_depth) - 1;
2573        let luma = self.planes.first();
2574        let (sub_x, sub_y) = (self.subsampling_x, self.subsampling_y);
2575        let luma_at =
2576            |lx: usize, ly: usize| i32::from(luma.and_then(|p| p.get(lx, ly)).unwrap_or(0));
2577        // L holds the (subsampled) co-located luma with 3 fractional bits.
2578        let mut l = vec![0_i32; w * h];
2579        let mut sum = 0_i32;
2580        for i in 0..h {
2581            let luma_y = ((y + i) << sub_y).min(self.max_luma_h.saturating_sub(1 << sub_y));
2582            for j in 0..w {
2583                let luma_x = ((x + j) << sub_x).min(self.max_luma_w.saturating_sub(1 << sub_x));
2584                let mut t = 0;
2585                for dy in 0..=sub_y {
2586                    for dx in 0..=sub_x {
2587                        t += luma_at(luma_x + dx, luma_y + dy);
2588                    }
2589                }
2590                let v = t << (3 - sub_x - sub_y);
2591                if let Some(cell) = l.get_mut(i * w + j) {
2592                    *cell = v;
2593                }
2594                sum += v;
2595            }
2596        }
2597        // lumaAvg = Round2(sum, log2W + log2H).
2598        let shift = w.trailing_zeros() + h.trailing_zeros();
2599        let luma_avg = (sum + (1 << (shift - 1))) >> shift;
2600        for i in 0..h {
2601            for j in 0..w {
2602                let ac = l.get(i * w + j).copied().unwrap_or(0) - luma_avg;
2603                let scaled = round2_signed(alpha * ac, 6);
2604                if let Some(cell) = pred.get_mut(i * w + j) {
2605                    *cell = (i32::from(*cell) + scaled).clamp(0, max) as u16;
2606                }
2607            }
2608        }
2609    }
2610
2611    /// Build the extended `AboveRow`/`LeftCol` edge arrays for a `w` by `h`
2612    /// block (§7.11.2 general), with the `haveAboveRight`/`haveBelowLeft`
2613    /// extension from `BlockDecoded`. The same edge serves every prediction mode;
2614    /// the non-directional modes simply never read past index `w`/`h`.
2615    fn gather_edges(&self, tb: &TxBlock, w: usize, h: usize) -> (Edge, Edge) {
2616        let (plane, x, y) = (tb.plane, tb.x, tb.y);
2617        let mid = 1_i32 << (self.bit_depth - 1);
2618        let p = self.planes.get(plane);
2619        let at =
2620            |px: usize, py: usize| -> i32 { p.and_then(|pl| pl.get(px, py)).map_or(0, i32::from) };
2621        let (max_x, max_y) = self.frame_bounds(plane);
2622
2623        let x4 = x / MI_SIZE;
2624        let y4 = y / MI_SIZE;
2625        let w4 = (w / MI_SIZE).max(1);
2626        let h4 = (h / MI_SIZE).max(1);
2627        let (mask_x, mask_y) = self.plane_sb_mask_xy(plane);
2628        let sub_row = (y4 as isize) & mask_y;
2629        let sub_col = (x4 as isize) & mask_x;
2630        let have_above_right = self.block_decoded_at(plane, sub_row - 1, sub_col + w4 as isize);
2631        let have_below_left = self.block_decoded_at(plane, sub_row + h4 as isize, sub_col - 1);
2632
2633        let mut above = Edge::new();
2634        let mut left = Edge::new();
2635        let num = (w + h) as isize;
2636        // AboveRow[0..w+h-1].
2637        if tb.have_above {
2638            let extent = if have_above_right { 2 * w } else { w };
2639            let above_limit = (x + extent - 1).min(max_x);
2640            for i in 0..num {
2641                above.set(i, at((x + i as usize).min(above_limit), y.wrapping_sub(1)));
2642            }
2643        } else if tb.have_left {
2644            let v = at(x.wrapping_sub(1), y);
2645            for i in 0..num {
2646                above.set(i, v);
2647            }
2648        } else {
2649            for i in 0..num {
2650                above.set(i, mid - 1);
2651            }
2652        }
2653        // LeftCol[0..w+h-1].
2654        if tb.have_left {
2655            let extent = if have_below_left { 2 * h } else { h };
2656            let left_limit = (y + extent - 1).min(max_y);
2657            for i in 0..num {
2658                left.set(i, at(x.wrapping_sub(1), (y + i as usize).min(left_limit)));
2659            }
2660        } else if tb.have_above {
2661            let v = at(x, y.wrapping_sub(1));
2662            for i in 0..num {
2663                left.set(i, v);
2664            }
2665        } else {
2666            for i in 0..num {
2667                left.set(i, mid + 1);
2668            }
2669        }
2670        let corner = match (tb.have_above, tb.have_left) {
2671            (true, true) => at(x.wrapping_sub(1), y.wrapping_sub(1)),
2672            (true, false) => at(x, y.wrapping_sub(1)),
2673            (false, true) => at(x.wrapping_sub(1), y),
2674            (false, false) => mid,
2675        };
2676        above.set(-1, corner);
2677        left.set(-1, corner);
2678        (above, left)
2679    }
2680
2681    /// Slanted directional prediction (§7.11.2.4) at any size, returned as `w * h`
2682    /// row-major samples.
2683    fn predict_directional(&self, tb: &TxBlock, p_angle: i32, w: usize, h: usize) -> Vec<u16> {
2684        let (x, y) = (tb.x, tb.y);
2685        let (mut above, mut left) = self.gather_edges(tb, w, h);
2686        let (max_x, max_y) = self.frame_bounds(tb.plane);
2687        let avail_above_px = (max_x as i32) - (x as i32) + 1;
2688        let avail_left_px = (max_y as i32) - (y as i32) + 1;
2689        predict_directional(
2690            p_angle,
2691            &mut above,
2692            &mut left,
2693            w,
2694            h,
2695            tb.have_left,
2696            tb.have_above,
2697            tb.filter_type,
2698            self.enable_edge_filter,
2699            avail_above_px,
2700            avail_left_px,
2701            self.bit_depth,
2702        )
2703    }
2704
2705    /// Extract the `AboveRow[0..w]`, `LeftCol[0..h]` and corner from the edge
2706    /// arrays as the non-directional predictors consume them.
2707    #[allow(clippy::too_many_arguments, reason = "mirrors the §7.11.2 edge inputs")]
2708    fn gather_neighbours(
2709        &self,
2710        plane: usize,
2711        x: usize,
2712        y: usize,
2713        have_left: bool,
2714        have_above: bool,
2715        w: usize,
2716        h: usize,
2717    ) -> (Vec<i32>, Vec<i32>, i32, bool, bool) {
2718        let tb = TxBlock {
2719            plane,
2720            x,
2721            y,
2722            tx_size: TxSize::Tx4x4,
2723            mode: IntraMode::Dc,
2724            mode_index: 0,
2725            angle_delta: 0,
2726            have_left,
2727            have_above,
2728            filter_type: false,
2729            filter_intra: None,
2730            cfl_alpha: None,
2731            palette: None,
2732            skip: false,
2733            plane_bw4: 0,
2734            plane_bh4: 0,
2735        };
2736        let (above_edge, left_edge) = self.gather_edges(&tb, w, h);
2737        let above: Vec<i32> = (0..w as isize).map(|j| above_edge.get(j)).collect();
2738        let left: Vec<i32> = (0..h as isize).map(|i| left_edge.get(i)).collect();
2739        (above, left, above_edge.get(-1), have_above, have_left)
2740    }
2741
2742    /// `all_zero` context (§8.3.2) for a `w4` by `h4` (4x4 units) transform block.
2743    /// A block whose coding size equals the transform is context 0 for luma;
2744    /// otherwise the neighbour level contexts over the block's span select it.
2745    /// `bw4`/`bh4` are the coding block's residual size on this plane, in the
2746    /// plane's 4x4 units.
2747    #[allow(clippy::too_many_arguments, reason = "mirrors the §8.3.2 ctx inputs")]
2748    fn all_zero_ctx(
2749        &self,
2750        plane: usize,
2751        x4: usize,
2752        y4: usize,
2753        w4: usize,
2754        h4: usize,
2755        tx_size: TxSize,
2756        bw4: usize,
2757        bh4: usize,
2758    ) -> usize {
2759        let Some(ctx) = self.ctx.get(plane) else {
2760            return 0;
2761        };
2762        // maxX4/maxY4: the frame's mode-info dimensions in this plane's units.
2763        let (sub_x, sub_y) = self.plane_subsampling(plane);
2764        let (max_x4, max_y4) = (self.mi_cols >> sub_x, self.mi_rows >> sub_y);
2765        let w = tx_size.width();
2766        let h = tx_size.height();
2767        if plane == 0 {
2768            let mut top = 0_u32;
2769            let mut left = 0_u32;
2770            for k in 0..w4 {
2771                if x4 + k < max_x4 {
2772                    top = top.max(u32::from(ctx.above_level.get(x4 + k).copied().unwrap_or(0)));
2773                }
2774            }
2775            for k in 0..h4 {
2776                if y4 + k < max_y4 {
2777                    left = left.max(u32::from(ctx.left_level.get(y4 + k).copied().unwrap_or(0)));
2778                }
2779            }
2780            let top = top.min(255);
2781            let left = left.min(255);
2782            if bw4 * MI_SIZE == w && bh4 * MI_SIZE == h {
2783                0
2784            } else if top == 0 && left == 0 {
2785                1
2786            } else if top == 0 || left == 0 {
2787                2 + usize::from(top.max(left) > 3)
2788            } else if top.max(left) <= 3 {
2789                4
2790            } else if top.min(left) <= 3 {
2791                5
2792            } else {
2793                6
2794            }
2795        } else {
2796            let mut above = 0_u8;
2797            let mut left = 0_u8;
2798            for i in 0..w4 {
2799                if x4 + i < max_x4 {
2800                    above |= ctx.above_level.get(x4 + i).copied().unwrap_or(0);
2801                    above |= ctx.above_dc.get(x4 + i).copied().unwrap_or(0);
2802                }
2803            }
2804            for i in 0..h4 {
2805                if y4 + i < max_y4 {
2806                    left |= ctx.left_level.get(y4 + i).copied().unwrap_or(0);
2807                    left |= ctx.left_dc.get(y4 + i).copied().unwrap_or(0);
2808                }
2809            }
2810            let mut c = 7 + usize::from(above != 0) + usize::from(left != 0);
2811            if bw4 * MI_SIZE * bh4 * MI_SIZE > w * h {
2812                c += 3;
2813            }
2814            c
2815        }
2816    }
2817
2818    /// `dc_sign` context (§8.3.2) over a `w4` by `h4` transform block.
2819    fn dc_sign_ctx(&self, plane: usize, x4: usize, y4: usize, w4: usize, h4: usize) -> usize {
2820        let Some(ctx) = self.ctx.get(plane) else {
2821            return 0;
2822        };
2823        let (sub_x, sub_y) = self.plane_subsampling(plane);
2824        let (max_x4, max_y4) = (self.mi_cols >> sub_x, self.mi_rows >> sub_y);
2825        let mut dc_sign = 0_i32;
2826        for k in 0..w4 {
2827            if x4 + k < max_x4 {
2828                match ctx.above_dc.get(x4 + k).copied().unwrap_or(0) {
2829                    1 => dc_sign -= 1,
2830                    2 => dc_sign += 1,
2831                    _ => {}
2832                }
2833            }
2834        }
2835        for k in 0..h4 {
2836            if y4 + k < max_y4 {
2837                match ctx.left_dc.get(y4 + k).copied().unwrap_or(0) {
2838                    1 => dc_sign -= 1,
2839                    2 => dc_sign += 1,
2840                    _ => {}
2841                }
2842            }
2843        }
2844        if dc_sign < 0 {
2845            1
2846        } else if dc_sign > 0 {
2847            2
2848        } else {
2849            0
2850        }
2851    }
2852
2853    /// Spread the block's `culLevel`/`dcCategory` across the `w4` above columns
2854    /// and `h4` left rows it covers (§7.12.3 / level-context update).
2855    #[allow(clippy::too_many_arguments, reason = "one level entry per plane axis")]
2856    fn update_level_context(
2857        &mut self,
2858        plane: usize,
2859        x4: usize,
2860        y4: usize,
2861        w4: usize,
2862        h4: usize,
2863        cul: u8,
2864        dc: u8,
2865    ) {
2866        if let Some(ctx) = self.ctx.get_mut(plane) {
2867            for i in 0..w4 {
2868                if let Some(v) = ctx.above_level.get_mut(x4 + i) {
2869                    *v = cul;
2870                }
2871                if let Some(v) = ctx.above_dc.get_mut(x4 + i) {
2872                    *v = dc;
2873                }
2874            }
2875            for i in 0..h4 {
2876                if let Some(v) = ctx.left_level.get_mut(y4 + i) {
2877                    *v = cul;
2878                }
2879                if let Some(v) = ctx.left_dc.get_mut(y4 + i) {
2880                    *v = dc;
2881                }
2882            }
2883        }
2884    }
2885
2886    /// `reset_block_context` (§5.11.5) for a skip block: clear the level and DC
2887    /// contexts it covers, on each plane it codes, in that plane's 4x4 units.
2888    fn reset_block_context(
2889        &mut self,
2890        r: usize,
2891        c: usize,
2892        bw4: usize,
2893        bh4: usize,
2894        has_chroma: bool,
2895    ) {
2896        let planes = if has_chroma { self.num_planes } else { 1 };
2897        for plane in 0..planes {
2898            let (sub_x, sub_y) = self.plane_subsampling(plane);
2899            let Some(ctx) = self.ctx.get_mut(plane) else {
2900                continue;
2901            };
2902            for i in (c >> sub_x)..((c + bw4) >> sub_x) {
2903                if let Some(v) = ctx.above_level.get_mut(i) {
2904                    *v = 0;
2905                }
2906                if let Some(v) = ctx.above_dc.get_mut(i) {
2907                    *v = 0;
2908                }
2909            }
2910            for i in (r >> sub_y)..((r + bh4) >> sub_y) {
2911                if let Some(v) = ctx.left_level.get_mut(i) {
2912                    *v = 0;
2913                }
2914                if let Some(v) = ctx.left_dc.get_mut(i) {
2915                    *v = 0;
2916                }
2917            }
2918        }
2919    }
2920
2921    #[allow(clippy::too_many_arguments, reason = "records every per-block field")]
2922    fn record_block(
2923        &mut self,
2924        r: usize,
2925        c: usize,
2926        bw4: usize,
2927        bh4: usize,
2928        y_mode: usize,
2929        uv_mode: usize,
2930        has_chroma: bool,
2931        skip: bool,
2932        palette: &Palette,
2933    ) {
2934        let wide = floor_log2_usize(bw4) as u8;
2935        let high = floor_log2_usize(bh4) as u8;
2936        for y in r..(r + bh4).min(self.mi_rows) {
2937            for x in c..(c + bw4).min(self.mi_cols) {
2938                let idx = y * self.mi_cols + x;
2939                if let Some(v) = self.y_modes.get_mut(idx) {
2940                    *v = y_mode as u8;
2941                }
2942                // UVModes keeps the previous value where this block codes no
2943                // chroma: a later chroma block's filter type reads it.
2944                if has_chroma {
2945                    if let Some(v) = self.uv_modes.get_mut(idx) {
2946                        *v = uv_mode as u8;
2947                    }
2948                }
2949                if let Some(v) = self.skips.get_mut(idx) {
2950                    *v = u8::from(skip);
2951                }
2952                if let Some(v) = self.mi_wide_log2.get_mut(idx) {
2953                    *v = wide;
2954                }
2955                if let Some(v) = self.mi_high_log2.get_mut(idx) {
2956                    *v = high;
2957                }
2958                let [ps_y, ps_uv] = &mut self.palette_sizes;
2959                if let Some(v) = ps_y.get_mut(idx) {
2960                    *v = palette.size_y as u8;
2961                }
2962                if let Some(v) = ps_uv.get_mut(idx) {
2963                    *v = palette.size_uv as u8;
2964                }
2965                let [pc_y, pc_uv] = &mut self.palette_colors;
2966                if let Some(v) = pc_y.get_mut(idx) {
2967                    *v = palette.colors_y;
2968                }
2969                if let Some(v) = pc_uv.get_mut(idx) {
2970                    *v = palette.colors_u;
2971                }
2972            }
2973        }
2974    }
2975
2976    fn y_mode_at(&self, r: usize, c: usize) -> usize {
2977        self.y_modes
2978            .get(r * self.mi_cols + c)
2979            .map_or(DC_PRED, |&v| usize::from(v))
2980    }
2981
2982    fn uv_mode_at(&self, r: usize, c: usize) -> usize {
2983        self.uv_modes
2984            .get(r * self.mi_cols + c)
2985            .map_or(DC_PRED, |&v| usize::from(v))
2986    }
2987
2988    fn palette_size_at(&self, plane: usize, r: usize, c: usize) -> u8 {
2989        self.palette_sizes
2990            .get(plane)
2991            .and_then(|p| p.get(r * self.mi_cols + c))
2992            .copied()
2993            .unwrap_or(0)
2994    }
2995
2996    fn palette_colors_at(&self, plane: usize, r: usize, c: usize, n: usize) -> Vec<u16> {
2997        self.palette_colors
2998            .get(plane)
2999            .and_then(|p| p.get(r * self.mi_cols + c))
3000            .map(|colors| colors.iter().take(n).copied().collect())
3001            .unwrap_or_default()
3002    }
3003
3004    fn skip_at(&self, r: usize, c: usize) -> u8 {
3005        self.skips.get(r * self.mi_cols + c).copied().unwrap_or(0)
3006    }
3007}
3008
3009/// A coding block's modes, threaded from `intra_frame_mode_info` into the
3010/// residual loop.
3011struct BlockModes {
3012    r: usize,
3013    c: usize,
3014    avail_u: bool,
3015    avail_l: bool,
3016    /// `AvailUChroma`/`AvailLChroma`: neighbour availability on the chroma
3017    /// planes, which differs from luma for a subsampled block one unit wide or
3018    /// high (§5.11.5).
3019    avail_u_chroma: bool,
3020    avail_l_chroma: bool,
3021    y_mode: usize,
3022    uv_mode: usize,
3023    y_delta: i32,
3024    uv_delta: i32,
3025    /// The luma filter-intra kernel, if this block uses recursive filter-intra.
3026    filter_intra: Option<usize>,
3027    /// The chroma-from-luma alphas `(alphaU, alphaV)`, if this block is CfL.
3028    cfl: Option<(i32, i32)>,
3029    /// The block's palette state (sizes zero when unused).
3030    palette: Palette,
3031    /// The luma transform size (`read_block_tx_size`); chroma derives its own.
3032    luma_tx_size: TxSize,
3033}
3034
3035/// One block's palette: the colours and the per-sample colour-index maps.
3036#[derive(Default, Clone)]
3037struct Palette {
3038    /// `PaletteSizeY` (0 when the luma plane is not palette-coded).
3039    size_y: usize,
3040    /// `PaletteSizeUV`.
3041    size_uv: usize,
3042    /// `palette_colors_y`, ascending.
3043    colors_y: [u16; PALETTE_COLORS],
3044    /// `palette_colors_u`.
3045    colors_u: [u16; PALETTE_COLORS],
3046    /// `palette_colors_v`.
3047    colors_v: [u16; PALETTE_COLORS],
3048    /// `ColorMapY`, `block_h * block_w` row-major.
3049    map_y: Vec<u8>,
3050    /// `ColorMapUV`, `uv_w` wide (the subsampled block, widened to at least 4).
3051    map_uv: Vec<u8>,
3052    /// The luma block width and height in samples.
3053    block_w: usize,
3054    block_h: usize,
3055    /// The chroma colour-index map's width.
3056    uv_w: usize,
3057}
3058
3059/// A colour-index map's dimensions and the part of it on screen.
3060#[derive(Clone, Copy)]
3061struct MapDims {
3062    w: usize,
3063    h: usize,
3064    onscreen_w: usize,
3065    onscreen_h: usize,
3066}
3067
3068/// One transform block's prediction inputs.
3069struct TxBlock<'a> {
3070    plane: usize,
3071    x: usize,
3072    y: usize,
3073    /// This transform block's size.
3074    tx_size: TxSize,
3075    mode: IntraMode,
3076    mode_index: usize,
3077    angle_delta: i32,
3078    have_left: bool,
3079    have_above: bool,
3080    filter_type: bool,
3081    filter_intra: Option<usize>,
3082    /// The chroma-from-luma alpha for this plane, if the block is CfL.
3083    cfl_alpha: Option<i32>,
3084    /// The plane's palette view when the block is palette-coded on this plane.
3085    palette: Option<PaletteView<'a>>,
3086    skip: bool,
3087    /// The coding block's residual size on this plane, in the plane's 4x4 units
3088    /// (`get_plane_residual_size(MiSize, plane)`), for the `all_zero` context.
3089    plane_bw4: usize,
3090    plane_bh4: usize,
3091}
3092
3093/// A palette-coded plane's data for one transform block: the block-relative
3094/// colour-index map plus the colours it selects.
3095#[derive(Clone, Copy)]
3096struct PaletteView<'a> {
3097    map: &'a [u8],
3098    colors: &'a [u16; PALETTE_COLORS],
3099    block_w: usize,
3100    base_x: usize,
3101    base_y: usize,
3102}
3103
3104/// `get_tx_size` for a chroma plane (§5.11.37), given the block's residual size
3105/// on that plane: its largest rectangular transform, with any 64-sample side
3106/// reduced to 32 (chroma codes no 64-wide/high transform).
3107/// `neg_deinterleave` (§5.11.9): undo the coding of a `segment_id` as its
3108/// distance from the predicted one, alternating either side of it.
3109fn neg_deinterleave(diff: i32, reference: i32, max: i32) -> i32 {
3110    if reference == 0 {
3111        return diff;
3112    }
3113    if reference >= max - 1 {
3114        return max - diff - 1;
3115    }
3116    if 2 * reference < max {
3117        if diff <= 2 * reference {
3118            return if diff & 1 == 1 {
3119                reference + ((diff + 1) >> 1)
3120            } else {
3121                reference - (diff >> 1)
3122            };
3123        }
3124        return diff;
3125    }
3126    if diff <= 2 * (max - reference - 1) {
3127        if diff & 1 == 1 {
3128            reference + ((diff + 1) >> 1)
3129        } else {
3130            reference - (diff >> 1)
3131        }
3132    } else {
3133        max - (diff + 1)
3134    }
3135}
3136
3137/// The magnitude-and-sign coding shared by `delta_qindex` and `delta_lf`: a
3138/// symbol up to `DELTA_Q_SMALL` (= `DELTA_LF_SMALL` = 3), escaping to a
3139/// literal-length literal, then a sign bit when nonzero.
3140fn read_delta(dec: &mut impl TileCoder, cdf: &mut [u16]) -> Result<i32> {
3141    let mut abs = dec.symbol(cdf, Site::Other)? as i32;
3142    if abs == DELTA_SMALL {
3143        let rem_bits = dec.literal(3, Site::Other)? + 1;
3144        let abs_bits = dec.literal(rem_bits, Site::Other)? as i32;
3145        abs = abs_bits + (1 << rem_bits) + 1;
3146    }
3147    if abs != 0 && dec.literal(1, Site::Other)? == 1 {
3148        abs = -abs;
3149    }
3150    Ok(abs)
3151}
3152
3153fn chroma_tx_size(block: usize) -> TxSize {
3154    match max_tx_size_rect(block) {
3155        TxSize::Tx64x64 | TxSize::Tx32x64 | TxSize::Tx64x32 => TxSize::Tx32x32,
3156        TxSize::Tx16x64 => TxSize::Tx16x32,
3157        TxSize::Tx64x16 => TxSize::Tx32x16,
3158        other => other,
3159    }
3160}
3161
3162/// Flat index into a `BlockDecoded` grid with a one-unit border (origin at
3163/// `[1][1]`). Out-of-border coordinates fold to 0, harmless for a miss.
3164fn bd_index(stride: usize, row: isize, col: isize) -> usize {
3165    let r = usize::try_from(row + 1).unwrap_or(0);
3166    let c = usize::try_from(col + 1).unwrap_or(0);
3167    r.saturating_mul(stride).saturating_add(c)
3168}
3169
3170/// `FloorLog2` for a `usize`.
3171fn floor_log2_usize(x: usize) -> u32 {
3172    (usize::BITS - 1) - x.max(1).leading_zeros()
3173}
3174
3175/// The `BLOCK_SIZES` index for a block `bw4` by `bh4` 4x4 units.
3176fn block_size_index(bw4: usize, bh4: usize) -> usize {
3177    match (bw4, bh4) {
3178        (1, 1) => 0,
3179        (1, 2) => 1,
3180        (2, 1) => 2,
3181        (2, 2) => 3,
3182        (2, 4) => 4,
3183        (4, 2) => 5,
3184        (4, 4) => 6,
3185        (4, 8) => 7,
3186        (8, 4) => 8,
3187        (8, 8) => 9,
3188        (8, 16) => 10,
3189        (16, 8) => 11,
3190        (16, 16) => 12,
3191        (16, 32) => 13,
3192        (32, 16) => 14,
3193        (32, 32) => 15,
3194        (1, 4) => 16,
3195        (4, 1) => 17,
3196        (2, 8) => 18,
3197        (8, 2) => 19,
3198        (4, 16) => 20,
3199        (16, 4) => 21,
3200        _ => 15,
3201    }
3202}
3203
3204/// `BLOCK_64X64` (§9.3): the chunk size `residual` walks larger blocks in.
3205const BLOCK_64X64: usize = 12;
3206
3207/// `BLOCK_INVALID`: a block shape a subsampled plane cannot take.
3208const BLOCK_INVALID: usize = usize::MAX;
3209
3210/// `Num_4x4_Blocks_Wide[BLOCK_SIZES]` / `Num_4x4_Blocks_High[BLOCK_SIZES]`
3211/// (§9.3): a block size's dimensions in 4x4 units, the inverse of
3212/// `block_size_index`.
3213const BLOCK_4X4_DIMS: [(usize, usize); 22] = [
3214    (1, 1),
3215    (1, 2),
3216    (2, 1),
3217    (2, 2),
3218    (2, 4),
3219    (4, 2),
3220    (4, 4),
3221    (4, 8),
3222    (8, 4),
3223    (8, 8),
3224    (8, 16),
3225    (16, 8),
3226    (16, 16),
3227    (16, 32),
3228    (32, 16),
3229    (32, 32),
3230    (1, 4),
3231    (4, 1),
3232    (2, 8),
3233    (8, 2),
3234    (4, 16),
3235    (16, 4),
3236];
3237
3238/// `Subsampled_Size[BLOCK_SIZES][subX][subY]` (§5.11.38): the residual block a
3239/// plane with the given subsampling takes for each luma block size.
3240const SUBSAMPLED_SIZE: [[[usize; 2]; 2]; 22] = {
3241    const I: usize = BLOCK_INVALID;
3242    [
3243        [[0, 0], [0, 0]],
3244        [[1, 0], [I, 0]],
3245        [[2, I], [0, 0]],
3246        [[3, 2], [1, 0]],
3247        [[4, 3], [I, 1]],
3248        [[5, I], [3, 2]],
3249        [[6, 5], [4, 3]],
3250        [[7, 6], [I, 4]],
3251        [[8, I], [6, 5]],
3252        [[9, 8], [7, 6]],
3253        [[10, 9], [I, 7]],
3254        [[11, I], [9, 8]],
3255        [[12, 11], [10, 9]],
3256        [[13, 12], [I, 10]],
3257        [[14, I], [12, 11]],
3258        [[15, 14], [13, 12]],
3259        [[16, 1], [I, 1]],
3260        [[17, I], [2, 2]],
3261        [[18, 4], [I, 16]],
3262        [[19, I], [5, 17]],
3263        [[20, 7], [I, 18]],
3264        [[21, I], [8, 19]],
3265    ]
3266};
3267
3268/// `get_plane_residual_size(block, plane)` (§5.11.38) for a plane subsampled
3269/// by `(sub_x, sub_y)`; `BLOCK_INVALID` for a shape the stream may not use.
3270fn plane_residual_size(block: usize, sub_x: usize, sub_y: usize) -> usize {
3271    SUBSAMPLED_SIZE
3272        .get(block)
3273        .and_then(|row| row.get(sub_x))
3274        .and_then(|col| col.get(sub_y))
3275        .copied()
3276        .unwrap_or(BLOCK_INVALID)
3277}
3278
3279/// A block size's dimensions in 4x4 units, `(0, 0)` for `BLOCK_INVALID`.
3280fn block_4x4_dims(block: usize) -> (usize, usize) {
3281    BLOCK_4X4_DIMS.get(block).copied().unwrap_or((0, 0))
3282}
3283
3284/// `array[i]` for a palette colour array, 0 outside range.
3285fn at(colors: &[u16; PALETTE_COLORS], i: usize) -> u16 {
3286    colors.get(i).copied().unwrap_or(0)
3287}
3288
3289/// Set `array[i]` for a palette colour array; out-of-range writes are dropped.
3290fn set_at(colors: &mut [u16; PALETTE_COLORS], i: usize, value: u16) {
3291    if let Some(slot) = colors.get_mut(i) {
3292        *slot = value;
3293    }
3294}
3295
3296/// `Round2Signed(x, n)` (§4.7).
3297fn round2_signed(x: i32, n: u32) -> i32 {
3298    if x >= 0 {
3299        (x + (1 << (n - 1))) >> n
3300    } else {
3301        -((-x + (1 << (n - 1))) >> n)
3302    }
3303}
3304
3305/// `CeilLog2(x)` (§4.7).
3306fn ceil_log2(x: u32) -> u32 {
3307    if x < 2 {
3308        0
3309    } else {
3310        u32::BITS - (x - 1).leading_zeros()
3311    }
3312}
3313
3314/// `slice.get_mut(index)`, mapping a miss to a malformed-stream error.
3315fn get_mut<T>(slice: &mut [T], index: usize) -> Result<&mut T> {
3316    slice
3317        .get_mut(index)
3318        .ok_or_else(|| PixelsError::malformed("avif", "an AV1 tile CDF index ran out of range"))
3319}
3320
3321#[cfg(test)]
3322#[allow(
3323    clippy::unwrap_used,
3324    clippy::indexing_slicing,
3325    clippy::panic,
3326    reason = "tests operate on known-good values and assert shapes directly"
3327)]
3328mod tests {
3329    use super::*;
3330
3331    fn tiling(cols: u32, rows: u32, size_bytes: u32) -> TileInfo {
3332        TileInfo {
3333            cols_log2: cols.trailing_zeros(),
3334            rows_log2: rows.trailing_zeros(),
3335            cols,
3336            rows,
3337            col_starts_sb: (0..=cols).collect(),
3338            row_starts_sb: (0..=rows).collect(),
3339            context_update_tile_id: 0,
3340            tile_size_bytes: size_bytes,
3341        }
3342    }
3343
3344    fn numbers_and_data<'a>(tiles: &[Tile<'a>]) -> Vec<(u32, &'a [u8])> {
3345        tiles.iter().map(|t| (t.number, t.data)).collect()
3346    }
3347
3348    #[test]
3349    fn a_single_tile_group_has_no_header_and_no_sizes() {
3350        let group = [9, 8, 7];
3351        let tiles = split_tile_group(&group, &tiling(1, 1, 4)).unwrap();
3352        assert_eq!(numbers_and_data(&tiles), [(0, &group[..])]);
3353    }
3354
3355    #[test]
3356    fn tiles_are_split_by_their_little_endian_sizes() {
3357        // Flag 0 (whole frame), aligned to a byte; then tile 0 with a two-byte
3358        // size of 2 (coded as 1), tile 1 likewise with 1, and tile 2 last.
3359        let group = [0x00, 0x01, 0x00, 0xA, 0xB, 0x00, 0x00, 0xC, 0xD, 0xE];
3360        let tiles = split_tile_group(&group, &tiling(3, 1, 2)).unwrap();
3361        assert_eq!(
3362            numbers_and_data(&tiles),
3363            [(0, &[0xA, 0xB][..]), (1, &[0xC][..]), (2, &[0xD, 0xE][..])]
3364        );
3365    }
3366
3367    #[test]
3368    fn a_group_can_name_the_tiles_it_carries() {
3369        // 2x2 tiles: flag 1, tg_start = 2 and tg_end = 3 in two bits each
3370        // (1 10 11 + padding = 0b1101_1000), then one one-byte size.
3371        let group = [0b1101_1000, 0x00, 0xA, 0xB];
3372        let tiles = split_tile_group(&group, &tiling(2, 2, 1)).unwrap();
3373        assert_eq!(numbers_and_data(&tiles), [(2, &[0xA][..]), (3, &[0xB][..])]);
3374    }
3375
3376    #[test]
3377    fn broken_tile_groups_are_malformed_not_panics() {
3378        let info = tiling(2, 1, 4);
3379        // A size larger than what follows it.
3380        let error = split_tile_group(&[0x00, 0xFF, 0, 0, 0, 1], &info).unwrap_err();
3381        assert_eq!(error.code(), otf_pixels_core::ErrorCode::Malformed);
3382        // Too short to hold the size field at all.
3383        let error = split_tile_group(&[0x00, 0x01], &info).unwrap_err();
3384        assert_eq!(error.code(), otf_pixels_core::ErrorCode::Malformed);
3385        // tg_start after tg_end (2x2: flag 1, start 3, end 0).
3386        let error = split_tile_group(&[0b1110_0000], &tiling(2, 2, 1)).unwrap_err();
3387        assert_eq!(error.code(), otf_pixels_core::ErrorCode::Malformed);
3388        // Empty input.
3389        assert!(split_tile_group(&[], &info).is_err());
3390    }
3391
3392    #[test]
3393    fn neg_deinterleave_maps_each_difference_to_a_distinct_segment() {
3394        // For every prediction and segment count, the coded differences
3395        // 0..max reach every segment exactly once, nearest first.
3396        for max in 1..=8 {
3397            for reference in 0..max {
3398                let mut seen: Vec<i32> = (0..max)
3399                    .map(|diff| neg_deinterleave(diff, reference, max))
3400                    .collect();
3401                assert_eq!(seen[0], reference, "difference 0 is the prediction");
3402                seen.sort_unstable();
3403                assert_eq!(
3404                    seen,
3405                    (0..max).collect::<Vec<_>>(),
3406                    "max {max} ref {reference}"
3407                );
3408            }
3409        }
3410        // The alternation around a middle prediction: 3, 4, 2, 5, 1, ...
3411        let order: Vec<i32> = (0..8).map(|d| neg_deinterleave(d, 3, 8)).collect();
3412        assert_eq!(order, [3, 4, 2, 5, 1, 6, 0, 7]);
3413    }
3414
3415    #[test]
3416    fn block_size_indices_match_the_spec_ordering() {
3417        assert_eq!(block_size_index(1, 1), 0);
3418        assert_eq!(block_size_index(2, 2), 3);
3419        assert_eq!(block_size_index(16, 16), 12);
3420        assert_eq!(block_size_index(16, 4), 21);
3421    }
3422
3423    #[test]
3424    fn at_least_block_8x8_follows_the_enum_order_not_the_dimensions() {
3425        // Every block shape (in 4x4 units, up to 64x64): the gate must agree
3426        // with `MiSize >= BLOCK_8X8` on the spec's enum. 4x16 and 16x4 sort
3427        // after 8x8 though one side is 4 — missing that desynchronised
3428        // `angle_delta` and `palette_mode_info` reads on those shapes.
3429        let shapes = [
3430            (1, 1),
3431            (1, 2),
3432            (2, 1),
3433            (2, 2),
3434            (2, 4),
3435            (4, 2),
3436            (4, 4),
3437            (4, 8),
3438            (8, 4),
3439            (8, 8),
3440            (8, 16),
3441            (16, 8),
3442            (16, 16),
3443            (1, 4),
3444            (4, 1),
3445            (2, 8),
3446            (8, 2),
3447            (4, 16),
3448            (16, 4),
3449        ];
3450        for (bw4, bh4) in shapes {
3451            assert_eq!(
3452                at_least_block_8x8(bw4, bh4),
3453                block_size_index(bw4, bh4) >= 3,
3454                "{bw4}x{bh4}"
3455            );
3456        }
3457        assert!(at_least_block_8x8(1, 4));
3458        assert!(at_least_block_8x8(4, 1));
3459        assert!(!at_least_block_8x8(1, 2));
3460    }
3461
3462    #[test]
3463    fn subsampled_sizes_halve_each_subsampled_axis() {
3464        // Every valid Subsampled_Size entry is the luma block with each
3465        // subsampled axis halved (never below one 4x4 unit); 4:4:4 is the
3466        // identity. Catches a mistranscribed table cell.
3467        for block in 0..22 {
3468            let (bw4, bh4) = block_4x4_dims(block);
3469            assert_eq!(block_size_index(bw4, bh4), block);
3470            assert_eq!(plane_residual_size(block, 0, 0), block);
3471            for (sub_x, sub_y) in [(1, 0), (1, 1)] {
3472                let sub = plane_residual_size(block, sub_x, sub_y);
3473                if sub == BLOCK_INVALID {
3474                    continue;
3475                }
3476                assert_eq!(
3477                    block_4x4_dims(sub),
3478                    ((bw4 >> sub_x).max(1), (bh4 >> sub_y).max(1)),
3479                    "block {block} at {sub_x}x{sub_y}"
3480                );
3481            }
3482        }
3483        // 4:2:0 always has a chroma block; 4:2:2 has none for the tall shapes
3484        // whose halved width would change their aspect class.
3485        assert!((0..22).all(|b| plane_residual_size(b, 1, 1) != BLOCK_INVALID));
3486        assert_eq!(plane_residual_size(1, 1, 0), BLOCK_INVALID);
3487    }
3488
3489    #[test]
3490    fn floor_log2_of_block_units() {
3491        assert_eq!(floor_log2_usize(1), 0);
3492        assert_eq!(floor_log2_usize(2), 1);
3493        assert_eq!(floor_log2_usize(16), 4);
3494    }
3495}