filmcraft/crates/av1/README.md

9.8 KiB
Raw Blame History

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 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 (tiles decoded in parallel)
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
Inter mode info: segment id prediction, skip mode, reference frames (single / compound, uni / bi), modes, DRL, MV coding, inter-intra, motion mode, compound type, interpolation filters 5.11.18–5.11.32 done, bit-exact
MV prediction: spatial / temporal candidate stacks, global MV, extra search, clamping, warp samples 7.10 done, bit-exact
Motion field estimation (projection) and motion vector storage 7.9, 7.19 done, bit-exact
Inter prediction: MV scaling (scaled references), 8-tap sub-pixel filters, global and local warp (warp estimation, shear), OBMC, wedge / difference-weighted / inter-intra masks, distance weights, averaging 7.11.3 done, bit-exact
Intra block copy 7.11.3 done, bit-exact
Output policy with scalability (highest spatial layer per temporal unit) 7.18.1 done, bit-exact
Loop filter (all filter sizes, deltas, segment / ref / mode adjustments) 7.14 done, bit-exact
CDEF 7.15 done, bit-exact
Super-resolution upscaling 7.16 done, bit-exact
Loop restoration (Wiener, self-guided, switchable; stripes) 7.17 done, bit-exact
Film grain synthesis (output pictures only; Decoder::apply_film_grain turns it off) 7.18.3 done, bit-exact
Large-scale tile / tile list OBUs 7.3 not planned

Accuracy

tests/oracle_intra.rs encodes all-intra streams with ffmpeg's libsvtav1 (with and without the in-loop filters; 64×64 and 128×128 superblocks; intra edge filter on and off) and compares every decoded frame with libdav1d: bit-exact on all 13 fixtures (8- and 10-bit 4:2:0; 128×128 to 1280×720 including odd sizes; testsrc2, mandelbrot, gradients and noise; presets 1–8).

tests/conformance.rs downloads libaom's conformance test vectors on first use (storage.googleapis.com/aom-test-data) and compares with libdav1d: av1-1-b8-02-allintra (39 frames, deblocking + CDEF + self-guided restoration), 05-mv, 06-mfmv, 24-monochrome and b10-23-film_grain-50 are bit-exact in the default run. The ignored conformance_vectors_extended test covers the wider set; bit-exact today: 01-size-16x16, -66x66, -196x196, -226x226, 00-quantizer-00/31/63 (8-bit) and -00/40 (10-bit), 04-cdfupdate, 05-mv, 06-mfmv, 22-svc-L1T2, 22-svc-L2T1, 22-svc-L2T2 (spatial layers with scaled inter-layer prediction), 24-monochrome (8 and 10-bit) and 16-intra_only-intrabc-extreme-dv (1080p intra block copy), 23-film_grain-50 (8 and 10-bit).

tests/oracle_inter.rs also checks draft mode: every unflagged frame stays bit-exact with libdav1d and only flagged frames differ, with 1 and 4 threads. tests/oracle_inter.rs encodes SVT-AV1 GOPs (hierarchical references, compound prediction, OBMC / warped motion, motion-field projection, all loop filters; presets 3–8, 8- and 10-bit, odd sizes), plus super-resolution (denominator 12) and film-grain streams, and compares every frame with libdav1d: bit-exact on all 6 fixtures.

Threading and performance

Decoder::new() uses all cores (Decoder::with_threads(1) decodes on the calling thread; on wasm32, or without the default threads feature, everything runs on the calling thread). Headers are parsed in order; each frame is then decoded by a frame worker once the reference frames it reads are finished, so independent frames (hierarchical GOP layers) decode concurrently; tiles of a frame decode in parallel into private buffers that are merged afterwards; the in-loop filters (deblocking, CDEF, loop restoration) each run in parallel bands of 64 rows (postfilter.rs explains why the bands are independent). Results never depend on scheduling (the oracle tests compare 1 thread, many threads and libdav1d). With frame threads, pictures may be returned by a later decode_pts call or by flush; each carries the pts of the temporal unit that showed it.

