oxideav_av1/decoder/frame_driver.rs
1//! Spec-faithful frame decode driver — §7.4 `decode_frame_wrapup` order
2//! over the §5.11 `PartitionWalker` syntax walk.
3//!
4//! This is the decode path for **real encoder-produced bitstreams** (as
5//! opposed to the encoder-mirror drivers retired in r428, which
6//! accepted only this crate's own
7//! constrained encoder output). It wires the crate's spec modules end to
8//! end for one frame:
9//!
10//! 1. §5.9 frame-header derived state → [`TileDecodeParams`] +
11//! [`QuantizerParams`] (including the §5.9.2 `CodedLossless` /
12//! `LosslessArray` derivations and the §5.9.14 segmentation
13//! feature plumbing).
14//! 2. §8.2.2 `init_symbol` over the tile bytes + §8.3.1 default CDF
15//! load (`new_from_defaults` + the q-context-selected
16//! `init_coeff_cdfs( base_q_idx )` copy).
17//! 3. §5.11.2 [`PartitionWalker::decode_tile_syntax`] — the
18//! superblock loop over §5.11.4 `decode_partition` → §5.11.5
19//! `decode_block` → §5.11.34 `residual`, reconstructing every
20//! intra transform block into `CurrFrame[ plane ]`.
21//! 4. The §7.4 in-loop / post passes in decode order over the
22//! mi-grid-padded planes: §7.14 deblock (only when a luma filter
23//! level is nonzero) → §7.15 CDEF → §7.16 superres (both the CDEF
24//! output and the retained post-deblock frame) → §7.17 loop
25//! restoration → the §7.18.2 crop → §7.18.3 film grain.
26//!
27//! ## Scope
28//!
29//! * Intra-only frames (KEY / INTRA_ONLY), 8-bit output, single- and
30//! multi-tile layouts.
31//! * 4:2:0 / 4:2:2 / 4:4:4 and monochrome layouts (the walker threads
32//! `subsampling_x/y` + `mono_chrome`; only 8-bit output is surfaced).
33//! * Inter frames, `show_existing_frame`, and quantizer-matrix streams
34//! return [`Error::PartitionWalkOutOfRange`] (follow-ups).
35//!
36//! Spec provenance: `docs/video/av1/av1-spec.txt` §5.9, §5.10, §5.11,
37//! §7.4, §7.12.2, §7.14, §7.15, §7.16, §7.17, §7.18.
38
39use crate::cdf::{
40 FrameInterOrderHints, InterFrameContext, InterWalkPixels, MotionFieldMvs, PartitionWalker,
41 QuantizerParams, TileCdfContext, TileDecodeParams, TileGeometry,
42};
43use crate::encoder::ivf::IvfReader;
44use crate::encoder::tile_group_obu::{parse_tile_group_obu_body, TilePayload};
45use crate::film_grain::film_grain_synthesis;
46use crate::frame_header::{
47 parse_frame_header_with_refs, FrameHeader, RefInfo, NUM_REF_FRAMES, PRIMARY_REF_NONE,
48};
49use crate::inter_pred::get_relative_dist;
50use crate::loop_filter::PlaneBuffer;
51use crate::obu::{ObuIter, ObuType};
52use crate::sequence_header::{parse_sequence_header, SequenceHeader};
53use crate::symbol_decoder::SymbolDecoder;
54use crate::uncompressed_header_tail::{
55 SegmentationParams, ALTREF_FRAME, LAST_FRAME, MAX_SEGMENTS, SEG_LVL_ALT_Q, SEG_LVL_SKIP,
56};
57use crate::Error;
58use crate::{PlaneRefSpec, RefFrameStoreEntry};
59use std::sync::Arc;
60use std::time::{Duration, Instant};
61
62/// r475 — wall time per §7.4 decode stage of the LAST temporal unit
63/// a [`SpecDecodeSession`] decoded (summed over its frames). A
64/// measurement hook for the decode-budget harness
65/// (`examples/decode_bench`); every field is a plain [`Duration`].
66#[doc(hidden)]
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
68#[non_exhaustive]
69pub struct DecodeStageTimings {
70 /// OBU framing + §5.5 / §5.9 header parse + §5.11.1 tile-group
71 /// splitting.
72 pub parse: Duration,
73 /// §5.11 tile syntax walk + §7.11 / §7.12 / §7.13 reconstruction
74 /// (every tile).
75 pub tiles: Duration,
76 /// §7.14 deblocking.
77 pub deblock: Duration,
78 /// §7.15 CDEF.
79 pub cdef: Duration,
80 /// §7.16 superres upscaling.
81 pub superres: Duration,
82 /// §7.17 loop restoration.
83 pub restoration: Duration,
84 /// §7.18.2 crop + the §7.20 reference-store payload (`u16`).
85 pub ref_store: Duration,
86 /// §7.18.3 film-grain synthesis.
87 pub grain: Duration,
88 /// Narrowing to the output byte layout + the §7.19 motion-field
89 /// payload.
90 pub output: Duration,
91}
92
93impl DecodeStageTimings {
94 /// The all-zero record.
95 #[must_use]
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 /// Sum of every stage.
101 #[must_use]
102 pub fn total(&self) -> Duration {
103 self.parse
104 + self.tiles
105 + self.deblock
106 + self.cdef
107 + self.superres
108 + self.restoration
109 + self.ref_store
110 + self.grain
111 + self.output
112 }
113}
114
115/// One frame decoded by the spec-faithful driver. Planes are surfaced
116/// at their §5.9.8 cropped extents (`FrameWidth` × `FrameHeight` for
117/// luma, the §5.5.2 subsampled extent for chroma), row-major, 8-bit.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct SpecFrame {
120 /// Luma width in pixels (`FrameWidth`, post-superres if any).
121 pub width: u32,
122 /// Luma height in pixels (`FrameHeight`).
123 pub height: u32,
124 /// Decoded planes: `[Y]` (monochrome) or `[Y, U, V]`. For
125 /// `bit_depth == 8` one byte per sample; for 10/12-bit output two
126 /// bytes per sample, little-endian (the `yuv4xxp1{0,2}le` layout).
127 pub planes: Vec<Vec<u8>>,
128 /// `(width, height)` per surfaced plane (in samples).
129 pub plane_dims: Vec<(u32, u32)>,
130 /// §5.5.2 `BitDepth` (8, 10 or 12).
131 pub bit_depth: u8,
132}
133
134/// §7.20 per-slot reference store — the decoded frame a later inter
135/// frame motion-compensates against, plus the per-mi grids the §7.9
136/// temporal projection and the §7.21 `show_existing_frame` output
137/// path consume.
138#[derive(Debug, Clone)]
139struct SpecRefSlot {
140 /// §7.20 `FrameStore[ i ][ plane ]` — the post-§7.17 (pre-§7.18.3
141 /// film-grain) planes at their §7.18.2 cropped extents
142 /// (`UpscaledWidth × FrameHeight`, per-plane subsampled), `u16`
143 /// (post-`Clip1`, so the widening from the walker's `i32` is
144 /// lossless).
145 planes: Vec<Vec<u16>>,
146 /// `(width, height)` per stored plane.
147 plane_dims: Vec<(u32, u32)>,
148 /// §7.20 `SavedMvs[ i ]` — the §7.19 `MfMvs[ row ][ col ][ 0..2 ]`
149 /// grid snapshot (2 `i16` per mi cell), the §7.9.2 projection
150 /// source.
151 mf_mvs: Vec<i16>,
152 /// §7.20 `SavedRefFrames[ i ]` — the §7.19 `MfRefFrames[ row ][
153 /// col ]` grid snapshot (1 `i8` per mi cell).
154 mf_ref_frames: Vec<i8>,
155 /// §7.20 `SavedOrderHints[ i ][ ref ]` — the stored frame's
156 /// `OrderHints[]` array (what ITS references' output order was),
157 /// consumed by the §7.9.1/§7.9.2 `SavedOrderHints[ srcIdx ][ .. ]`
158 /// reads.
159 saved_order_hints: [i32; ALTREF_FRAME + 1],
160 /// The stored frame's mi extent.
161 mi_rows: u32,
162 mi_cols: u32,
163 /// §7.20 `RefFrameType[ i ]` — `FrameIsIntra` of the stored frame
164 /// (the §7.9.2 projection skips KEY / INTRA_ONLY sources).
165 frame_is_intra: bool,
166 /// §7.20 `RefFrameType[ i ] == KEY_FRAME` — the §7.21 trigger.
167 frame_type_is_key: bool,
168 /// §7.20 `RefBitDepth[ i ]` — the stored frame's `BitDepth`.
169 bit_depth: u8,
170 /// §7.20 `save_cdfs( i )` — the frame-end CDF state (§8.4
171 /// `frame_end_update_cdf`), loaded back by §8.3.1 `load_cdfs()`
172 /// when a later frame names this slot as its primary reference.
173 cdfs: Box<TileCdfContext>,
174 /// §7.20 `SavedGmParams[ i ]` — this frame's `gm_params`.
175 gm_params: [[i32; 6]; 8],
176 /// §7.20 `save_loop_filter_params( i )` — the §5.9.11 running
177 /// delta state at the end of this frame's header parse.
178 lf_ref_deltas: [i8; 8],
179 /// Mode-delta half of `save_loop_filter_params( i )`.
180 lf_mode_deltas: [i8; 2],
181 /// §7.20 `SavedSegmentIds[ i ]` — the decoded frame's
182 /// `SegmentIds[ row ][ col ]` grid (row-major, one `i32` per mi
183 /// cell), loaded back by `load_previous_segment_ids()` as the
184 /// §5.11.21 `PrevSegmentIds` prediction source.
185 segment_ids: Vec<i32>,
186 /// §7.20 `save_grain_params( i )` — the frame's resolved §5.9.30
187 /// film-grain state (post-`load_grain_params` on the
188 /// `update_grain == 0` predicted path). `None` when the frame
189 /// carries no grain (`reset_grain_params()`). Loaded back by the
190 /// §5.9.30 `update_grain == 0` arm and by the §5.9.2
191 /// `show_existing_frame` `load_grain_params(
192 /// frame_to_show_map_idx )` output path.
193 grain_params: Option<crate::uncompressed_header_tail::FilmGrainParams>,
194}
195
196/// Cross-frame decoder session state: the §5.9.2 `RefInfo` arrays the
197/// inter `uncompressed_header()` parse consumes plus the §7.20
198/// per-slot pixel/grid stores.
199#[derive(Debug, Clone)]
200struct SpecRefState {
201 info: RefInfo,
202 /// r453 — `Arc`-shared: §7.20 stores the SAME immutable payload
203 /// into every slot `refresh_frame_flags` selects (and the §7.21
204 /// show-existing re-store copies a stored one wholesale), so an
205 /// all-refresh KEY frame keeps ONE payload alive instead of
206 /// eight deep copies of planes + grids + CDFs. Nothing mutates a
207 /// stored payload in place.
208 slots: [Option<Arc<SpecRefSlot>>; NUM_REF_FRAMES as usize],
209}
210
211impl SpecRefState {
212 fn new() -> Self {
213 Self {
214 info: RefInfo::default(),
215 slots: Default::default(),
216 }
217 }
218}
219
220/// One frame decoded by [`decode_frame_spec_full`] — the surfaced
221/// [`SpecFrame`] plus the §7.20 reference-update payload.
222struct DecodedFrameInternal {
223 /// The §7.18 output frame (post-film-grain).
224 frame: SpecFrame,
225 /// The §7.20 store payload: pre-grain cropped planes (`u16`).
226 ref_planes: Vec<Vec<u16>>,
227 /// `(width, height)` per `ref_planes` entry.
228 ref_plane_dims: Vec<(u32, u32)>,
229 /// §7.19 `MfMvs[]` grid (2 `i16` per mi cell).
230 mf_mvs: Vec<i16>,
231 /// §7.19 `MfRefFrames[]` grid (1 `i8` per mi cell).
232 mf_ref_frames: Vec<i8>,
233 /// This frame's `OrderHints[]` array (§5.9.2), the §7.20
234 /// `SavedOrderHints` payload.
235 order_hints_by_ref: [i32; ALTREF_FRAME + 1],
236 /// §8.4 frame-end CDF state (`frame_end_update_cdf` output — the
237 /// `context_update_tile_id` tile's adapted CDFs, or the frame-start
238 /// state under `disable_frame_end_update_cdf == 1`).
239 end_cdfs: Box<TileCdfContext>,
240 /// The decoded frame's mi extent.
241 mi_rows: u32,
242 mi_cols: u32,
243 /// §7.20 `SegmentIds[][]` snapshot (unwritten `-1` cells clamped
244 /// to `0`, matching the spec's always-written map).
245 segment_ids: Vec<i32>,
246 /// The frame's RESOLVED film-grain state (the §5.9.30
247 /// `update_grain == 0` predicted path replaced by the referenced
248 /// slot's saved params with this frame's `grain_seed`), the §7.20
249 /// `save_grain_params` payload.
250 grain_params: Option<crate::uncompressed_header_tail::FilmGrainParams>,
251}
252
253/// §5.9.2 `LosslessArray[ segmentId ]` — `get_qindex( 1, segmentId ) ==
254/// 0 && DeltaQYDc == 0 && DeltaQ{U,V}{Ac,Dc} == 0` for every segment.
255pub(crate) fn lossless_array(
256 qp: &crate::uncompressed_header_tail::QuantizationParams,
257 sp: &SegmentationParams,
258) -> [bool; MAX_SEGMENTS] {
259 let deltas_all_zero = qp.delta_q_y_dc == 0
260 && qp.delta_q_u_dc == 0
261 && qp.delta_q_u_ac == 0
262 && qp.delta_q_v_dc == 0
263 && qp.delta_q_v_ac == 0;
264 let mut out = [false; MAX_SEGMENTS];
265 for (segment_id, slot) in out.iter_mut().enumerate() {
266 // §7.12.2 get_qindex( ignoreDeltaQ = 1, segmentId ).
267 let qindex = if sp.enabled && sp.segment_feature_active[segment_id][SEG_LVL_ALT_Q] {
268 let data = i32::from(sp.segment_feature_data[segment_id][SEG_LVL_ALT_Q]);
269 (i32::from(qp.base_q_idx) + data).clamp(0, 255)
270 } else {
271 i32::from(qp.base_q_idx)
272 };
273 *slot = qindex == 0 && deltas_all_zero;
274 }
275 out
276}
277
278/// Decode one intra frame through the §5.11 syntax walker + the §7.4
279/// post-pass chain, given its already-parsed sequence header, frame
280/// header, and the §5.11.1 tile-group OBU body.
281pub fn decode_frame_spec(
282 seq: &SequenceHeader,
283 fh: &FrameHeader,
284 tile_group_body: &[u8],
285) -> Result<SpecFrame, Error> {
286 // The historical intra-only entry: no cross-frame reference state,
287 // so inter frames are rejected inside the full driver.
288 let mut stages = DecodeStageTimings::default();
289 Ok(decode_frame_spec_full(seq, fh, tile_group_body, None, &mut stages, 1)?.frame)
290}
291
292/// Parse one §5.11.1 tile-group OBU body against the frame's
293/// [`TileInfo`] — the shared derivation (`NumTiles`, `tileBits`,
294/// `TileSizeBytes`) both the whole-frame path and the r433
295/// multi-tile-group accumulation ride.
296fn parse_tile_group_body_with_ti(
297 ti: &crate::tile_info::TileInfo,
298 body: &[u8],
299) -> Result<crate::encoder::tile_group_obu::ParsedTileGroup, Error> {
300 let ceil_log2 = |v: u32| -> u32 {
301 if v <= 1 {
302 0
303 } else {
304 32 - (v - 1).leading_zeros()
305 }
306 };
307 if ti.mi_col_starts.len() != (ti.tile_cols as usize) + 1
308 || ti.mi_row_starts.len() != (ti.tile_rows as usize) + 1
309 {
310 return Err(Error::PartitionWalkOutOfRange);
311 }
312 parse_tile_group_obu_body(
313 body,
314 ti.tile_cols * ti.tile_rows,
315 ceil_log2(ti.tile_cols),
316 ceil_log2(ti.tile_rows),
317 u32::from(ti.tile_size_bytes),
318 )
319}
320
321/// Parse a tile-group body that must carry the WHOLE frame — the
322/// §5.10 `frame_obu` shape (§6.10.1 requires
323/// `tile_start_and_end_present_flag == 0` there, which forces
324/// `tg_start = 0` / `tg_end = NumTiles - 1`) and the historical
325/// single-tile-group packing the standalone entries accept.
326fn parse_whole_frame_tile_group(
327 ti: &crate::tile_info::TileInfo,
328 body: &[u8],
329 require_flag_zero: bool,
330) -> Result<Vec<TilePayload>, Error> {
331 let parsed = parse_tile_group_body_with_ti(ti, body)?;
332 let num_tiles = ti.tile_cols * ti.tile_rows;
333 if parsed.tg_start != 0 || parsed.tg_end + 1 != num_tiles {
334 return Err(Error::TileGroupInvalid);
335 }
336 if require_flag_zero && parsed.tile_start_and_end_present_flag {
337 return Err(Error::TileGroupInvalid);
338 }
339 Ok(parsed.tiles)
340}
341
342/// [`decode_frame_spec`] with the cross-frame reference state — the
343/// full §5.11 + §7.4 decode for one KEY / INTRA_ONLY / INTER frame
344/// whose tile-group body carries every tile, returning both the
345/// output frame and the §7.20 reference-update payload.
346fn decode_frame_spec_full(
347 seq: &SequenceHeader,
348 fh: &FrameHeader,
349 tile_group_body: &[u8],
350 refs: Option<&SpecRefState>,
351 stages: &mut DecodeStageTimings,
352 threads: usize,
353) -> Result<DecodedFrameInternal, Error> {
354 let ti = fh
355 .tile_info
356 .as_ref()
357 .ok_or(Error::PartitionWalkOutOfRange)?;
358 let t0 = Instant::now();
359 let tiles = parse_whole_frame_tile_group(ti, tile_group_body, false)?;
360 stages.parse += t0.elapsed();
361 decode_frame_spec_tiles(seq, fh, &tiles, refs, stages, threads)
362}
363
364/// The frame decode proper, over already-parsed per-tile §8.2
365/// payloads — `tiles` must hold exactly `NumTiles` entries in tile
366/// raster order (§5.11.1 `TileNum` order; the r433 multi-tile-group
367/// accumulation concatenates conformant `tg_start..=tg_end` slices to
368/// get here).
369fn decode_frame_spec_tiles(
370 seq: &SequenceHeader,
371 fh: &FrameHeader,
372 tiles: &[TilePayload],
373 refs: Option<&SpecRefState>,
374 stages: &mut DecodeStageTimings,
375 threads: usize,
376) -> Result<DecodedFrameInternal, Error> {
377 if fh.show_existing_frame {
378 // §7.21 output-existing path is the caller's (no tile group).
379 return Err(Error::PartitionWalkOutOfRange);
380 }
381 if !fh.frame_is_intra && refs.is_none() {
382 // Inter frames are undecodable without the §7.20 store.
383 return Err(Error::PartitionWalkOutOfRange);
384 }
385 let cc = &seq.color_config;
386 if !matches!(cc.bit_depth, 8 | 10 | 12) {
387 return Err(Error::PartitionWalkOutOfRange);
388 }
389 let fs = fh
390 .frame_size
391 .as_ref()
392 .ok_or(Error::PartitionWalkOutOfRange)?;
393 if fs.use_superres && fs.upscaled_width <= fs.frame_width {
394 // §5.9.8 conformance: superres only ever widens.
395 return Err(Error::PartitionWalkOutOfRange);
396 }
397 let ti = fh
398 .tile_info
399 .as_ref()
400 .ok_or(Error::PartitionWalkOutOfRange)?;
401 let qp = fh
402 .quantization_params
403 .as_ref()
404 .ok_or(Error::PartitionWalkOutOfRange)?;
405 let default_seg = SegmentationParams::disabled();
406 let sp = fh.segmentation_params.as_ref().unwrap_or(&default_seg);
407 let dq = fh.delta_q_params.unwrap_or_default();
408 let dlf = fh.delta_lf_params.unwrap_or_default();
409 let lr = fh
410 .lr_params
411 .as_ref()
412 .ok_or(Error::PartitionWalkOutOfRange)?;
413 // §5.11.57 loop-restoration state for the per-superblock `read_lr`
414 // interleave (walker-facing shape).
415 let lr_walk = crate::cdf::LrParams {
416 num_planes: cc.num_planes as usize,
417 frame_restoration_type: [
418 lr.frame_restoration_type[0] as u8,
419 lr.frame_restoration_type[1] as u8,
420 lr.frame_restoration_type[2] as u8,
421 ],
422 loop_restoration_size: lr.loop_restoration_size,
423 subsampling_x: u8::from(cc.subsampling_x),
424 subsampling_y: u8::from(cc.subsampling_y),
425 frame_height: fs.frame_height,
426 upscaled_width: fs.upscaled_width,
427 use_superres: fs.use_superres,
428 superres_denom: fs.superres_denom,
429 allow_intrabc: fh.allow_intrabc,
430 };
431
432 // §5.9.2 CodedLossless / LosslessArray.
433 let lossless = lossless_array(qp, sp);
434 let coded_lossless = lossless.iter().all(|&l| l);
435
436 // §7.12.2 quantizer state for the walk. `CurrentQIndex` starts at
437 // `base_q_idx` (§5.9.18); the walker's §5.11.13 read_delta_qindex
438 // maintains the running value internally.
439 let seg_alt_q_active = {
440 let mut a = [false; MAX_SEGMENTS];
441 for (i, slot) in a.iter_mut().enumerate() {
442 *slot = sp.segment_feature_active[i][SEG_LVL_ALT_Q];
443 }
444 a
445 };
446 let seg_alt_q_data = {
447 let mut a = [0i16; MAX_SEGMENTS];
448 for (i, slot) in a.iter_mut().enumerate() {
449 *slot = sp.segment_feature_data[i][SEG_LVL_ALT_Q];
450 }
451 a
452 };
453 // §5.9.2 `SegQMLevel[ plane ][ segmentId ]`: when `using_qmatrix`,
454 // a lossless segment takes the no-QM sentinel `15`, every other
455 // segment `[ qm_y, qm_u, qm_v ]`. With `using_qmatrix == 0` the
456 // §7.12.3 QM arm never fires and the neutral all-`15` fill stands.
457 let mut seg_qm_level = [[15u8; MAX_SEGMENTS]; 3];
458 if qp.using_qmatrix {
459 for segment_id in 0..MAX_SEGMENTS {
460 if !lossless[segment_id] {
461 seg_qm_level[0][segment_id] = qp.qm_y;
462 seg_qm_level[1][segment_id] = qp.qm_u;
463 seg_qm_level[2][segment_id] = qp.qm_v;
464 }
465 }
466 }
467 let quant = QuantizerParams {
468 base_q_idx: qp.base_q_idx,
469 delta_q_y_dc: qp.delta_q_y_dc,
470 delta_q_u_dc: qp.delta_q_u_dc,
471 delta_q_u_ac: qp.delta_q_u_ac,
472 delta_q_v_dc: qp.delta_q_v_dc,
473 delta_q_v_ac: qp.delta_q_v_ac,
474 using_qmatrix: qp.using_qmatrix,
475 bit_depth: cc.bit_depth,
476 delta_q_present: dq.delta_q_present,
477 current_q_index: qp.base_q_idx,
478 segmentation_enabled: sp.enabled,
479 seg_alt_q_active,
480 seg_alt_q_data,
481 seg_qm_level,
482 };
483
484 // r426 — per-segment SEG_LVL_SKIP active flags: the §5.11.11
485 // short-circuit derives per block from the coded segment id.
486 let seg_skip: [bool; MAX_SEGMENTS] = if sp.enabled {
487 core::array::from_fn(|s| sp.segment_feature_active[s][SEG_LVL_SKIP])
488 } else {
489 [false; MAX_SEGMENTS]
490 };
491
492 let cdef_bits = fh
493 .cdef_params
494 .as_ref()
495 .map_or(0, |c| u32::from(c.cdef_bits));
496 let tx_mode_select = matches!(
497 fh.tx_mode,
498 Some(crate::uncompressed_header_tail::TxMode::TxModeSelect)
499 );
500
501 let params = TileDecodeParams {
502 frame_is_intra: fh.frame_is_intra,
503 subsampling_x: u8::from(cc.subsampling_x),
504 subsampling_y: u8::from(cc.subsampling_y),
505 num_planes: cc.num_planes,
506 seg_id_pre_skip: sp.seg_id_pre_skip,
507 segmentation_enabled: sp.enabled,
508 seg_skip,
509 last_active_seg_id: sp.last_active_seg_id,
510 lossless_array: &lossless,
511 coded_lossless,
512 enable_cdef: seq.enable_cdef,
513 allow_intrabc: fh.allow_intrabc,
514 cdef_bits,
515 use_128x128_superblock: seq.use_128x128_superblock,
516 delta_q_res: dq.delta_q_res,
517 delta_lf_present: dlf.delta_lf_present,
518 delta_lf_multi: dlf.delta_lf_multi,
519 mono_chrome: cc.mono_chrome,
520 delta_lf_res: dlf.delta_lf_res,
521 allow_screen_content_tools: fh.allow_screen_content_tools,
522 enable_filter_intra: seq.enable_filter_intra,
523 bit_depth: cc.bit_depth,
524 tx_mode_select,
525 reduced_tx_set: fh.reduced_tx_set.unwrap_or(false),
526 enable_intra_edge_filter: seq.enable_intra_edge_filter,
527 };
528
529 // Per-tile §8.2 byte ranges — the caller parsed the §5.11.1
530 // grouping; this driver requires the full frame's tiles.
531 let num_tiles = ti.tile_cols * ti.tile_rows;
532 if ti.mi_col_starts.len() != (ti.tile_cols as usize) + 1
533 || ti.mi_row_starts.len() != (ti.tile_rows as usize) + 1
534 {
535 return Err(Error::PartitionWalkOutOfRange);
536 }
537 if tiles.len() != num_tiles as usize {
538 return Err(Error::TileGroupInvalid);
539 }
540
541 let mi_rows = fs.mi_rows;
542 let mi_cols = fs.mi_cols;
543 let mut walker = PartitionWalker::new(
544 mi_rows,
545 mi_cols,
546 TileGeometry {
547 mi_row_start: 0,
548 mi_row_end: mi_rows,
549 mi_col_start: 0,
550 mi_col_end: mi_cols,
551 },
552 )
553 .ok_or(Error::PartitionWalkOutOfRange)?;
554
555 // ---- Inter-frame state: §5.11.18 context + §7.11.3 ref pixels. ----
556 // Owned buffers first (the `InterFrameContext` borrows them), then
557 // the context itself. All of it is inert on intra frames.
558 let is_inter_frame = !fh.frame_is_intra;
559 let sub_x_u8 = u8::from(cc.subsampling_x);
560 let sub_y_u8 = u8::from(cc.subsampling_y);
561 let num_planes_usize = cc.num_planes as usize;
562 // §7.9 motion-field grid — the temporal-scan source. §5.9.2
563 // invokes `motion_field_estimation()` only when `use_ref_frame_mvs
564 // == 1`; the projection walks the §7.20-stored `SavedMvs` /
565 // `SavedRefFrames` / `SavedOrderHints` of up to four reference
566 // frames (LAST backwards, BWDREF / ALTREF2 / ALTREF forwards,
567 // LAST2 as the stack filler).
568 let mfmvs = if is_inter_frame
569 && fh
570 .inter_refs
571 .as_ref()
572 .is_some_and(|ir| ir.use_ref_frame_mvs)
573 {
574 motion_field_estimation(
575 refs.ok_or(Error::PartitionWalkOutOfRange)?,
576 fh,
577 seq,
578 mi_rows,
579 mi_cols,
580 )?
581 } else {
582 MotionFieldMvs::new_invalid(mi_rows, mi_cols)
583 };
584 // §5.9.2 `load_previous_segment_ids()` — with a primary reference
585 // whose stored mi extent matches, `PrevSegmentIds` is that slot's
586 // §7.20 `SavedSegmentIds`; otherwise all-zero (also the
587 // `setup_past_independence()` state for PRIMARY_REF_NONE).
588 let prev_segment_ids: Option<Vec<i32>> =
589 if is_inter_frame && sp.enabled && fh.primary_ref_frame != PRIMARY_REF_NONE {
590 let st = refs.ok_or(Error::PartitionWalkOutOfRange)?;
591 let ir = fh
592 .inter_refs
593 .as_ref()
594 .ok_or(Error::PartitionWalkOutOfRange)?;
595 let slot = ir.ref_frame_idx[fh.primary_ref_frame as usize] as usize;
596 match st.slots.get(slot).and_then(|s| s.as_ref()) {
597 Some(prev)
598 if prev.mi_rows == mi_rows
599 && prev.mi_cols == mi_cols
600 && prev.segment_ids.len() == (mi_rows as usize) * (mi_cols as usize) =>
601 {
602 Some(prev.segment_ids.clone())
603 }
604 _ => Some(vec![0i32; (mi_rows as usize) * (mi_cols as usize)]),
605 }
606 } else if is_inter_frame && sp.enabled {
607 Some(vec![0i32; (mi_rows as usize) * (mi_cols as usize)])
608 } else {
609 None
610 };
611 // Never-referenced placeholder for empty §7.20 slots (a conformant
612 // stream only references `RefValid` slots).
613 let dummy_plane: Vec<u16> = vec![0u16; 64];
614 let dummy_entry = || RefFrameStoreEntry {
615 plane: &dummy_plane,
616 stride: 8,
617 upscaled_width: 8,
618 width: 8,
619 height: 8,
620 };
621 // Per-plane `FrameStore[ slot ]` views over the §7.20 store.
622 let mut plane_stores: Vec<[RefFrameStoreEntry<'_>; NUM_REF_FRAMES as usize]> = Vec::new();
623 let mut ref_frame_idx = [0u8; 7];
624 let mut order_hints_by_ref = [0i32; ALTREF_FRAME + 1];
625 let mut sign_bias = [0i32; 8];
626 let mut is_scaled_per_ref = [false; 7];
627 if is_inter_frame {
628 let st = refs.ok_or(Error::PartitionWalkOutOfRange)?;
629 let ir = fh
630 .inter_refs
631 .as_ref()
632 .ok_or(Error::PartitionWalkOutOfRange)?;
633 for plane in 0..num_planes_usize {
634 let arr: [RefFrameStoreEntry<'_>; NUM_REF_FRAMES as usize] =
635 core::array::from_fn(|slot| match st.slots[slot].as_ref() {
636 Some(s) if plane < s.planes.len() => {
637 let (w, _h) = s.plane_dims[plane];
638 // r405: the dimension fields carry the stored
639 // frame's LUMA extents (§7.11.3.3 scale ratio +
640 // §7.11.3.4/.5 clamps derive per-plane
641 // internally); only the buffer + stride are in
642 // this plane's own sample units.
643 let (lw, lh) = s.plane_dims[0];
644 RefFrameStoreEntry {
645 plane: &s.planes[plane],
646 stride: w as usize,
647 upscaled_width: lw,
648 width: lw,
649 height: lh,
650 }
651 }
652 _ => dummy_entry(),
653 });
654 plane_stores.push(arr);
655 }
656 for i in 0..7 {
657 let slot = ir.ref_frame_idx[i] as usize;
658 if slot >= NUM_REF_FRAMES as usize {
659 return Err(Error::PartitionWalkOutOfRange);
660 }
661 ref_frame_idx[i] = slot as u8;
662 // §5.9.2 `OrderHints[ LAST_FRAME + i ] =
663 // RefOrderHint[ ref_frame_idx[ i ] ]`.
664 let hint = st.info.order_hint[slot] as i32;
665 order_hints_by_ref[LAST_FRAME + i] = hint;
666 // §7.8 `RefFrameSignBias[ refFrame ] =
667 // get_relative_dist( hint, OrderHint ) > 0`.
668 if seq.enable_order_hint {
669 sign_bias[LAST_FRAME + i] = i32::from(
670 get_relative_dist(hint, fh.order_hint as i32, u32::from(seq.order_hint_bits))
671 > 0,
672 );
673 }
674 // §5.11.27 `is_scaled( refFrame )` — the literal spec body:
675 //
676 // xScale = ( ( RefUpscaledWidth[ refIdx ] << REF_SCALE_SHIFT )
677 // + ( FrameWidth / 2 ) ) / FrameWidth
678 // yScale = ( ( RefFrameHeight[ refIdx ] << REF_SCALE_SHIFT )
679 // + ( FrameHeight / 2 ) ) / FrameHeight
680 // return xScale != noScale || yScale != noScale
681 //
682 // The divisor is the CODED `FrameWidth`/`FrameHeight`, NOT
683 // `UpscaledWidth` (r408 fix): a superres frame codes at
684 // `FrameWidth < UpscaledWidth` while its references store
685 // `RefUpscaledWidth == UpscaledWidth`, so every reference
686 // IS scaled even though the upscaled extents match. The
687 // pre-r408 upscaled-vs-upscaled shortcut read the §5.11.27
688 // `motion_mode` symbol where the encoder wrote `use_obmc`,
689 // desynchronising the arithmetic decoder on the first
690 // superres inter frame.
691 let no_scale = 1u64 << crate::inter_pred::REF_SCALE_SHIFT;
692 let fw = u64::from(fs.frame_width.max(1));
693 let fhh = u64::from(fs.frame_height.max(1));
694 let x_scale = ((u64::from(st.info.upscaled_width[slot])
695 << crate::inter_pred::REF_SCALE_SHIFT)
696 + fw / 2)
697 / fw;
698 let y_scale = ((u64::from(st.info.frame_height[slot])
699 << crate::inter_pred::REF_SCALE_SHIFT)
700 + fhh / 2)
701 / fhh;
702 is_scaled_per_ref[i] = x_scale != no_scale || y_scale != no_scale;
703 }
704 }
705 let order_hints = FrameInterOrderHints {
706 order_hint_bits: if seq.enable_order_hint {
707 u32::from(seq.order_hint_bits)
708 } else {
709 0
710 },
711 current_order_hint: fh.order_hint as i32,
712 order_hints_by_ref,
713 };
714 let plane_ref_specs: Vec<PlaneRefSpec<'_>> = (0..plane_stores.len())
715 .map(|p| PlaneRefSpec {
716 plane: p as u8,
717 subsampling_x: if p > 0 { sub_x_u8 } else { 0 },
718 subsampling_y: if p > 0 { sub_y_u8 } else { 0 },
719 frame_store: &plane_stores[p],
720 // r405: LUMA extents for every plane — the §7.11.3.3
721 // scale ratio is luma-derived (an odd `FrameWidth` such
722 // as a 213-wide resized frame has no faithful per-plane
723 // representation).
724 frame_width: fs.frame_width,
725 frame_height: fs.frame_height,
726 })
727 .collect();
728 let pixels = InterWalkPixels {
729 ref_frame_idx,
730 bit_depth: cc.bit_depth,
731 plane_refs: &plane_ref_specs,
732 order_hints,
733 };
734 let ictx: Option<InterFrameContext<'_>> = if is_inter_frame {
735 let ir = fh
736 .inter_refs
737 .as_ref()
738 .ok_or(Error::PartitionWalkOutOfRange)?;
739 let mut c = InterFrameContext::identity_default(&mfmvs);
740 // §5.9.14 segmentation state: the update flags, the
741 // FeatureEnabled/FeatureData tables (per-block
742 // `seg_feature_active` gates), and the §5.11.21
743 // `PrevSegmentIds` prediction source.
744 c.segmentation_update_map = sp.update_map;
745 c.segmentation_temporal_update = sp.temporal_update;
746 c.seg_feature_active = sp.segment_feature_active;
747 c.seg_feature_data = sp.segment_feature_data;
748 c.prev_segment_ids = prev_segment_ids.as_deref();
749 // §5.9.22 skip-mode state: the §5.11.10 `read_skip_mode` gate
750 // plus the fixed compound reference pair the skip-mode arm
751 // predicts from.
752 c.skip_mode_present = fh.skip_mode_present.unwrap_or(false);
753 if let Some(smf) = fh.skip_mode_frame {
754 c.skip_mode_frame = [i32::from(smf[0]), i32::from(smf[1])];
755 }
756 c.reference_select = fh.reference_select.unwrap_or(false);
757 if let Some(g) = fh.global_motion_params.as_ref() {
758 for r in 0..8 {
759 c.gm_type[r] = g.gm_type[r] as i32;
760 c.gm_params[r] = g.gm_params[r];
761 }
762 }
763 c.ref_frame_sign_bias = sign_bias;
764 c.allow_high_precision_mv = ir.allow_high_precision_mv;
765 c.force_integer_mv = fh.force_integer_mv;
766 c.use_ref_frame_mvs = ir.use_ref_frame_mvs;
767 c.is_motion_mode_switchable = ir.is_motion_mode_switchable;
768 c.allow_warped_motion = fh.allow_warped_motion.unwrap_or(false);
769 c.is_scaled_per_ref = is_scaled_per_ref;
770 c.enable_interintra_compound = seq.enable_interintra_compound;
771 c.enable_masked_compound = seq.enable_masked_compound;
772 c.enable_jnt_comp = seq.enable_jnt_comp;
773 // §8.3.2 compound-idx ctx + §7.11.3.15 DISTANCE weights both
774 // derive per block from the frame's OrderHint / OrderHints[].
775 c.order_hints = order_hints;
776 c.interpolation_filter = ir.interpolation_filter as u8;
777 c.enable_dual_filter = seq.enable_dual_filter;
778 c.pixels = Some(&pixels);
779 Some(c)
780 } else {
781 None
782 };
783
784 // §8.3.1 frame-start CDF state: `load_cdfs( ref_frame_idx[
785 // primary_ref_frame ] )` when a primary reference exists (the
786 // §7.20-saved frame-end state of that slot, coefficient CDFs
787 // included), otherwise `init_non_coeff_cdfs()` (the §9.4 defaults)
788 // + the q-context-selected `init_coeff_cdfs( base_q_idx )`.
789 let frame_start_cdfs: Box<TileCdfContext> = if fh.primary_ref_frame != PRIMARY_REF_NONE {
790 let st = refs.ok_or(Error::PartitionWalkOutOfRange)?;
791 let ir = fh
792 .inter_refs
793 .as_ref()
794 .ok_or(Error::PartitionWalkOutOfRange)?;
795 let slot = ir.ref_frame_idx[fh.primary_ref_frame as usize] as usize;
796 let slot_state = st
797 .slots
798 .get(slot)
799 .and_then(|s| s.as_ref())
800 .ok_or(Error::PartitionWalkOutOfRange)?;
801 let mut loaded = slot_state.cdfs.clone();
802 // §6.8.21 `load_cdfs`: the symbol counts restart at zero (the
803 // probabilities carry over) — the §8.3 adaptation rate depends
804 // on the per-row count.
805 loaded.zero_counts();
806 loaded
807 } else {
808 let mut c = TileCdfContext::new_from_defaults();
809 c.init_coeff_cdfs(qp.base_q_idx);
810 Box::new(c)
811 };
812
813 // §5.11.2 decode_tile() per tile, in tile order — each tile gets a
814 // fresh §8.2.2 symbol decoder and a fresh copy of the frame-start
815 // CDF state (§8.2.2 `clear_above_context` etc. are per-tile inside
816 // `begin_tile`), while the walker's frame-scope decode grids
817 // accumulate across tiles. The §5.11.57 `read_lr` interleave runs
818 // when the frame signals loop restoration. Per §8.2.4
819 // `exit_symbol` / §8.4 `frame_end_update_cdf`, the tile numbered
820 // `context_update_tile_id` donates its adapted CDFs as the
821 // frame-end state (unless `disable_frame_end_update_cdf`, which
822 // keeps the frame-start state).
823 let mut end_cdfs: Option<Box<TileCdfContext>> = None;
824 let t_tiles = Instant::now();
825 // r475 — tile-parallel walk. §5.11 scopes every context, every
826 // neighbour availability (`is_inside` is tile-relative), the
827 // intra edges, the delta-q / delta-lf / loop-restoration
828 // reference state and the entropy state to the tile, and tile
829 // boundaries are superblock aligned — so an intra tile decoded on
830 // a TILE-SIZED walker (the tile at the origin of its own frame)
831 // reconstructs the samples and stamps the grids the frame walk
832 // would, bit for bit; only §5.11.57 `read_lr` addresses a
833 // frame-level grid (`set_lr_origin`). The inter arms read the
834 // frame-level motion field / reference planes at frame positions
835 // and intra block copy reads the frame-level `CurrFrame`, so
836 // those frames keep the serial walk. Finished tiles are stitched
837 // into the frame walker as they arrive (`import_rect_from` +
838 // the planes + the restoration units), so at most `threads`
839 // tile-sized states are live beyond the frame's own.
840 let tile_parallel =
841 threads > 1 && tiles.len() > 1 && fh.frame_is_intra && !fh.allow_intrabc && !is_inter_frame;
842 if tile_parallel {
843 use std::sync::atomic::{AtomicUsize, Ordering};
844 let worker_count = threads.min(tiles.len());
845 let next = AtomicUsize::new(0);
846 let lr_walk_ref = if lr.uses_lr { Some(&lr_walk) } else { None };
847 let decode_one =
848 |tile_num: usize| -> Result<(PartitionWalker, Box<TileCdfContext>), Error> {
849 let tile = &tiles[tile_num];
850 let tile_row = (tile_num as u32) / ti.tile_cols;
851 let tile_col = (tile_num as u32) % ti.tile_cols;
852 let (r0, r1) = (
853 ti.mi_row_starts[tile_row as usize],
854 ti.mi_row_starts[tile_row as usize + 1],
855 );
856 let (c0, c1) = (
857 ti.mi_col_starts[tile_col as usize],
858 ti.mi_col_starts[tile_col as usize + 1],
859 );
860 let (h4, w4) = (r1 - r0, c1 - c0);
861 let mut local = PartitionWalker::new(
862 h4,
863 w4,
864 TileGeometry {
865 mi_row_start: 0,
866 mi_row_end: h4,
867 mi_col_start: 0,
868 mi_col_end: w4,
869 },
870 )
871 .ok_or(Error::PartitionWalkOutOfRange)?;
872 local.set_lr_origin(r0, c0);
873 local.begin_tile(TileGeometry {
874 mi_row_start: 0,
875 mi_row_end: h4,
876 mi_col_start: 0,
877 mi_col_end: w4,
878 });
879 local.set_current_q_index(i32::from(qp.base_q_idx));
880 let mut decoder = SymbolDecoder::init_symbol(
881 &tile.bytes,
882 tile.bytes.len(),
883 fh.disable_cdf_update,
884 )?;
885 let mut cdfs = frame_start_cdfs.as_ref().clone();
886 local.decode_tile_syntax_with_lr(
887 &mut decoder,
888 &mut cdfs,
889 ¶ms,
890 lr_walk_ref,
891 None,
892 &quant,
893 dq.delta_q_present,
894 )?;
895 Ok((local, Box::new(cdfs)))
896 };
897 let mut first_err: Option<Error> = None;
898 std::thread::scope(|scope| {
899 let (tx, rx) = std::sync::mpsc::channel();
900 for _ in 0..worker_count {
901 let tx = tx.clone();
902 let next = &next;
903 let decode_one = &decode_one;
904 scope.spawn(move || loop {
905 let t = next.fetch_add(1, Ordering::SeqCst);
906 if t >= tiles.len() {
907 break;
908 }
909 let out = decode_one(t);
910 if tx.send((t, out)).is_err() {
911 break;
912 }
913 });
914 }
915 drop(tx);
916 for (t, out) in rx {
917 match out {
918 Ok((local, cdfs)) => {
919 let tile_row = (t as u32) / ti.tile_cols;
920 let tile_col = (t as u32) % ti.tile_cols;
921 let (r0, r1) = (
922 ti.mi_row_starts[tile_row as usize],
923 ti.mi_row_starts[tile_row as usize + 1],
924 );
925 let (c0, c1) = (
926 ti.mi_col_starts[tile_col as usize],
927 ti.mi_col_starts[tile_col as usize + 1],
928 );
929 walker.import_rect_from(
930 &local,
931 0,
932 0,
933 r0,
934 c0,
935 r1 - r0,
936 c1 - c0,
937 sub_x_u8,
938 sub_y_u8,
939 );
940 walker.import_curr_frame_rect_from(&local, r0, c0, sub_x_u8, sub_y_u8);
941 if lr.uses_lr {
942 walker.import_lr_units_from(&local, r0, r1, c0, c1, &lr_walk);
943 }
944 if !fh.disable_frame_end_update_cdf && t as u32 == ti.context_update_tile_id
945 {
946 end_cdfs = Some(cdfs);
947 }
948 }
949 Err(e) => {
950 if first_err.is_none() {
951 first_err = Some(e);
952 }
953 }
954 }
955 }
956 });
957 if let Some(e) = first_err {
958 return Err(e);
959 }
960 }
961 for (tile_num, tile) in tiles.iter().enumerate() {
962 if tile_parallel {
963 break;
964 }
965 let tile_row = (tile_num as u32) / ti.tile_cols;
966 let tile_col = (tile_num as u32) % ti.tile_cols;
967 walker.begin_tile(TileGeometry {
968 mi_row_start: ti.mi_row_starts[tile_row as usize],
969 mi_row_end: ti.mi_row_starts[tile_row as usize + 1],
970 mi_col_start: ti.mi_col_starts[tile_col as usize],
971 mi_col_end: ti.mi_col_starts[tile_col as usize + 1],
972 });
973 // §5.11.1 per-tile prologue: `CurrentQIndex = base_q_idx` —
974 // the §5.11.12 running delta-q accumulator restarts from the
975 // frame quantiser at every tile (r405: it previously started
976 // from the walker-construction zero, so every
977 // `delta_q_present` stream dequantised against a garbage
978 // base).
979 walker.set_current_q_index(i32::from(qp.base_q_idx));
980 let mut decoder =
981 SymbolDecoder::init_symbol(&tile.bytes, tile.bytes.len(), fh.disable_cdf_update)?;
982 let mut cdfs = frame_start_cdfs.as_ref().clone();
983 walker.decode_tile_syntax_with_lr(
984 &mut decoder,
985 &mut cdfs,
986 ¶ms,
987 if lr.uses_lr { Some(&lr_walk) } else { None },
988 /* inter_ctx = */ ictx.as_ref(),
989 &quant,
990 /* read_deltas = */ dq.delta_q_present,
991 )?;
992 if !fh.disable_frame_end_update_cdf && tile_num as u32 == ti.context_update_tile_id {
993 end_cdfs = Some(Box::new(cdfs));
994 }
995 }
996 let end_cdfs = end_cdfs.unwrap_or(frame_start_cdfs);
997 stages.tiles += t_tiles.elapsed();
998
999 // ---- Take `CurrFrame[ plane ]` at its FULL mi-grid extent. ----
1000 // The spec's CurrFrame covers the padded `MiRows x MiCols` grid
1001 // (frames whose dimensions are not mi-aligned carry decoded padding
1002 // columns/rows past the crop). The §7.4 in-loop passes read that
1003 // padding — the §7.14 wide-filter taps, the §7.15 mi-bounded
1004 // availability region, and especially the §7.16 upscaler's
1005 // `Clip3(0, miW * MI_SIZE - 1, ..)` source clamp — so the whole
1006 // chain runs at the padded extent and the §7.18.2 crop to the
1007 // output extents happens at the very end.
1008 let sub_x = u8::from(cc.subsampling_x) as u32;
1009 let sub_y = u8::from(cc.subsampling_y) as u32;
1010 let num_planes = cc.num_planes as usize;
1011 let mut plane_bufs: Vec<Vec<i32>> = Vec::with_capacity(num_planes);
1012 let mut plane_dims: Vec<(u32, u32)> = Vec::with_capacity(num_planes);
1013 for plane in 0..num_planes {
1014 // r453 — MOVE the plane out of the walker (the §7.14/§7.15/
1015 // §7.16/§7.17 in-loop passes below operate on these caller-
1016 // owned buffers; nothing in the walker reads `CurrFrame`
1017 // after the tile walk), saving a full-frame `i32` copy.
1018 let (rows, cols, src) = walker
1019 .take_curr_frame_plane(plane)
1020 .ok_or(Error::PartitionWalkOutOfRange)?;
1021 let (pw, ph) = if plane == 0 {
1022 (fs.frame_width, fs.frame_height)
1023 } else {
1024 (
1025 (fs.frame_width + sub_x) >> sub_x,
1026 (fs.frame_height + sub_y) >> sub_y,
1027 )
1028 };
1029 if rows < ph || cols < pw {
1030 return Err(Error::PartitionWalkOutOfRange);
1031 }
1032 plane_bufs.push(src);
1033 plane_dims.push((cols, rows));
1034 }
1035
1036 // ---- §7.4 in-loop passes: §7.14 deblock, then §7.15 CDEF. ----
1037 // §7.4 step 1: the loop filter is invoked ONLY when
1038 // `loop_filter_level[ 0 ] != 0 || loop_filter_level[ 1 ] != 0` —
1039 // with both luma levels zero the frame is NOT deblocked at all,
1040 // even though the §7.14.4 `loop_filter_delta_enabled` ref-delta
1041 // path could otherwise lift a per-edge strength above zero.
1042 let t_deblock = Instant::now();
1043 if let Some(lf) = fh.loop_filter_params.as_ref() {
1044 if (lf.loop_filter_level[0] != 0 || lf.loop_filter_level[1] != 0)
1045 && !coded_lossless
1046 && !fh.allow_intrabc
1047 {
1048 let mut bufs: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1049 for (buf, &(pw, ph)) in plane_bufs.iter_mut().zip(plane_dims.iter()) {
1050 bufs.push(PlaneBuffer {
1051 rows: ph,
1052 cols: pw,
1053 samples: buf,
1054 });
1055 }
1056 if threads > 1 {
1057 walker.loop_filter_frame_parallel_from_grid(
1058 lf,
1059 sp,
1060 dlf.delta_lf_multi,
1061 cc.num_planes,
1062 cc.bit_depth,
1063 sub_x as u8,
1064 sub_y as u8,
1065 fs.frame_width,
1066 fs.frame_height,
1067 &mut bufs,
1068 threads,
1069 );
1070 } else {
1071 walker.loop_filter_frame_from_grid(
1072 lf,
1073 sp,
1074 dlf.delta_lf_multi,
1075 cc.num_planes,
1076 cc.bit_depth,
1077 sub_x as u8,
1078 sub_y as u8,
1079 fs.frame_width,
1080 fs.frame_height,
1081 &mut bufs,
1082 );
1083 }
1084 }
1085 }
1086 // §7.4 steps 2-5: CDEF, then loop restoration. `plane_bufs` holds
1087 // the post-deblock CurrFrame here; §7.17 needs BOTH that frame (the
1088 // `UpscaledCurrFrame` — no superres on this path) and the CDEF
1089 // output (`UpscaledCdefFrame`), so keep the pre-CDEF copy around
1090 // when restoration is active.
1091 stages.deblock += t_deblock.elapsed();
1092 let t_cdef = Instant::now();
1093 let cdef_active = fh.cdef_params.as_ref().is_some_and(|cdef| {
1094 !coded_lossless && !fh.allow_intrabc && seq.enable_cdef && !cdef.short_circuited
1095 });
1096 let superres_active = fs.use_superres && fs.upscaled_width > fs.frame_width;
1097 if !superres_active {
1098 // r475 — the band / stripe path: §7.15 CDEF runs in place over
1099 // a rolling window of the unfiltered rows its taps read (the
1100 // block ± 2, §7.15.3 `cdef_get_at` bounded by §5.11.52), and
1101 // §7.17 restores one stripe at a time from a copy of the
1102 // stripe's CDEF rows plus the pre-CDEF rows §7.17.6 reads at
1103 // the stripe's 2-row skirts, captured here from the deblocked
1104 // frame. No frame-sized copy of `CurrFrame` / `CdefFrame` is
1105 // made (the pre-r475 driver held three); every sample is
1106 // computed from the same inputs, so the output is identical.
1107 // The §7.16 superres arm below keeps the whole-frame path (it
1108 // upscales both frames).
1109 let boundary: Vec<crate::loop_restoration::StripeBoundaryRows> = if lr.uses_lr {
1110 plane_bufs
1111 .iter_mut()
1112 .zip(plane_dims.iter())
1113 .enumerate()
1114 .map(|(plane, (buf, &(pw, ph)))| {
1115 let sy = if plane == 0 { 0 } else { sub_y };
1116 let plane_h = (fs.frame_height + sy) >> sy;
1117 let pb = PlaneBuffer {
1118 rows: ph,
1119 cols: pw,
1120 samples: buf,
1121 };
1122 crate::loop_restoration::StripeBoundaryRows::capture(
1123 &pb,
1124 plane_h as i32 - 1,
1125 sy as u8,
1126 )
1127 })
1128 .collect()
1129 } else {
1130 Vec::new()
1131 };
1132 let mut bufs: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1133 for (buf, &(pw, ph)) in plane_bufs.iter_mut().zip(plane_dims.iter()) {
1134 bufs.push(PlaneBuffer {
1135 rows: ph,
1136 cols: pw,
1137 samples: buf,
1138 });
1139 }
1140 if let (true, Some(cdef)) = (cdef_active, fh.cdef_params.as_ref()) {
1141 if threads > 1 {
1142 walker.cdef_frame_parallel_from_idx(
1143 cdef,
1144 cc.num_planes,
1145 cc.bit_depth,
1146 sub_x as u8,
1147 sub_y as u8,
1148 &mut bufs,
1149 threads,
1150 );
1151 } else {
1152 walker.cdef_frame_in_place_from_idx(
1153 cdef,
1154 cc.num_planes,
1155 cc.bit_depth,
1156 sub_x as u8,
1157 sub_y as u8,
1158 &mut bufs,
1159 );
1160 }
1161 }
1162 stages.cdef += t_cdef.elapsed();
1163 let t_lr = Instant::now();
1164 if lr.uses_lr {
1165 if threads > 1 {
1166 walker.loop_restore_frame_parallel_from_grid(
1167 lr,
1168 cc.num_planes,
1169 cc.bit_depth,
1170 sub_x as u8,
1171 sub_y as u8,
1172 mi_rows,
1173 mi_cols,
1174 fs.frame_height,
1175 fs.upscaled_width,
1176 &mut bufs,
1177 &boundary,
1178 threads,
1179 );
1180 } else {
1181 walker.loop_restore_frame_in_place_from_grid(
1182 lr,
1183 cc.num_planes,
1184 cc.bit_depth,
1185 sub_x as u8,
1186 sub_y as u8,
1187 mi_rows,
1188 mi_cols,
1189 fs.frame_height,
1190 fs.upscaled_width,
1191 &mut bufs,
1192 &boundary,
1193 );
1194 }
1195 }
1196 stages.restoration += t_lr.elapsed();
1197 } else {
1198 let mut deblocked: Option<Vec<Vec<i32>>> = if lr.uses_lr {
1199 Some(plane_bufs.clone())
1200 } else {
1201 None
1202 };
1203 if let Some(cdef) = fh.cdef_params.as_ref() {
1204 if !coded_lossless && !fh.allow_intrabc && seq.enable_cdef && !cdef.short_circuited {
1205 // §7.15 filters from the deblocked frame. r453 — when
1206 // §7.17 is active the `deblocked` copy above IS that
1207 // frame (CDEF only reads its source), so borrow it
1208 // instead of cloning a third full-frame buffer.
1209 let mut fallback: Option<Vec<Vec<i32>>> = None;
1210 let src_owned: &mut Vec<Vec<i32>> = match deblocked {
1211 Some(ref mut d) => d,
1212 None => fallback.insert(plane_bufs.clone()),
1213 };
1214 let mut src: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1215 for (buf, &(pw, ph)) in src_owned.iter_mut().zip(plane_dims.iter()) {
1216 src.push(PlaneBuffer {
1217 rows: ph,
1218 cols: pw,
1219 samples: buf,
1220 });
1221 }
1222 let mut dst: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1223 for (buf, &(pw, ph)) in plane_bufs.iter_mut().zip(plane_dims.iter()) {
1224 dst.push(PlaneBuffer {
1225 rows: ph,
1226 cols: pw,
1227 samples: buf,
1228 });
1229 }
1230 walker.cdef_frame_from_idx(
1231 cdef,
1232 cc.num_planes,
1233 cc.bit_depth,
1234 sub_x as u8,
1235 sub_y as u8,
1236 &src,
1237 &mut dst,
1238 );
1239 }
1240 }
1241 stages.cdef += t_cdef.elapsed();
1242 // §7.4 steps 3-4 / §7.16: horizontal superres upscaling of BOTH the
1243 // CDEF output (`plane_bufs` → UpscaledCdefFrame) and the post-
1244 // deblock copy (`deblocked` → UpscaledCurrFrame), ahead of loop
1245 // restoration. No-op when `use_superres == 0`.
1246 let t_superres = Instant::now();
1247 if fs.use_superres && fs.upscaled_width > fs.frame_width {
1248 let sr_ctx = crate::superres::SuperresFrameContext {
1249 use_superres: true,
1250 frame_width: fs.frame_width,
1251 upscaled_width: fs.upscaled_width,
1252 frame_height: fs.frame_height,
1253 mi_cols: fs.mi_cols,
1254 num_planes: cc.num_planes,
1255 bit_depth: cc.bit_depth,
1256 subsampling_x: u8::from(cc.subsampling_x),
1257 subsampling_y: u8::from(cc.subsampling_y),
1258 };
1259 // §7.16 output extents: `upscaledPlaneW x planeH` — exact
1260 // (un-padded) per-plane dimensions.
1261 let mut new_dims: Vec<(u32, u32)> = Vec::with_capacity(num_planes);
1262 for plane in 0..num_planes {
1263 let (out_w, out_h) = if plane == 0 {
1264 (fs.upscaled_width, fs.frame_height)
1265 } else {
1266 (
1267 (fs.upscaled_width + sub_x) >> sub_x,
1268 (fs.frame_height + sub_y) >> sub_y,
1269 )
1270 };
1271 new_dims.push((out_w, out_h));
1272 }
1273 let upscale_set = |bufs: &mut Vec<Vec<i32>>| -> Result<(), Error> {
1274 let mut inputs_owned = std::mem::take(bufs);
1275 let mut inputs: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1276 for (buf, &(pw, ph)) in inputs_owned.iter_mut().zip(plane_dims.iter()) {
1277 inputs.push(PlaneBuffer {
1278 rows: ph,
1279 cols: pw,
1280 samples: buf,
1281 });
1282 }
1283 let mut outputs_owned: Vec<Vec<i32>> = new_dims
1284 .iter()
1285 .map(|&(pw, ph)| vec![0i32; (pw as usize) * (ph as usize)])
1286 .collect();
1287 {
1288 let mut outputs: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1289 for (buf, &(pw, ph)) in outputs_owned.iter_mut().zip(new_dims.iter()) {
1290 outputs.push(PlaneBuffer {
1291 rows: ph,
1292 cols: pw,
1293 samples: buf,
1294 });
1295 }
1296 crate::superres::upscale_frame(&sr_ctx, &inputs, &mut outputs)
1297 .map_err(|_| Error::PartitionWalkOutOfRange)?;
1298 }
1299 *bufs = outputs_owned;
1300 Ok(())
1301 };
1302 upscale_set(&mut plane_bufs)?;
1303 if let Some(d) = deblocked.as_mut() {
1304 upscale_set(d)?;
1305 }
1306 plane_dims = new_dims;
1307 }
1308
1309 stages.superres += t_superres.elapsed();
1310 // §7.4 step 5 / §7.17: loop restoration from (UpscaledCurrFrame,
1311 // UpscaledCdefFrame) into LrFrame. The §5.11.57 units were decoded
1312 // by the tile walk's `read_lr` interleave above.
1313 let t_lr = Instant::now();
1314 if let Some(mut curr_owned) = deblocked {
1315 let mut cdef_owned: Vec<Vec<i32>> = plane_bufs.clone();
1316 let mut curr: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1317 for (buf, &(pw, ph)) in curr_owned.iter_mut().zip(plane_dims.iter()) {
1318 curr.push(PlaneBuffer {
1319 rows: ph,
1320 cols: pw,
1321 samples: buf,
1322 });
1323 }
1324 let mut cdef_bufs: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1325 for (buf, &(pw, ph)) in cdef_owned.iter_mut().zip(plane_dims.iter()) {
1326 cdef_bufs.push(PlaneBuffer {
1327 rows: ph,
1328 cols: pw,
1329 samples: buf,
1330 });
1331 }
1332 let mut lr_out: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1333 for (buf, &(pw, ph)) in plane_bufs.iter_mut().zip(plane_dims.iter()) {
1334 lr_out.push(PlaneBuffer {
1335 rows: ph,
1336 cols: pw,
1337 samples: buf,
1338 });
1339 }
1340 walker.loop_restore_frame_from_grid(
1341 lr,
1342 cc.num_planes,
1343 cc.bit_depth,
1344 sub_x as u8,
1345 sub_y as u8,
1346 mi_rows,
1347 mi_cols,
1348 fs.frame_height,
1349 fs.upscaled_width,
1350 &curr,
1351 &cdef_bufs,
1352 &mut lr_out,
1353 );
1354 }
1355 stages.restoration += t_lr.elapsed();
1356 }
1357 // ---- §7.18.2 intermediate-output crop to the surfaced extents. ----
1358 // On the superres path the buffers are already exact; otherwise
1359 // trim the mi-grid padding down to `UpscaledWidth x FrameHeight`
1360 // (per-plane subsampled). §7.18.3 film grain then applies to the
1361 // cropped output planes.
1362 let t_ref = Instant::now();
1363 for plane in 0..num_planes {
1364 let (out_w, out_h) = if plane == 0 {
1365 (fs.upscaled_width, fs.frame_height)
1366 } else {
1367 (
1368 (fs.upscaled_width + sub_x) >> sub_x,
1369 (fs.frame_height + sub_y) >> sub_y,
1370 )
1371 };
1372 let (pw, ph) = plane_dims[plane];
1373 if (pw, ph) == (out_w, out_h) {
1374 continue;
1375 }
1376 if pw < out_w || ph < out_h {
1377 return Err(Error::PartitionWalkOutOfRange);
1378 }
1379 let src = &plane_bufs[plane];
1380 let mut buf = vec![0i32; (out_w as usize) * (out_h as usize)];
1381 for y in 0..out_h as usize {
1382 let row = &src[y * pw as usize..y * pw as usize + out_w as usize];
1383 buf[y * out_w as usize..(y + 1) * out_w as usize].copy_from_slice(row);
1384 }
1385 plane_bufs[plane] = buf;
1386 plane_dims[plane] = (out_w, out_h);
1387 }
1388
1389 // ---- §7.19 motion field motion vector storage. ----
1390 // Filter the §5.11.5 `Mvs[]` / `RefFrames[]` grids down to the
1391 // `MfMvs[]` / `MfRefFrames[]` payload §7.20 stores: per cell, the
1392 // LAST candidate list whose reference lies in the past
1393 // (`get_relative_dist( RefOrderHint[ refIdx ], OrderHint ) < 0`)
1394 // and whose MV components sit within `REFMVS_LIMIT`.
1395 const REFMVS_LIMIT: i16 = (1 << 12) - 1;
1396 let cells = (mi_rows as usize) * (mi_cols as usize);
1397 let mut mf_ref_frames: Vec<i8> = vec![-1; cells];
1398 let mut mf_mvs: Vec<i16> = vec![0; cells * 2];
1399 if is_inter_frame {
1400 let st = refs.ok_or(Error::PartitionWalkOutOfRange)?;
1401 let raw_refs = walker.ref_frames();
1402 let raw_mvs = walker.mvs();
1403 let hint_bits = if seq.enable_order_hint {
1404 u32::from(seq.order_hint_bits)
1405 } else {
1406 0
1407 };
1408 for cell in 0..cells {
1409 for list in 0..2usize {
1410 let r = raw_refs[cell * 2 + list];
1411 if r > 0 {
1412 let slot = ref_frame_idx[(r - 1) as usize] as usize;
1413 let dist = get_relative_dist(
1414 st.info.order_hint[slot] as i32,
1415 fh.order_hint as i32,
1416 hint_bits,
1417 );
1418 if dist < 0 {
1419 let mv_row = raw_mvs[(cell * 2 + list) * 2];
1420 let mv_col = raw_mvs[(cell * 2 + list) * 2 + 1];
1421 if mv_row.abs() <= REFMVS_LIMIT && mv_col.abs() <= REFMVS_LIMIT {
1422 mf_ref_frames[cell] = r;
1423 mf_mvs[cell * 2] = mv_row;
1424 mf_mvs[cell * 2 + 1] = mv_col;
1425 }
1426 }
1427 }
1428 }
1429 }
1430 }
1431
1432 // §7.20 `SavedSegmentIds` payload — the walker's stamped
1433 // `SegmentIds[][]` grid; unwritten `-1` cells clamp to `0` (the
1434 // spec map is always written: `segment_id = 0` when segmentation
1435 // is disabled).
1436 let segment_ids: Vec<i32> = walker.segment_ids().iter().map(|&v| v.max(0)).collect();
1437 // r475 — nothing below reads the walker: release its per-mi grids
1438 // (tens of MB at 12 MP) before the output buffers are built.
1439 drop(walker);
1440
1441 // ---- §7.20 store payload: pre-grain cropped planes (u16). ----
1442 // §7.18.3 film grain applies to the OUTPUT copy only; the §7.20
1443 // reference store keeps the grain-free frame (the samples later
1444 // frames motion-compensate against). r475 — under
1445 // `reduced_still_picture_header` the coded video sequence holds
1446 // exactly this one frame (§5.5.1: it implies `still_picture`), no
1447 // later frame can reference the store, and `show_existing_frame`
1448 // is forced off — so the planes are not materialised (36 MiB at
1449 // 12 MP 4:2:0); the empty store reads as the never-referenced
1450 // placeholder if a non-conformant stream tries.
1451 let store_planes = !seq.reduced_still_picture_header;
1452 let ref_plane_dims: Vec<(u32, u32)> = plane_dims.clone();
1453 let to_u16 = |buf: &[i32]| -> Vec<u16> { buf.iter().map(|&v| v.max(0) as u16).collect() };
1454 let max_val: i32 = (1 << cc.bit_depth) - 1;
1455 let narrow = |buf: Vec<i32>| -> Vec<u8> {
1456 if cc.bit_depth == 8 {
1457 buf.into_iter().map(|v| v.clamp(0, 255) as u8).collect()
1458 } else {
1459 let mut out = Vec::with_capacity(buf.len() * 2);
1460 for v in buf {
1461 let v = v.clamp(0, max_val) as u16;
1462 out.extend_from_slice(&v.to_le_bytes());
1463 }
1464 out
1465 }
1466 };
1467
1468 // ---- §7.18.3 film grain. ----
1469 // §5.9.30 `update_grain == 0` predicted path: `load_grain_params(
1470 // film_grain_params_ref_idx )` — every field except `grain_seed`
1471 // comes from the referenced slot's §7.20-saved state (the
1472 // `tempGrainSeed` dance keeps the newly-read seed). A slot with
1473 // no saved grain state resolves to `reset_grain_params()`.
1474 let effective_grain: Option<crate::uncompressed_header_tail::FilmGrainParams> =
1475 match fh.film_grain_params.as_ref() {
1476 Some(fg) if fg.apply_grain && fg.predicted => {
1477 let slot = fg.film_grain_params_ref_idx as usize;
1478 refs.and_then(|st| st.slots.get(slot))
1479 .and_then(|s| s.as_ref())
1480 .and_then(|s| s.grain_params.clone())
1481 .map(|mut g| {
1482 g.grain_seed = fg.grain_seed;
1483 g.apply_grain = true;
1484 g
1485 })
1486 }
1487 Some(fg) if fg.apply_grain => Some(fg.clone()),
1488 _ => None,
1489 };
1490 let (ref_planes, planes): (Vec<Vec<u16>>, Vec<Vec<u8>>) = match effective_grain.as_ref() {
1491 Some(fg) if fg.apply_grain => {
1492 // Grain needs every plane at once (chroma scaling reads
1493 // luma): store first, synthesise, then narrow.
1494 let ref_planes: Vec<Vec<u16>> = if store_planes {
1495 plane_bufs.iter().map(|b| to_u16(b)).collect()
1496 } else {
1497 Vec::new()
1498 };
1499 stages.ref_store += t_ref.elapsed();
1500 let t_grain = Instant::now();
1501 let mut bufs: Vec<PlaneBuffer<'_>> = Vec::with_capacity(num_planes);
1502 for (buf, &(pw, ph)) in plane_bufs.iter_mut().zip(plane_dims.iter()) {
1503 bufs.push(PlaneBuffer {
1504 rows: ph,
1505 cols: pw,
1506 samples: buf,
1507 });
1508 }
1509 film_grain_synthesis(
1510 fg,
1511 cc.bit_depth,
1512 cc.num_planes,
1513 u8::from(cc.subsampling_x),
1514 u8::from(cc.subsampling_y),
1515 cc.matrix_coefficients,
1516 &mut bufs,
1517 );
1518 stages.grain += t_grain.elapsed();
1519 let t_out = Instant::now();
1520 let planes: Vec<Vec<u8>> = plane_bufs.into_iter().map(narrow).collect();
1521 stages.output += t_out.elapsed();
1522 (ref_planes, planes)
1523 }
1524 _ => {
1525 // r475 — no grain: one plane at a time, the `i32` plane
1526 // released as soon as its `u16` store copy and output bytes
1527 // exist (the peak holds one plane three ways, not all of
1528 // them).
1529 let t_out = Instant::now();
1530 let mut ref_planes: Vec<Vec<u16>> = Vec::with_capacity(num_planes);
1531 let mut planes: Vec<Vec<u8>> = Vec::with_capacity(num_planes);
1532 for buf in plane_bufs {
1533 if store_planes {
1534 ref_planes.push(to_u16(&buf));
1535 }
1536 planes.push(narrow(buf));
1537 }
1538 stages.ref_store += t_ref.elapsed();
1539 stages.output += t_out.elapsed();
1540 (ref_planes, planes)
1541 }
1542 };
1543
1544 Ok(DecodedFrameInternal {
1545 frame: SpecFrame {
1546 // Post-§7.16 the surfaced luma width is `UpscaledWidth`
1547 // (`== FrameWidth` when superres is off).
1548 width: fs.upscaled_width,
1549 height: fs.frame_height,
1550 planes,
1551 plane_dims,
1552 bit_depth: cc.bit_depth,
1553 },
1554 ref_planes,
1555 ref_plane_dims,
1556 mf_mvs,
1557 mf_ref_frames,
1558 order_hints_by_ref,
1559 end_cdfs,
1560 mi_rows,
1561 mi_cols,
1562 segment_ids,
1563 grain_params: effective_grain,
1564 })
1565}
1566
1567/// §7.9 `motion_field_estimation()` — adapter over the shared
1568/// [`crate::inter_pred::motion_field_estimation_core`] (the encoder's
1569/// write mirror runs the SAME core, so both sides project identical
1570/// `MotionFieldMvs`): builds one [`MotionFieldSlot`] view per stored
1571/// §7.20 slot plus the current frame's §5.9.2 scalars.
1572fn motion_field_estimation(
1573 refs: &SpecRefState,
1574 fh: &FrameHeader,
1575 seq: &SequenceHeader,
1576 mi_rows: u32,
1577 mi_cols: u32,
1578) -> Result<MotionFieldMvs, Error> {
1579 use crate::inter_pred::{motion_field_estimation_core, MotionFieldSlot};
1580 let ir = fh
1581 .inter_refs
1582 .as_ref()
1583 .ok_or(Error::PartitionWalkOutOfRange)?;
1584 let hint_bits = if seq.enable_order_hint {
1585 u32::from(seq.order_hint_bits)
1586 } else {
1587 0
1588 };
1589 let order_hint = fh.order_hint as i32;
1590 // §5.9.2 `OrderHints[ LAST_FRAME + i ]`.
1591 let mut order_hints = [0i32; ALTREF_FRAME + 1];
1592 for i in 0..7 {
1593 order_hints[LAST_FRAME + i] = refs.info.order_hint[ir.ref_frame_idx[i] as usize] as i32;
1594 }
1595 let mut slots: [Option<MotionFieldSlot<'_>>; 8] = [None; 8];
1596 for (i, slot) in refs.slots.iter().enumerate() {
1597 slots[i] = slot.as_ref().map(|st| MotionFieldSlot {
1598 mf_mvs: &st.mf_mvs,
1599 mf_ref_frames: &st.mf_ref_frames,
1600 saved_order_hints: st.saved_order_hints,
1601 mi_rows: st.mi_rows,
1602 mi_cols: st.mi_cols,
1603 frame_is_intra: st.frame_is_intra,
1604 });
1605 }
1606 Ok(motion_field_estimation_core(
1607 &slots,
1608 &ir.ref_frame_idx,
1609 &order_hints,
1610 order_hint,
1611 hint_bits,
1612 mi_rows,
1613 mi_cols,
1614 ))
1615}
1616
1617/// Decode an AV1 IVF v0 buffer through the spec-faithful frame driver.
1618///
1619/// Walks the IVF frame records, and within each temporal unit walks the
1620/// §5.2 OBU sequence: `OBU_TEMPORAL_DELIMITER` / `OBU_SEQUENCE_HEADER`
1621/// / `OBU_FRAME_HEADER` + `OBU_TILE_GROUP` / the combined `OBU_FRAME`
1622/// (§5.10: frame header + `byte_alignment()` + tile group in one OBU,
1623/// split via [`FrameHeader::bits_consumed`]). Padding and metadata OBUs
1624/// are skipped.
1625pub fn decode_av1_spec(input: &[u8]) -> Result<Vec<SpecFrame>, Error> {
1626 SpecDecodeSession::new().decode_ivf(input)
1627}
1628
1629/// r452 — default picture-size ceiling (luma samples,
1630/// `UpscaledWidth * FrameHeight`) a session accepts before reserving
1631/// frame-sized storage.
1632///
1633/// Annex A §A.3 bounds every DEFINED level's picture by `MaxPicSize`;
1634/// the largest defined value (levels 6.0–6.3) is 35 651 584 samples
1635/// (16384 × 2176, or 8192 × 4352). For `seq_level_idx = 31`
1636/// ("maximum parameters") the spec's note leaves the bound to the
1637/// decoder maker, and reserved level indices carry no table row at
1638/// all — this crate applies the largest defined `MaxPicSize` uniformly
1639/// as its documented ceiling rather than trusting the header's level
1640/// field (encoders routinely under-declare it). A header past the
1641/// ceiling surfaces [`Error::PictureSizeExceedsLimit`] before any
1642/// `MiRows * MiCols` grid or plane buffer is allocated.
1643pub const MAX_PICTURE_SIZE: u32 = 35_651_584;
1644
1645/// The Annex A picture-size gate: `UpscaledWidth * FrameHeight` must
1646/// not exceed `cap`. Runs on the parsed header before any frame-sized
1647/// reservation.
1648fn check_picture_size(fh: &FrameHeader, cap: u32) -> Result<(), Error> {
1649 if let Some(fs) = fh.frame_size.as_ref() {
1650 let samples = u64::from(fs.upscaled_width) * u64::from(fs.frame_height);
1651 if samples > u64::from(cap) {
1652 return Err(Error::PictureSizeExceedsLimit);
1653 }
1654 }
1655 Ok(())
1656}
1657
1658/// r430 — [`decode_av1_spec`] at an externally selected operating
1659/// point (§6.7.5): the §5.3.1 `drop_obu()` rule skips every OBU
1660/// outside the selected point's temporal/spatial layer set, so a
1661/// temporally scalable stream decodes to exactly the shown frames of
1662/// the surviving layers. `operating_point = 0` (the list's preferred
1663/// entry) reproduces [`decode_av1_spec`] byte for byte.
1664///
1665/// ## Errors
1666///
1667/// * [`Error::OperatingPointOutOfRange`] — `operating_point` exceeds
1668/// the sequence header's `operating_points_cnt_minus_1`.
1669/// * Every [`decode_av1_spec`] error surface otherwise.
1670pub fn decode_av1_spec_at_operating_point(
1671 input: &[u8],
1672 operating_point: u8,
1673) -> Result<Vec<SpecFrame>, Error> {
1674 let mut session = SpecDecodeSession::new();
1675 session.set_operating_point(operating_point)?;
1676 session.decode_ivf(input)
1677}
1678
1679/// Cross-packet decode session — the §7.20 reference-frame store, the
1680/// cached sequence header, and the per-slot CDF / motion-field /
1681/// segment-id state, held across successive temporal units so a
1682/// container demuxer can feed one §7.5 temporal unit (or one IVF
1683/// record payload) per packet and decode whole GOPs.
1684///
1685/// [`decode_av1_spec`] is the one-shot IVF convenience wrapper over
1686/// this type; the `oxideav-core` registry decoder drives it directly.
1687#[derive(Debug)]
1688pub struct SpecDecodeSession {
1689 seq: Option<SequenceHeader>,
1690 refs: SpecRefState,
1691 /// r430 — the operating point selected by external means (the
1692 /// §6.7.5 `choose_operating_point()` return value). Defaults to
1693 /// `0` — the earliest (preferred) entry of the sequence header's
1694 /// operating-point list.
1695 chosen_op: u8,
1696 /// §5.5.1 `OperatingPointIdc = operating_point_idc[
1697 /// operatingPoint ]` — refreshed at every sequence-header parse.
1698 /// `0` = scalability not in use (the §5.3.1 `drop_obu()` arm
1699 /// never fires).
1700 op_idc: u16,
1701 /// r452 — the picture-size ceiling (luma samples) enforced on
1702 /// every parsed frame header before frame-sized storage is
1703 /// reserved. Defaults to [`MAX_PICTURE_SIZE`].
1704 max_picture_size: u32,
1705 /// r475 — per-stage wall time of the last `decode_temporal_unit`.
1706 stages: DecodeStageTimings,
1707 /// r475 — the worker budget (`1` = serial).
1708 threads: usize,
1709}
1710
1711impl Default for SpecDecodeSession {
1712 fn default() -> Self {
1713 Self::new()
1714 }
1715}
1716
1717impl SpecDecodeSession {
1718 /// Fresh session: no sequence header, every §7.20 slot empty.
1719 #[must_use]
1720 pub fn new() -> Self {
1721 Self {
1722 seq: None,
1723 refs: SpecRefState::new(),
1724 chosen_op: 0,
1725 op_idc: 0,
1726 max_picture_size: MAX_PICTURE_SIZE,
1727 stages: DecodeStageTimings::default(),
1728 threads: 1,
1729 }
1730 }
1731
1732 /// r475 — grant the session a worker budget: a multi-tile frame
1733 /// decodes its tiles `threads`-wide (the post-filters by row
1734 /// band), byte-identical to the serial decode for any budget.
1735 /// `0` / `1` = serial.
1736 pub fn set_threads(&mut self, threads: usize) {
1737 self.threads = threads.max(1);
1738 }
1739
1740 /// The worker budget granted through [`Self::set_threads`].
1741 #[must_use]
1742 pub fn threads(&self) -> usize {
1743 self.threads
1744 }
1745
1746 /// r452 — lower (or raise) the picture-size ceiling
1747 /// ([`MAX_PICTURE_SIZE`] by default) applied to every frame
1748 /// header this session parses: a header whose
1749 /// `UpscaledWidth * FrameHeight` exceeds `max_samples` is rejected
1750 /// with [`Error::PictureSizeExceedsLimit`] before any frame-sized
1751 /// buffer is reserved. Memory-constrained hosts (and the fuzz
1752 /// harness, which runs under a hard RSS limit) bound their
1753 /// worst-case reservation here.
1754 pub fn set_max_picture_size(&mut self, max_samples: u32) {
1755 self.max_picture_size = max_samples;
1756 }
1757
1758 /// The session's current picture-size ceiling (luma samples).
1759 #[must_use]
1760 pub fn max_picture_size(&self) -> u32 {
1761 self.max_picture_size
1762 }
1763
1764 /// Decode a whole IVF v0 buffer through this session — the
1765 /// one-shot [`decode_av1_spec`] walk (every IVF record is one §7.5
1766 /// temporal unit) over a caller-configured session, so the
1767 /// operating point and picture-size ceiling apply.
1768 ///
1769 /// ## Errors
1770 ///
1771 /// [`Error::UnexpectedEnd`] on a malformed IVF wrapper, plus every
1772 /// [`Self::decode_temporal_unit`] error surface.
1773 pub fn decode_ivf(&mut self, input: &[u8]) -> Result<Vec<SpecFrame>, Error> {
1774 let reader = IvfReader::new(input).map_err(|_| Error::UnexpectedEnd)?;
1775 let records = reader.read_all().map_err(|_| Error::UnexpectedEnd)?;
1776 let mut out = Vec::new();
1777 for record in records {
1778 out.extend(self.decode_temporal_unit(&record.payload)?);
1779 }
1780 Ok(out)
1781 }
1782
1783 /// r430 — select the operating point (§6.7.5) by external means.
1784 ///
1785 /// `operating_point` is an index into the sequence header's
1786 /// operating-point list (`0..=operating_points_cnt_minus_1`; `0`
1787 /// is the list's preferred entry and the session default). The
1788 /// §5.5.1 `OperatingPointIdc` derivation re-runs at every
1789 /// sequence-header parse, so the selection naturally applies to
1790 /// each new coded video sequence; when a sequence header has
1791 /// already been parsed the selection is re-derived immediately
1792 /// against it.
1793 ///
1794 /// With a non-zero `OperatingPointIdc`, every OBU that carries an
1795 /// extension header and lies outside the operating point's
1796 /// temporal/spatial layer set is dropped per the §5.3.1
1797 /// `drop_obu()` rule — decoding a temporally scalable stream at a
1798 /// reduced operating point yields exactly the shown frames of the
1799 /// selected layer subset.
1800 ///
1801 /// ## Errors
1802 ///
1803 /// [`Error::OperatingPointOutOfRange`] when a sequence header is
1804 /// cached and `operating_point` exceeds its
1805 /// `operating_points_cnt_minus_1` (the same reject surfaces from
1806 /// [`Self::decode_temporal_unit`] at the next sequence-header
1807 /// parse otherwise).
1808 pub fn set_operating_point(&mut self, operating_point: u8) -> Result<(), Error> {
1809 self.chosen_op = operating_point;
1810 if let Some(s) = self.seq.as_ref() {
1811 let idx = usize::from(operating_point);
1812 if idx >= s.operating_points.len() {
1813 return Err(Error::OperatingPointOutOfRange);
1814 }
1815 self.op_idc = s.operating_points[idx].operating_point_idc;
1816 }
1817 Ok(())
1818 }
1819
1820 /// The current §5.5.1 `OperatingPointIdc` (`0` until a sequence
1821 /// header with a non-zero `operating_point_idc` for the selected
1822 /// operating point is parsed).
1823 #[must_use]
1824 pub fn operating_point_idc(&self) -> u16 {
1825 self.op_idc
1826 }
1827
1828 /// Decode one §7.5 temporal-unit body (a low-overhead OBU
1829 /// bytestream: temporal delimiter / sequence header / frame
1830 /// header / tile group / frame OBUs), returning every SHOWN frame
1831 /// in output order and updating the session's reference state.
1832 ///
1833 /// ## Errors
1834 ///
1835 /// Every [`decode_av1_spec`] error surface — malformed OBUs, a
1836 /// frame arriving before any sequence header, reference slots the
1837 /// stream names but never filled, or feature shapes outside the
1838 /// decoder's scope.
1839 pub fn decode_temporal_unit(&mut self, payload: &[u8]) -> Result<Vec<SpecFrame>, Error> {
1840 let mut out = Vec::new();
1841 self.stages = DecodeStageTimings::default();
1842 decode_temporal_unit_spec(
1843 payload,
1844 &mut self.seq,
1845 &mut self.refs,
1846 &mut out,
1847 self.chosen_op,
1848 &mut self.op_idc,
1849 self.max_picture_size,
1850 &mut self.stages,
1851 self.threads,
1852 )?;
1853 Ok(out)
1854 }
1855
1856 /// r475 — wall time per decode stage of the last
1857 /// [`Self::decode_temporal_unit`] call (summed over the unit's
1858 /// frames; all-zero before the first call).
1859 #[doc(hidden)]
1860 #[must_use]
1861 pub fn last_stage_timings(&self) -> DecodeStageTimings {
1862 self.stages
1863 }
1864
1865 /// Drop the §7.20 reference store (a seek discontinuity) while
1866 /// keeping the cached sequence header — the next temporal unit
1867 /// must start a new coded video sequence (a KEY frame), which
1868 /// rebuilds every slot before anything can reference it.
1869 pub fn reset_references(&mut self) {
1870 self.refs = SpecRefState::new();
1871 }
1872}
1873
1874/// §7.20 `reference_frame_update()` — store the just-decoded frame
1875/// into every slot `refresh_frame_flags` selects, updating the
1876/// §5.9.2 `RefInfo` arrays in lockstep.
1877fn reference_frame_update(
1878 refs: &mut SpecRefState,
1879 fh: &FrameHeader,
1880 decoded: DecodedFrameInternal,
1881) -> Result<SpecFrame, Error> {
1882 if fh.refresh_frame_flags == 0 {
1883 return Ok(decoded.frame);
1884 }
1885 let fs = fh
1886 .frame_size
1887 .as_ref()
1888 .ok_or(Error::PartitionWalkOutOfRange)?;
1889 // §7.20 per-slot payload: pixels + §7.19 motion-field grids +
1890 // `SavedOrderHints` + `save_cdfs` + `SavedGmParams` +
1891 // `save_loop_filter_params`.
1892 let mut gm_params = crate::uncompressed_header_tail::prev_gm_params_default();
1893 if let Some(g) = fh.global_motion_params.as_ref() {
1894 gm_params = g.gm_params;
1895 }
1896 let (lf_ref_deltas, lf_mode_deltas) = fh
1897 .loop_filter_params
1898 .as_ref()
1899 .map(|lf| (lf.loop_filter_ref_deltas, lf.loop_filter_mode_deltas))
1900 .unwrap_or((
1901 crate::uncompressed_header_tail::LOOP_FILTER_REF_DELTAS_DEFAULT,
1902 [0i8; 2],
1903 ));
1904 // §7.20 `save_segmentation_params( i )` — the current frame's
1905 // FeatureEnabled / FeatureData (post-§5.9.14, including the
1906 // `segmentation_update_data == 0` loaded values).
1907 let (seg_active, seg_data) = fh
1908 .segmentation_params
1909 .as_ref()
1910 .map(|sp| (sp.segment_feature_active, sp.segment_feature_data))
1911 .unwrap_or((
1912 [[false; crate::uncompressed_header_tail::SEG_LVL_MAX]
1913 as [bool; crate::uncompressed_header_tail::SEG_LVL_MAX];
1914 crate::uncompressed_header_tail::MAX_SEGMENTS],
1915 [[0i16; crate::uncompressed_header_tail::SEG_LVL_MAX];
1916 crate::uncompressed_header_tail::MAX_SEGMENTS],
1917 ));
1918 // r453 — the payload MOVES out of the decode result (the surfaced
1919 // output frame is a separate buffer) and is Arc-shared across the
1920 // selected slots.
1921 let DecodedFrameInternal {
1922 frame,
1923 ref_planes,
1924 ref_plane_dims,
1925 mf_mvs,
1926 mf_ref_frames,
1927 order_hints_by_ref,
1928 end_cdfs,
1929 mi_rows,
1930 mi_cols,
1931 segment_ids,
1932 grain_params,
1933 } = decoded;
1934 let payload = Arc::new(SpecRefSlot {
1935 planes: ref_planes,
1936 plane_dims: ref_plane_dims,
1937 mf_mvs,
1938 mf_ref_frames,
1939 saved_order_hints: order_hints_by_ref,
1940 mi_rows,
1941 mi_cols,
1942 frame_is_intra: fh.frame_is_intra,
1943 frame_type_is_key: matches!(fh.frame_type, crate::frame_header::FrameType::Key),
1944 bit_depth: frame.bit_depth,
1945 cdfs: end_cdfs,
1946 gm_params,
1947 lf_ref_deltas,
1948 lf_mode_deltas,
1949 segment_ids,
1950 grain_params,
1951 });
1952 for i in 0..NUM_REF_FRAMES as usize {
1953 if (fh.refresh_frame_flags >> i) & 1 != 0 {
1954 refs.info.valid[i] = true;
1955 refs.info.order_hint[i] = fh.order_hint;
1956 refs.info.frame_id[i] = fh.current_frame_id;
1957 refs.info.upscaled_width[i] = fs.upscaled_width;
1958 refs.info.frame_height[i] = fs.frame_height;
1959 refs.info.render_width[i] = fs.render_width;
1960 refs.info.render_height[i] = fs.render_height;
1961 refs.info.frame_type_is_key[i] = payload.frame_type_is_key;
1962 refs.info.saved_gm_params[i] = payload.gm_params;
1963 refs.info.saved_lf_ref_deltas[i] = payload.lf_ref_deltas;
1964 refs.info.saved_lf_mode_deltas[i] = payload.lf_mode_deltas;
1965 refs.info.saved_seg_feature_active[i] = seg_active;
1966 refs.info.saved_seg_feature_data[i] = seg_data;
1967 refs.slots[i] = Some(Arc::clone(&payload));
1968 }
1969 }
1970 Ok(frame)
1971}
1972
1973/// §7.21-adjacent `show_existing_frame` output: surface the stored
1974/// slot's planes as a [`SpecFrame`], applying §7.18.3 film grain from
1975/// the slot's §7.20-saved grain state (§5.9.2 `show_existing_frame`
1976/// runs `load_grain_params( frame_to_show_map_idx )` when
1977/// `film_grain_params_present == 1`; the stored planes themselves stay
1978/// grain-free).
1979fn output_existing_frame(
1980 refs: &SpecRefState,
1981 fh: &FrameHeader,
1982 seq: &SequenceHeader,
1983) -> Result<SpecFrame, Error> {
1984 let idx = fh
1985 .frame_to_show_map_idx
1986 .ok_or(Error::PartitionWalkOutOfRange)? as usize;
1987 if idx >= NUM_REF_FRAMES as usize {
1988 return Err(Error::PartitionWalkOutOfRange);
1989 }
1990 let slot = refs.slots[idx]
1991 .as_ref()
1992 .ok_or(Error::PartitionWalkOutOfRange)?;
1993 // §7.18.3 on the OUTPUT copy only.
1994 let mut grain_bufs: Option<Vec<Vec<i32>>> = None;
1995 if seq.film_grain_params_present {
1996 if let Some(fg) = slot.grain_params.as_ref() {
1997 if fg.apply_grain {
1998 let mut bufs: Vec<Vec<i32>> = slot
1999 .planes
2000 .iter()
2001 .map(|p| p.iter().map(|&v| v as i32).collect())
2002 .collect();
2003 let cc = &seq.color_config;
2004 let mut views: Vec<PlaneBuffer<'_>> = Vec::with_capacity(bufs.len());
2005 for (buf, &(pw, ph)) in bufs.iter_mut().zip(slot.plane_dims.iter()) {
2006 views.push(PlaneBuffer {
2007 rows: ph,
2008 cols: pw,
2009 samples: buf,
2010 });
2011 }
2012 film_grain_synthesis(
2013 fg,
2014 slot.bit_depth,
2015 cc.num_planes,
2016 u8::from(cc.subsampling_x),
2017 u8::from(cc.subsampling_y),
2018 cc.matrix_coefficients,
2019 &mut views,
2020 );
2021 grain_bufs = Some(bufs);
2022 }
2023 }
2024 }
2025 let max_val: i32 = (1i32 << slot.bit_depth) - 1;
2026 let planes: Vec<Vec<u8>> = match grain_bufs {
2027 Some(bufs) => bufs
2028 .into_iter()
2029 .map(|p| {
2030 if slot.bit_depth == 8 {
2031 p.into_iter().map(|v| v.clamp(0, 255) as u8).collect()
2032 } else {
2033 let mut out = Vec::with_capacity(p.len() * 2);
2034 for v in p {
2035 out.extend_from_slice(&(v.clamp(0, max_val) as u16).to_le_bytes());
2036 }
2037 out
2038 }
2039 })
2040 .collect(),
2041 None => slot
2042 .planes
2043 .iter()
2044 .map(|p| {
2045 if slot.bit_depth == 8 {
2046 p.iter().map(|&v| v.min(255) as u8).collect()
2047 } else {
2048 let mut out = Vec::with_capacity(p.len() * 2);
2049 for &v in p {
2050 out.extend_from_slice(&v.to_le_bytes());
2051 }
2052 out
2053 }
2054 })
2055 .collect(),
2056 };
2057 let (w, h) = *slot
2058 .plane_dims
2059 .first()
2060 .ok_or(Error::PartitionWalkOutOfRange)?;
2061 Ok(SpecFrame {
2062 width: w,
2063 height: h,
2064 planes,
2065 plane_dims: slot.plane_dims.clone(),
2066 bit_depth: slot.bit_depth,
2067 })
2068}
2069
2070/// Decode one §7.5 temporal-unit body, appending every SHOWN frame to
2071/// `out` (`show_frame == 1` coded frames and `show_existing_frame`
2072/// outputs — the §7.4 output discipline) and updating the cached
2073/// sequence header + the §7.20 reference state.
2074#[allow(clippy::too_many_arguments)]
2075fn decode_temporal_unit_spec(
2076 payload: &[u8],
2077 seq: &mut Option<SequenceHeader>,
2078 refs: &mut SpecRefState,
2079 out: &mut Vec<SpecFrame>,
2080 chosen_op: u8,
2081 op_idc: &mut u16,
2082 max_picture_size: u32,
2083 stages: &mut DecodeStageTimings,
2084 threads: usize,
2085) -> Result<(), Error> {
2086 // §5.9.1 `SeenFrameHeader` state: `Some` between a frame's
2087 // `OBU_FRAME_HEADER` and the tile group whose `tg_end ==
2088 // NumTiles - 1` (which per §5.11.1 resets `SeenFrameHeader = 0`).
2089 // r433 — a frame's tiles may arrive across SEVERAL tile-group
2090 // OBUs; §6.10.1 requires each group's `tg_start` to equal the
2091 // running `TileNum`, so the accumulator tracks the next expected
2092 // index and concatenation reconstructs tile raster order.
2093 struct PendingFrame {
2094 fh: FrameHeader,
2095 /// The original `OBU_FRAME_HEADER` payload bytes — the §6.8.1
2096 /// `frame_header_copy` rule says every
2097 /// `OBU_REDUNDANT_FRAME_HEADER` for this frame must carry
2098 /// identical contents.
2099 fh_payload: Vec<u8>,
2100 /// Accumulated §8.2 per-tile payloads, tile raster order.
2101 tiles: Vec<TilePayload>,
2102 /// §6.10.1: the `TileNum` the next tile group must start at.
2103 next_tg_start: u32,
2104 }
2105 let mut pending: Option<PendingFrame> = None;
2106 for desc in ObuIter::new(payload) {
2107 let desc = desc?;
2108 // §5.3.1 `drop_obu()`: with a non-zero OperatingPointIdc,
2109 // every OBU other than a sequence header or temporal
2110 // delimiter that carries an extension header and lies outside
2111 // the selected operating point's layer set is dropped before
2112 // any payload parse:
2113 //
2114 // inTemporalLayer = (OperatingPointIdc >> temporal_id) & 1
2115 // inSpatialLayer = (OperatingPointIdc >> (spatial_id + 8)) & 1
2116 if *op_idc != 0
2117 && desc.extension_flag
2118 && !matches!(
2119 desc.obu_type,
2120 ObuType::SequenceHeader | ObuType::TemporalDelimiter
2121 )
2122 {
2123 let in_temporal = (*op_idc >> desc.temporal_id) & 1 != 0;
2124 let in_spatial = (*op_idc >> (desc.spatial_id + 8)) & 1 != 0;
2125 if !in_temporal || !in_spatial {
2126 continue;
2127 }
2128 }
2129 match desc.obu_type {
2130 ObuType::TemporalDelimiter | ObuType::Padding | ObuType::Metadata => {}
2131 // §7.3.1: tile-list decoding is a separate operating mode
2132 // whose AnchorFrames array exists only by external means
2133 // ("a decoder is recommended to support decoding of tile
2134 // list OBUs, but this is not a requirement for decoder
2135 // conformance") — the general driver skips them; the
2136 // dedicated [`crate::tile_list::decode_tile_list`] entry
2137 // decodes them against a caller-supplied anchor set.
2138 ObuType::TileList => {}
2139 ObuType::SequenceHeader => {
2140 let sh = parse_sequence_header(desc.payload)?;
2141 // §5.5.1: `operatingPoint = choose_operating_point()`
2142 // (the session's externally-selected index, default
2143 // 0); `OperatingPointIdc = operating_point_idc[
2144 // operatingPoint ]`. §6.7.5: an index outside
2145 // `0..=operating_points_cnt_minus_1` abandons the
2146 // decoding process.
2147 let idx = usize::from(chosen_op);
2148 if idx >= sh.operating_points.len() {
2149 return Err(Error::OperatingPointOutOfRange);
2150 }
2151 *op_idc = sh.operating_points[idx].operating_point_idc;
2152 *seq = Some(sh);
2153 }
2154 ObuType::FrameHeader => {
2155 // §6.8.1: `OBU_FRAME_HEADER` requires `SeenFrameHeader
2156 // == 0` — a header arriving after this frame's tile
2157 // groups started must use the REDUNDANT type. (A
2158 // pending header with NO tiles yet is tolerated as a
2159 // replacement, preserving the historical driver's
2160 // acceptance.)
2161 if pending.as_ref().is_some_and(|p| !p.tiles.is_empty()) {
2162 return Err(Error::TileGroupInvalid);
2163 }
2164 let s = seq.as_ref().ok_or(Error::PartitionWalkOutOfRange)?;
2165 let t_parse = Instant::now();
2166 let fh = parse_frame_header_with_refs(desc.payload, s, &refs.info)?;
2167 stages.parse += t_parse.elapsed();
2168 check_picture_size(&fh, max_picture_size)?;
2169 if fh.show_existing_frame {
2170 out.push(output_existing_frame(refs, &fh, s)?);
2171 // §7.4 / §7.21: a shown KEY frame re-loads the
2172 // stored frame state and re-stores it into every
2173 // slot (`refresh_frame_flags == allFrames` per the
2174 // §5.9.2 show_existing arm). The wholesale slot
2175 // clone IS the §7.21 load followed by the §7.20
2176 // store — pixels, §7.19 grids, `SavedOrderHints`,
2177 // CDFs, gm params and loop-filter deltas all ride
2178 // the payload.
2179 if fh.refresh_frame_flags != 0 {
2180 let idx = fh
2181 .frame_to_show_map_idx
2182 .ok_or(Error::PartitionWalkOutOfRange)?
2183 as usize;
2184 let payload = refs.slots[idx]
2185 .as_ref()
2186 .cloned()
2187 .ok_or(Error::PartitionWalkOutOfRange)?;
2188 let loaded_order_hint = refs.info.order_hint[idx];
2189 let loaded_frame_id = refs.info.frame_id[idx];
2190 let loaded_uw = refs.info.upscaled_width[idx];
2191 let loaded_fh = refs.info.frame_height[idx];
2192 let loaded_rw = refs.info.render_width[idx];
2193 let loaded_rh = refs.info.render_height[idx];
2194 for i in 0..NUM_REF_FRAMES as usize {
2195 if (fh.refresh_frame_flags >> i) & 1 != 0 {
2196 refs.info.valid[i] = true;
2197 refs.info.order_hint[i] = loaded_order_hint;
2198 refs.info.frame_id[i] = loaded_frame_id;
2199 refs.info.upscaled_width[i] = loaded_uw;
2200 refs.info.frame_height[i] = loaded_fh;
2201 refs.info.render_width[i] = loaded_rw;
2202 refs.info.render_height[i] = loaded_rh;
2203 refs.info.frame_type_is_key[i] = payload.frame_type_is_key;
2204 refs.info.saved_gm_params[i] = payload.gm_params;
2205 refs.info.saved_lf_ref_deltas[i] = payload.lf_ref_deltas;
2206 refs.info.saved_lf_mode_deltas[i] = payload.lf_mode_deltas;
2207 // §7.21 load + §7.20 re-store: the
2208 // segmentation params ride the RefInfo
2209 // copy from the loaded slot.
2210 refs.info.saved_seg_feature_active[i] =
2211 refs.info.saved_seg_feature_active[idx];
2212 refs.info.saved_seg_feature_data[i] =
2213 refs.info.saved_seg_feature_data[idx];
2214 refs.slots[i] = Some(payload.clone());
2215 }
2216 }
2217 }
2218 pending = None;
2219 } else {
2220 pending = Some(PendingFrame {
2221 fh,
2222 fh_payload: desc.payload.to_vec(),
2223 tiles: Vec::new(),
2224 next_tg_start: 0,
2225 });
2226 }
2227 }
2228 ObuType::RedundantFrameHeader => {
2229 // §5.9.1 `frame_header_copy()`: only legal while
2230 // `SeenFrameHeader == 1` (§6.8.1), and the copy must
2231 // carry contents identical to the original
2232 // `OBU_FRAME_HEADER`. Nothing re-parses — the byte
2233 // comparison IS the identity check.
2234 let p = pending.as_ref().ok_or(Error::TileGroupInvalid)?;
2235 if desc.payload != &p.fh_payload[..] {
2236 return Err(Error::TileGroupInvalid);
2237 }
2238 }
2239 ObuType::TileGroup => {
2240 let s = seq.as_ref().ok_or(Error::PartitionWalkOutOfRange)?;
2241 let p = pending.as_mut().ok_or(Error::TileGroupInvalid)?;
2242 let ti =
2243 p.fh.tile_info
2244 .as_ref()
2245 .ok_or(Error::PartitionWalkOutOfRange)?;
2246 let num_tiles = ti.tile_cols * ti.tile_rows;
2247 let t_parse = Instant::now();
2248 let parsed = parse_tile_group_body_with_ti(ti, desc.payload)?;
2249 stages.parse += t_parse.elapsed();
2250 // §6.10.1: `tg_start` equals the running `TileNum`.
2251 if parsed.tg_start != p.next_tg_start {
2252 return Err(Error::TileGroupInvalid);
2253 }
2254 p.next_tg_start = parsed.tg_end + 1;
2255 p.tiles.extend(parsed.tiles);
2256 // §5.11.1: the frame decodes (frame_end_update_cdf +
2257 // decode_frame_wrapup, SeenFrameHeader = 0) only once
2258 // `tg_end == NumTiles - 1`.
2259 if parsed.tg_end + 1 == num_tiles {
2260 let pf = pending.take().expect("pending frame checked above");
2261 let decoded =
2262 decode_frame_spec_tiles(s, &pf.fh, &pf.tiles, Some(refs), stages, threads)?;
2263 let frame = reference_frame_update(refs, &pf.fh, decoded)?;
2264 if pf.fh.show_frame {
2265 out.push(frame);
2266 }
2267 }
2268 }
2269 ObuType::Frame => {
2270 // §5.10 frame_obu: frame_header_obu() + byte_alignment()
2271 // + tile_group_obu(). The tile group starts at the next
2272 // byte boundary after the frame header, and §6.10.1
2273 // requires `tile_start_and_end_present_flag == 0` — the
2274 // embedded group always covers the whole frame.
2275 if pending.as_ref().is_some_and(|p| !p.tiles.is_empty()) {
2276 return Err(Error::TileGroupInvalid);
2277 }
2278 pending = None;
2279 let s = seq.as_ref().ok_or(Error::PartitionWalkOutOfRange)?;
2280 let t_parse = Instant::now();
2281 let fh = parse_frame_header_with_refs(desc.payload, s, &refs.info)?;
2282 check_picture_size(&fh, max_picture_size)?;
2283 let tg_offset = fh.bits_consumed.div_ceil(8);
2284 if tg_offset > desc.payload.len() {
2285 return Err(Error::UnexpectedEnd);
2286 }
2287 let tg_body = &desc.payload[tg_offset..];
2288 let ti = fh
2289 .tile_info
2290 .as_ref()
2291 .ok_or(Error::PartitionWalkOutOfRange)?;
2292 let tiles = parse_whole_frame_tile_group(ti, tg_body, true)?;
2293 stages.parse += t_parse.elapsed();
2294 let decoded = decode_frame_spec_tiles(s, &fh, &tiles, Some(refs), stages, threads)?;
2295 let frame = reference_frame_update(refs, &fh, decoded)?;
2296 if fh.show_frame {
2297 out.push(frame);
2298 }
2299 }
2300 _ => return Err(Error::PartitionWalkOutOfRange),
2301 }
2302 }
2303 // §7.5 / §6.10.1: a coded frame's tile groups all live inside its
2304 // temporal unit — the last group (`tg_end == NumTiles - 1`) must
2305 // have arrived before the unit ends.
2306 if pending.as_ref().is_some_and(|p| !p.tiles.is_empty()) {
2307 return Err(Error::TileGroupInvalid);
2308 }
2309 Ok(())
2310}