M9.7: AV1 headers, symbol decoder, CDFs, intra decoding, transforms, intra prediction

New L0 crate filmcraft-av1 from the AV1 Bitstream & Decoding Process
Specification v1.0.0 with Errata 1 (tables extracted from the spec's Markdown
source by tools/extract_tables.py).

Stages: OBU parsing, sequence and uncompressed frame headers (all fields),
reference state (set_frame_refs, load_previous, reference update,
show_existing_frame), symbol decoder with CDF adaptation and
init/load/save/frame-end CDF handling, tiles, intra frame mode info
(partitions, segment ids, delta q/lf, CDEF index, modes, angle deltas, CfL,
palette, filter intra, tx size), coefficient decoding, dequantisation,
inverse transforms (DCT 4-64, ADST, identity, WHT), intra prediction (all
modes, edge filter/upsampling, recursive filter, CfL, palette), LR syntax.

All-intra libsvtav1 streams with in-loop filters disabled decode bit-exactly
against libdav1d (8/10-bit, 128x128..1280x720, odd sizes, presets 2-8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-01 11:00:03 -04:00
parent 226c139741
commit 1813bbaca2
23 changed files with 15165 additions and 1 deletions

10
Cargo.lock generated
View File

@ -1352,6 +1352,16 @@ dependencies = [
"tokio",
]
[[package]]
name = "filmcraft-av1"
version = "0.1.0"
dependencies = [
"filmcraft-bitstream",
"filmcraft-testkit",
"rayon",
"thiserror 2.0.21",
]
[[package]]
name = "filmcraft-bitstream"
version = "0.1.0"

View File

@ -23,6 +23,7 @@ filmcraft-hevc = { path = "crates/hevc" }
filmcraft-vp9 = { path = "crates/vp9" }
filmcraft-prores = { path = "crates/prores" }
filmcraft-dnx = { path = "crates/dnx" }
filmcraft-av1 = { path = "crates/av1" }
filmcraft-aac = { path = "crates/aac" }
filmcraft-opus = { path = "crates/opus" }
filmcraft-h264enc = { path = "crates/h264enc" }
@ -91,6 +92,9 @@ opt-level = 3
[profile.dev.package.filmcraft-dnx]
opt-level = 3
[profile.dev.package.filmcraft-av1]
opt-level = 3
[profile.dev.package.filmcraft-aac]
opt-level = 3

22
crates/av1/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "filmcraft-av1"
description = "Clean-room AV1 decoder (AV1 Bitstream & Decoding Process Specification v1.0.0 with Errata 1)"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[features]
default = ["threads"]
threads = ["dep:rayon"]
[dependencies]
filmcraft-bitstream = { workspace = true }
thiserror = { workspace = true }
rayon = { workspace = true, optional = true }
[dev-dependencies]
filmcraft-testkit = { workspace = true }

55
crates/av1/README.md Normal file
View File

@ -0,0 +1,55 @@
# filmcraft-av1
Clean-room, pure-Rust (no `unsafe`) AV1 decoder, implemented from the public **AV1 Bitstream &
Decoding Process Specification, version 1.0.0 with Errata 1** (AOMediaCodec/av1-spec, git
commit `5e04f3f`, 2023-06-12). It is an L0 crate: it depends only on `filmcraft-bitstream`,
`thiserror` and (optional, feature `threads`) `rayon`, and builds for `wasm32-unknown-unknown`.
All constant tables (default CDFs, scan orders, quantiser lookups, filter taps, …) and the
named constants are extracted from the specification's Markdown source by
[`tools/extract_tables.py`](tools/extract_tables.py) into `src/spec_tables.rs` (every table's
shape is checked against its declared dimensions; one missing comma in the spec's
`Split_Tx_Size` initialiser is reported and repaired). No code was taken from libaom, dav1d,
SVT-AV1 or any other implementation; ffmpeg (libsvtav1 to encode fixtures, libdav1d to decode the
reference) is used only as an external test oracle.
## Conformance status
| Stage | Spec | Status |
|---|---|---|
| OBU parsing (low-overhead format), temporal delimiters, operating-point dropping | 5.3, 7.5 | done |
| Sequence header, color config, timing / decoder model info | 5.5 | done |
| Uncompressed frame header (all fields, frame size / superres / render size, tile info, quantiser, segmentation, delta q/lf, loop filter, CDEF, LR, tx mode, skip mode, global motion, film grain params) | 5.9 | done |
| Reference frame state: set_frame_refs, setup_past_independence, load_previous, reference update, show_existing_frame | 7.8, 7.20, 7.21 | done |
| Symbol decoder, CDF adaptation, init / load / save / frame-end CDF update | 8.2, 8.3 | done |
| Tiles and tile groups (uniform and explicit spacing, tile size bytes) | 5.11 | done (decoded sequentially) |
| Intra frame mode info: partitions, skip, segment id, CDEF index, delta q / lf, y / uv modes, angle deltas, CfL alphas, palette (colors, cache, color index map), filter intra, tx size (incl. var-tx syntax) | 5.11 | done, bit-exact |
| Coefficients (all_zero, eob, base / br levels, Golomb, dc sign, tx type sets, scans) | 5.11.39 | done, bit-exact |
| Dequantisation incl. quantiser matrices | 7.12 | done (qmatrix not yet oracle-tested) |
| Inverse transforms: DCT 4–64, ADST 4/8/16, flip ADST, identity 4–32, WHT (lossless), rectangular | 7.13 | done, bit-exact |
| Intra prediction: DC, V/H + directional with edge filter and upsampling, smooth (3), Paeth, recursive filter intra, CfL, palette | 7.11.2, 7.11.4, 7.11.5 | done, bit-exact |
| Intra block copy | 7.11.3 | missing |
| Inter prediction: MV prediction, motion field, scaling, filters, warp / global motion, OBMC, compound / masks, inter-intra | 7.9–7.11.3 | missing |
| Loop filter | 7.14 | missing |
| CDEF | 7.15 | missing |
| Super-resolution upscaling | 7.16 | missing |
| Loop restoration (Wiener, self-guided) | 7.17 | syntax done, filter missing |
| Film grain synthesis | 7.18.3 | missing |
| Large-scale tile / tile list OBUs | 7.3 | not planned |
## Accuracy
`tests/oracle_intra.rs` encodes all-intra streams with ffmpeg's libsvtav1 (in-loop filters off)
and compares every decoded frame with libdav1d: **bit-exact** on all fixtures (8- and 10-bit
4:2:0; 128×128 to 1280×720 including odd sizes; testsrc2, mandelbrot and noise; presets 2–8).
## API
```rust
let mut dec = filmcraft_av1::Decoder::new();
for sample in samples { // one temporal unit (MP4 / Matroska sample) each
for pic in dec.decode(&sample)? { // shown frames, planar u16
// pic.width, pic.height, pic.bit_depth, pic.planes[0..3]
}
}
```

124
crates/av1/src/bits.rs Normal file
View File

@ -0,0 +1,124 @@
//! MSB-first bit reader for OBU headers and the uncompressed frame header (spec 4.10 descriptors).
use crate::{Error, Result};
#[derive(Clone)]
pub(crate) struct BitReader<'a> {
data: &'a [u8],
/// Position in bits.
pos: usize,
}
impl<'a> BitReader<'a> {
pub fn new(data: &'a [u8]) -> Self {
BitReader { data, pos: 0 }
}
/// `get_position()`: bits consumed.
pub fn position(&self) -> usize {
self.pos
}
pub fn byte_pos(&self) -> usize {
self.pos.div_ceil(8)
}
#[inline]
pub fn bit(&mut self) -> Result<u32> {
let byte = *self.data.get(self.pos >> 3).ok_or(Error::Truncated)?;
let b = (byte >> (7 - (self.pos & 7))) & 1;
self.pos += 1;
Ok(b as u32)
}
/// `f(n)`, n ≤ 32.
pub fn f(&mut self, n: u32) -> Result<u32> {
let mut x: u64 = 0;
for _ in 0..n {
x = (x << 1) | self.bit()? as u64;
}
Ok(x as u32)
}
pub fn flag(&mut self) -> Result<bool> {
Ok(self.bit()? == 1)
}
/// `uvlc()`
pub fn uvlc(&mut self) -> Result<u32> {
let mut leading = 0u32;
while self.bit()? == 0 {
leading += 1;
if leading > 40 {
return Err(Error::Invalid("uvlc"));
}
}
if leading >= 32 {
return Ok(u32::MAX);
}
let v = self.f(leading)?;
Ok(v + ((1u64 << leading) - 1) as u32)
}
/// `le(n)`
pub fn le(&mut self, n: u32) -> Result<u32> {
let mut t = 0u32;
for i in 0..n {
t |= self.f(8)? << (8 * i);
}
Ok(t)
}
/// `leb128()`
pub fn leb128(&mut self) -> Result<u64> {
let mut v = 0u64;
for i in 0..8 {
let b = self.f(8)? as u64;
v |= (b & 0x7f) << (7 * i);
if b & 0x80 == 0 {
break;
}
}
Ok(v)
}
/// `su(n)`
pub fn su(&mut self, n: u32) -> Result<i32> {
let v = self.f(n)? as i64;
let sign = 1i64 << (n - 1);
Ok(if v & sign != 0 { (v - 2 * sign) as i32 } else { v as i32 })
}
/// `ns(n)`
pub fn ns(&mut self, n: u32) -> Result<u32> {
if n <= 1 {
return Ok(0);
}
let w = 32 - n.leading_zeros(); // FloorLog2(n) + 1
let m = (1u32 << w) - n;
let v = self.f(w - 1)?;
if v < m {
return Ok(v);
}
let extra = self.bit()?;
Ok((v << 1) - m + extra)
}
/// `byte_alignment()`
pub fn byte_align(&mut self) {
self.pos = self.pos.div_ceil(8) * 8;
}
}
/// Read a leb128 from a byte slice; returns (value, bytes used).
pub(crate) fn leb128(data: &[u8]) -> Result<(u64, usize)> {
let mut v = 0u64;
for i in 0..8 {
let b = *data.get(i).ok_or(Error::Truncated)? as u64;
v |= (b & 0x7f) << (7 * i);
if b & 0x80 == 0 {
return Ok((v, i + 1));
}
}
Ok((v, 8))
}

359
crates/av1/src/cdf.rs Normal file
View File