tests/perf.rs (ignored) prints single-threaded fps (thread CPU time), per-stage ms/frame (Decoder::stats()) and all-core fps with its per-stage ms/frame for 1080p fixtures (key frames, GOPs, tiles, low delay) and a single-tile 2160p GOP; AV1_PERF_ONLY=a,b picks fixtures. The hot loops (sub-pixel filters, CDEF, inverse transforms, deblocking, entropy decoding) are written for auto-vectorisation.

Measured 2026-10-01 on a 14-core Apple Silicon Mac (release build, SVT-AV1 preset 8 1080p fixtures from tests/perf.rs), before (31f8b3d: hot loops only, no threading) and after tile / frame / post-filter threading. The machine was heavily loaded by other builds during both runs (load average 200–400), so the all-core wall figures understate what an idle machine reaches and are noisy; single-threaded figures use thread CPU time and are comparable.

Fixture 1 thread before 1 thread after 14 threads before 14 threads after
perf_1080p_intra (10 key frames) 43.3 fps 43.0 fps 6.5 fps 20.8 fps
perf_1080p_gop (60 frames, crf 30) 45.5 fps 40.7 fps 7.7 fps 11.0 fps
perf_1080p_gop_10bit 41.3 fps 35.9 fps 8.0 fps 6.6 fps
perf_1080p_gop_hq (crf 18) 19.5 fps 19.4 fps 2.4 fps 3.6 fps
perf_1080p_gop_tiles (4x2 tiles) 44.4 fps 44.7 fps 2.7 fps 16.3 fps

M4.10 (2026-10-03)

CPU cycles per frame counted by the kernel (/usr/bin/time -l, all threads summed; mean of two interleaved base / after rounds at load average 190-320) with AV1_THREADS=n cargo run --release -p filmcraft-av1 --example av1dec -- in.ivf on the cargo xtask bench decode clips (SVT-AV1 preset 10, CRF 35, keyint 48, testsrc2 + grain), bit-exact with libdav1d:

stream threads Mcycles / frame before after after, draft mode
1080p (120 frames) 1 60.5 48.3 (−20 %) 46.2
1080p 14 63.6 51.9 (−18 %) 50.3
2160p (72 frames) 1 212.5 172.4 (−19 %) 167.9
2160p 14 220.0 177.1 (−20 %) 171.1

What changed: the sub-pixel filter passes run over fixed block widths (taps outer, columns inner); frame and tile-region planes come from a bounded pool of recycled buffers (zeroed on reuse) instead of a fresh calloc and munmap per frame (allocation, page faults and zeroing were ~10 % of the time); the palette mode-info arrays (40 bytes per 4x4 unit) are only allocated when screen content tools are allowed; CDF adaptation is branch-free (a branch-free symbol search was tried and was slower: most symbols have 2-4 values). Single-threaded 4K now splits into coefficient parsing ~20 %, inter prediction ~18 %, other block-level parsing ~12 %, deblocking ~11 % (still one edge at a time), inverse transforms ~12 %, CDEF ~5 %.

Draft mode (Decoder::set_draft(true), AV1_DRAFT=1; reduced-resolution playback only): shown frames whose refresh_frame_flags is 0 — no reference slot keeps them, so neither their samples, motion vectors nor CDFs reach another frame — skip deblocking, CDEF and loop restoration (super-resolution still runs, it sets the output size) and are flagged Picture::draft. In SVT-AV1's hierarchical GOPs that is the top layer, half of the frames.

API

let mut dec = filmcraft_av1::Decoder::new();
for sample in samples {                  // one temporal unit (MP4 / Matroska sample) each
    for pic in dec.decode_pts(&sample, pts)? {   // shown frames ready so far, planar u16
        // pic.width, pic.height, pic.bit_depth, pic.planes[0..3], pic.pts
    }
}
let rest = dec.flush();                  // pictures still in flight at the end