@ -0,0 +1,359 @@
//! The adaptive CDF arrays (spec 6.8.2 init_non_coeff_cdfs / init_coeff_cdfs, load_cdfs).
use crate::spec_tables::*;
/// Every CDF array listed by init_non_coeff_cdfs and init_coeff_cdfs. Each innermost array holds
/// N cumulative values (the last is 32768) plus the adaptation counter.
#[derive(Clone)]
pub(crate) struct Cdfs {
pub y_mode: [[u16; 14]; 4],
pub uv_mode_cfl_not_allowed: [[u16; 14]; 13],
pub uv_mode_cfl_allowed: [[u16; 15]; 13],
pub angle_delta: [[u16; 8]; 8],
pub intrabc: [u16; 3],
pub partition_w8: [[u16; 5]; 4],
pub partition_w16: [[u16; 11]; 4],
pub partition_w32: [[u16; 11]; 4],
pub partition_w64: [[u16; 11]; 4],
pub partition_w128: [[u16; 9]; 4],
pub segment_id: [[u16; 9]; 3],
pub segment_id_predicted: [[u16; 3]; 3],
pub tx_8x8: [[u16; 3]; 3],
pub tx_16x16: [[u16; 4]; 3],
pub tx_32x32: [[u16; 4]; 3],
pub tx_64x64: [[u16; 4]; 3],
pub txfm_split: [[u16; 3]; 21],
pub filter_intra_mode: [u16; 6],
pub filter_intra: [[u16; 3]; 22],
pub interp_filter: [[u16; 4]; 16],
pub motion_mode: [[u16; 4]; 22],
pub new_mv: [[u16; 3]; 6],
pub zero_mv: [[u16; 3]; 2],
pub ref_mv: [[u16; 3]; 6],
pub compound_mode: [[u16; 9]; 8],
pub drl_mode: [[u16; 3]; 3],
pub is_inter: [[u16; 3]; 4],
pub comp_mode: [[u16; 3]; 5],
pub skip_mode: [[u16; 3]; 3],
pub skip: [[u16; 3]; 3],
pub comp_ref: [[[u16; 3]; 3]; 3],
pub comp_bwd_ref: [[[u16; 3]; 2]; 3],
pub single_ref: [[[u16; 3]; 6]; 3],
pub mv_joint: [[u16; 5]; MV_CONTEXTS],
pub mv_class: [[[u16; 12]; 2]; MV_CONTEXTS],
pub mv_class0_bit: [[[u16; 3]; 2]; MV_CONTEXTS],
pub mv_fr: [[[u16; 5]; 2]; MV_CONTEXTS],
pub mv_class0_fr: [[[[u16; 5]; 2]; 2]; MV_CONTEXTS],
pub mv_class0_hp: [[[u16; 3]; 2]; MV_CONTEXTS],
pub mv_sign: [[[u16; 3]; 2]; MV_CONTEXTS],
pub mv_bit: [[[[u16; 3]; 10]; 2]; MV_CONTEXTS],
pub mv_hp: [[[u16; 3]; 2]; MV_CONTEXTS],
pub palette_y_mode: [[[u16; 3]; 3]; 7],
pub palette_uv_mode: [[u16; 3]; 2],
pub palette_y_size: [[u16; 8]; 7],
pub palette_uv_size: [[u16; 8]; 7],
pub palette_2_y_color: [[u16; 3]; 5],
pub palette_2_uv_color: [[u16; 3]; 5],
pub palette_3_y_color: [[u16; 4]; 5],
pub palette_3_uv_color: [[u16; 4]; 5],
pub palette_4_y_color: [[u16; 5]; 5],
pub palette_4_uv_color: [[u16; 5]; 5],
pub palette_5_y_color: [[u16; 6]; 5],
pub palette_5_uv_color: [[u16; 6]; 5],
pub palette_6_y_color: [[u16; 7]; 5],
pub palette_6_uv_color: [[u16; 7]; 5],
pub palette_7_y_color: [[u16; 8]; 5],
pub palette_7_uv_color: [[u16; 8]; 5],
pub palette_8_y_color: [[u16; 9]; 5],
pub palette_8_uv_color: [[u16; 9]; 5],
pub delta_q: [u16; 5],
pub delta_lf: [u16; 5],
pub delta_lf_multi: [[u16; 5]; FRAME_LF_COUNT],
pub intra_tx_type_set1: [[[u16; 8]; 13]; 2],
pub intra_tx_type_set2: [[[u16; 6]; 13]; 3],
pub inter_tx_type_set1: [[u16; 17]; 2],
pub inter_tx_type_set2: [u16; 13],
pub inter_tx_type_set3: [[u16; 3]; 4],
pub use_obmc: [[u16; 3]; 22],
pub inter_intra: [[u16; 3]; 3],
pub comp_ref_type: [[u16; 3]; 5],
pub cfl_sign: [u16; 9],
pub uni_comp_ref: [[[u16; 3]; 3]; 3],
pub wedge_inter_intra: [[u16; 3]; 22],
pub comp_group_idx: [[u16; 3]; 6],
pub compound_idx: [[u16; 3]; 6],
pub compound_type: [[u16; 3]; 22],
pub inter_intra_mode: [[u16; 5]; 3],
pub wedge_index: [[u16; 17]; 22],
pub cfl_alpha: [[u16; 17]; 6],
pub use_wiener: [u16; 3],
pub use_sgrproj: [u16; 3],
pub restoration_type: [u16; 4],
// coefficient CDFs
pub txb_skip: [[[u16; 3]; 13]; 5],
pub eob_pt_16: [[[u16; 6]; 2]; 2],
pub eob_pt_32: [[[u16; 7]; 2]; 2],
pub eob_pt_64: [[[u16; 8]; 2]; 2],
pub eob_pt_128: [[[u16; 9]; 2]; 2],
pub eob_pt_256: [[[u16; 10]; 2]; 2],
pub eob_pt_512: [[u16; 11]; 2],
pub eob_pt_1024: [[u16; 12]; 2],
pub eob_extra: [[[[u16; 3]; 9]; 2]; 5],
pub dc_sign: [[[u16; 3]; 3]; 2],
pub coeff_base_eob: [[[[u16; 4]; 4]; 2]; 5],
pub coeff_base: [[[[u16; 5]; 42]; 2]; 5],
pub coeff_br: [[[[u16; 5]; 21]; 2]; 5],
}
impl Cdfs {
/// init_non_coeff_cdfs( ) followed by init_coeff_cdfs( ) for `base_q_idx`.
pub fn new(base_q_idx: u32) -> Box<Cdfs> {
let mut c = Box::new(Cdfs {
y_mode: DEFAULT_Y_MODE_CDF,
uv_mode_cfl_not_allowed: DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF,
uv_mode_cfl_allowed: DEFAULT_UV_MODE_CFL_ALLOWED_CDF,
angle_delta: DEFAULT_ANGLE_DELTA_CDF,
intrabc: DEFAULT_INTRABC_CDF,
partition_w8: DEFAULT_PARTITION_W8_CDF,
partition_w16: DEFAULT_PARTITION_W16_CDF,
partition_w32: DEFAULT_PARTITION_W32_CDF,
partition_w64: DEFAULT_PARTITION_W64_CDF,
partition_w128: DEFAULT_PARTITION_W128_CDF,
segment_id: DEFAULT_SEGMENT_ID_CDF,
segment_id_predicted: DEFAULT_SEGMENT_ID_PREDICTED_CDF,
tx_8x8: DEFAULT_TX_8X8_CDF,
tx_16x16: DEFAULT_TX_16X16_CDF,
tx_32x32: DEFAULT_TX_32X32_CDF,
tx_64x64: DEFAULT_TX_64X64_CDF,
txfm_split: DEFAULT_TXFM_SPLIT_CDF,
filter_intra_mode: DEFAULT_FILTER_INTRA_MODE_CDF,
filter_intra: DEFAULT_FILTER_INTRA_CDF,
interp_filter: DEFAULT_INTERP_FILTER_CDF,
motion_mode: DEFAULT_MOTION_MODE_CDF,
new_mv: DEFAULT_NEW_MV_CDF,
zero_mv: DEFAULT_ZERO_MV_CDF,
ref_mv: DEFAULT_REF_MV_CDF,
compound_mode: DEFAULT_COMPOUND_MODE_CDF,
drl_mode: DEFAULT_DRL_MODE_CDF,
is_inter: DEFAULT_IS_INTER_CDF,
comp_mode: DEFAULT_COMP_MODE_CDF,
skip_mode: DEFAULT_SKIP_MODE_CDF,
skip: DEFAULT_SKIP_CDF,
comp_ref: DEFAULT_COMP_REF_CDF,
comp_bwd_ref: DEFAULT_COMP_BWD_REF_CDF,
single_ref: DEFAULT_SINGLE_REF_CDF,
mv_joint: [DEFAULT_MV_JOINT_CDF; MV_CONTEXTS],
mv_class: [DEFAULT_MV_CLASS_CDF; MV_CONTEXTS],
mv_class0_bit: [[DEFAULT_MV_CLASS0_BIT_CDF; 2]; MV_CONTEXTS],
mv_fr: [DEFAULT_MV_FR_CDF; MV_CONTEXTS],
mv_class0_fr: [DEFAULT_MV_CLASS0_FR_CDF; MV_CONTEXTS],
mv_class0_hp: [[DEFAULT_MV_CLASS0_HP_CDF; 2]; MV_CONTEXTS],
mv_sign: [[DEFAULT_MV_SIGN_CDF; 2]; MV_CONTEXTS],
mv_bit: [[DEFAULT_MV_BIT_CDF; 2]; MV_CONTEXTS],
mv_hp: [[DEFAULT_MV_HP_CDF; 2]; MV_CONTEXTS],
palette_y_mode: DEFAULT_PALETTE_Y_MODE_CDF,
palette_uv_mode: DEFAULT_PALETTE_UV_MODE_CDF,
palette_y_size: DEFAULT_PALETTE_Y_SIZE_CDF,
palette_uv_size: DEFAULT_PALETTE_UV_SIZE_CDF,
palette_2_y_color: DEFAULT_PALETTE_SIZE_2_Y_COLOR_CDF,
palette_2_uv_color: DEFAULT_PALETTE_SIZE_2_UV_COLOR_CDF,
palette_3_y_color: DEFAULT_PALETTE_SIZE_3_Y_COLOR_CDF,
palette_3_uv_color: DEFAULT_PALETTE_SIZE_3_UV_COLOR_CDF,
palette_4_y_color: DEFAULT_PALETTE_SIZE_4_Y_COLOR_CDF,
palette_4_uv_color: DEFAULT_PALETTE_SIZE_4_UV_COLOR_CDF,
palette_5_y_color: DEFAULT_PALETTE_SIZE_5_Y_COLOR_CDF,
palette_5_uv_color: DEFAULT_PALETTE_SIZE_5_UV_COLOR_CDF,
palette_6_y_color: DEFAULT_PALETTE_SIZE_6_Y_COLOR_CDF,
palette_6_uv_color: DEFAULT_PALETTE_SIZE_6_UV_COLOR_CDF,
palette_7_y_color: DEFAULT_PALETTE_SIZE_7_Y_COLOR_CDF,
palette_7_uv_color: DEFAULT_PALETTE_SIZE_7_UV_COLOR_CDF,
palette_8_y_color: DEFAULT_PALETTE_SIZE_8_Y_COLOR_CDF,
palette_8_uv_color: DEFAULT_PALETTE_SIZE_8_UV_COLOR_CDF,
delta_q: DEFAULT_DELTA_Q_CDF,
delta_lf: DEFAULT_DELTA_LF_CDF,
delta_lf_multi: [DEFAULT_DELTA_LF_CDF; FRAME_LF_COUNT],
intra_tx_type_set1: DEFAULT_INTRA_TX_TYPE_SET1_CDF,
intra_tx_type_set2: DEFAULT_INTRA_TX_TYPE_SET2_CDF,
inter_tx_type_set1: DEFAULT_INTER_TX_TYPE_SET1_CDF,
inter_tx_type_set2: DEFAULT_INTER_TX_TYPE_SET2_CDF,
inter_tx_type_set3: DEFAULT_INTER_TX_TYPE_SET3_CDF,
use_obmc: DEFAULT_USE_OBMC_CDF,
inter_intra: DEFAULT_INTER_INTRA_CDF,
comp_ref_type: DEFAULT_COMP_REF_TYPE_CDF,
cfl_sign: DEFAULT_CFL_SIGN_CDF,
uni_comp_ref: DEFAULT_UNI_COMP_REF_CDF,
wedge_inter_intra: DEFAULT_WEDGE_INTER_INTRA_CDF,
comp_group_idx: DEFAULT_COMP_GROUP_IDX_CDF,
compound_idx: DEFAULT_COMPOUND_IDX_CDF,
compound_type: DEFAULT_COMPOUND_TYPE_CDF,
inter_intra_mode: DEFAULT_INTER_INTRA_MODE_CDF,
wedge_index: DEFAULT_WEDGE_INDEX_CDF,
cfl_alpha: DEFAULT_CFL_ALPHA_CDF,
use_wiener: DEFAULT_USE_WIENER_CDF,
use_sgrproj: DEFAULT_USE_SGRPROJ_CDF,
restoration_type: DEFAULT_RESTORATION_TYPE_CDF,
txb_skip: DEFAULT_TXB_SKIP_CDF[0],
eob_pt_16: DEFAULT_EOB_PT_16_CDF[0],
eob_pt_32: DEFAULT_EOB_PT_32_CDF[0],
eob_pt_64: DEFAULT_EOB_PT_64_CDF[0],
eob_pt_128: DEFAULT_EOB_PT_128_CDF[0],
eob_pt_256: DEFAULT_EOB_PT_256_CDF[0],
eob_pt_512: DEFAULT_EOB_PT_512_CDF[0],
eob_pt_1024: DEFAULT_EOB_PT_1024_CDF[0],
eob_extra: DEFAULT_EOB_EXTRA_CDF[0],
dc_sign: DEFAULT_DC_SIGN_CDF[0],
coeff_base_eob: DEFAULT_COEFF_BASE_EOB_CDF[0],
coeff_base: DEFAULT_COEFF_BASE_CDF[0],
coeff_br: DEFAULT_COEFF_BR_CDF[0],
});
c.init_coeff(base_q_idx);
c
}
/// init_coeff_cdfs( )
pub fn init_coeff(&mut self, base_q_idx: u32) {
let idx = match base_q_idx {
0..=20 => 0,
21..=60 => 1,
61..=120 => 2,
_ => 3,
};
self.txb_skip = DEFAULT_TXB_SKIP_CDF[idx];
self.eob_pt_16 = DEFAULT_EOB_PT_16_CDF[idx];
self.eob_pt_32 = DEFAULT_EOB_PT_32_CDF[idx];
self.eob_pt_64 = DEFAULT_EOB_PT_64_CDF[idx];
self.eob_pt_128 = DEFAULT_EOB_PT_128_CDF[idx];
self.eob_pt_256 = DEFAULT_EOB_PT_256_CDF[idx];
self.eob_pt_512 = DEFAULT_EOB_PT_512_CDF[idx];
self.eob_pt_1024 = DEFAULT_EOB_PT_1024_CDF[idx];
self.eob_extra = DEFAULT_EOB_EXTRA_CDF[idx];
self.dc_sign = DEFAULT_DC_SIGN_CDF[idx];
self.coeff_base_eob = DEFAULT_COEFF_BASE_EOB_CDF[idx];
self.coeff_base = DEFAULT_COEFF_BASE_CDF[idx];
self.coeff_br = DEFAULT_COEFF_BR_CDF[idx];
}
/// The counter reset of load_cdfs( ): the last entry of every CDF array is set to 0.
pub fn clear_counts(&mut self) {
macro_rules! clr {
($($f:ident),*) => { $( self.$f.clear_count(); )* };
}
clr!(
y_mode,
uv_mode_cfl_not_allowed,
uv_mode_cfl_allowed,
angle_delta,
intrabc,
partition_w8,
partition_w16,
partition_w32,
partition_w64,
partition_w128,
segment_id,
segment_id_predicted,
tx_8x8,
tx_16x16,
tx_32x32,
tx_64x64,
txfm_split,
filter_intra_mode,
filter_intra,
interp_filter,
motion_mode,
new_mv,
zero_mv,
ref_mv,
compound_mode,
drl_mode,
is_inter,
comp_mode,
skip_mode,
skip,
comp_ref,
comp_bwd_ref,
single_ref,
mv_joint,
mv_class,
mv_class0_bit,
mv_fr,
mv_class0_fr,
mv_class0_hp,
mv_sign,
mv_bit,
mv_hp,
palette_y_mode,
palette_uv_mode,
palette_y_size,
palette_uv_size,
palette_2_y_color,
palette_2_uv_color,
palette_3_y_color,
palette_3_uv_color,
palette_4_y_color,
palette_4_uv_color,
palette_5_y_color,
palette_5_uv_color,
palette_6_y_color,
palette_6_uv_color,
palette_7_y_color,
palette_7_uv_color,
palette_8_y_color,
palette_8_uv_color,
delta_q,
delta_lf,
delta_lf_multi,
intra_tx_type_set1,
intra_tx_type_set2,
inter_tx_type_set1,
inter_tx_type_set2,
inter_tx_type_set3,
use_obmc,
inter_intra,
comp_ref_type,
cfl_sign,
uni_comp_ref,
wedge_inter_intra,
comp_group_idx,
compound_idx,
compound_type,
inter_intra_mode,
wedge_index,
cfl_alpha,
use_wiener,
use_sgrproj,
restoration_type,
txb_skip,
eob_pt_16,
eob_pt_32,
eob_pt_64,
eob_pt_128,
eob_pt_256,
eob_pt_512,
eob_pt_1024,
eob_extra,
dc_sign,
coeff_base_eob,
coeff_base,
coeff_br
);
}
}
/// Zeroes the adaptation counter (last element) of every innermost CDF array.
trait ClearCount {
fn clear_count(&mut self);
}
impl<const N: usize> ClearCount for [u16; N] {
fn clear_count(&mut self) {
self[N - 1] = 0;
}
}
impl<T: ClearCount, const M: usize> ClearCount for [T; M] {
fn clear_count(&mut self) {
for e in self.iter_mut() {
e.clear_count();
}
}
}

365
crates/av1/src/decoder.rs Normal file
View File

@ -0,0 +1,365 @@
//! Top-level decoder: OBU parsing (5.3), frame / tile group handling, reference frame update
//! (7.20), show-existing-frame and output (7.18).
use std::sync::Arc;
use crate::bits::{BitReader, leb128};
use crate::cdf::Cdfs;
use crate::frame::FrameBuf;
use crate::header::{FrameHeader, HeaderState, LoopFilterDeltas, RefInfo, SegmentationFeatures, SequenceHeader, default_gm_params};
use crate::spec_tables::*;
use crate::state::FrameState;
use crate::tile::TileDecoder;
use crate::{Error, Picture, Result};
const OBU_SEQUENCE_HEADER_T: u32 = 1;
const OBU_TEMPORAL_DELIMITER_T: u32 = 2;
const OBU_FRAME_HEADER_T: u32 = 3;
const OBU_TILE_GROUP_T: u32 = 4;
const OBU_METADATA_T: u32 = 5;
const OBU_FRAME_T: u32 = 6;
const OBU_REDUNDANT_FRAME_HEADER_T: u32 = 7;
const OBU_TILE_LIST_T: u32 = 8;
/// A stored reference frame (FrameStore plus the saved per-frame state).
pub(crate) struct RefFrame {
pub buf: FrameBuf,
pub cdfs: Box<Cdfs>,
pub bit_depth: u8,
pub subsampling_x: u8,
pub subsampling_y: u8,
pub segment_ids: Vec<u8>,
pub mi_cols: usize,
pub mi_rows: usize,
pub film_grain_present: bool,
pub color: (u8, u8, u8, bool),
}
/// An AV1 decoder. Feed it temporal units (one MP4 / Matroska sample each, or any run of
/// OBUs in the low-overhead format) and collect the shown frames.
pub struct Decoder {
seq: Option<SequenceHeader>,
refs: [RefInfo; NUM_REF_FRAMES],
ref_frames: [Option<Arc<RefFrame>>; NUM_REF_FRAMES],
lf_deltas: LoopFilterDeltas,
seg_features: SegmentationFeatures,
prev_gm_params: [[i32; 6]; 8],
current_frame_id: u32,
/// Frame being decoded (between its header and its last tile group).
cur: Option<Box<CurFrame>>,
seen_frame_header: bool,
/// Apply film grain synthesis to output frames (default true).
pub apply_film_grain: bool,
}
struct CurFrame {
fs: FrameState,
cdfs: Box<Cdfs>,
saved_cdfs: Option<Box<Cdfs>>,
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
impl Decoder {
pub fn new() -> Decoder {
Decoder {
seq: None,
refs: Default::default(),
ref_frames: Default::default(),
lf_deltas: LoopFilterDeltas::defaults(),
seg_features: SegmentationFeatures::default(),
prev_gm_params: default_gm_params(),
current_frame_id: 0,
cur: None,
seen_frame_header: false,
apply_film_grain: true,
}
}
/// The active sequence header, once one has been decoded.
pub fn sequence_header(&self) -> Option<&SequenceHeader> {
self.seq.as_ref()
}
/// Decode a chunk of OBUs; returns the frames shown by it.
pub fn decode(&mut self, data: &[u8]) -> Result<Vec<Picture>> {
let mut out = Vec::new();
let mut pos = 0;
while pos < data.len() {
let h = data[pos];
if h & 0x80 != 0 {
return Err(Error::Invalid("obu_forbidden_bit"));
}
let obu_type = ((h >> 3) & 0xf) as u32;
let ext = h & 4 != 0;
let has_size = h & 2 != 0;
let mut p = pos + 1;
let (mut temporal_id, mut spatial_id) = (0, 0);
if ext {
let e = *data.get(p).ok_or(Error::Truncated)?;
temporal_id = (e >> 5) as u32;
spatial_id = ((e >> 3) & 3) as u32;
p += 1;
}
let size = if has_size {
let (v, n) = leb128(&data[p..])?;
p += n;
v as usize
} else {
data.len() - p
};
let end = p.checked_add(size).filter(|&e| e <= data.len()).ok_or(Error::Truncated)?;
let payload = &data[p..end];
pos = end;
if obu_type != OBU_SEQUENCE_HEADER_T
&& obu_type != OBU_TEMPORAL_DELIMITER_T
&& ext
&& let Some(seq) = &self.seq
&& seq.op_idc != 0
{
let idc = seq.op_idc;
let in_t = (idc >> temporal_id) & 1;
let in_s = (idc >> (spatial_id + 8)) & 1;
if in_t == 0 || in_s == 0 {
continue;
}
}
match obu_type {
OBU_SEQUENCE_HEADER_T => {
let s = SequenceHeader::parse(payload)?;
self.seq = Some(s);
}
OBU_TEMPORAL_DELIMITER_T => self.seen_frame_header = false,
OBU_FRAME_HEADER_T | OBU_REDUNDANT_FRAME_HEADER_T | OBU_FRAME_T => {
if self.seen_frame_header {
// frame_header_copy(): identical to the active header
if obu_type != OBU_FRAME_T {
continue;
}
}
let mut r = BitReader::new(payload);
let shown = self.frame_header(&mut r, temporal_id, spatial_id)?;
if let Some(pic) = shown {
out.push(pic);
continue;
}
if obu_type == OBU_FRAME_T {
r.byte_align();
let rest = &payload[r.byte_pos()..];
if let Some(pic) = self.tile_group(rest)? {
out.push(pic);
}
}
}
OBU_TILE_GROUP_T => {
if let Some(pic) = self.tile_group(payload)? {
out.push(pic);
}
}
OBU_METADATA_T | OBU_TILE_LIST_T => {}
_ => {}
}
}
Ok(out)
}
/// frame_header_obu( ): returns a picture for show_existing_frame.
fn frame_header(&mut self, r: &mut BitReader, temporal_id: u32, spatial_id: u32) -> Result<Option<Picture>> {
let seq = self.seq.clone().ok_or(Error::Invalid("frame before sequence header"))?;
self.seen_frame_header = true;
let mut st = HeaderState {
seq: &seq,
refs: &mut self.refs,
lf_deltas: self.lf_deltas,
seg_features: self.seg_features,
prev_gm_params: self.prev_gm_params,
current_frame_id: self.current_frame_id,
};
let fh = FrameHeader::parse(r, &mut st, temporal_id, spatial_id)?;
self.lf_deltas = st.lf_deltas;
self.seg_features = st.seg_features;
self.prev_gm_params = st.prev_gm_params;
self.current_frame_id = st.current_frame_id;
if fh.show_existing_frame {
self.seen_frame_header = false;
let idx = fh.frame_to_show_map_idx;
let rf = self.ref_frames[idx].clone().ok_or(Error::Invalid("show_existing_frame of an empty slot"))?;
let info = self.refs[idx].clone();
let pic = self.output_picture(&seq, &rf, info.upscaled_width as usize, info.frame_height as usize, &fh.film_grain);
if fh.frame_type == KEY_FRAME as u8 {
// reference frame loading process (7.21) then refresh every slot (7.20)
self.lf_deltas = info.lf_deltas;
self.seg_features = info.seg_features;
for i in 0..NUM_REF_FRAMES {
self.refs[i] = info.clone();
self.ref_frames[i] = Some(rf.clone());
}
}
return Ok(Some(pic));
}
if fh.frame_width > seq.max_frame_width || fh.frame_height > seq.max_frame_height {
return Err(Error::Invalid("frame larger than the sequence maximum"));
}
let fs = FrameState::new(&seq, &fh);
let cdfs = if fh.primary_ref_frame == PRIMARY_REF_NONE {
Cdfs::new(fh.quant.base_q_idx)
} else {
let slot = fh.ref_frame_idx[fh.primary_ref_frame];
let rf = self.ref_frames[slot].as_ref().ok_or(Error::Invalid("primary reference frame missing"))?;
let mut c = rf.cdfs.clone();
c.clear_counts();
c
};
self.cur = Some(Box::new(CurFrame { fs, cdfs, saved_cdfs: None }));
Ok(None)
}
/// tile_group_obu( sz ): returns the output picture when the frame completes and is shown.
fn tile_group(&mut self, data: &[u8]) -> Result<Option<Picture>> {
let mut cur = self.cur.take().ok_or(Error::Invalid("tile group without a frame header"))?;
let (cols, rows, cols_log2, rows_log2, tsb, ctx_tile) = {
let t = &cur.fs.fh.tile_info;
(t.cols, t.rows, t.cols_log2, t.rows_log2, t.tile_size_bytes, t.context_update_tile_id as usize)
};
let num_tiles = cols * rows;
let mut r = BitReader::new(data);
let mut tg_start = 0;
let mut tg_end = num_tiles - 1;
if num_tiles > 1 && r.flag()? {
let bits = cols_log2 + rows_log2;
tg_start = r.f(bits)? as usize;
tg_end = r.f(bits)? as usize;
}
r.byte_align();
if tg_end >= num_tiles || tg_start > tg_end {
return Err(Error::Invalid("tile group range"));
}
let mut pos = r.byte_pos();
for tile_num in tg_start..=tg_end {
let tile_row = tile_num / cols;
let tile_col = tile_num % cols;
let size = if tile_num == tg_end {
data.len() - pos
} else {
let mut v = 0usize;
for i in 0..tsb as usize {
v |= (*data.get(pos + i).ok_or(Error::Truncated)? as usize) << (8 * i);
}
pos += tsb as usize;
v + 1
};
let end = pos.checked_add(size).filter(|&e| e <= data.len()).ok_or(Error::Truncated)?;
let tile_data = &data[pos..end];
pos = end;
let cdfs = cur.cdfs.clone();
cur.fs.intra_frame_y_mode_cdf = DEFAULT_INTRA_FRAME_Y_MODE_CDF;
let mut td = TileDecoder::new(&mut cur.fs, tile_data, cdfs, tile_row, tile_col);
td.decode_tile()?;
if !td.fs.fh.disable_frame_end_update_cdf && tile_num == ctx_tile {
cur.saved_cdfs = Some(td.cdf);
}
}
if tg_end != num_tiles - 1 {
self.cur = Some(cur);
return Ok(None);
}
let mut cur = *cur;
if !cur.fs.fh.disable_frame_end_update_cdf
&& let Some(saved) = cur.saved_cdfs.take()
{
cur.cdfs = saved;
}
self.seen_frame_header = false;
self.decode_frame_wrapup(cur)
}
fn decode_frame_wrapup(&mut self, cur: CurFrame) -> Result<Option<Picture>> {
let CurFrame { mut fs, cdfs, .. } = cur;
crate::postfilter::apply(&mut fs);
let seq = fs.seq.clone();
let fh = fs.fh.clone();
if fh.seg.enabled && !fh.seg.update_map {
// SegmentIds = PrevSegmentIds (load_previous_segment_ids)
if fh.primary_ref_frame != PRIMARY_REF_NONE
&& let Some(rf) = &self.ref_frames[fh.ref_frame_idx[fh.primary_ref_frame]]
&& rf.mi_cols == fs.mi.cols
&& rf.mi_rows == fs.mi.rows
{
fs.mi.segment_id.copy_from_slice(&rf.segment_ids);
}
}
let rf = Arc::new(RefFrame {
buf: std::mem::take(&mut fs.cur),
cdfs,
bit_depth: seq.color.bit_depth,
subsampling_x: seq.color.subsampling_x,
subsampling_y: seq.color.subsampling_y,
segment_ids: std::mem::take(&mut fs.mi.segment_id),
mi_cols: fs.mi.cols,
mi_rows: fs.mi.rows,
film_grain_present: seq.film_grain_params_present,
color: (seq.color.color_primaries, seq.color.transfer_characteristics, seq.color.matrix_coefficients, seq.color.color_range),
});
// reference frame update process (7.20)
for i in 0..NUM_REF_FRAMES {
if (fh.refresh_frame_flags >> i) & 1 == 1 {
let ri = &mut self.refs[i];
ri.valid = true;
ri.frame_id = fh.current_frame_id;
ri.upscaled_width = fh.upscaled_width;
ri.frame_width = fh.frame_width;
ri.frame_height = fh.frame_height;
ri.render_width = fh.render_width;
ri.render_height = fh.render_height;
ri.mi_cols = fh.mi_cols;
ri.mi_rows = fh.mi_rows;
ri.frame_type = fh.frame_type;
ri.order_hint = fh.order_hint;
ri.saved_order_hints = fh.order_hints;
ri.gm_params = fh.gm_params;
ri.lf_deltas = fh.lf.deltas;
ri.seg_features = fh.seg.features;
ri.grain = fh.film_grain.clone();
self.ref_frames[i] = Some(rf.clone());
}
}
if fh.show_frame { Ok(Some(self.output_picture(&seq, &rf, fh.upscaled_width as usize, fh.frame_height as usize, &fh.film_grain))) } else { Ok(None) }
}
fn output_picture(&self, seq: &SequenceHeader, rf: &RefFrame, w: usize, h: usize, grain: &crate::header::FilmGrainParams) -> Picture {
let ssx = rf.subsampling_x as usize;
let ssy = rf.subsampling_y as usize;
let num_planes = rf.buf.num_planes;
let mut planes: [Vec<u16>; 3] = Default::default();
for p in 0..num_planes {
let (pw, ph) = if p == 0 { (w, h) } else { ((w + ssx) >> ssx, (h + ssy) >> ssy) };
let src = &rf.buf.planes[p];
let mut v = Vec::with_capacity(pw * ph);
for y in 0..ph {
v.extend_from_slice(&src.row(y)[..pw]);
}
planes[p] = v;
}
let mut pic = Picture {
width: w as u32,
height: h as u32,
bit_depth: rf.bit_depth,
subsampling_x: rf.subsampling_x,
subsampling_y: rf.subsampling_y,
mono_chrome: num_planes == 1,
planes,
color_primaries: rf.color.0,
transfer_characteristics: rf.color.1,
matrix_coefficients: rf.color.2,
full_range: rf.color.3,
};
if self.apply_film_grain && seq.film_grain_params_present && grain.apply_grain {
crate::grain::apply(&mut pic, grain, seq);
}
pic
}
}

142
crates/av1/src/frame.rs Normal file
View File

@ -0,0 +1,142 @@
//! Frame sample buffers and per-4x4 mode info storage.
/// One plane of samples (u16 at every bit depth).
#[derive(Clone, Default)]
pub struct Plane {
pub data: Vec<u16>,
pub stride: usize,
/// Allocated rows.
pub rows: usize,
}
impl Plane {
pub fn new(width: usize, height: usize) -> Plane {
Plane { data: vec![0; width * height], stride: width, rows: height }
}
#[inline(always)]
pub fn at(&self, x: usize, y: usize) -> u16 {
self.data[y * self.stride + x]
}
#[inline(always)]
pub fn set(&mut self, x: usize, y: usize, v: u16) {
self.data[y * self.stride + x] = v;
}
pub fn row(&self, y: usize) -> &[u16] {
&self.data[y * self.stride..(y + 1) * self.stride]
}
pub fn row_mut(&mut self, y: usize) -> &mut [u16] {
&mut self.data[y * self.stride..(y + 1) * self.stride]
}
}
/// A frame's three planes, sized to the macroblock-aligned coded area plus margins.
#[derive(Clone, Default)]
pub struct FrameBuf {
pub planes: [Plane; 3],
pub num_planes: usize,
pub subsampling_x: usize,
pub subsampling_y: usize,
pub bit_depth: u8,
/// Frame dimensions (luma) the samples are valid for.
pub width: usize,
pub height: usize,
}
impl FrameBuf {
/// Allocate a frame for a coded area of `width` x `height` luma samples (rounded up to the
/// 128x128 superblock grid plus 160 samples of slack for blocks extending past the edge).
pub fn new(width: usize, height: usize, num_planes: usize, subsampling_x: usize, subsampling_y: usize, bit_depth: u8) -> FrameBuf {
let aw = width.div_ceil(128) * 128 + 160;
let ah = height.div_ceil(128) * 128 + 160;
let mut planes: [Plane; 3] = Default::default();
planes[0] = Plane::new(aw, ah);
for p in planes.iter_mut().take(num_planes).skip(1) {
*p = Plane::new(aw >> subsampling_x, ah >> subsampling_y);
}
FrameBuf { planes, num_planes, subsampling_x, subsampling_y, bit_depth, width, height }
}
pub fn plane_width(&self, plane: usize) -> usize {
if plane == 0 { self.width } else { (self.width + self.subsampling_x) >> self.subsampling_x }
}
pub fn plane_height(&self, plane: usize) -> usize {
if plane == 0 { self.height } else { (self.height + self.subsampling_y) >> self.subsampling_y }
}
}
/// A motion vector (row, col) in 1/8 sample units.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Mv {
pub row: i16,
pub col: i16,
}
impl Mv {
pub const ZERO: Mv = Mv { row: 0, col: 0 };
pub fn new(row: i32, col: i32) -> Mv {
Mv { row: row as i16, col: col as i16 }
}
}
/// Mode info stored for every 4x4 luma position (the arrays the spec indexes by
/// `[ row ][ col ]` in mode-info units).
#[derive(Clone, Default)]
pub struct MiInfo {
pub cols: usize,
pub rows: usize,
pub y_mode: Vec<u8>,
pub uv_mode: Vec<u8>,
pub ref_frame: Vec<[i8; 2]>,
pub mv: Vec<[Mv; 2]>,
pub is_inter: Vec<bool>,
pub skip_mode: Vec<bool>,
pub skip: Vec<bool>,
pub tx_size: Vec<u8>,
pub inter_tx_size: Vec<u8>,
pub mi_size: Vec<u8>,
pub segment_id: Vec<u8>,
pub palette_size: [Vec<u8>; 2],
pub palette_colors: [Vec<[u16; 8]>; 2],
pub delta_lf: Vec<[i8; 4]>,
pub comp_group_idx: Vec<u8>,
pub compound_idx: Vec<u8>,
pub interp_filter: Vec<[u8; 2]>,
pub motion_mode: Vec<u8>,
/// TxTypes[ row ][ col ] (luma 4x4 units).
pub tx_type: Vec<u8>,
}
impl MiInfo {
pub fn new(cols: usize, rows: usize) -> MiInfo {
let n = cols * rows;
MiInfo {
cols,
rows,
y_mode: vec![0; n],
uv_mode: vec![0; n],
ref_frame: vec![[0, -1]; n],
mv: vec![[Mv::ZERO; 2]; n],
is_inter: vec![false; n],
skip_mode: vec![false; n],
skip: vec![false; n],
tx_size: vec![0; n],
inter_tx_size: vec![0; n],
mi_size: vec![0; n],
segment_id: vec![0; n],
palette_size: [vec![0; n], vec![0; n]],
palette_colors: [vec![[0; 8]; n], vec![[0; 8]; n]],
delta_lf: vec![[0; 4]; n],
comp_group_idx: vec![0; n],
compound_idx: vec![0; n],
interp_filter: vec![[0; 2]; n],
motion_mode: vec![0; n],
tx_type: vec![0; n],
}
}
#[inline(always)]
pub fn idx(&self, row: usize, col: usize) -> usize {
row * self.cols + col
}
}

8
crates/av1/src/grain.rs Normal file
View File

@ -0,0 +1,8 @@
//! Film grain synthesis (7.18.3).
use crate::Picture;
use crate::header::{FilmGrainParams, SequenceHeader};
pub(crate) fn apply(pic: &mut Picture, g: &FilmGrainParams, seq: &SequenceHeader) {
let _ = (pic, g, seq);
}

1361
crates/av1/src/header.rs Normal file

File diff suppressed because it is too large Load Diff

403
crates/av1/src/intra.rs Normal file
View File

@ -0,0 +1,403 @@
//! Intra prediction (spec 7.11.2), palette prediction (7.11.4) and chroma from luma (7.11.5).
use crate::frame::Plane;
use crate::spec_tables::*;
use crate::transform::round2;
/// Inputs of the intra prediction process that come from the block.
pub(crate) struct IntraParams {
pub plane: usize,
pub x: usize,
pub y: usize,
pub have_left: bool,
pub have_above: bool,
pub have_above_right: bool,
pub have_below_left: bool,
pub mode: usize,
pub log2w: u32,
pub log2h: u32,
/// Largest valid x / y in the plane: ((MiCols * MI_SIZE) >> subX) - 1.
pub max_x: i32,
pub max_y: i32,
pub bit_depth: u32,
pub angle_delta: i32,
pub use_filter_intra: bool,
pub filter_intra_mode: usize,
pub enable_intra_edge_filter: bool,
/// Result of the intra filter type process (7.11.2.8).
pub filter_type: bool,
}
const EDGE: usize = 32; // offset of index 0 in the edge buffers (they start at index -2 .. -1)
#[inline(always)]
fn clip1(v: i32, bd: u32) -> u16 {
v.clamp(0, (1 << bd) - 1) as u16
}
pub(crate) fn is_directional_mode(mode: usize) -> bool {
(V_PRED..=D67_PRED).contains(&mode)
}
/// Predict the w x h block at (x, y) of `pl` (writes into the plane).
pub(crate) fn predict_intra(pl: &mut Plane, p: &IntraParams) {
let w = 1usize << p.log2w;
let h = 1usize << p.log2h;
let bd = p.bit_depth;
let (x, y) = (p.x, p.y);
// edge arrays with room for index -2 .. 2*(w+h)
let mut above = [0i32; 2 * 128 + 2 * EDGE];
let mut left = [0i32; 2 * 128 + 2 * EDGE];
let n = w + h;
if !p.have_above && p.have_left {
let v = pl.at(x - 1, y) as i32;
above[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
} else if !p.have_above && !p.have_left {
let v = (1 << (bd - 1)) - 1;
above[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
} else {
let above_limit = (p.max_x).min(x as i32 + if p.have_above_right { 2 * w as i32 } else { w as i32 } - 1);
let row = pl.row(y - 1);
for i in 0..n {
above[EDGE + i] = row[(above_limit.min(x as i32 + i as i32)) as usize] as i32;
}
}
if !p.have_left && p.have_above {
let v = pl.at(x, y - 1) as i32;
left[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
} else if !p.have_left && !p.have_above {
let v = (1 << (bd - 1)) + 1;
left[EDGE..EDGE + n].iter_mut().for_each(|a| *a = v);
} else {
let left_limit = (p.max_y).min(y as i32 + if p.have_below_left { 2 * h as i32 } else { h as i32 } - 1);
for i in 0..n {
left[EDGE + i] = pl.at(x - 1, left_limit.min(y as i32 + i as i32) as usize) as i32;
}
}
let corner = if p.have_above && p.have_left {
pl.at(x - 1, y - 1) as i32
} else if p.have_above {
pl.at(x, y - 1) as i32
} else if p.have_left {
pl.at(x - 1, y) as i32
} else {
1 << (bd - 1)
};
above[EDGE - 1] = corner;
left[EDGE - 1] = corner;
let mut pred = [0u16; 64 * 64];
if p.plane == 0 && p.use_filter_intra {
recursive_intra(&above, &left, w, h, p.filter_intra_mode, bd, &mut pred);
} else if is_directional_mode(p.mode) {
directional(&mut above, &mut left, p, w, h, &mut pred);
} else if p.mode == SMOOTH_PRED || p.mode == SMOOTH_V_PRED || p.mode == SMOOTH_H_PRED {
smooth(&above, &left, p.mode, p.log2w, p.log2h, w, h, &mut pred);
} else if p.mode == DC_PRED {
dc(&above, &left, p.have_left, p.have_above, p.log2w, p.log2h, w, h, bd, &mut pred);
} else {
// PAETH_PRED
let tl = above[EDGE - 1];
for i in 0..h {
for j in 0..w {
let a = above[EDGE + j];
let l = left[EDGE + i];
let base = a + l - tl;
let p_left = (base - l).abs();
let p_top = (base - a).abs();
let p_tl = (base - tl).abs();
pred[i * w + j] = if p_left <= p_top && p_left <= p_tl {
l as u16
} else if p_top <= p_tl {
a as u16
} else {
tl as u16
};
}
}
}
for i in 0..h {
pl.row_mut(y + i)[x..x + w].copy_from_slice(&pred[i * w..i * w + w]);
}
}
fn recursive_intra(above: &[i32], left: &[i32], w: usize, h: usize, mode: usize, bd: u32, pred: &mut [u16]) {
let w4 = w >> 2;
let h2 = h >> 1;
for i2 in 0..h2 {
for j4 in 0..w4 {
let mut pv = [0i32; 7];
for (i, v) in pv.iter_mut().enumerate() {
*v = if i < 5 {
if i2 == 0 {
above[EDGE + (j4 << 2) + i - 1]
} else if j4 == 0 && i == 0 {
left[EDGE + (i2 << 1) - 1]
} else {
pred[((i2 << 1) - 1) * w + (j4 << 2) + i - 1] as i32
}
} else if j4 == 0 {
left[EDGE + (i2 << 1) + i - 5]
} else {
pred[((i2 << 1) + i - 5) * w + (j4 << 2) - 1] as i32
};
}
for i1 in 0..2 {
for j1 in 0..4 {
let mut pr = 0i32;
for i in 0..7 {
pr += INTRA_FILTER_TAPS[mode][(i1 << 2) + j1][i] as i32 * pv[i];
}
pred[((i2 << 1) + i1) * w + (j4 << 2) + j1] = clip1(round2_signed(pr, INTRA_FILTER_SCALE_BITS as u32), bd);
}
}
}
}
}
#[inline(always)]
pub(crate) fn round2_signed(x: i32, n: u32) -> i32 {
if x >= 0 { round2(x, n) } else { -round2(-x, n) }
}
fn sm_weights(log2: u32) -> &'static [u8] {
match log2 {
2 => &SM_WEIGHTS_TX_4X4,
3 => &SM_WEIGHTS_TX_8X8,
4 => &SM_WEIGHTS_TX_16X16,
5 => &SM_WEIGHTS_TX_32X32,
_ => &SM_WEIGHTS_TX_64X64,
}
}
#[allow(clippy::too_many_arguments)]
fn smooth(above: &[i32], left: &[i32], mode: usize, log2w: u32, log2h: u32, w: usize, h: usize, pred: &mut [u16]) {
let wx = sm_weights(log2w);
let wy = sm_weights(log2h);
for i in 0..h {
for j in 0..w {
let v = if mode == SMOOTH_PRED {
let s = wy[i] as i32 * above[EDGE + j]
+ (256 - wy[i] as i32) * left[EDGE + h - 1]
+ wx[j] as i32 * left[EDGE + i]
+ (256 - wx[j] as i32) * above[EDGE + w - 1];
round2(s, 9)
} else if mode == SMOOTH_V_PRED {
round2(wy[i] as i32 * above[EDGE + j] + (256 - wy[i] as i32) * left[EDGE + h - 1], 8)
} else {
round2(wx[j] as i32 * left[EDGE + i] + (256 - wx[j] as i32) * above[EDGE + w - 1], 8)
};
pred[i * w + j] = v as u16;
}
}
}
#[allow(clippy::too_many_arguments)]
fn dc(above: &[i32], left: &[i32], have_left: bool, have_above: bool, log2w: u32, log2h: u32, w: usize, h: usize, bd: u32, pred: &mut [u16]) {
let v = if have_left && have_above {
let mut sum: i32 = left[EDGE..EDGE + h].iter().sum::<i32>() + above[EDGE..EDGE + w].iter().sum::<i32>();
sum += ((w + h) >> 1) as i32;
sum / (w + h) as i32
} else if have_left {
let sum: i32 = left[EDGE..EDGE + h].iter().sum();
(sum + (h >> 1) as i32) >> log2h
} else if have_above {
let sum: i32 = above[EDGE..EDGE + w].iter().sum();
(sum + (w >> 1) as i32) >> log2w
} else {
1 << (bd - 1)
};
let v = clip1(v, bd);
pred[..w * h].iter_mut().for_each(|p| *p = v);
}
fn edge_filter_strength(w: usize, h: usize, filter_type: bool, delta: i32) -> u32 {
let d = delta.abs();
let blk_wh = w + h;
let mut strength = 0;
if !filter_type {
if blk_wh <= 8 {
if d >= 56 {
strength = 1;
}
} else if blk_wh <= 16 {
if d >= 40 {
strength = 1;
}
} else if blk_wh <= 24 {
if d >= 8 {
strength = 1;
}
if d >= 16 {
strength = 2;
}
if d >= 32 {
strength = 3;
}
} else if blk_wh <= 32 {
strength = 1;
if d >= 4 {
strength = 2;
}
if d >= 32 {
strength = 3;
}
} else {
strength = 3;
}
} else if blk_wh <= 8 {
if d >= 40 {
strength = 1;
}
if d >= 64 {
strength = 2;
}
} else if blk_wh <= 16 {
if d >= 20 {
strength = 1;
}
if d >= 48 {
strength = 2;
}
} else if blk_wh <= 24 {
if d >= 4 {
strength = 3;
}
} else {
strength = 3;
}
strength
}
fn use_upsample(w: usize, h: usize, filter_type: bool, delta: i32) -> bool {
let d = delta.abs();
let blk_wh = w + h;
if d <= 0 || d >= 40 {
false
} else if !filter_type {
blk_wh <= 16
} else {
blk_wh <= 8
}
}
/// Intra edge filter (7.11.2.12) on buf[-1 .. sz-2] (edge[i] = buf[i - 1]).
fn edge_filter(buf: &mut [i32], sz: usize, strength: u32) {
if strength == 0 {
return;
}
let mut edge = [0i32; 2 * 128 + 2 * EDGE];
for i in 0..sz {
edge[i] = buf[EDGE + i - 1];
}
for i in 1..sz {
let mut s = 0;
for j in 0..INTRA_EDGE_TAPS {
let k = (i as i32 - 2 + j as i32).clamp(0, sz as i32 - 1) as usize;
s += INTRA_EDGE_KERNEL[strength as usize - 1][j] as i32 * edge[k];
}
buf[EDGE + i - 1] = (s + 8) >> 4;
}
}
/// Intra edge upsample (7.11.2.11).
fn edge_upsample(buf: &mut [i32], num_px: usize, bd: u32) {
let mut dup = [0i32; 2 * 128 + 8];
dup[0] = buf[EDGE - 1];
for i in -1..num_px as i32 {
dup[(i + 2) as usize] = buf[(EDGE as i32 + i) as usize];
}
dup[num_px + 2] = buf[EDGE + num_px - 1];
buf[EDGE - 2] = dup[0];
for i in 0..num_px {
let s = -dup[i] + 9 * dup[i + 1] + 9 * dup[i + 2] - dup[i + 3];
let s = clip1(round2(s, 4), bd) as i32;
buf[EDGE + 2 * i - 1] = s;
buf[EDGE + 2 * i] = dup[i + 2];
}
}
fn directional(above: &mut [i32], left: &mut [i32], p: &IntraParams, w: usize, h: usize, pred: &mut [u16]) {
let p_angle = MODE_TO_ANGLE[p.mode] as i32 + p.angle_delta * ANGLE_STEP as i32;
let mut upsample_above = 0u32;
let mut upsample_left = 0u32;
if p.enable_intra_edge_filter {
if p_angle != 90 && p_angle != 180 {
if p_angle > 90 && p_angle < 180 && (w + h) >= 24 {
let s = left[EDGE] * 5 + above[EDGE - 1] * 6 + above[EDGE] * 5;
let c = round2(s, 4);
left[EDGE - 1] = c;
above[EDGE - 1] = c;
}
if p.have_above {
let strength = edge_filter_strength(w, h, p.filter_type, p_angle - 90);
let num_px = w.min((p.max_x - p.x as i32 + 1) as usize) + if p_angle < 90 { h } else { 0 } + 1;
edge_filter(above, num_px, strength);
}
if p.have_left {
let strength = edge_filter_strength(w, h, p.filter_type, p_angle - 180);
let num_px = h.min((p.max_y - p.y as i32 + 1) as usize) + if p_angle > 180 { w } else { 0 } + 1;
edge_filter(left, num_px, strength);
}
}
if use_upsample(w, h, p.filter_type, p_angle - 90) {
upsample_above = 1;
let num_px = w + if p_angle < 90 { h } else { 0 };
edge_upsample(above, num_px, p.bit_depth);
}
if use_upsample(w, h, p.filter_type, p_angle - 180) {
upsample_left = 1;
let num_px = h + if p_angle > 180 { w } else { 0 };
edge_upsample(left, num_px, p.bit_depth);
}
}
let dx = if p_angle < 90 {
DR_INTRA_DERIVATIVE[p_angle as usize] as i32
} else if p_angle > 90 && p_angle < 180 {
DR_INTRA_DERIVATIVE[(180 - p_angle) as usize] as i32
} else {
0
};
let dy = if p_angle > 90 && p_angle < 180 {
DR_INTRA_DERIVATIVE[(p_angle - 90) as usize] as i32
} else if p_angle > 180 {
DR_INTRA_DERIVATIVE[(270 - p_angle) as usize] as i32
} else {
0
};
let a = |i: i32| above[(EDGE as i32 + i) as usize];
let l = |i: i32| left[(EDGE as i32 + i) as usize];
for i in 0..h as i32 {
for j in 0..w as i32 {
let v = if p_angle < 90 {
let idx = (i + 1) * dx;
let base = (idx >> (6 - upsample_above)) + (j << upsample_above);
let shift = ((idx << upsample_above) >> 1) & 0x1f;
let max_base_x = (w as i32 + h as i32 - 1) << upsample_above;
if base < max_base_x { round2(a(base) * (32 - shift) + a(base + 1) * shift, 5) } else { a(max_base_x) }
} else if p_angle > 90 && p_angle < 180 {
let idx = (j << 6) - (i + 1) * dx;
let base = idx >> (6 - upsample_above);
if base >= -(1 << upsample_above) {
let shift = ((idx << upsample_above) >> 1) & 0x1f;
round2(a(base) * (32 - shift) + a(base + 1) * shift, 5)
} else {
let idx = (i << 6) - (j + 1) * dy;
let base = idx >> (6 - upsample_left);
let shift = ((idx << upsample_left) >> 1) & 0x1f;
round2(l(base) * (32 - shift) + l(base + 1) * shift, 5)
}
} else if p_angle > 180 {
let idx = (j + 1) * dy;
let base = (idx >> (6 - upsample_left)) + (i << upsample_left);
let shift = ((idx << upsample_left) >> 1) & 0x1f;
round2(l(base) * (32 - shift) + l(base + 1) * shift, 5)
} else if p_angle == 90 {
a(j)
} else {
l(i)
};
pred[(i as usize) * w + j as usize] = v as u16;
}
}
}

106
crates/av1/src/lib.rs Normal file
View File

@ -0,0 +1,106 @@
//! Clean-room AV1 decoder, implemented from the AV1 Bitstream & Decoding Process Specification
//! v1.0.0 with Errata 1 (AOMediaCodec/av1-spec).
//!
//! See the crate README for the conformance status of each decoding stage.
// Items used only by stages still being written (inter prediction, post filters).
#![allow(dead_code)]
mod bits;
mod cdf;
mod decoder;
mod frame;
mod grain;
pub mod header;
mod intra;
mod postfilter;
mod spec_tables;
mod state;
mod symbol;
mod tile;
mod transform;
pub use decoder::Decoder;
pub use header::{ColorConfig, FrameHeader, SequenceHeader};
/// Decoder errors.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("truncated AV1 data")]
Truncated,
#[error("invalid AV1 data: {0}")]
Invalid(&'static str),
#[error("unsupported AV1 feature: {0}")]
Unsupported(&'static str),
}
pub type Result<T> = std::result::Result<T, Error>;
/// A decoded (shown) frame: planar samples without row padding, `bit_depth` significant bits in
/// `u16`. Chroma planes are `((width + ssx) >> ssx) x ((height + ssy) >> ssy)`; they are empty
/// for monochrome streams.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Picture {
pub width: u32,
pub height: u32,
pub bit_depth: u8,
pub subsampling_x: u8,
pub subsampling_y: u8,
pub mono_chrome: bool,
pub planes: [Vec<u16>; 3],
pub color_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
pub full_range: bool,
}
impl Picture {
pub fn plane_width(&self, plane: usize) -> u32 {
if plane == 0 { self.width } else { (self.width + self.subsampling_x as u32) >> self.subsampling_x }
}
pub fn plane_height(&self, plane: usize) -> u32 {
if plane == 0 { self.height } else { (self.height + self.subsampling_y as u32) >> self.subsampling_y }
}
}
/// Whether a temporal unit starts a random access point: it contains a sequence header and a
/// shown key frame (used for keyframe detection by demuxers without sync-sample tables).
pub fn is_key_frame_unit(data: &[u8]) -> bool {
let mut pos = 0;
let mut seq_seen = false;
while pos < data.len() {
let h = data[pos];
let obu_type = (h >> 3) & 0xf;
let ext = h & 4 != 0;
let has_size = h & 2 != 0;
let mut p = pos + 1 + ext as usize;
let size = if has_size {
match bits::leb128(data.get(p..).unwrap_or(&[])) {
Ok((v, n)) => {
p += n;
v as usize
}
Err(_) => return false,
}
} else {
data.len().saturating_sub(p)
};
if p > data.len() {
return false;
}
match obu_type {
1 => seq_seen = true,
3 | 6 => {
let b = data.get(p).copied().unwrap_or(0);
// show_existing_frame = 0, frame_type = KEY_FRAME (0), show_frame = 1
let show_existing = b >> 7;
let frame_type = (b >> 5) & 3;
let show_frame = (b >> 4) & 1;
return seq_seen && show_existing == 0 && frame_type == 0 && show_frame == 1;
}
_ => {}
}
pos = p.saturating_add(size);
}
false
}

View File

@ -0,0 +1,8 @@
//! In-loop post filters run by decode_frame_wrapup: loop filter (7.14), CDEF (7.15),
//! super-resolution upscaling (7.16) and loop restoration (7.17).
use crate::state::FrameState;
pub(crate) fn apply(fs: &mut FrameState) {
let _ = fs;
}

File diff suppressed because it is too large Load Diff

130
crates/av1/src/state.rs Normal file
View File

@ -0,0 +1,130 @@
//! Per-frame decoding state shared by the tiles and the post-filters.
use crate::frame::{FrameBuf, MiInfo};
use crate::header::{FrameHeader, SequenceHeader};
use crate::spec_tables::*;
pub(crate) struct FrameState {
pub seq: SequenceHeader,
pub fh: FrameHeader,
pub num_planes: usize,
pub ssx: usize,
pub ssy: usize,
pub bit_depth: u32,
pub cur: FrameBuf,
pub mi: MiInfo,
pub above_level: [Vec<u8>; 3],
pub above_dc: [Vec<u8>; 3],
pub above_seg_pred: Vec<u8>,
pub left_level: [Vec<u8>; 3],
pub left_dc: [Vec<u8>; 3],
pub left_seg_pred: Vec<u8>,
/// cdef_idx per 64x64 block.
cdef: Vec<i8>,
cdef_cols: usize,
/// LoopfilterTxSizes[ plane ][ row ][ col ] in 4x4 units of each plane.
pub lf_tx_size: [Vec<u8>; 3],
pub lr_unit_rows: [usize; 3],
pub lr_unit_cols: [usize; 3],
pub lr_type: [Vec<u8>; 3],
pub lr_wiener: [Vec<[[i8; 3]; 2]>; 3],
pub lr_sgr_set: [Vec<u8>; 3],
pub lr_sgr_xqd: [Vec<[i8; 2]>; 3],
/// TileIntraFrameYModeCdf (reset per tile).
pub intra_frame_y_mode_cdf: [[[u16; 14]; 5]; 5],
}
fn count_units_in_frame(unit_size: usize, frame_size: usize) -> usize {
((frame_size + (unit_size >> 1)) / unit_size).max(1)
}
impl FrameState {
pub fn new(seq: &SequenceHeader, fh: &FrameHeader) -> FrameState {
let c = &seq.color;
let (ssx, ssy) = (c.subsampling_x as usize, c.subsampling_y as usize);
let mi_cols = fh.mi_cols as usize;
let mi_rows = fh.mi_rows as usize;
let cur = FrameBuf::new(fh.frame_width as usize, fh.frame_height as usize, c.num_planes, ssx, ssy, c.bit_depth);
let cdef_cols = mi_cols.div_ceil(16) + 2;
let cdef_rows = mi_rows.div_ceil(16) + 2;
let mut lr_unit_rows = [0; 3];
let mut lr_unit_cols = [0; 3];
let mut lr_type: [Vec<u8>; 3] = Default::default();
let mut lr_wiener: [Vec<[[i8; 3]; 2]>; 3] = Default::default();
let mut lr_sgr_set: [Vec<u8>; 3] = Default::default();
let mut lr_sgr_xqd: [Vec<[i8; 2]>; 3] = Default::default();
for plane in 0..c.num_planes {
if fh.lr.frame_restoration_type[plane] == RESTORE_NONE as u8 {
continue;
}
let sub_x = if plane == 0 { 0 } else { ssx };
let sub_y = if plane == 0 { 0 } else { ssy };
let unit = fh.lr.loop_restoration_size[plane] as usize;
lr_unit_rows[plane] = count_units_in_frame(unit, (fh.frame_height as usize + sub_y) >> sub_y);
lr_unit_cols[plane] = count_units_in_frame(unit, (fh.upscaled_width as usize + sub_x) >> sub_x);
let n = lr_unit_rows[plane] * lr_unit_cols[plane];
lr_type[plane] = vec![RESTORE_NONE as u8; n];
lr_wiener[plane] = vec![[[0; 3]; 2]; n];
lr_sgr_set[plane] = vec![0; n];
lr_sgr_xqd[plane] = vec![[0; 2]; n];
}
FrameState {
seq: seq.clone(),
fh: fh.clone(),
num_planes: c.num_planes,
ssx,
ssy,
bit_depth: c.bit_depth as u32,
cur,
mi: MiInfo::new(mi_cols, mi_rows),
above_level: std::array::from_fn(|_| vec![0; mi_cols + 64]),
above_dc: std::array::from_fn(|_| vec![0; mi_cols + 64]),
above_seg_pred: vec![0; mi_cols + 64],
left_level: std::array::from_fn(|_| vec![0; mi_rows + 64]),
left_dc: std::array::from_fn(|_| vec![0; mi_rows + 64]),
left_seg_pred: vec![0; mi_rows + 64],
cdef: vec![-1; cdef_cols * cdef_rows],
cdef_cols,
lf_tx_size: std::array::from_fn(|_| vec![0; (mi_cols + 32) * (mi_rows + 32)]),
lr_unit_rows,
lr_unit_cols,
lr_type,
lr_wiener,
lr_sgr_set,
lr_sgr_xqd,
intra_frame_y_mode_cdf: DEFAULT_INTRA_FRAME_Y_MODE_CDF,
}
}
pub fn plane_residual_size(&self, subsize: usize, plane: usize) -> usize {
let sx = if plane > 0 { self.ssx } else { 0 };
let sy = if plane > 0 { self.ssy } else { 0 };
SUBSAMPLED_SIZE[subsize][sx][sy] as usize
}
#[inline]
pub fn cdef_idx(&self, r: usize, c: usize) -> i8 {
self.cdef[(r >> 4) * self.cdef_cols + (c >> 4)]
}
#[inline]
pub fn set_cdef_idx(&mut self, r: usize, c: usize, v: i8) {
let i = (r >> 4) * self.cdef_cols + (c >> 4);
if i < self.cdef.len() {
self.cdef[i] = v;
}
}
#[inline]
pub fn lf_tx_size(&self, plane: usize, row: usize, col: usize) -> usize {
self.lf_tx_size[plane][row * (self.mi.cols + 32) + col] as usize
}
#[inline]
pub fn set_lf_tx_size(&mut self, plane: usize, row: usize, col: usize, v: u8) {
let stride = self.mi.cols + 32;
if col < stride && row < self.mi.rows + 32 {
self.lf_tx_size[plane][row * stride + col] = v;
}
}
}

145
crates/av1/src/symbol.rs Normal file
View File

@ -0,0 +1,145 @@
//! Symbol (arithmetic) decoder, spec 8.2.
//!
//! The state follows the specification literally: `SymbolValue` / `SymbolRange` are 15/16-bit
//! quantities renormalised by reading `bits` new bits at a time, and `SymbolMaxBits` counts the
//! bits still available (going negative while zero padding is consumed).
const EC_PROB_SHIFT: u32 = 6;
const EC_MIN_PROB: u32 = 4;
pub(crate) struct SymbolDecoder<'a> {
data: &'a [u8],
/// Bit position of the next bit to read.
pos: usize,
end: usize,
value: u32,
range: u32,
max_bits: i64,
pub disable_update: bool,
}
impl<'a> SymbolDecoder<'a> {
/// `init_symbol( sz )` over `data` (the tile's bytes).
pub fn new(data: &'a [u8], disable_update: bool) -> Self {
let sz = data.len();
let mut d = SymbolDecoder { data, pos: 0, end: sz * 8, value: 0, range: 1 << 15, max_bits: 8 * sz as i64 - 15, disable_update };
let num_bits = (sz * 8).min(15) as u32;
let buf = d.read_bits(num_bits);
let padded = buf << (15 - num_bits);
d.value = ((1 << 15) - 1) ^ padded;
d
}
#[inline(always)]
fn read_bits(&mut self, n: u32) -> u32 {
let mut x = 0u32;
for _ in 0..n {
let bit = if self.pos < self.end { (self.data[self.pos >> 3] >> (7 - (self.pos & 7))) & 1 } else { 0 };
self.pos += 1;
x = (x << 1) | bit as u32;
}
x
}
/// `read_symbol( cdf )`: `cdf` has N + 1 entries (the last is the adaptation counter).
#[inline]
pub fn read_symbol(&mut self, cdf: &mut [u16]) -> usize {
let n = cdf.len() - 1;
let symbol = self.decode(cdf, n);
if !self.disable_update {
update_cdf(cdf, n, symbol);
}
symbol
}
#[inline(always)]
fn decode(&mut self, cdf: &[u16], n: usize) -> usize {
let mut cur = self.range;
let mut symbol = 0usize;
let mut prev;
loop {
prev = cur;
let f = (1u32 << 15) - cdf[symbol] as u32;
cur = ((self.range >> 8) * (f >> EC_PROB_SHIFT)) >> (7 - EC_PROB_SHIFT);
cur += EC_MIN_PROB * (n - symbol - 1) as u32;
if self.value >= cur {
break;
}
symbol += 1;
}
self.range = prev - cur;
self.value -= cur;
self.renormalize();
symbol
}
#[inline(always)]
fn renormalize(&mut self) {
let bits = 15 - (31 - self.range.leading_zeros());
if bits == 0 {
return;
}
self.range <<= bits;
let num_bits = (bits as i64).min(self.max_bits.max(0)) as u32;
let new_data = self.read_bits(num_bits);
let padded = new_data << (bits - num_bits);
self.value = padded ^ (((self.value + 1) << bits) - 1);
self.max_bits -= bits as i64;
}
/// `read_bool()`
#[inline]
pub fn read_bool(&mut self) -> u32 {
let cdf = [1u16 << 14, 1 << 15, 0];
self.decode(&cdf, 2) as u32
}
/// `read_literal( n )` (L(n))
pub fn read_literal(&mut self, n: u32) -> u32 {
let mut x = 0;
for _ in 0..n {
x = (x << 1) | self.read_bool();
}
x
}
/// NS(n)
pub fn read_ns(&mut self, n: u32) -> u32 {
if n <= 1 {
return 0;
}
let w = 32 - n.leading_zeros();
let m = (1u32 << w) - n;
let v = self.read_literal(w - 1);
if v < m {
return v;
}
let extra = self.read_literal(1);
(v << 1) - m + extra
}
/// SymbolMaxBits (for the exit process checks).
pub fn max_bits(&self) -> i64 {
self.max_bits
}
}
/// CDF adaptation (8.2.6).
#[inline(always)]
pub(crate) fn update_cdf(cdf: &mut [u16], n: usize, symbol: usize) {
let count = cdf[n];
let rate = 3 + (count > 15) as u32 + (count > 31) as u32 + (31 - (n as u32).leading_zeros()).min(2);
let mut tmp = 0u32;
for i in 0..n - 1 {
if i == symbol {
tmp = 1 << 15;
}
let c = cdf[i] as u32;
if tmp < c {
cdf[i] = (c - ((c - tmp) >> rate)) as u16;
} else {
cdf[i] = (c + ((tmp - c) >> rate)) as u16;
}
}
cdf[n] += (count < 32) as u16;
}

2123
crates/av1/src/tile.rs Normal file

File diff suppressed because it is too large Load Diff

540
crates/av1/src/transform.rs Normal file
View File

@ -0,0 +1,540 @@
//! Inverse transforms (spec 7.13), implemented step by step as specified.
use crate::spec_tables::*;
#[inline(always)]
fn round2_64(x: i64, n: u32) -> i32 {
if n == 0 { x as i32 } else { ((x + (1i64 << (n - 1))) >> n) as i32 }
}
#[inline(always)]
pub(crate) fn round2(x: i32, n: u32) -> i32 {
if n == 0 { x } else { (x + (1 << (n - 1))) >> n }
}
#[inline(always)]
fn cos128(angle: i32) -> i64 {
let a = (angle & 255) as usize;
(match a {
0..=64 => COS128_LOOKUP[a] as i32,
65..=128 => -(COS128_LOOKUP[128 - a] as i32),
129..=192 => -(COS128_LOOKUP[a - 128] as i32),
_ => COS128_LOOKUP[256 - a] as i32,
}) as i64
}
#[inline(always)]
fn sin128(angle: i32) -> i64 {
cos128(angle - 64)
}
fn brev(num_bits: u32, x: usize) -> usize {
let mut t = 0;
for i in 0..num_bits {
let bit = (x >> i) & 1;
t += bit << (num_bits - 1 - i);
}
t
}
/// Butterfly rotation B( a, b, angle, flip ).
#[inline(always)]
fn bf(t: &mut [i32], a: usize, b: usize, angle: i32, flip: bool) {
let x = t[a] as i64 * cos128(angle) - t[b] as i64 * sin128(angle);
let y = t[a] as i64 * sin128(angle) + t[b] as i64 * cos128(angle);
t[a] = round2_64(x, 12);
t[b] = round2_64(y, 12);
if flip {
t.swap(a, b);
}
}
/// Hadamard rotation H( a, b, flip, r ).
#[inline(always)]
fn hd(t: &mut [i32], a: usize, b: usize, flip: bool, r: u32) {
let (a, b) = if flip { (b, a) } else { (a, b) };
let x = t[a];
let y = t[b];
let lo = -(1i32 << (r - 1));
let hi = (1i32 << (r - 1)) - 1;
t[a] = (x + y).clamp(lo, hi);
t[b] = (x - y).clamp(lo, hi);
}
/// Inverse DCT of length 2^n (7.13.2.3), in place.
pub(crate) fn inverse_dct(t: &mut [i32], n: u32, r: u32) {
// 7.13.2.2 permutation
let len = 1usize << n;
let mut copy = [0i32; 64];
copy[..len].copy_from_slice(&t[..len]);
for i in 0..len {
t[i] = copy[brev(n, i)];
}
if n == 6 {
for i in 0..16 {
bf(t, 32 + i, 63 - i, 63 - 4 * brev(4, i) as i32, false);
}
}
if n >= 5 {
for i in 0..8 {
bf(t, 16 + i, 31 - i, 6 + ((brev(3, 7 - i) as i32) << 3), false);
}
}
if n == 6 {
for i in 0..16 {
hd(t, 32 + i * 2, 33 + i * 2, i & 1 == 1, r);
}
}
if n >= 4 {
for i in 0..4 {
bf(t, 8 + i, 15 - i, 12 + ((brev(2, 3 - i) as i32) << 4), false);
}
}
if n >= 5 {
for i in 0..8 {
hd(t, 16 + 2 * i, 17 + 2 * i, i & 1 == 1, r);
}
}
if n == 6 {
for i in 0..4 {
for j in 0..2 {
bf(t, 62 - i * 4 - j, 33 + i * 4 + j, 60 - 16 * brev(2, i) as i32 + 64 * j as i32, true);
}
}
}
if n >= 3 {
for i in 0..2 {
bf(t, 4 + i, 7 - i, 56 - 32 * i as i32, false);
}
}
if n >= 4 {
for i in 0..4 {
hd(t, 8 + 2 * i, 9 + 2 * i, i & 1 == 1, r);
}
}
if n >= 5 {
for i in 0..2 {
for j in 0..2 {
bf(t, 30 - 4 * i - j, 17 + 4 * i + j, 24 + ((j as i32) << 6) + (((1 - i) as i32) << 5), true);
}
}
}
if n == 6 {
for i in 0..8 {
for j in 0..2 {
hd(t, 32 + i * 4 + j, 35 + i * 4 - j, i & 1 == 1, r);
}
}
}
for i in 0..2 {
bf(t, 2 * i, 2 * i + 1, 32 + 16 * i as i32, i == 0);
}
if n >= 3 {
for i in 0..2 {
hd(t, 4 + 2 * i, 5 + 2 * i, i == 1, r);
}
}
if n >= 4 {
for i in 0..2 {
bf(t, 14 - i, 9 + i, 48 + 64 * i as i32, true);
}
}
if n >= 5 {
for i in 0..4 {
for j in 0..2 {
hd(t, 16 + 4 * i + j, 19 + 4 * i - j, i & 1 == 1, r);
}
}
}
if n == 6 {
for i in 0..2 {
for j in 0..4 {
bf(t, 61 - i * 8 - j, 34 + i * 8 + j, 56 - i as i32 * 32 + (j as i32 >> 1) * 64, true);
}
}
}
for i in 0..2 {
hd(t, i, 3 - i, false, r);
}
if n >= 3 {
bf(t, 6, 5, 32, true);
}
if n >= 4 {
for i in 0..2 {
for j in 0..2 {
hd(t, 8 + 4 * i + j, 11 + 4 * i - j, i == 1, r);
}
}
}
if n >= 5 {
for i in 0..4 {
bf(t, 29 - i, 18 + i, 48 + (i as i32 >> 1) * 64, true);
}
}
if n == 6 {
for i in 0..4 {
for j in 0..4 {
hd(t, 32 + 8 * i + j, 39 + 8 * i - j, i & 1 == 1, r);
}
}
}
if n >= 3 {
for i in 0..4 {
hd(t, i, 7 - i, false, r);
}
}
if n >= 4 {
for i in 0..2 {
bf(t, 13 - i, 10 + i, 32, true);
}
}
if n >= 5 {
for i in 0..2 {
for j in 0..4 {
hd(t, 16 + i * 8 + j, 23 + i * 8 - j, i == 1, r);
}
}
}
if n == 6 {
for i in 0..8 {
bf(t, 59 - i, 36 + i, if i < 4 { 48 } else { 112 }, true);
}
}
if n >= 4 {
for i in 0..8 {
hd(t, i, 15 - i, false, r);
}
}
if n >= 5 {
for i in 0..4 {
bf(t, 27 - i, 20 + i, 32, true);
}
}
if n == 6 {
for i in 0..8 {
hd(t, 32 + i, 47 - i, false, r);
hd(t, 48 + i, 63 - i, true, r);
}
}
if n >= 5 {
for i in 0..16 {
hd(t, i, 31 - i, false, r);
}
}
if n == 6 {
for i in 0..8 {
bf(t, 55 - i, 40 + i, 32, true);
}
}
if n == 6 {
for i in 0..32 {
hd(t, i, 63 - i, false, r);
}
}
}
const SINPI_1_9: i64 = 1321;
const SINPI_2_9: i64 = 2482;
const SINPI_3_9: i64 = 3344;
const SINPI_4_9: i64 = 3803;
fn inverse_adst4(t: &mut [i32]) {
let mut s = [0i64; 7];
let mut x = [0i64; 4];
let t0 = t[0] as i64;
let t1 = t[1] as i64;
let t2 = t[2] as i64;
let t3 = t[3] as i64;
s[0] = SINPI_1_9 * t0;
s[1] = SINPI_2_9 * t0;
s[2] = SINPI_3_9 * t1;
s[3] = SINPI_4_9 * t2;
s[4] = SINPI_1_9 * t2;
s[5] = SINPI_2_9 * t3;
s[6] = SINPI_4_9 * t3;
let a7 = t0 - t2;
let b7 = a7 + t3;
s[0] += s[3];
s[1] -= s[4];
s[3] = s[2];
s[2] = SINPI_3_9 * b7;
s[0] += s[5];
s[1] -= s[6];
x[0] = s[0] + s[3];
x[1] = s[1] + s[3];
x[2] = s[2];
x[3] = s[0] + s[1];
x[3] -= s[3];
for i in 0..4 {
t[i] = round2_64(x[i], 12);
}
}
fn adst_in_permute(t: &mut [i32], n: u32) {
let n0 = 1usize << n;
let mut copy = [0i32; 16];
copy[..n0].copy_from_slice(&t[..n0]);
for i in 0..n0 {
let idx = if i & 1 == 1 { i - 1 } else { n0 - i - 1 };
t[i] = copy[idx];
}
}
fn adst_out_permute(t: &mut [i32], n: u32) {
let n0 = 1usize << n;
let mut copy = [0i32; 16];
copy[..n0].copy_from_slice(&t[..n0]);
for i in 0..n0 {
let a = (i >> 3) & 1;
let b = ((i >> 2) & 1) ^ ((i >> 3) & 1);
let c = ((i >> 1) & 1) ^ ((i >> 2) & 1);
let d = (i & 1) ^ ((i >> 1) & 1);
let idx = ((d << 3) | (c << 2) | (b << 1) | a) >> (4 - n);
t[i] = if i & 1 == 1 { -copy[idx] } else { copy[idx] };
}
}
fn inverse_adst8(t: &mut [i32], r: u32) {
adst_in_permute(t, 3);
for i in 0..4 {
bf(t, 2 * i, 2 * i + 1, 60 - 16 * i as i32, true);
}
for i in 0..4 {
hd(t, i, 4 + i, false, r);
}
for i in 0..2 {
bf(t, 4 + 3 * i, 5 + i, 48 - 32 * i as i32, true);
}
for i in 0..2 {
for j in 0..2 {
hd(t, 4 * j + i, 2 + 4 * j + i, false, r);
}
}
for i in 0..2 {
bf(t, 2 + 4 * i, 3 + 4 * i, 32, true);
}
adst_out_permute(t, 3);
}
fn inverse_adst16(t: &mut [i32], r: u32) {
adst_in_permute(t, 4);
for i in 0..8 {
bf(t, 2 * i, 2 * i + 1, 62 - 8 * i as i32, true);
}
for i in 0..8 {
hd(t, i, 8 + i, false, r);
}
for i in 0..2 {
bf(t, 8 + 2 * i, 9 + 2 * i, 56 - 32 * i as i32, true);
bf(t, 13 + 2 * i, 12 + 2 * i, 8 + 32 * i as i32, true);
}
for i in 0..4 {
for j in 0..2 {
hd(t, 8 * j + i, 4 + 8 * j + i, false, r);
}
}
for i in 0..2 {
for j in 0..2 {
bf(t, 4 + 8 * j + 3 * i, 5 + 8 * j + i, 48 - 32 * i as i32, true);
}
}
for i in 0..2 {
for j in 0..4 {
hd(t, 4 * j + i, 2 + 4 * j + i, false, r);
}
}
for i in 0..4 {
bf(t, 2 + 4 * i, 3 + 4 * i, 32, true);
}
adst_out_permute(t, 4);
}
fn inverse_adst(t: &mut [i32], n: u32, r: u32) {
match n {
2 => inverse_adst4(t),
3 => inverse_adst8(t, r),
_ => inverse_adst16(t, r),
}
}
fn inverse_identity(t: &mut [i32], n: u32) {
match n {
2 => {
for v in t[..4].iter_mut() {
*v = round2_64(*v as i64 * 5793, 12);
}
}
3 => {
for v in t[..8].iter_mut() {
*v *= 2;
}
}
4 => {
for v in t[..16].iter_mut() {
*v = round2_64(*v as i64 * 11586, 12);
}
}
_ => {
for v in t[..32].iter_mut() {
*v *= 4;
}
}
}
}
fn inverse_wht(t: &mut [i32], shift: u32) {
let mut a = t[0] >> shift;
let mut c = t[1] >> shift;
let mut d = t[2] >> shift;
let mut b = t[3] >> shift;
a += c;
d -= b;
let e = (a - d) >> 1;
b = e - b;
c = e - c;
a -= b;
d += c;
t[0] = a;
t[1] = b;
t[2] = c;
t[3] = d;
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind1d {
Dct,
Adst,
Identity,
}
fn row_kind(tx_type: usize) -> Kind1d {
match tx_type {
DCT_DCT | ADST_DCT | FLIPADST_DCT | H_DCT => Kind1d::Dct,
DCT_ADST | ADST_ADST | DCT_FLIPADST | FLIPADST_FLIPADST | ADST_FLIPADST | FLIPADST_ADST | H_ADST | H_FLIPADST => Kind1d::Adst,
_ => Kind1d::Identity,
}
}
fn col_kind(tx_type: usize) -> Kind1d {
match tx_type {
DCT_DCT | DCT_ADST | DCT_FLIPADST | V_DCT => Kind1d::Dct,
ADST_DCT | ADST_ADST | FLIPADST_DCT | FLIPADST_FLIPADST | ADST_FLIPADST | FLIPADST_ADST | V_ADST | V_FLIPADST => Kind1d::Adst,
_ => Kind1d::Identity,
}
}
/// 2D inverse transform (7.13.3). `dequant` is 64x64 (row stride 64, only the top-left 32x32
/// may be non-zero); the residual is written to `residual` with row stride `w`.
pub(crate) fn inverse_transform_2d(dequant: &[i32], residual: &mut [i32], tx_sz: usize, tx_type: usize, lossless: bool, bit_depth: u32) {
let log2w = TX_WIDTH_LOG2[tx_sz] as u32;
let log2h = TX_HEIGHT_LOG2[tx_sz] as u32;
let w = 1usize << log2w;
let h = 1usize << log2h;
let row_shift = if lossless { 0 } else { TRANSFORM_ROW_SHIFT[tx_sz] as u32 };
let col_shift = if lossless { 0 } else { 4 };
let row_clamp = bit_depth + 8;
let col_clamp = (bit_depth + 6).max(16);
let rk = row_kind(tx_type);
let ck = col_kind(tx_type);
let mut t = [0i32; 64];
let rect2 = log2w.abs_diff(log2h) == 1;
for i in 0..h {
if i >= 32 {
// rows beyond 32 have no coefficients but still go through the transform
t[..w].iter_mut().for_each(|v| *v = 0);
} else {
for j in 0..w {
t[j] = if j < 32 { dequant[i * 64 + j] } else { 0 };
}
}
if rect2 {
for v in t[..w].iter_mut() {
*v = round2_64(*v as i64 * 2896, 12);
}
}
if lossless {
inverse_wht(&mut t, 2);
} else {
match rk {
Kind1d::Dct => inverse_dct(&mut t, log2w, row_clamp),
Kind1d::Adst => inverse_adst(&mut t, log2w, row_clamp),
Kind1d::Identity => inverse_identity(&mut t, log2w),
}
}
let lo = -(1i32 << (col_clamp - 1));
let hi = (1i32 << (col_clamp - 1)) - 1;
for j in 0..w {
residual[i * w + j] = round2(t[j], row_shift).clamp(lo, hi);
}
}
for j in 0..w {
for i in 0..h {
t[i] = residual[i * w + j];
}
if lossless {
inverse_wht(&mut t, 0);
} else {
match ck {
Kind1d::Dct => inverse_dct(&mut t, log2h, col_clamp),
Kind1d::Adst => inverse_adst(&mut t, log2h, col_clamp),
Kind1d::Identity => inverse_identity(&mut t, log2h),
}
}
for i in 0..h {
residual[i * w + j] = round2(t[i], col_shift);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The DCT step list must implement an orthogonal transform: a DC-only input gives a flat
/// output equal to DC * cos(pi/4) (in the 2^12 fixed-point scale).
#[test]
fn dct_dc_is_flat() {
for n in 2..=6 {
let len = 1 << n;
let mut t = [0i32; 64];
t[0] = 4096;
inverse_dct(&mut t, n, 24);
for v in &t[..len] {
assert_eq!(*v, 2896, "n={n}: {:?}", &t[..len]);
}
}
}
/// Compare each inverse DCT basis vector with the real-valued DCT-III.
#[test]
fn dct_matches_float() {
for n in 2..=6u32 {
let len = 1usize << n;
for k in 0..len {
let mut t = [0i32; 64];
t[k] = 1 << 12;
inverse_dct(&mut t, n, 24);
for x in 0..len {
let c = if k == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
let want = 4096.0 * c * ((2 * x + 1) as f64 * k as f64 * std::f64::consts::PI / (2 * len) as f64).cos();
assert!((t[x] as f64 - want).abs() < 4.0 + 0.5 * n as f64, "n={n} k={k} x={x}: {} vs {want}", t[x]);
}
}
}
}
#[test]
fn adst_is_close_to_sine_transform() {
for n in 3..=4u32 {
let len = 1usize << n;
for k in 0..len {
let mut t = [0i32; 64];
t[k] = 1 << 12;
inverse_adst(&mut t, n, 24);
let energy: f64 = t[..len].iter().map(|&v| (v as f64 / 4096.0).powi(2)).sum();
// like the DCT, the AV1 ADST is scaled by sqrt(N / 2)
assert!((energy - len as f64 / 2.0).abs() < 0.02, "n={n} k={k} energy {energy}");
}
}
}
}

View File

@ -0,0 +1,145 @@
//! Shared helpers for the AV1 oracle tests. ffmpeg (libsvtav1 to encode fixtures, libdav1d to
//! decode the reference) is used only as an external process.
#![allow(dead_code)]
use filmcraft_av1::{Decoder, Picture};
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn ffmpeg() -> Option<PathBuf> {
filmcraft_testkit::ffmpeg_or_skip("av1 oracle")
}
pub fn fixture_dir() -> PathBuf {
filmcraft_testkit::fixtures_dir("av1")
}
pub fn run(ff: &Path, args: &[&str]) {
let out = Command::new(ff).args(["-hide_banner", "-loglevel", "error", "-y"]).args(args).output().expect("run ffmpeg");
assert!(out.status.success(), "ffmpeg {:?} failed: {}", args, String::from_utf8_lossy(&out.stderr));
}
/// An AV1 fixture: a lavfi source encoded by ffmpeg's libsvtav1 into IVF.
pub struct Spec {
pub name: &'static str,
pub lavfi: String,
pub frames: u32,
pub pix_fmt: &'static str,
/// Extra encoder arguments.
pub enc: Vec<String>,
}
impl Spec {
pub fn new(name: &'static str, lavfi: impl Into<String>, frames: u32, pix_fmt: &'static str, enc: &[&str]) -> Spec {
Spec { name, lavfi: lavfi.into(), frames, pix_fmt, enc: enc.iter().map(|s| s.to_string()).collect() }
}
}
pub fn make(ff: &Path, spec: &Spec) -> PathBuf {
let path = fixture_dir().join(format!("{}.ivf", spec.name));
if !path.exists() {
let tmp = filmcraft_testkit::temp_path(&path);
let frames = spec.frames.to_string();
let t = tmp.to_str().unwrap().to_string();
let mut args: Vec<&str> = vec!["-f", "lavfi", "-i", spec.lavfi.as_str(), "-frames:v", frames.as_str(), "-pix_fmt", spec.pix_fmt, "-c:v", "libsvtav1"];
args.extend(spec.enc.iter().map(String::as_str));
args.extend(["-f", "ivf", t.as_str()]);
run(ff, &args);
std::fs::rename(&tmp, &path).unwrap();
}
path
}
/// Frames of an IVF file.
pub fn ivf_frames(path: &Path) -> Vec<Vec<u8>> {
let d = std::fs::read(path).unwrap();
assert_eq!(&d[0..4], b"DKIF");
let hdr = u16::from_le_bytes([d[6], d[7]]) as usize;
let mut pos = hdr;
let mut out = Vec::new();
while pos + 12 <= d.len() {
let n = u32::from_le_bytes([d[pos], d[pos + 1], d[pos + 2], d[pos + 3]]) as usize;
pos += 12;
out.push(d[pos..pos + n].to_vec());
pos += n;
}
out
}
/// Reference decode with ffmpeg's libdav1d (8-bit formats widened to u16).
pub fn reference(ff: &Path, path: &Path, pix_fmt: &str) -> Vec<u16> {
let out = Command::new(ff)
.args(["-hide_banner", "-loglevel", "error", "-c:v", "libdav1d", "-i"])
.arg(path)
.args(["-f", "rawvideo", "-pix_fmt", pix_fmt, "-"])
.output()
.expect("run ffmpeg");
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
if pix_fmt.ends_with("le") {
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
} else {
out.stdout.iter().map(|&b| b as u16).collect()
}
}
/// Decode every frame of an IVF file with our decoder.
pub fn decode_all(path: &Path) -> Result<Vec<Picture>, String> {
let mut dec = Decoder::new();
let mut pics = Vec::new();
for (i, f) in ivf_frames(path).iter().enumerate() {
let p = dec.decode(f).map_err(|e| format!("frame {i}: {e}"))?;
pics.extend(p);
}
Ok(pics)
}
pub fn picture_samples(p: &Picture) -> usize {
let mut n = (p.width * p.height) as usize;
if !p.mono_chrome {
n += 2 * (p.plane_width(1) * p.plane_height(1)) as usize;
}
n
}
/// Compare a decoded picture with the raw reference; returns the first mismatch as
/// (plane, x, y, ours, reference) and the mismatch count.
pub fn compare(p: &Picture, raw: &[u16]) -> (Option<(usize, u32, u32, u16, u16)>, usize) {
let mut first = None;
let mut count = 0;
let mut off = 0;
for plane in 0..if p.mono_chrome { 1 } else { 3 } {
let w = p.plane_width(plane);
let h = p.plane_height(plane);
for y in 0..h {
for x in 0..w {
let a = p.planes[plane][(y * w + x) as usize];
let b = raw[off + (y * w + x) as usize];
if a != b {
count += 1;
if first.is_none() {
first = Some((plane, x, y, a, b));
}
}
}
}
off += (w * h) as usize;
}
(first, count)
}
/// Decode a fixture and compare every frame with libdav1d; panics with details on mismatch.
pub fn check_bit_exact(ff: &Path, spec: &Spec) {
let path = make(ff, spec);
let pics = decode_all(&path).unwrap_or_else(|e| panic!("{}: {e}", spec.name));
let raw = reference(ff, &path, spec.pix_fmt);
assert!(!pics.is_empty(), "{}: no frames", spec.name);
let per = picture_samples(&pics[0]);
assert_eq!(raw.len(), per * pics.len(), "{}: frame count/size ({} pictures)", spec.name, pics.len());
for (i, p) in pics.iter().enumerate() {
let (first, count) = compare(p, &raw[i * per..(i + 1) * per]);
if let Some((plane, x, y, a, b)) = first {
panic!("{}: frame {i} differs in {count} samples; first at plane {plane} ({x},{y}): ours {a}, reference {b}", spec.name);
}
}
println!("{:<28} {} frames {}x{} bit-exact", spec.name, pics.len(), pics[0].width, pics[0].height);
}

View File

@ -0,0 +1,26 @@
//! Intra-frame bit-exactness against libdav1d (run as an external ffmpeg process).
mod common;
use common::*;
const NO_FILTERS: &str = "enable-cdef=0:enable-restoration=0:enable-dlf=0:keyint=1";
fn specs_no_filters() -> Vec<Spec> {
let p = |preset: &'static str, crf: &'static str| -> Vec<&'static str> { vec!["-preset", preset, "-crf", crf, "-svtav1-params", NO_FILTERS] };
vec![
Spec::new("intra_nofilt_testsrc_128", "testsrc2=s=128x128:r=25", 1, "yuv420p", &p("8", "30")),
Spec::new("intra_nofilt_mandel_352x288_p4", "mandelbrot=s=352x288:r=25", 2, "yuv420p", &p("4", "20")),
Spec::new("intra_nofilt_testsrc_odd_p2", "testsrc2=s=203x117:r=25", 2, "yuv420p", &p("2", "35")),
Spec::new("intra_nofilt_noise_p6", "testsrc2=s=320x240:r=25,noise=alls=30:allf=t", 2, "yuv420p", &p("6", "10")),
Spec::new("intra_nofilt_10bit_p4", "mandelbrot=s=256x192:r=25", 2, "yuv420p10le", &p("4", "25")),
Spec::new("intra_nofilt_720p_p8", "testsrc2=s=1280x720:r=25", 1, "yuv420p", &p("8", "40")),
]
}
#[test]
fn intra_no_filters() {
let Some(ff) = ffmpeg() else { return };
for s in specs_no_filters() {
check_bit_exact(&ff, &s);
}
}

View File

@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Extract the constants and constant tables of the AV1 Bitstream & Decoding Process
Specification (v1.0.0 with Errata 1) from its Markdown source.
Usage:
curl -L -o av1-spec.zip https://github.com/AOMediaCodec/av1-spec/archive/5e04f3f75e73a5898d7616c47c52f032144b8f80.zip
unzip av1-spec.zip
python3 extract_tables.py av1-spec-5e04f3f75e73a5898d7616c47c52f032144b8f80 > ../src/spec_tables.rs
Only the specification text is read:
* named constants from the symbol table of section 3 and the value/name tables of section 6
(e.g. `| 1 | BLOCK_4X8`);
* every C table initialiser `Name[ dims ] = { ... }` in any section.
Each table is emitted as a nested Rust array whose shape is checked against the declared
dimensions; the element type is the narrowest of u8/i8/u16/i16/i32 that holds every value.
"""
import os
import re
import sys
root = sys.argv[1]
files = sorted(f for f in os.listdir(root) if f.endswith(".md"))
texts = {f: open(os.path.join(root, f), encoding="utf-8").read() for f in files}
consts = {}
def ev(expr):
e = expr.strip().replace("\\", "")
e = re.sub(r"\b([A-Z][A-Z0-9_]*)\b", lambda m: str(consts[m.group(1)]) if m.group(1) in consts else m.group(1), e)
e = e.replace("/", "//")
return int(eval(e, {"__builtins__": {}}, {}))
# --- constants: symbol table (section 3) ---
pending = []
for line in texts["03.symbols.md"].split("\n"):
m = re.match(r"^\|\s*`([A-Z][A-Z0-9_]*)`\s*\|\s*([^|]+?)\s*\|", line)
if m:
pending.append((m.group(1), m.group(2)))
# --- constants: value/name tables (section 6 semantics, section 8/9) ---
# A row cell holding a NAME takes its value from the integer cell just before it, unless the
# next cell is another NAME (multi-column mapping tables such as the CFL sign table).
NAME = re.compile(r"^[A-Z][A-Z0-9_]*[A-Z0-9]$")
for f in files:
for line in texts[f].split("\n"):
if not line.startswith("|"):
continue
cells = [c.strip().rstrip(".") for c in line.strip().strip("|").split("|")]
for k in range(1, len(cells)):
if NAME.match(cells[k]) and "_" in cells[k] and re.match(r"^-?\d+$", cells[k - 1]):
if k + 1 < len(cells) and NAME.match(cells[k + 1]) and "_" in cells[k + 1]:
continue
pending.append((cells[k], cells[k - 1]))
# `| NAME | value` rows
if len(cells) >= 2 and NAME.match(cells[0]) and "_" in cells[0] and re.match(r"^-?\d+$", cells[1]):
pending.append((cells[0], cells[1]))
for _ in range(10):
rest = []
for name, val in pending:
try:
v = ev(val)
except Exception:
rest.append((name, val))
continue
if name in consts and consts[name] != v:
# same name, different meaning in two tables: keep the first (reported)
sys.stderr.write("constant %s: %d vs %d (kept first)\n" % (name, consts[name], v))
continue
consts[name] = v
pending = rest
for name, val in pending:
sys.stderr.write("unresolved constant %s = %s\n" % (name, val))
# --- tables ---
def cells(text):
"""Split one initialiser cell. A missing comma between two operands on separate lines
(Split_Tx_Size in the spec text: `TX_32X32` / `TX_4X8`) yields two cells."""
parts = re.split(r"(?<=[A-Za-z0-9_])\s*\n\s*(?=[A-Za-z0-9_])", text.strip())
if len(parts) > 1:
sys.stderr.write("note: missing comma in spec table between %s\n" % parts)
return parts
def parse_braces(s, i):
"""Parse a brace initialiser starting at s[i] == '{'; returns (nested list, end index)."""
assert s[i] == "{"
i += 1
items = []
cur = ""
while True:
ch = s[i]
if ch == "{":
sub, i = parse_braces(s, i)
items.append(sub)
cur = ""
continue
if ch == "}":
if cur.strip():
items.extend(cells(cur))
return items, i + 1
if ch == ",":
if cur.strip():
items.extend(cells(cur))
cur = ""
else:
cur += ch
i += 1
def shape(x):
if isinstance(x, list):
subs = [shape(e) for e in x]
if any(s != subs[0] for s in subs):
return None
return [len(x)] + (subs[0] if subs else [])
return []
tables = {}
for f in files:
t = texts[f]
for block in re.findall(r"~~~~~?\s*c?\n(.*?)~~~~~?", t, re.S):
b = re.sub(r"//[^\n]*", "", block)
b = re.sub(r"/\*.*?\*/", "", b, flags=re.S)
for m in re.finditer(r"(?m)^\s*([A-Z][A-Za-z0-9_]*)\s*((?:\[[^\]=]*\]\s*)+)=\s*", b):
name = m.group(1)
try:
dims = [ev(d) for d in re.findall(r"\[([^\]]*)\]", m.group(2))]
except Exception:
continue # an assignment in pseudo code, not a table
j = b.find("{", m.end())
if j < 0 or b[m.end():j].strip():
continue
val, _ = parse_braces(b, j)
sh = shape(val)
if sh != dims:
sys.stderr.write("table %s: shape %s != declared %s (skipped)\n" % (name, sh, dims))
continue
flat = []
def walk(x):
if isinstance(x, list):
out = []
for e in x:
out.append(walk(e))
return out
v = ev(x)
flat.append(v)
return v
nested = walk(val)
if name in tables and tables[name][1] != nested:
sys.stderr.write("table %s defined twice with different values\n" % name)
tables[name] = (dims, nested, flat)
def rust_type(flat):
lo, hi = min(flat), max(flat)
for t, a, b in (("u8", 0, 255), ("i8", -128, 127), ("u16", 0, 65535), ("i16", -32768, 32767)):
if lo >= a and hi <= b:
return t
return "i32"
def fmt(x, depth):
if isinstance(x, list) and x and isinstance(x[0], list):
inner = ",\n".join(" " * (depth + 1) + fmt(e, depth + 1) for e in x)
return "[\n" + inner + ",\n" + " " * depth + "]"
return "[" + ", ".join(str(v) for v in x) + "]"
out = [
"// Generated by tools/extract_tables.py from the AV1 Bitstream & Decoding Process Specification",
"// v1.0.0 with Errata 1 (AOMediaCodec/av1-spec 5e04f3f). Do not edit by hand.",
"#![allow(dead_code, clippy::unreadable_literal, clippy::excessive_precision)]",
"",
]
for name in sorted(consts):
v = consts[name]
out.append("pub const %s: %s = %d;" % (name, "i32" if v < 0 else "usize", v))
out.append("")
for name in sorted(tables):
dims, nested, flat = tables[name]
# scan tables share one element type so they can be returned as &[u16]
t = "u16" if "_Scan_" in name else rust_type(flat)
rname = name.upper()
if rname in consts:
rname += "_TABLE" # e.g. Max_Tx_Depth (table) vs MAX_TX_DEPTH (constant)
ty = t
for d in reversed(dims):
ty = "[%s; %d]" % (ty, d)
out.append("pub static %s: %s = %s;" % (rname, ty, fmt(nested, 0)))
out.append("")
print("\n".join(out))
sys.stderr.write("%d constants, %d tables\n" % (len(consts), len(tables)))

View File

@ -27,7 +27,7 @@ Design principles:
L2 edit · codecs · interchange · captions
L1 frame · media · project · audio-dsp · text
L0 foundation: time · geom · color · bitstream · testkit (dev-dependency only)
codecs/containers: isobmff · matroska · h264 · h264enc · hevc · vp9 · prores · dnx · aac · opus
codecs/containers: isobmff · matroska · h264 · h264enc · hevc · vp9 · av1 · prores · dnx · aac · opus
```
Crates are named `filmcraft-<dir>` (`crates/time` is `filmcraft-time`). The apps are `filmcraft`
@ -45,6 +45,7 @@ and `filmcraft-cli`.
| `hevc` | L0 | H.265 Main/Main 10 decoder |
| `prores` | L0 | ProRes decoder and encoder |
| `dnx` | L0 | DNxHD / DNxHR (SMPTE ST 2019-1 VC-3) decoder and DNxHR encoder |
| `av1` | L0 | AV1 decoder (in progress; see its README for the stage table) |
| `aac` | L0 | AAC-LC decoder and encoder |
| `testkit` | L0 | test-only helpers, used only as a dev-dependency: ffmpeg/ffprobe discovery, fixture dirs, golden images ([testing.md](testing.md)) |
| `frame` | L1 | `VideoFrame` (planar YUV / RGBA8 / linear RGBA f32, colour metadata), `AudioBuffer` |

View File

@ -31,6 +31,7 @@ const LAYERS: &[(&str, u8)] = &[
("prores", 0),
("mjpeg", 0),
("dnx", 0),
("av1", 0),
("aac", 0),
("opus", 0),
("frame", 1),