From 6ac2a6a34ae83fc0f0bfb25a569ba6388b72266a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 13:47:53 -0500 Subject: [PATCH] craft-video: VP9 decoder from FilmCraft, with DeckCraft's memory budget (`vp9-decode`) (#821) * craft-video: VP9 decoder from FilmCraft (`vp9-decode` feature) FilmCraft's crates/vp9 at 4c0d2ad7 becomes craft_video::vp9::decode. The encoder moves to vp9/encode and is still exported from craft_video::vp9. Feature vp9 now turns on vp9-decode and vp9-encode, so either half can be built without the other. The decoder's tests find ffmpeg and the fixture directory through craft-media-testkit; a fixture that cannot be generated fails under CRAFT_MEDIA_REQUIRE_ORACLES=1. craft-video is built at opt-level 3 in the dev profile, as FilmCraft built its codecs. * craft-video: VP9 decoder checks its picture store against a memory budget DeckCraft's copy of the decoder refuses a frame size whose pictures (eight reference slots, the new frame and those in flight) would not fit a byte budget. The budget type comes with it as craft_video::budget; FilmCraft's 2^26 luma sample cap stays. --- Cargo.toml | 4 + crates/video/Cargo.toml | 14 +- crates/video/README.md | 2 +- crates/video/examples/vp9_vp9dec.rs | 45 + crates/video/src/budget.rs | 82 + crates/video/src/lib.rs | 8 +- crates/video/src/vp9/decode/README.md | 220 +++ crates/video/src/vp9/decode/boolcoder.rs | 190 +++ crates/video/src/vp9/decode/decoder.rs | 1090 +++++++++++++ crates/video/src/vp9/decode/error.rs | 31 + crates/video/src/vp9/decode/frame.rs | 311 ++++ crates/video/src/vp9/decode/header.rs | 758 +++++++++ crates/video/src/vp9/decode/inter.rs | 394 +++++ crates/video/src/vp9/decode/intra.rs | 230 +++ crates/video/src/vp9/decode/loopfilter.rs | 378 +++++ crates/video/src/vp9/decode/mod.rs | 169 ++ crates/video/src/vp9/decode/probs.rs | 322 ++++ crates/video/src/vp9/decode/spec_tables.rs | 1427 +++++++++++++++++ crates/video/src/vp9/decode/tables.rs | 288 ++++ crates/video/src/vp9/decode/tile.rs | 1097 +++++++++++++ crates/video/src/vp9/decode/tile/recon.rs | 389 +++++ crates/video/src/vp9/decode/transform.rs | 547 +++++++ crates/video/src/vp9/decode/transform_gen.rs | 489 ++++++ crates/video/src/vp9/{ => encode}/README.md | 0 crates/video/src/vp9/{ => encode}/bool.rs | 0 crates/video/src/vp9/{ => encode}/lf.rs | 2 +- crates/video/src/vp9/{ => encode}/mc.rs | 2 +- crates/video/src/vp9/encode/mod.rs | 510 ++++++ crates/video/src/vp9/{ => encode}/probs.rs | 4 +- crates/video/src/vp9/{ => encode}/tables.rs | 0 crates/video/src/vp9/{ => encode}/tile.rs | 12 +- .../video/src/vp9/{ => encode}/transform.rs | 0 crates/video/src/vp9/mod.rs | 518 +----- crates/video/tests/vp9_decode/common/mod.rs | 421 +++++ crates/video/tests/vp9_decode/conformance.rs | 193 +++ crates/video/tests/vp9_decode/fuzz_hostile.rs | 221 +++ crates/video/tests/vp9_decode/main.rs | 6 + crates/video/tests/vp9_decode/perf.rs | 48 + crates/video/tests/vp9_decode/robustness.rs | 131 ++ crates/video/tests/vp9_decode/synthetic.rs | 362 +++++ crates/video/tools/vp9_extract_tables.py | 109 ++ crates/video/tools/vp9_gen_transforms.py | 258 +++ 42 files changed, 10761 insertions(+), 521 deletions(-) create mode 100644 crates/video/examples/vp9_vp9dec.rs create mode 100644 crates/video/src/budget.rs create mode 100644 crates/video/src/vp9/decode/README.md create mode 100644 crates/video/src/vp9/decode/boolcoder.rs create mode 100644 crates/video/src/vp9/decode/decoder.rs create mode 100644 crates/video/src/vp9/decode/error.rs create mode 100644 crates/video/src/vp9/decode/frame.rs create mode 100644 crates/video/src/vp9/decode/header.rs create mode 100644 crates/video/src/vp9/decode/inter.rs create mode 100644 crates/video/src/vp9/decode/intra.rs create mode 100644 crates/video/src/vp9/decode/loopfilter.rs create mode 100644 crates/video/src/vp9/decode/mod.rs create mode 100644 crates/video/src/vp9/decode/probs.rs create mode 100644 crates/video/src/vp9/decode/spec_tables.rs create mode 100644 crates/video/src/vp9/decode/tables.rs create mode 100644 crates/video/src/vp9/decode/tile.rs create mode 100644 crates/video/src/vp9/decode/tile/recon.rs create mode 100644 crates/video/src/vp9/decode/transform.rs create mode 100644 crates/video/src/vp9/decode/transform_gen.rs rename crates/video/src/vp9/{ => encode}/README.md (100%) rename crates/video/src/vp9/{ => encode}/bool.rs (100%) rename crates/video/src/vp9/{ => encode}/lf.rs (99%) rename crates/video/src/vp9/{ => encode}/mc.rs (99%) create mode 100644 crates/video/src/vp9/encode/mod.rs rename crates/video/src/vp9/{ => encode}/probs.rs (99%) rename crates/video/src/vp9/{ => encode}/tables.rs (100%) rename crates/video/src/vp9/{ => encode}/tile.rs (99%) rename crates/video/src/vp9/{ => encode}/transform.rs (100%) create mode 100644 crates/video/tests/vp9_decode/common/mod.rs create mode 100644 crates/video/tests/vp9_decode/conformance.rs create mode 100644 crates/video/tests/vp9_decode/fuzz_hostile.rs create mode 100644 crates/video/tests/vp9_decode/main.rs create mode 100644 crates/video/tests/vp9_decode/perf.rs create mode 100644 crates/video/tests/vp9_decode/robustness.rs create mode 100644 crates/video/tests/vp9_decode/synthetic.rs create mode 100644 crates/video/tools/vp9_extract_tables.py create mode 100644 crates/video/tools/vp9_gen_transforms.py diff --git a/Cargo.toml b/Cargo.toml index 12047dc96..a32e1df27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,3 +121,7 @@ winit = { git = "https://github.com/storytold/winit.git", rev = "a517b3846ee8457 # craft-audio's `decode` need the same override in their own workspace. [profile.dev.package.symphonia-bundle-flac] overflow-checks = false + +# Codec hot loops: the oracle and conformance tests of a debug build take minutes below opt-level 3. +[profile.dev.package.craft-video] +opt-level = 3 diff --git a/crates/video/Cargo.toml b/crates/video/Cargo.toml index ee4544386..c63827f5e 100644 --- a/crates/video/Cargo.toml +++ b/crates/video/Cargo.toml @@ -25,8 +25,10 @@ prores = ["craft-media/bitstream"] h264 = ["craft-media/isobmff"] # H.265 / HEVC (ITU-T H.265): encoder. hevc = ["craft-media/isobmff"] -# VP9: encoder. -vp9 = ["craft-media/isobmff"] +# VP9: decoder and encoder; `vp9-decode` or `vp9-encode` for one of them. +vp9 = ["vp9-decode", "vp9-encode"] +vp9-decode = ["craft-media/bitstream"] +vp9-encode = ["craft-media/isobmff"] # AV1 (AOMedia Video 1): encoder. av1 = ["craft-media/isobmff"] # Decode and encode across worker threads (rayon) in the codecs that support it. @@ -56,6 +58,14 @@ required-features = ["vp8"] name = "vp8_vp8dec" required-features = ["vp8"] +[[test]] +name = "vp9_decode" +required-features = ["vp9-decode"] + +[[example]] +name = "vp9_vp9dec" +required-features = ["vp9-decode"] + [[test]] name = "prores" required-features = ["prores"] diff --git a/crates/video/README.md b/crates/video/README.md index 413805d37..812bd58c0 100644 --- a/crates/video/README.md +++ b/crates/video/README.md @@ -23,6 +23,6 @@ No codec feature is on by default. | `prores` | `prores` | Apple ProRes (SMPTE RDD 36): decoder and encoder | | `h264` | `h264` | ITU-T H.264 / AVC: encoder | | `hevc` | `hevc` | ITU-T H.265 / HEVC: encoder | -| `vp9` | `vp9` | VP9: encoder | +| `vp9` | [`vp9`](src/vp9/decode/README.md) | VP9: decoder (`vp9-decode` alone) and encoder (`vp9-encode` alone) | | `av1` | `av1` | AV1 (AOMedia Video 1): encoder | | `threads` | — | Multi-threaded slice/row parallelism via `rayon` | diff --git a/crates/video/examples/vp9_vp9dec.rs b/crates/video/examples/vp9_vp9dec.rs new file mode 100644 index 000000000..fda2a3fb8 --- /dev/null +++ b/crates/video/examples/vp9_vp9dec.rs @@ -0,0 +1,45 @@ +//! Decode an IVF file: `cargo run --release -p craft-video --features vp9-decode,threads --example vp9_vp9dec -- in.ivf [out.yuv]`. +//! Writes raw planar output (8-bit or 16-bit little endian) and prints decoder statistics. + +use std::io::Write; + +fn main() { + let args: Vec = std::env::args().collect(); + let data = std::fs::read(&args[1]).expect("read input"); + let mut out = args.get(2).map(|p| std::io::BufWriter::new(std::fs::File::create(p).expect("create output"))); + let threads = std::env::var("VP9_THREADS").ok().and_then(|s| s.parse().ok()).unwrap_or(0); + let mut dec = if threads > 0 { craft_video::vp9::decode::Decoder::with_threads(threads) } else { craft_video::vp9::decode::Decoder::new() }; + // VP9_DRAFT=1: draft mode (non-reference frames skip the loop filter). + dec.set_draft(std::env::var_os("VP9_DRAFT").is_some()); + // VP9_LOOPS=n decodes the file n times (for profiling; the stream starts with a key frame). + let loops = std::env::var("VP9_LOOPS").ok().and_then(|s| s.parse().ok()).unwrap_or(1usize); + let hl = u16::from_le_bytes([data[6], data[7]]) as usize; + let mut i = 0i64; + let t0 = std::time::Instant::now(); + let mut n = 0; + for _ in 0..loops { + let mut p = hl; + while p + 12 <= data.len() { + let sz = u32::from_le_bytes(data[p..p + 4].try_into().unwrap()) as usize; + p += 12; + let Some(frame) = data.get(p..p + sz) else { break }; + p += sz; + match dec.decode(frame, i) { + Ok(pics) => { + for pic in pics.into_iter().chain(if p >= data.len() { dec.flush() } else { Vec::new() }) { + n += 1; + if let Some(o) = out.as_mut() { + for pl in [&pic.y, &pic.u, &pic.v] { + o.write_all(&pl.to_le_bytes()).unwrap(); + } + } + } + } + Err(e) => eprintln!("chunk {i}: {e}"), + } + i += 1; + } + } + eprintln!("{n} pictures in {:.3}s", t0.elapsed().as_secs_f64()); + eprintln!("{:#?}", dec.stats()); +} diff --git a/crates/video/src/budget.rs b/crates/video/src/budget.rs new file mode 100644 index 000000000..cf98df2c3 --- /dev/null +++ b/crates/video/src/budget.rs @@ -0,0 +1,82 @@ +//! Memory budget for decoded pictures, shared by the video decoders. +//! +//! A per-axis size cap does not bound memory: a decoder holds the current picture, every +//! reference picture and the pictures still being decoded on worker threads. Each decoder checks +//! the whole picture store against [`DECODE_MEMORY_BUDGET`] with checked arithmetic before it +//! allocates for a new picture size, so a hostile header is an error, not an out-of-memory abort. + +/// Bytes the decoded pictures of one decoder may take: 4 GiB on 64-bit targets, 1 GiB on 32-bit +/// ones (wasm32 has a 4 GiB address space in all). +pub const DECODE_MEMORY_BUDGET: u64 = if cfg!(target_pointer_width = "64") { 4 << 30 } else { 1 << 30 }; + +/// The shape of one decoded picture as a decoder allocates it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PictureShape { + /// Allocated luma samples per row and rows (padding included). + pub width: u64, + pub height: u64, + /// Chroma subsampling shifts (0 or 1); ignored when `chroma_planes` is 0. + pub shift_x: u32, + pub shift_y: u32, + /// Chroma planes (0 for monochrome, 2 otherwise). + pub chroma_planes: u64, + /// Bytes per sample (1 for 8-bit buffers, 2 for 16-bit ones). + pub bytes_per_sample: u64, +} + +impl PictureShape { + /// Bytes of one picture, `None` if it does not fit in a `u64`. + pub fn bytes(&self) -> Option { + let luma = self.width.checked_mul(self.height)?; + let cw = self.width.div_ceil(1 << self.shift_x.min(1)); + let ch = self.height.div_ceil(1 << self.shift_y.min(1)); + let chroma = cw.checked_mul(ch)?.checked_mul(self.chroma_planes)?; + luma.checked_add(chroma)?.checked_mul(self.bytes_per_sample) + } + + /// Bytes of `pictures` such pictures, `None` on overflow. + pub fn store_bytes(&self, pictures: u64) -> Option { + self.bytes()?.checked_mul(pictures) + } + + /// Whether `pictures` such pictures fit in `budget` bytes. + pub fn fits(&self, pictures: u64, budget: u64) -> bool { + self.store_bytes(pictures).is_some_and(|b| b <= budget) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn shape(w: u64, h: u64) -> PictureShape { + PictureShape { width: w, height: h, shift_x: 1, shift_y: 1, chroma_planes: 2, bytes_per_sample: 2 } + } + + #[test] + fn bytes_count_luma_and_subsampled_chroma() { + assert_eq!(shape(4, 4).bytes(), Some((16 + 2 * 4) * 2)); + // Odd sizes round chroma up. + assert_eq!(shape(3, 3).bytes(), Some((9 + 2 * 4) * 2)); + let mono = PictureShape { chroma_planes: 0, bytes_per_sample: 1, ..shape(10, 10) }; + assert_eq!(mono.bytes(), Some(100)); + let full = PictureShape { shift_x: 0, shift_y: 0, ..shape(10, 10) }; + assert_eq!(full.bytes(), Some(600)); + } + + #[test] + fn exact_budget_boundary() { + let s = shape(4, 4); // 48 bytes + assert!(s.fits(10, 480)); + assert!(!s.fits(10, 479)); + assert!(!s.fits(11, 480)); + } + + #[test] + fn overflow_is_never_within_budget() { + let huge = shape(u64::MAX / 2, 3); + assert_eq!(huge.bytes(), None); + assert!(!huge.fits(1, u64::MAX)); + assert!(!shape(1 << 31, 1 << 31).fits(u64::MAX, u64::MAX)); + } +} diff --git a/crates/video/src/lib.rs b/crates/video/src/lib.rs index f23eaf135..e24ac2a6f 100644 --- a/crates/video/src/lib.rs +++ b/crates/video/src/lib.rs @@ -10,15 +10,19 @@ //! | `prores` | [`prores`] | Apple ProRes (SMPTE RDD 36): decoder and encoder | //! | `h264` | [`h264`] | ITU-T H.264 / AVC: encoder | //! | `hevc` | [`hevc`] | ITU-T H.265 / HEVC: encoder | -//! | `vp9` | [`vp9`] | VP9: encoder | +//! | `vp9` | [`vp9`] | VP9: decoder (`vp9-decode`) and encoder (`vp9-encode`) | //! | `av1` | [`av1`] | AV1 (AOMedia Video 1): encoder | //! //! No codec is on by default. Feature `threads` spreads the work of the codecs that support it //! over rayon worker threads. //! +//! The decoders that keep reference pictures bound them with a memory budget ([`budget`]). +//! //! Containers and bit-level I/O are in `craft-media`; the codecs here share its `bitstream` //! module and describe their streams with `craft_media::CodecParams`. +pub mod budget; + #[cfg(feature = "dnx")] pub mod dnx; @@ -34,7 +38,7 @@ pub mod h264; #[cfg(feature = "hevc")] pub mod hevc; -#[cfg(feature = "vp9")] +#[cfg(any(feature = "vp9-decode", feature = "vp9-encode"))] pub mod vp9; #[cfg(feature = "av1")] diff --git a/crates/video/src/vp9/decode/README.md b/crates/video/src/vp9/decode/README.md new file mode 100644 index 000000000..9895894ab --- /dev/null +++ b/crates/video/src/vp9/decode/README.md @@ -0,0 +1,220 @@ +# VP9 decoder (`craft_video::vp9::decode`, feature `vp9-decode`) + +Clean-room, pure-Rust (no `unsafe`) VP9 decoder, implemented from the public *VP9 Bitstream & +Decoding Process Specification* v0.6 (WebM project). It depends only on +`craft_media::bitstream`, `thiserror`, and optionally `rayon` (feature `threads`), and builds +for `wasm32-unknown-unknown` with and without `threads`. + +The constant tables (probability defaults, scans, coefficient bands, quantizer lookups, Pareto +table, interpolation filters, `mv_ref_blocks`...) were extracted from the specification text by +[`tools/vp9_extract_tables.py`](../../../tools/vp9_extract_tables.py) into `spec_tables.rs`. The 1D inverse +DCT / ADST butterflies are generated by [`tools/vp9_gen_transforms.py`](../../../tools/vp9_gen_transforms.py), +which symbolically executes the step lists of spec section 8.7.1 into `src/transform_gen.rs`. +No code was taken or ported from libvpx, FFmpeg or any other decoder; ffmpeg (with libvpx-vp9) +is used only as an external fixture generator and test oracle. + +## Status + +| Area | Status | +|---|---| +| Profiles 0 / 2 (8, 10, 12-bit 4:2:0) | done, bit-exact | +| Profiles 1 / 3 (4:2:2, 4:4:0, 4:4:4; 8 / 10 / 12-bit) | done, bit-exact | +| Uncompressed + compressed headers, superframe index (Annex B) | done | +| Boolean decoder, forward updates (`diff_update_prob`, MV probs), backward adaptation (8.4) | done | +| Segmentation (tree / temporal prediction, all four features), loop filter (levels, sharpness, ref / mode deltas, segment levels) | done | +| All 10 intra modes incl. edge rules, sub-8x8 blocks | done | +| Inter: 8-tap regular / smooth / sharp + bilinear, compound prediction, MV prediction (incl. previous-frame MVs, sub-8x8 append), reference scaling (8.5.2.3) | done | +| DCT / ADST 4..32, WHT (lossless) | done | +| Tiles (columns and rows); tile columns decode in parallel | done | +| Hidden frames, show_existing_frame, intra-only frames, resolution changes, error resilient / frame parallel modes | done | +| Frame threading: references published per superblock row, the loop filter / output of a frame overlaps the next frames | done | +| Draft mode (non-reference frames skip the loop filter; reduced-resolution playback only) | done | + +## API + +```rust +use craft_video::vp9::decode::{Decoder, Plane}; + +let mut dec = Decoder::new(); // or Decoder::with_threads(n) +for (pts, chunk) in chunks { // one IVF / WebM block / MP4 sample each (frame or superframe) + for pic in dec.decode(chunk, pts)? { + // pic.width / pic.height (frame size), pic.render_width / render_height (render_size), + // pic.bit_depth, pic.subsampling_x / subsampling_y, pic.chroma_width / chroma_height, + // pic.y / pic.u / pic.v: Plane::U8(Vec) for 8-bit, Plane::U16(Vec) above, + // pic.y_stride / pic.uv_stride, pic.pts, pic.key, pic.intra_only, + // pic.color { color_space (0..7 as coded), full_range } + } +} +let rest = dec.flush(); // pictures still in flight (frame threads) +// dec.set_draft(true): draft mode (playback at reduced resolution only), dec.threads() +dec.reset(); // forget references / contexts (seek), keeps the thread pool +let stats = dec.stats(); // what the stream exercised (frame kinds, tiles, compound, scaling...) + +// Random access points: does a container sample start with a key frame (and its parameters)? +let key: bool = craft_video::vp9::decode::is_keyframe(chunk); +let info: Option = craft_video::vp9::decode::keyframe_info(chunk); +``` + +`is_keyframe` lets a caller check a container sync sample before starting a decode there, so +wrongly flagged samples (e.g. an MP4 without `stss`) still seek correctly. + +A superframe produces every shown frame it contains (normally one); hidden frames produce none. +With frame threads (`Decoder::new()`, or `with_threads(n)` with n > 1) a picture can come out of +a later `decode` call or `flush` (each carries the `pts` of the chunk that showed it); +`with_threads(1)` returns every picture from the call that completes it. + +### Architecture + +The calling thread parses the uncompressed and compressed headers and manages the eight reference +slots, the four probability contexts, segmentation map and previous-frame motion vectors. Each tile +column is decoded by one `TileDecoder` (rayon pool) into a private strip of samples and mode info +(tile rows of a column are decoded in order: they share above contexts). Counts are then merged +for backward adaptation, the mode info is assembled (the next frame reads it for motion vector +prediction and segment ids) and the frame goes into the reference slots at once, while its +**post stage** runs on a thread of its own: it assembles each superblock row ("band") from the +strips, loop filters it superblock by superblock in raster order (so the result is the +specification's), publishes the band above it once that can no longer change (`OnceLock` per +band; the 16-wide filter of row r reads 8 and changes 7 rows of row r - 1, which travel in 8 +padding rows on top of each band) and copies it into the output picture. Motion compensation of +later frames reads reference rows through `Frame::row` / `Frame::span`, which wait for exactly +the bands a block reaches, so the next frames' tile columns decode while earlier frames are still +being filtered. Probability adaptation needs only the counts, so even backward-adaptive streams +start the next frame before the loop filter is done; frame-parallel / error-resilient streams +(no backward adaptation) never wait at all. Results never depend on scheduling: the conformance +suite decodes every fixture with 1, 3 and all threads. Samples are `u16` internally for every bit +depth. + +Draft mode (`Decoder::set_draft(true)`, meant for playback at reduced resolution): frames with `refresh_frame_flags` 0 — +no reference slot receives them, so nothing predicts from their samples — skip the loop filter +and are flagged `Picture::draft`. The loop filter changes neither mode info, segmentation map nor +probabilities, so every other frame is unchanged. Such frames occur in temporally layered streams +(e.g. libvpx `-ts-parameters`); single-layer realtime streams refresh LAST on every frame. + +Modules: `header` (6.2 / 6.3, superframes), `boolcoder` (9.2), `probs` (frame contexts, counts, +8.4), `tile` (partition / mode info / MV prediction), `tile::recon` (residual, tokens, intra / +inter prediction calls, reconstruction), `intra`, `inter`, `transform`, `loopfilter`, `frame` +(banded frames, pools), `decoder` (scheduling, post stage). + +## Memory + +A frame is refused when its luma plane has more than 2^26 samples, or when the pictures the +decoder would hold at that size (eight reference slots, the new frame and those in flight, at two +bytes a sample on the 64-sample grid) do not fit `Decoder::set_memory_budget` +(`craft_video::budget::DECODE_MEMORY_BUDGET` by default: 4 GiB, 1 GiB on 32-bit targets). + +## Tests + +`cargo test --release -p craft-video --features vp9-decode,threads`. Fixtures are generated on first use into +`target/fixtures/vp9/` with ffmpeg + libvpx-vp9 (tests print a message and skip when ffmpeg or +libvpx is unavailable). Every frame is compared sample-exactly with `ffmpeg -c:v vp9 -f rawvideo` +output with 1 thread, 3 threads (frame threading with a small pool) and every core; the first +mismatch reports frame, plane, position and superblock. No picture may be flagged draft without +draft mode. + +- Unit tests: boolean decoder against a literal model of 9.2, tables / trees, inverse DCT / ADST + against floating-point bases, DC-only shortcut vs. full transform, the column-parallel 2D + transform against the per-column formulation on random blocks of every size and type, scaled + MC path vs. unscaled (through a banded reference frame), superframe index, `inv_remap_prob`, + probability merging. +- Draft mode (`draft_mode_changes_only_flagged_non_reference_frames`): on a two-layer temporal + scalability stream, single- and frame-threaded, every unflagged frame is bit-exact and only + flagged frames differ. +- Conformance (`tests/it/conformance.rs`): the fixture matrix below plus a stream concatenating two + sizes. `compare_detects_mismatch` guards the harness. `coverage_report` (ignored) prints the + decoder statistics for each fixture. +- Synthetic (`tests/it/synthetic.rs`): headers of libvpx output are rewritten to produce what ffmpeg's + libvpx wrapper cannot: inter frames of a stream encoded at another size spliced after a 352x288 + key frame with explicit frame sizes, i.e. **scaled reference prediction** (downscale 256x208, + upscale 480x400, odd 301x199; a same-size splice is the control); a key frame rewritten as a + hidden **intra-only frame** (reset_frame_context 3 / 2 / 0, various refresh masks) shown with + **show_existing_frame**; show_existing_frame of every slot, also inside a superframe. +- Robustness (`tests/it/robustness.rs`): bit flips, byte replacement and truncation of real streams, + truncated frames, random data and oversized frames never panic (debug and release) or hang, + single- and multi-threaded. +- Superframe length regressions (`header::tests::hostile_lengths_*`, #2801) run without media + fixtures on native and 32-bit WASI targets. Oversized frame lengths cannot wrap an offset; + truncated indexes retain the existing remaining-payload fallback. See + [32-bit parser tests](../../docs/testing.md#32-bit-parser-regressions). + +### Fixture matrix (all bit-exact, 1 thread and thread pool) + +Backward adaptation is on (`-frame-parallel 0`) unless noted. + +| fixture | content | configuration | +|---|---|---| +| intra_only_keyframes | 352x288 | `-g 1` (key frames only, frame parallel) | +| default_good | 352x288, 20 frames | 800 kb/s | +| profile2_10bit / profile2_12bit | 352x288 / 176x144 | profile 2, 10 / 12-bit | +| profile1_444 / _422 / _440 | 176x144 | profile 1, 4:4:4 / 4:2:2 / 4:4:0 | +| profile3_444_10bit / profile3_422_12bit | 176x144 | profile 3 | +| lossless / lossless_10bit | 176x144 | `-lossless 1` (WHT) | +| tiles_cols4 | 1280x720 | 4 tile columns | +| tiles_rows_cols / tiles_rows4 | 1280x720 / 640x480 | tile columns + rows / 4 tile rows | +| altref_hidden / altref_10bit / altref_tiles | 352x288 / 720p | 2-pass, `-auto-alt-ref 1`: hidden frames, superframes, compound prediction | +| frame_parallel | 352x288 | `-frame-parallel 1` (no backward adaptation) | +| error_resilient | 352x288 | `-error-resilient 1` | +| odd_size / tiny_odd | 347x251 / 33x17 | odd sizes, partial superblocks | +| row_mt | 640x360 | `-row-mt 1`, 2 tile columns | +| speed0 .. speed8 | 176x144 / 352x288 | every speed (0 two-pass, 5..8 realtime) | +| cq_low / cq_q4 / cq_high / cq_10bit_low / cq_12bit_high | | `-crf 0 / 4 / 63 / 2 / 60` | +| aq_segmentation / aq_variance / aq_complexity | 352x288 | `-aq-mode 3 / 1 / 2` (segmentation, temporal segment-id prediction) | +| sharpness | 352x288 | `-sharpness 5` | +| fade_intra_heavy | 352x288 | fade-in, key frame every 8 | +| hd_1080p | 1920x1080 | realtime speed 5 | +| resize_concatenated | 352x288 then 347x251 | size change at a key frame | +| temporal_layers | 352x288 | realtime, 2 temporal layers (`-ts-parameters`): non-reference frames | + +## Performance + +`cargo test --release -p craft-video --features vp9-decode,threads --test vp9_decode perf:: -- --ignored --nocapture` (bit-exactness is +verified first), or `VP9_THREADS=n VP9_LOOPS=k cargo run --release -p craft-video --features vp9-decode,threads --example +vp9_vp9dec -- in.ivf` (decodes the file k times and prints the statistics; `VP9_DRAFT=1` for draft +mode). + +M4.10 (2026-10-03), Apple M4 Pro (14 cores) shared with other builds (load average 190-320), so +the figures are CPU cycles per frame counted by the kernel (`/usr/bin/time -l`, all threads +summed; mean of two interleaved base / after rounds) — wall clock only within a round. Fixtures: +the `cargo xtask bench` decode clips (testsrc2 + grain, libvpx realtime speed 8, CRF 32; 1080p +4 tile columns, 2160p 8 tile columns, frame-parallel mode), bit-exact with ffmpeg. + +| stream | threads | Mcycles / frame before | after | wall ms / frame before → after (same load) | +|---|---|---|---|---| +| 1080p (120 frames) | 1 | 68.0 | **52.0** (−24 %) | | +| 1080p | 14 | 100.7 | **57.9** (−43 %) | 95, 82 → 18, 17 | +| 2160p (72 frames) | 1 | 273.6 | **205.8** (−25 %) | | +| 2160p | 14 | 391.7 | **219.2** (−44 %) | 146, 118 → 38, 36 | + +Before M4.10 the frame-level work was serial (tile columns of one frame, then a loop-filter +wavefront, then assembly and output conversion), so 14 threads spent 40-50 % more cycles than one +thread (spinning between short parallel phases) and a 4K frame still took ~120 ms of wall time +at this load; now the post stage overlaps the next frames and 4K decodes at ~27 fps on the +loaded machine. What changed: + +- **Frame threading** (see Architecture). +- **Loop filter** (was ~50 % of single-threaded time): the lane masks were `bool`s with + `clamp` and branchy selects, which kept the "vectorised" group filter scalar; masks are now + 0 / -1 integers with bitwise selects and min / max clipping, so it compiles to NEON. +- **Inverse transforms**: the column pass runs the generated butterflies + (`transform_gen::cols4` / `cols8`) on 4 / 8 columns at once. +- **Motion compensation**: 8-bit sub-pixel filters accumulate in 16-bit lanes (8 per vector + instead of 4): every sum of the VP9 filters plus rounding lies in [-13706, 46474], so the sum + modulo 2^16 identifies it (negative sums clip to 0). +- Bool decoder: branch-free update. + +Single-threaded 4K time now splits into coefficient tokens (bool decoding) ~27 %, loop filter +~25 % (two thirds of it the vertical-edge transposes and per-group decisions), motion +compensation ~15 %, inverse transforms ~8 %, intra prediction ~6 %. + +## Known gaps + +- A frame's tile decoding is still a barrier: the next frame starts once every tile column of + the current one is done (its post stage then runs alongside), so streams with a single tile + column decode their tiles on one thread at a time; the post stage (loop filter) of one frame + runs on one thread. +- Scaled references combined with compound prediction are not covered by a fixture (both are + verified separately; libvpx through ffmpeg cannot change size mid-stream and the synthetic + splice cannot keep the compound streams' probability contexts intact). +- Frames larger than 16384x16384 are rejected (`Error::Invalid`) to bound memory. +- Damaged streams are decoded as far as the data allows (the boolean decoder reads zeros past the + end, as specified); there is no concealment beyond that, and inter frames whose reference slots + are empty return `Error::MissingReference`. diff --git a/crates/video/src/vp9/decode/boolcoder.rs b/crates/video/src/vp9/decode/boolcoder.rs new file mode 100644 index 000000000..b4aa4435a --- /dev/null +++ b/crates/video/src/vp9/decode/boolcoder.rs @@ -0,0 +1,190 @@ +//! Boolean (arithmetic) decoder of section 9.2. +//! +//! The specification keeps an 8-bit `BoolValue` and shifts in one bit per renormalisation step. +//! This implementation keeps `BoolValue` in the top 8 bits of a 64-bit window whose lower bits +//! hold the next bits of the input, which is equivalent and needs a refill only every few bytes. +//! Reading past the end of the data yields zero bits, as the specification does once +//! `BoolMaxBits` is exhausted. + +pub struct BoolDecoder<'a> { + data: &'a [u8], + pos: usize, + /// BoolValue in bits 56..63, followed by `count` valid input bits. + value: u64, + count: i32, + range: u32, +} + +impl<'a> BoolDecoder<'a> { + /// init_bool (9.2.1): the marker bit is read and returned in `Err` position when non-zero is + /// not treated as fatal (libvpx ignores it too), so this only returns the decoder. + pub fn new(data: &'a [u8]) -> Self { + let mut d = BoolDecoder { data, pos: 0, value: 0, count: -8, range: 255 }; + d.fill(); + d.read_bool(128); + d + } + + #[inline] + fn fill(&mut self) { + // Room for whole bytes below the top 8 bits and the `count` valid bits. + let mut shift = 48 - self.count; + if let Some(&bytes) = self.data.get(self.pos..).and_then(|d| d.first_chunk::<8>()) { + let v = u64::from_be_bytes(bytes); + let n = ((shift + 8) / 8) as usize; // bytes that fit + let n = n.min(7); + // Take the top n bytes of v. + let take = v >> (64 - 8 * n); + self.value |= take << (shift + 8 - 8 * n as i32); + self.pos += n; + self.count += 8 * n as i32; + return; + } + while shift >= 0 { + let b = if self.pos < self.data.len() { + let b = self.data[self.pos]; + self.pos += 1; + b + } else { + 0 + }; + self.value |= (b as u64) << shift; + self.count += 8; + shift -= 8; + } + } + + /// read_bool (9.2.2). + #[inline(always)] + pub fn read_bool(&mut self, p: u8) -> bool { + if self.count < 8 { + self.fill(); + } + let split = 1 + (((self.range - 1) * p as u32) >> 8); + let bigsplit = (split as u64) << 56; + // The outcome is close to random: select instead of branching on it. + let bit = self.value >= bigsplit; + let m = (bit as u64).wrapping_neg(); + self.value -= bigsplit & m; + let m32 = m as u32; + self.range = ((self.range - split) & m32) | (split & !m32); + let shift = self.range.leading_zeros() - 24; + self.range <<= shift; + self.value <<= shift; + self.count -= shift as i32; + bit + } + + /// `read_bool(128)`: the split is `1 + ((range - 1) >> 1) == (range + 1) >> 1`. + #[inline(always)] + pub fn read_bool_half(&mut self) -> bool { + if self.count < 8 { + self.fill(); + } + let split = (self.range + 1) >> 1; + let bigsplit = (split as u64) << 56; + let bit = self.value >= bigsplit; + let m = (bit as u64).wrapping_neg(); + self.value -= bigsplit & m; + let m32 = m as u32; + self.range = ((self.range - split) & m32) | (split & !m32); + let shift = self.range.leading_zeros() - 24; + self.range <<= shift; + self.value <<= shift; + self.count -= shift as i32; + bit + } + + /// read_literal (9.2.4). + pub fn read_literal(&mut self, n: u32) -> u32 { + let mut x = 0; + for _ in 0..n { + x = (x << 1) | self.read_bool(128) as u32; + } + x + } + + /// Tree decoding process (9.3.3) with probabilities `probs[node]`. + #[inline] + pub fn read_tree(&mut self, tree: &[i8], probs: &[u8]) -> u8 { + let mut n = 0i8; + loop { + n = tree[(n + self.read_bool(probs[(n >> 1) as usize]) as i8) as usize]; + if n <= 0 { + return (-n) as u8; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Straightforward implementation of 9.2.2 for comparison. + struct SpecDecoder<'a> { + data: &'a [u8], + bitpos: usize, + value: u32, + range: u32, + max_bits: i64, + } + + impl<'a> SpecDecoder<'a> { + fn new(data: &'a [u8]) -> Self { + let mut d = SpecDecoder { data, bitpos: 0, value: 0, range: 255, max_bits: 8 * data.len() as i64 - 8 }; + for _ in 0..8 { + d.value = (d.value << 1) | d.bit(); + } + d.read(128); + d + } + fn bit(&mut self) -> u32 { + let b = self.data.get(self.bitpos / 8).map(|b| (b >> (7 - self.bitpos % 8)) & 1).unwrap_or(0); + self.bitpos += 1; + b as u32 + } + fn read(&mut self, p: u8) -> bool { + let split = 1 + (((self.range - 1) * p as u32) >> 8); + let bit = if self.value < split { + self.range = split; + false + } else { + self.range -= split; + self.value -= split; + true + }; + while self.range < 128 { + let nb = if self.max_bits > 0 { + self.max_bits -= 1; + self.bit() + } else { + 0 + }; + self.range *= 2; + self.value = (self.value << 1) + nb; + } + bit + } + } + + #[test] + fn matches_spec_model() { + let mut seed = 0x1234_5678u32; + let mut rnd = || { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + seed + }; + for len in [1usize, 2, 3, 7, 8, 9, 15, 16, 17, 100, 1000] { + let data: Vec = (0..len).map(|_| rnd() as u8).collect(); + let mut a = BoolDecoder::new(&data); + let mut b = SpecDecoder::new(&data); + for i in 0..len * 12 { + let p = (rnd() % 255 + 1) as u8; + assert_eq!(a.read_bool(p), b.read(p), "len {len} bit {i}"); + } + } + } +} diff --git a/crates/video/src/vp9/decode/decoder.rs b/crates/video/src/vp9/decode/decoder.rs new file mode 100644 index 000000000..18b85ebab --- /dev/null +++ b/crates/video/src/vp9/decode/decoder.rs @@ -0,0 +1,1090 @@ +//! Top-level decoder: superframes, frame header handling, tile scheduling (tile columns in +//! parallel), probability adaptation, reference frame management, and the per-frame "post" stage +//! (band assembly, loop filter, publication, output picture) that runs behind the next frame +//! (frame threading). + +use crate::vp9::decode::error::{Error, Result, ensure}; +use crate::vp9::decode::frame::{BAND_PAD, Band, Frame, FrameInfo, MiGrid, MiInfo, Pools}; +use crate::vp9::decode::header::{FrameHeader, HeaderState, KEY_FRAME, RefInfo, parse_compressed, parse_uncompressed, split_superframe}; +use crate::vp9::decode::loopfilter::{LfFrame, PlaneView, filter_superblock}; +use crate::vp9::decode::probs::{Counts, adapt_coef_probs, adapt_noncoef_probs}; +use crate::vp9::decode::tables::{AC_QLOOKUP, DC_QLOOKUP, SEG_LVL_ALT_Q, SWITCHABLE, TX_MODE_SELECT}; +use crate::vp9::decode::tile::{FrameShared, RefUse, Strip, TileDecoder}; +use crate::vp9::decode::{ColorInfo, Picture, Plane}; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, RwLock}; + +/// Maximum luma samples accepted for one decoded reference frame: 8192x8192, above level 6.2's +/// 8192x4352 (35 651 584 samples). +const MAX_LUMA_SAMPLES: u64 = 1 << 26; + +/// Counters describing what the decoder has seen (useful to check test coverage). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DecodeStats { + pub frames: u64, + pub shown: u64, + pub hidden: u64, + pub key_frames: u64, + pub intra_only: u64, + pub inter_frames: u64, + pub show_existing: u64, + pub superframes: u64, + pub error_resilient: u64, + pub no_backward_adaptation: u64, + pub size_changes: u64, + pub scaled_ref_blocks: u64, + pub compound_blocks: u64, + pub intra_blocks: u64, + pub inter_blocks: u64, + pub lossless_frames: u64, + pub max_tile_cols: u32, + pub max_tile_rows: u32, + pub segmentation_frames: u64, + pub tx_select_frames: u64, + pub switchable_interp_frames: u64, + pub high_precision_mv_frames: u64, + pub compound_frames: u64, + pub bit_depths: [u64; 3], + pub profiles: [u64; 4], + /// Frames decoded in draft mode without the loop filter ([`Decoder::set_draft`]). + pub draft_frames: u64, +} + +/// A picture on its way out, in output order. +enum Pending { + Ready(Picture), + /// Produced by a post job running on another thread. + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + Job(std::sync::mpsc::Receiver), + /// show_existing_frame: converted once the frame is complete. + Existing(Arc, i64, bool), +} + +/// VP9 decoder. +pub struct Decoder { + st: HeaderState, + slots: [Option>; 8], + prev_mi: Option>, + prev_seg_ids: Vec, + last_size: Option<(u32, u32)>, + last_show_frame: bool, + stats: DecodeStats, + /// Bytes the picture store may take ([`crate::budget`]). + memory_budget: u64, + threads: usize, + bufs: Arc, + draft: bool, + pending: VecDeque, + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + jobs: VecDeque>, + /// Tile columns and the loop filter wavefront of post jobs run on it. + #[cfg(feature = "threads")] + pool: Option>, +} + +impl Default for Decoder { + fn default() -> Self { + Self::new() + } +} + +fn default_threads() -> usize { + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + { + std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).min(16) + } + #[cfg(not(all(feature = "threads", not(target_arch = "wasm32"))))] + { + 1 + } +} + +/// get_tile_offset (6.4.1). +fn tile_offset(i: usize, mis: usize, log2: u32) -> usize { + let sbs = (mis + 7) >> 3; + (((i * sbs) >> log2) << 3).min(mis) +} + +impl Decoder { + /// A decoder using all available cores (with the `threads` feature). + pub fn new() -> Self { + Self::with_threads(default_threads()) + } + + /// Cap the bytes of decoded pictures (references, current, in flight) this decoder may hold. + pub fn set_memory_budget(&mut self, bytes: u64) { + self.memory_budget = bytes; + } + + /// A decoder using up to `threads` worker threads (1 = decode on the calling thread, and + /// every picture is returned by the `decode` call that completes it). + pub fn with_threads(threads: usize) -> Self { + #[cfg(feature = "threads")] + let pool = if threads > 1 && cfg!(not(target_arch = "wasm32")) { + rayon::ThreadPoolBuilder::new().num_threads(threads).thread_name(|i| format!("vp9-{i}")).build().ok().map(Arc::new) + } else { + None + }; + Decoder { + st: HeaderState::default(), + slots: Default::default(), + prev_mi: None, + prev_seg_ids: Vec::new(), + last_size: None, + last_show_frame: false, + stats: DecodeStats::default(), + threads: threads.max(1), + memory_budget: crate::budget::DECODE_MEMORY_BUDGET, + bufs: Arc::default(), + draft: false, + pending: VecDeque::new(), + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + jobs: VecDeque::new(), + #[cfg(feature = "threads")] + pool, + } + } + + /// Statistics accumulated so far. + pub fn stats(&self) -> DecodeStats { + self.stats.clone() + } + + /// Worker threads in use (1: everything runs on the calling thread). + pub fn threads(&self) -> usize { + if self.frame_threads() { self.threads } else { 1 } + } + + /// Draft mode for reduced-resolution playback (off by default): frames that no other frame + /// can reference (`refresh_frame_flags` 0) skip the loop filter and come out flagged + /// [`Picture::draft`]. Every other picture is unchanged: the loop filter changes neither the + /// mode info, segmentation map nor probabilities that later frames read. + pub fn set_draft(&mut self, on: bool) { + self.draft = on; + } + + /// Decode one chunk (a frame or a superframe, as stored in one IVF / WebM / MP4 sample). + /// Returns the pictures finished so far, in order; each carries the `pts` of the chunk that + /// showed it. With frame threads a picture may come out of a later call or [`Self::flush`]. + pub fn decode(&mut self, data: &[u8], pts: i64) -> Result> { + let frames = split_superframe(data); + if frames.len() > 1 { + self.stats.superframes += 1; + } + for f in frames { + if f.is_empty() { + continue; + } + self.decode_frame(f, pts)?; + } + Ok(self.drain(false)) + } + + /// End of stream: every picture still in flight. + pub fn flush(&mut self) -> Vec { + let out = self.drain(true); + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + for j in self.jobs.drain(..) { + let _ = j.join(); + } + out + } + + /// Forget all decoding state (references, probability contexts, segmentation) and pictures + /// in flight, e.g. before decoding from another key frame after a seek. The thread pool and + /// statistics are kept. + pub fn reset(&mut self) { + self.st = HeaderState::default(); + self.slots = Default::default(); + self.prev_mi = None; + self.prev_seg_ids = Vec::new(); + self.last_size = None; + self.last_show_frame = false; + self.pending.clear(); + } + + fn frame_threads(&self) -> bool { + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + { + self.pool.is_some() + } + #[cfg(not(all(feature = "threads", not(target_arch = "wasm32"))))] + { + false + } + } + + /// Pictures at the front of the queue that are finished (`all`: wait for every one). The + /// queue is kept short: beyond a few pictures in flight the oldest is waited for. + fn drain(&mut self, all: bool) -> Vec { + let max_lag = (self.threads / 2).clamp(2, 6); + let mut out = Vec::new(); + loop { + let must = all || self.pending.len() > max_lag; + let Some(front) = self.pending.front_mut() else { break }; + let pic = match front { + Pending::Ready(_) => match self.pending.pop_front() { + Some(Pending::Ready(p)) => Some(p), + _ => None, + }, + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + Pending::Job(rx) => { + let p = if must { + rx.recv().ok() + } else { + match rx.try_recv() { + Ok(p) => Some(p), + Err(std::sync::mpsc::TryRecvError::Empty) => break, + Err(std::sync::mpsc::TryRecvError::Disconnected) => None, + } + }; + self.pending.pop_front(); + p + } + Pending::Existing(f, pts, draft) => { + if !must && !f.is_complete() { + break; + } + let p = picture_of(f, *pts, *draft); + self.pending.pop_front(); + Some(p) + } + }; + out.extend(pic); + } + out + } + + fn decode_frame(&mut self, data: &[u8], pts: i64) -> Result<()> { + let refs: [Option; 8] = + std::array::from_fn(|i| self.slots[i].as_ref().map(|f| RefInfo { width: f.info.width, height: f.info.height })); + let mut h = parse_uncompressed(data, &mut self.st, &refs)?; + if h.show_existing_frame { + self.stats.show_existing += 1; + let f = self.slots[h.frame_to_show_map_idx as usize] + .clone() + .ok_or_else(|| Error::MissingReference(format!("show_existing_frame of empty slot {}", h.frame_to_show_map_idx)))?; + self.stats.shown += 1; + self.pending.push_back(Pending::Existing(f, pts, false)); + return Ok(()); + } + ensure!(h.width <= 16384 && h.height <= 16384, "frame size {}x{} too large", h.width, h.height); + ensure!( + u64::from(h.width).checked_mul(u64::from(h.height)).is_some_and(|n| n <= MAX_LUMA_SAMPLES), + "frame size {}x{} exceeds the decoder memory limit", + h.width, + h.height + ); + // Eight reference slots, the new frame and those still decoding, on the 64-sample grid. + ensure!( + store_fits(h.width, h.height, &h.color, 8 + 1 + self.threads + 2, self.memory_budget), + "frame size {}x{} exceeds the decoder memory budget", + h.width, + h.height + ); + let (mi_rows, mi_cols) = (h.mi_rows as usize, h.mi_cols as usize); + // compute_image_size semantics (7.2.6). + let size = (h.width, h.height); + let size_changed = self.last_size != Some(size); + if size_changed && self.last_size.is_some() { + self.stats.size_changes += 1; + } + if self.st.reset_segment_map || size_changed || self.prev_seg_ids.len() != mi_rows * mi_cols { + self.prev_seg_ids = vec![0; mi_rows * mi_cols]; + self.st.reset_segment_map = false; + } + let use_prev = !size_changed + && self.last_show_frame + && !h.error_resilient_mode + && !h.frame_is_intra + && self.prev_mi.as_ref().is_some_and(|m| m.rows == mi_rows && m.cols == mi_cols); + self.last_size = Some(size); + self.last_show_frame = h.show_frame; + + let off = h.uncompressed_size; + let hs = h.header_size_in_bytes as usize; + ensure!(off + hs <= data.len(), "compressed header exceeds frame data"); + ensure!(off + hs < data.len(), "frame has no tile data"); + let mut fc = self.st.contexts[h.frame_context_idx as usize].clone(); + parse_compressed(&data[off..off + hs], &mut h, &mut fc)?; + self.count_frame(&h); + + // References (8.5.2.3). + let mut refs: [Option; 3] = [None, None, None]; + let ref_frames: Vec>> = + (0..3).map(|i| if h.frame_is_intra { None } else { self.slots[h.ref_frame_idx[i] as usize].clone() }).collect(); + if !h.frame_is_intra { + for i in 0..3 { + let Some(rf) = &ref_frames[i] else { + return Err(Error::MissingReference(format!("reference slot {} is empty", h.ref_frame_idx[i]))); + }; + let ri = &rf.info; + ensure!( + ri.bit_depth == h.color.bit_depth && ri.ss_x == h.color.subsampling_x && ri.ss_y == h.color.subsampling_y, + "reference frame format differs from the current frame" + ); + let (rw, rh) = (ri.width as i64, ri.height as i64); + let (w, hh) = (h.width as i64, h.height as i64); + if 2 * w >= rw && 2 * hh >= rh && w <= 16 * rw && hh <= 16 * rh { + let x_scale = ((rw << 14) / w) as i32; + let y_scale = ((rh << 14) / hh) as i32; + refs[i] = Some(RefUse { frame: rf, x_scale, y_scale, x_step: (16 * x_scale) >> 14, y_step: (16 * y_scale) >> 14 }); + } + } + } + // Quantizers per segment (8.6.1). + let bd_idx = ((h.color.bit_depth - 8) >> 1) as usize; + let seg = self.st.seg.clone(); + let mut seg_q = [[[0i32; 2]; 2]; 8]; + for (s, q) in seg_q.iter_mut().enumerate() { + let qindex = if seg.feature_active(s as u8, SEG_LVL_ALT_Q) { + let d = seg.feature_data[s][SEG_LVL_ALT_Q] as i32; + (if seg.abs_or_delta_update { d } else { h.base_q_idx as i32 + d }).clamp(0, 255) + } else { + h.base_q_idx as i32 + }; + let dc = |b: i32| DC_QLOOKUP[bd_idx * 256 + b.clamp(0, 255) as usize]; + let ac = |b: i32| AC_QLOOKUP[bd_idx * 256 + b.clamp(0, 255) as usize]; + q[0] = [dc(qindex + h.delta_q_y_dc as i32), ac(qindex)]; + q[1] = [dc(qindex + h.delta_q_uv_dc as i32), ac(qindex + h.delta_q_uv_ac as i32)]; + } + // Tile layout and data (6.4). + let tile_cols = 1usize << h.tile_cols_log2; + let tile_rows = 1usize << h.tile_rows_log2; + let mut pos = off + hs; + let mut tile_data: Vec> = vec![Vec::with_capacity(tile_rows); tile_cols]; + for tr in 0..tile_rows { + for (tc, td) in tile_data.iter_mut().enumerate() { + let last = tr == tile_rows - 1 && tc == tile_cols - 1; + let sz = if last { + data.len() - pos + } else { + let Some(&b) = data.get(pos..).and_then(|d| d.first_chunk::<4>()) else { + return Err(Error::Invalid("tile size beyond frame data".into())); + }; + let s = u32::from_be_bytes(b) as usize; + pos += 4; + ensure!(s <= data.len() - pos, "tile size {s} beyond frame data"); + s + }; + ensure!(sz > 0, "empty tile payload"); + td.push(&data[pos..pos + sz]); + pos += sz; + } + } + let counting = !h.error_resilient_mode && !h.frame_parallel_decoding_mode; + let prev_mi = if use_prev { self.prev_mi.clone() } else { None }; + let shared = FrameShared { + h: &h, + fc: &fc, + seg: &seg, + prev_seg_ids: &self.prev_seg_ids, + prev_mi: prev_mi.as_deref(), + refs, + seg_q, + counting, + pools: &self.bufs, + }; + let col_bounds: Vec<(usize, usize)> = + (0..tile_cols).map(|i| (tile_offset(i, mi_cols, h.tile_cols_log2), tile_offset(i + 1, mi_cols, h.tile_cols_log2))).collect(); + let row_bounds: Vec<(usize, usize)> = + (0..tile_rows).map(|i| (tile_offset(i, mi_rows, h.tile_rows_log2), tile_offset(i + 1, mi_rows, h.tile_rows_log2))).collect(); + let decode_col = |ci: usize| -> (Strip, Option) { + let (cs, ce) = col_bounds[ci]; + let mut td = TileDecoder::new(&shared, cs, ce); + let mut err = None; + if ce > cs { + for (ri, &(rs, re)) in row_bounds.iter().enumerate() { + if !td.decode_tile(tile_data[ci][ri], rs, re) { + err = Some(Error::Invalid("tile data exhausted".into())); + break; + } + } + } + let e = td.error.take().or(err); + (td.strip, e) + }; + let results: Vec<(Strip, Option)> = self.run_parallel(tile_cols, &decode_col); + let mut counts = Counts::default(); + let mut strips = Vec::with_capacity(results.len()); + let mut error = None; + for (s, e) in results { + if let Some(e) = e { + error.get_or_insert(e); + } + if counting { + counts.add(&s.counts); + } + self.stats.compound_blocks += s.compound_blocks; + self.stats.scaled_ref_blocks += s.scaled_blocks; + self.stats.intra_blocks += s.intra_blocks; + self.stats.inter_blocks += s.inter_blocks; + strips.push(s); + } + if let Some(e) = error { + recycle_strips(strips, &self.bufs); + return Err(e); + } + // Release the references before the slots are refreshed, so that evicted frames can be + // recycled. + drop(ref_frames); + drop(prev_mi); + let (mi, seg_ids) = assemble_mi(&mut strips, mi_rows, mi_cols, &self.bufs); + let mi = Arc::new(mi); + let frame = Arc::new(Frame::new( + FrameInfo { + width: h.width, + height: h.height, + ss_x: h.color.subsampling_x, + ss_y: h.color.subsampling_y, + bit_depth: h.color.bit_depth, + color_space: h.color.color_space, + color_range: h.color.color_range, + render_width: h.render_width, + render_height: h.render_height, + key: h.frame_type == KEY_FRAME, + intra_only: h.intra_only, + }, + self.bufs.clone(), + )); + // Loop filter (8.8), skipped in draft mode for frames nothing can reference. + let draft = self.draft && h.refresh_frame_flags == 0 && h.show_frame && self.st.lf.level > 0; + self.stats.draft_frames += draft as u64; + let lf = (self.st.lf.level > 0 && !draft) + .then(|| LfFrame::new(&self.st.lf, &seg, mi_rows, mi_cols, h.color.subsampling_x, h.color.subsampling_y, h.color.bit_depth)); + let job = PostJob { + frame: frame.clone(), + strips, + mi: mi.clone(), + lf, + out: h.show_frame.then_some((pts, draft)), + pools: self.bufs.clone(), + #[cfg(feature = "threads")] + pool: self.pool.clone(), + }; + self.run_post(job); + // refresh_probs (6.1.2). + if counting { + let saved = &self.st.contexts[h.frame_context_idx as usize]; + let mut a = saved.clone(); + a.tx = fc.tx; + a.skip = fc.skip; + adapt_coef_probs(&mut a, &counts, h.frame_is_intra, self.st.last_frame_type == KEY_FRAME); + if !h.frame_is_intra { + a.tx = saved.tx; + a.skip = saved.skip; + adapt_noncoef_probs(&mut a, &counts, h.interp_filter == SWITCHABLE, h.tx_mode == TX_MODE_SELECT, h.allow_high_precision_mv); + } + fc = a; + } + if h.refresh_frame_context { + self.st.contexts[h.frame_context_idx as usize] = fc; + } + if seg.enabled && seg.update_map { + self.prev_seg_ids = seg_ids; + } + for i in 0..8 { + if (h.refresh_frame_flags >> i) & 1 == 1 { + self.slots[i] = Some(frame.clone()); + } + } + if let Some(old) = self.prev_mi.replace(mi) + && let Ok(old) = Arc::try_unwrap(old) + { + self.bufs.mi.put(old.mi); + } + if h.show_frame { + self.stats.shown += 1; + } else { + self.stats.hidden += 1; + } + Ok(()) + } + + /// Run the post stage of a frame: on its own thread with frame threads (later frames wait + /// per band for the samples they read), else right here. + fn run_post(&mut self, job: PostJob) { + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + let job = if self.frame_threads() { + // Bound the post stages in flight (each is one thread). + while self.jobs.front().is_some_and(|j| j.is_finished()) { + let _ = self.jobs.pop_front().map(|j| j.join()); + } + let max_jobs = (self.threads / 3).clamp(2, 6); + while self.jobs.len() >= max_jobs { + let _ = self.jobs.pop_front().map(|j| j.join()); + } + let shown = job.out.is_some(); + let (tx, rx) = std::sync::mpsc::channel(); + // The job reaches the thread over a channel, so it is still here if no thread can + // be spawned and then runs inline below. + let (job_tx, job_rx) = std::sync::mpsc::channel::(); + let spawned = std::thread::Builder::new().name("vp9-post".into()).spawn(move || { + if let Ok(job) = job_rx.recv() + && let Some(p) = job.run() + { + let _ = tx.send(p); + } + }); + match spawned { + Ok(handle) => match job_tx.send(job) { + Ok(()) => { + self.jobs.push_back(handle); + if shown { + self.pending.push_back(Pending::Job(rx)); + } + return; + } + // The thread is gone already: take the job back and run it here. + Err(std::sync::mpsc::SendError(back)) => back, + }, + // No thread available: run it here. + Err(_) => job, + } + } else { + job + }; + if let Some(p) = job.run() { + self.pending.push_back(Pending::Ready(p)); + } + } + + fn count_frame(&mut self, h: &FrameHeader) { + let s = &mut self.stats; + s.frames += 1; + if h.frame_type == KEY_FRAME { + s.key_frames += 1; + } else if h.intra_only { + s.intra_only += 1; + } else { + s.inter_frames += 1; + } + s.error_resilient += h.error_resilient_mode as u64; + s.no_backward_adaptation += (h.error_resilient_mode || h.frame_parallel_decoding_mode) as u64; + s.lossless_frames += h.lossless as u64; + s.max_tile_cols = s.max_tile_cols.max(1 << h.tile_cols_log2); + s.max_tile_rows = s.max_tile_rows.max(1 << h.tile_rows_log2); + s.segmentation_frames += self.st.seg.enabled as u64; + s.tx_select_frames += (h.tx_mode == TX_MODE_SELECT) as u64; + s.switchable_interp_frames += (!h.frame_is_intra && h.interp_filter == SWITCHABLE) as u64; + s.high_precision_mv_frames += (!h.frame_is_intra && h.allow_high_precision_mv) as u64; + s.compound_frames += (!h.frame_is_intra && h.reference_mode != 0) as u64; + s.bit_depths[((h.color.bit_depth - 8) >> 1) as usize] += 1; + s.profiles[h.profile as usize] += 1; + } + + /// Run `f(0..n)`, in parallel when a pool is available. + fn run_parallel(&self, n: usize, f: &(dyn Fn(usize) -> T + Sync)) -> Vec { + #[cfg(feature = "threads")] + if let Some(pool) = &self.pool + && n > 1 + { + use rayon::prelude::*; + return pool.install(|| (0..n).into_par_iter().map(f).collect()); + } + (0..n).map(f).collect() + } +} + +impl Drop for Decoder { + fn drop(&mut self) { + // Post jobs own everything they use; let them finish rather than leaving threads behind. + #[cfg(all(feature = "threads", not(target_arch = "wasm32")))] + for j in self.jobs.drain(..) { + let _ = j.join(); + } + } +} + +fn recycle_strips(strips: Vec, pools: &Pools) { + for s in strips { + for p in s.planes { + pools.samples.put(p); + } + pools.mi.put(s.mi); + } +} + +/// The post stage of one frame: build each band from the tile column strips, loop filter it, +/// publish it once no later filtering can change it, and copy the frame into the output picture. +/// +/// The specification filters superblock by superblock in raster order (8.8.1). Superblock (r, c) +/// changes samples of (r, c - 1) (the vertical edges on its left) and of (r - 1, c) (the +/// horizontal edges on its top reach 7 rows up), and the last superblock before it to change the +/// bottom rows of (r - 1, c) is (r - 1, c + 1). So row r may filter superblock c once row r - 1 +/// has filtered superblock c + 1: rows run as a wavefront, each two superblocks behind the row +/// above, with the specification's result. Every row filters its own band: before superblock +/// c it copies the bottom rows of band r - 1 in that superblock column (shared by row r - 1 once +/// final for it) into its scratch rows, and once done it writes them back into band r - 1, which +/// is then final and published. The post thread takes rows in order and helpers on the +/// decoder's pool join in; with one thread the rows run one after the other, as before. +struct PostJob { + frame: Arc, + strips: Vec, + mi: Arc, + lf: Option, + /// (pts, draft) when the frame is shown. + out: Option<(i64, bool)>, + pools: Arc, + #[cfg(feature = "threads")] + pool: Option>, +} + +/// Publishes placeholder bands if a post job dies or fails, so that waiting frames cannot hang. +struct PublishGuard<'a>(&'a Frame); + +impl Drop for PublishGuard<'_> { + fn drop(&mut self) { + for r in 0..self.0.sb_rows() { + if !self.0.is_published(r) { + self.0.publish(r, self.0.new_band()); + } + } + } +} + +impl PostJob { + fn run(self) -> Option { + let f = &*self.frame; + let _guard = PublishGuard(f); + let mut pic = self.out.map(|(pts, draft)| PictureBuilder::new(f, pts, draft)); + let Some(lf) = self.lf else { + // Without the loop filter every band is final as built. + for r in 0..f.sb_rows() { + let band = build_band(f, &self.strips, r); + if let Some(pic) = &mut pic { + pic.add(f, r, &band); + } + f.publish(r, band); + } + recycle_strip_planes(self.strips, &self.pools); + return pic.map(PictureBuilder::finish); + }; + let wave = Arc::new(Wave::new(self.frame.clone(), self.strips, self.mi, lf, self.pools, pic)); + #[cfg(feature = "threads")] + if let Some(pool) = &self.pool { + // Rows that can be filtered at the same time, the post thread being one of them. + let rows = wave.sb_rows.min(wave.sb_cols.div_ceil(2)).min(pool.current_num_threads() + 1); + for _ in 1..rows { + let w = wave.clone(); + pool.spawn(move || w.work()); + } + } + wave.work(); + if !wave.wait_done() { + return None; + } + // Every row is done, so no helper reads the strips any more. + let strips = std::mem::take(&mut *wave.strips.write().unwrap_or_else(std::sync::PoisonError::into_inner)); + recycle_strip_planes(strips, &wave.pools); + lock(&wave.pic).take().map(PictureBuilder::finish) + } +} + +fn recycle_strip_planes(strips: Vec, pools: &Pools) { + for s in strips { + for p in s.planes { + pools.samples.put(p); + } + } +} + +/// Band `r` of a frame from the tile column strips, before the loop filter. +fn build_band(f: &Frame, strips: &[Strip], r: usize) -> Band { + let mut band = f.new_band(); + for p in 0..3 { + let (fs, bh) = (f.strides[p], f.band_h[p]); + let dst = &mut band.planes[p]; + for s in strips { + let ss = s.strides[p]; + let x0 = s.x_off[p]; + let w = ss.min(fs.saturating_sub(x0)); + for k in 0..bh { + let y = r * bh + k; + let d = (BAND_PAD + k) * fs + x0; + if let (Some(d), Some(s)) = (dst.get_mut(d..d + w), s.planes[p].get(y * ss..y * ss + w)) { + d.copy_from_slice(s); + } + } + } + } + band +} + +/// Progress of one superblock row of the wavefront. +struct RowProgress { + /// Superblocks filtered; `sb_cols` once the row is done (and band r - 1 published). + done: AtomicUsize, + /// Threads waiting on `cv` (so that progress without a waiter costs no wake-up call). + waiters: AtomicUsize, + lock: Mutex<()>, + cv: Condvar, + /// The bottom `BAND_PAD` rows of the band of each plane, shared superblock column by column + /// once final for this row, for the row below. + bottom: Mutex<[Vec; 3]>, + /// The band once the row is done, until the row below has written its scratch rows back. + band: Mutex>, +} + +/// The shared state of a frame's loop filter wavefront (see [`PostJob`]). No lock is held while +/// a superblock is filtered: each row filters its own band and shares only copies. +struct Wave { + frame: Arc, + strips: RwLock>, + mi: Arc, + lf: LfFrame, + pools: Arc, + sb_rows: usize, + sb_cols: usize, + rows: Vec, + /// The next row to take. + next: AtomicUsize, + failed: AtomicBool, + /// The output picture (shown frames), filled band by band as bands are published. + pic: Mutex>, +} + +fn lock(m: &Mutex) -> MutexGuard<'_, T> { + m.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + +impl Wave { + fn new(frame: Arc, strips: Vec, mi: Arc, lf: LfFrame, pools: Arc, pic: Option) -> Wave { + let sb_rows = frame.sb_rows(); + let sb_cols = lf.mi_cols.div_ceil(8); + let rows = (0..sb_rows) + .map(|_| RowProgress { + done: AtomicUsize::new(0), + waiters: AtomicUsize::new(0), + lock: Mutex::new(()), + cv: Condvar::new(), + bottom: Mutex::new(std::array::from_fn(|p| pools.bands.take_any(BAND_PAD * frame.strides[p], 0))), + band: Mutex::new(None), + }) + .collect(); + Wave { + frame, + strips: RwLock::new(strips), + mi, + lf, + pools, + sb_rows, + sb_cols, + rows, + next: AtomicUsize::new(0), + failed: AtomicBool::new(false), + pic: Mutex::new(pic), + } + } + + /// Take rows and filter them until none is left. A row is only taken by a running thread, so + /// a row waiting for the one above always waits for a thread that is making progress. + fn work(&self) { + while !self.failed.load(Ordering::Acquire) { + let r = self.next.fetch_add(1, Ordering::Relaxed); + if r >= self.sb_rows { + return; + } + let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.row(r))).unwrap_or(false); + if !ok { + self.fail(); + return; + } + } + } + + /// Wait until every row is done (false if one failed). + fn wait_done(&self) -> bool { + self.sb_rows.checked_sub(1).is_none_or(|last| self.wait(last, self.sb_cols)) + } + + /// Wait until row `r` has filtered `need` superblocks (false if a row failed). + fn wait(&self, r: usize, need: usize) -> bool { + let Some(row) = self.rows.get(r) else { return false }; + if row.done.load(Ordering::SeqCst) >= need { + return true; + } + let mut g = lock(&row.lock); + row.waiters.fetch_add(1, Ordering::SeqCst); + let ok = loop { + if row.done.load(Ordering::SeqCst) >= need { + break true; + } + if self.failed.load(Ordering::SeqCst) { + break false; + } + g = row.cv.wait(g).unwrap_or_else(std::sync::PoisonError::into_inner); + }; + row.waiters.fetch_sub(1, Ordering::SeqCst); + ok + } + + fn set_done(&self, r: usize, n: usize) { + if let Some(row) = self.rows.get(r) { + // A waiter registers under the lock before it checks `done`, so either it sees the + // new value or it is counted here and woken. + row.done.store(n, Ordering::SeqCst); + if row.waiters.load(Ordering::SeqCst) > 0 { + let _g = lock(&row.lock); + row.cv.notify_all(); + } + } + } + + /// Stop the wavefront: waiting rows give up, the post job publishes placeholders. + fn fail(&self) { + self.failed.store(true, Ordering::SeqCst); + for row in &self.rows { + let _g = lock(&row.lock); + row.cv.notify_all(); + } + } + + /// Publish final band `r` and copy it into the output picture. + fn publish(&self, r: usize, band: Band) { + if let Some(pic) = lock(&self.pic).as_mut() { + pic.add(&self.frame, r, &band); + } + self.frame.publish(r, band); + } + + /// Share the bottom rows of superblock column `c` of row `r`'s band (final for this row). + fn share_bottom(&self, r: usize, band: &Band, c: usize) { + let Some(row) = self.rows.get(r) else { return }; + let mut bottom = lock(&row.bottom); + for (p, dst) in bottom.iter_mut().enumerate() { + copy_column(&self.frame, Column { p, c, sb_cols: self.sb_cols }, (&band.planes[p], self.frame.band_h[p]), (dst, 0)); + } + } + + /// Build and filter row `r`; publish band r - 1 (and the last band) when done. + fn row(&self, r: usize) -> bool { + let f = &*self.frame; + let sb_cols = self.sb_cols; + let mut band = build_band(f, &self.strips.read().unwrap_or_else(std::sync::PoisonError::into_inner), r); + let above = r.checked_sub(1).and_then(|a| self.rows.get(a)); + for c in 0..sb_cols { + if let Some(above) = above { + // Superblock c + 1 of the row above is filtered: the bottom rows of its column c + // are final for that row and go into our scratch rows. + if !self.wait(r - 1, (c + 2).min(sb_cols)) { + return false; + } + let bottom = lock(&above.bottom); + for (p, src) in bottom.iter().enumerate() { + copy_column(f, Column { p, c, sb_cols }, (src, 0), (&mut band.planes[p], 0)); + } + } + let [a, b, d] = &mut band.planes; + let mut views = [band_view(f, a, 0, r), band_view(f, b, 1, r), band_view(f, d, 2, r)]; + filter_superblock(&mut views, &self.mi, &self.lf, r * 8, c * 8); + // Superblock c filtered the left edge of column c, so column c - 1 is final here. + if c > 0 { + self.share_bottom(r, &band, c - 1); + } + if c + 1 < sb_cols { + self.set_done(r, c + 1); + } + } + self.share_bottom(r, &band, sb_cols.saturating_sub(1)); + if let Some(above) = above { + // The scratch rows now hold the final bottom rows of band r - 1. + let Some(mut pb) = lock(&above.band).take() else { return false }; + for p in 0..3 { + let (fs, bh) = (f.strides[p], f.band_h[p]); + if let (Some(d), Some(s)) = (pb.planes[p].get_mut(bh * fs..(bh + BAND_PAD) * fs), band.planes[p].get(..BAND_PAD * fs)) { + d.copy_from_slice(s); + } + } + self.publish(r - 1, pb); + } + if r + 1 == self.sb_rows { + self.publish(r, band); + } else if let Some(row) = self.rows.get(r) { + *lock(&row.band) = Some(band); + } + self.set_done(r, sb_cols); + true + } +} + +impl Drop for Wave { + fn drop(&mut self) { + recycle_strip_planes(std::mem::take(self.strips.get_mut().unwrap_or_else(std::sync::PoisonError::into_inner)), &self.pools); + for row in &self.rows { + for p in std::mem::take(&mut *lock(&row.bottom)) { + self.pools.bands.put(p); + } + if let Some(b) = lock(&row.band).take() { + for p in b.planes { + self.pools.bands.put(p); + } + } + } + } +} + +/// Superblock column `c` of plane `p` in a frame `sb_cols` superblocks wide. +#[derive(Clone, Copy)] +struct Column { + p: usize, + c: usize, + sb_cols: usize, +} + +/// Copy `BAND_PAD` rows of superblock column `c` of plane `p` (the last column takes the rest of +/// the stride) from row `src_row` of `src` to row `dst_row` of `dst` (each given as `(buffer, row)`), both `strides[p]` wide. +fn copy_column(f: &Frame, col: Column, (src, src_row): (&[u16], usize), (dst, dst_row): (&mut [u16], usize)) { + let Column { p, c, sb_cols } = col; + let fs = f.strides[p]; + let cw = fs / sb_cols.max(1); + let x0 = c * cw; + let x1 = if c + 1 >= sb_cols { fs } else { x0 + cw }; + for k in 0..BAND_PAD { + let (s, d) = ((src_row + k) * fs, (dst_row + k) * fs); + if let (Some(d), Some(s)) = (dst.get_mut(d + x0..d + x1), src.get(s + x0..s + x1)) { + d.copy_from_slice(s); + } + } +} + +/// The loop filter's view of band `r` of plane `p`: the band's rows plus (below row 0) the +/// scratch rows holding the bottom of band r - 1. +fn band_view<'a>(f: &Frame, d: &'a mut [u16], p: usize, r: usize) -> PlaneView<'a> { + let (fs, bh) = (f.strides[p], f.band_h[p]); + if r == 0 { + PlaneView { data: &mut d[BAND_PAD * fs..], stride: fs, ox: 0, oy: 0 } + } else { + PlaneView { data: d, stride: fs, ox: 0, oy: r * bh - BAND_PAD } + } +} + +/// Mode info and segment ids of the whole frame from the tile column strips (whose mode info +/// buffers go back to the pool). +fn assemble_mi(strips: &mut [Strip], mi_rows: usize, mi_cols: usize, pools: &Pools) -> (MiGrid, Vec) { + if strips.len() == 1 { + let s = &mut strips[0]; + return (MiGrid { cols: mi_cols, rows: mi_rows, mi: std::mem::take(&mut s.mi) }, std::mem::take(&mut s.seg_ids)); + } + // Every position is copied from a strip below. + let mut mi = pools.mi.take_any(mi_rows * mi_cols, MiInfo::default()); + let mut seg = vec![0u8; mi_rows * mi_cols]; + for s in strips.iter_mut() { + let mw = s.mi_col_end - s.mi_col_start; + for r in 0..mi_rows { + mi[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.mi[r * mw..r * mw + mw]); + seg[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.seg_ids[r * mw..r * mw + mw]); + } + pools.mi.put(std::mem::take(&mut s.mi)); + } + (MiGrid { cols: mi_cols, rows: mi_rows, mi }, seg) +} + +/// Builds the cropped output picture band by band (narrowing 8-bit samples). +struct PictureBuilder { + planes: [Plane; 3], + info: FrameInfo, + vis: [(usize, usize); 3], + pts: i64, + draft: bool, +} + +impl PictureBuilder { + fn new(f: &Frame, pts: i64, draft: bool) -> Self { + let planes = f.vis.map(|(w, h)| if f.info.bit_depth == 8 { Plane::U8(vec![0; w * h]) } else { Plane::U16(vec![0; w * h]) }); + PictureBuilder { planes, info: f.info, vis: f.vis, pts, draft } + } + + fn add(&mut self, f: &Frame, r: usize, band: &Band) { + for p in 0..3 { + let (w, h) = f.vis[p]; + let (fs, bh) = (f.strides[p], f.band_h[p]); + let y0 = r * bh; + for y in y0..(y0 + bh).min(h) { + let src = &band.planes[p][(y - y0 + BAND_PAD) * fs..][..w]; + match &mut self.planes[p] { + Plane::U8(d) => { + for (d, &s) in d[y * w..y * w + w].iter_mut().zip(src) { + *d = s as u8; + } + } + Plane::U16(d) => d[y * w..y * w + w].copy_from_slice(src), + } + } + } + } + + fn finish(self) -> Picture { + let i = self.info; + let [y, u, v] = self.planes; + Picture { + width: i.width, + height: i.height, + chroma_width: self.vis[1].0 as u32, + chroma_height: self.vis[1].1 as u32, + bit_depth: i.bit_depth as u32, + subsampling_x: i.ss_x, + subsampling_y: i.ss_y, + y, + u, + v, + y_stride: self.vis[0].0, + uv_stride: self.vis[1].0, + pts: self.pts, + key: i.key, + intra_only: i.intra_only, + color: ColorInfo { color_space: i.color_space, full_range: i.color_range }, + render_width: i.render_width, + render_height: i.render_height, + draft: self.draft, + } + } +} + +/// The output picture of a complete (or completing) frame (show_existing_frame). +fn picture_of(f: &Frame, pts: i64, draft: bool) -> Picture { + let mut b = PictureBuilder::new(f, pts, draft); + for r in 0..f.sb_rows() { + b.add(f, r, f.band(r)); + } + b.finish() +} + +/// Whether `pictures` frames of `width` x `height` (on the 64-sample superblock grid) fit in +/// `budget` bytes. +fn store_fits(width: u32, height: u32, color: &crate::vp9::decode::header::ColorConfig, pictures: usize, budget: u64) -> bool { + let shape = crate::budget::PictureShape { + width: u64::from(width).div_ceil(64) * 64, + height: u64::from(height).div_ceil(64) * 64, + shift_x: u32::from(color.subsampling_x), + shift_y: u32::from(color.subsampling_y), + chroma_planes: 2, + bytes_per_sample: 2, + }; + shape.fits(pictures as u64, budget) +} + +#[cfg(test)] +mod budget_tests { + use super::*; + use crate::budget::DECODE_MEMORY_BUDGET; + + fn c420() -> crate::vp9::decode::header::ColorConfig { + crate::vp9::decode::header::ColorConfig { subsampling_x: true, subsampling_y: true, ..Default::default() } + } + + #[test] + fn the_axis_maximum_is_over_budget_and_4k_is_not() { + assert!(!store_fits(16384, 16384, &c420(), 8 + 1 + 1 + 2, DECODE_MEMORY_BUDGET)); + assert!(store_fits(3840, 2160, &c420(), 8 + 1 + 8 + 2, DECODE_MEMORY_BUDGET)); + } + + #[test] + fn exact_budget_boundary_and_reference_count() { + let one = (64 * 64 + 2 * 32 * 32) * 2; + assert!(store_fits(10, 10, &c420(), 11, 11 * one)); + assert!(!store_fits(10, 10, &c420(), 11, 11 * one - 1)); + assert!(!store_fits(10, 10, &c420(), 12, 11 * one)); + } +} diff --git a/crates/video/src/vp9/decode/error.rs b/crates/video/src/vp9/decode/error.rs new file mode 100644 index 000000000..80b741ba4 --- /dev/null +++ b/crates/video/src/vp9/decode/error.rs @@ -0,0 +1,31 @@ +use craft_media::bitstream::BitError; + +/// Errors produced by the decoder. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// The uncompressed header ended early. + #[error("bitstream error: {0}")] + Bitstream(#[from] BitError), + /// A syntax element had a value outside its legal range, or a size did not fit the data. + #[error("invalid bitstream: {0}")] + Invalid(String), + /// A valid feature this decoder does not implement. + #[error("unsupported: {0}")] + Unsupported(String), + /// An inter frame referenced a reference slot that was never filled (e.g. decoding started + /// at a non-key frame). + #[error("missing reference frame: {0}")] + MissingReference(String), +} + +pub type Result = std::result::Result; + +/// `ensure!(cond, "message")` returns `Error::Invalid` when `cond` is false. +macro_rules! ensure { + ($cond:expr, $($arg:tt)+) => { + if !$cond { + return Err($crate::vp9::decode::error::Error::Invalid(format!($($arg)+))); + } + }; +} +pub(crate) use ensure; diff --git a/crates/video/src/vp9/decode/frame.rs b/crates/video/src/vp9/decode/frame.rs new file mode 100644 index 000000000..6cc28270b --- /dev/null +++ b/crates/video/src/vp9/decode/frame.rs @@ -0,0 +1,311 @@ +//! Decoded frame storage and the per-8x8 mode info grid. + +use crate::vp9::decode::tables::{INTRA_FRAME, NONE}; + +/// A motion vector 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 of one 8x8 block (the arrays of 6.4.4: Skips, TxSizes, MiSizes, YModes, SubModes, +/// RefFrames, InterpFilters, Mvs / SubMvs). +#[derive(Clone, Copy, Debug)] +pub struct MiInfo { + pub sb_size: u8, + pub skip: bool, + pub tx_size: u8, + pub y_mode: u8, + pub sub_modes: [u8; 4], + pub seg_id: u8, + pub ref_frame: [i8; 2], + pub interp_filter: u8, + /// SubMvs[refList][b]; Mvs[refList] is `mv[refList][3]`. + pub mv: [[Mv; 4]; 2], +} + +impl Default for MiInfo { + fn default() -> Self { + MiInfo { + sb_size: 0, + skip: false, + tx_size: 0, + y_mode: 0, + sub_modes: [0; 4], + seg_id: 0, + ref_frame: [INTRA_FRAME, NONE], + interp_filter: 0, + mv: [[Mv::ZERO; 4]; 2], + } + } +} + +/// Mode info of a whole frame (MiRows x MiCols). +#[derive(Clone, Debug, Default)] +pub struct MiGrid { + pub cols: usize, + pub rows: usize, + pub mi: Vec, +} + +impl MiGrid { + #[inline] + pub fn at(&self, r: usize, c: usize) -> &MiInfo { + &self.mi[r * self.cols + c] + } +} + +/// Scratch rows above the samples of every band: the loop filter of superblock row r reads 8 +/// and modifies up to 7 rows of row r - 1 (the 16-wide filter of the horizontal edges on top of +/// row r), so those rows are copied there while row r is filtered (see `decoder::post`). +pub const BAND_PAD: usize = 8; + +/// One superblock row (64 luma rows) of a frame: per plane `BAND_PAD` scratch rows followed by +/// the band's rows, all `Frame::strides[p]` samples wide. +pub struct Band { + pub planes: [Vec; 3], +} + +/// Frame metadata from the frame header. +#[derive(Clone, Copy, Debug)] +pub struct FrameInfo { + pub width: u32, + pub height: u32, + pub ss_x: bool, + pub ss_y: bool, + pub bit_depth: u8, + pub color_space: u8, + pub color_range: bool, + pub render_width: u32, + pub render_height: u32, + pub key: bool, + pub intra_only: bool, +} + +/// A decoded frame (also used as reference). Its samples are published one superblock row +/// ("band") at a time, once loop filtered, so that later frames can predict from the finished +/// top of a frame while its bottom is still being filtered (frame threading). +pub struct Frame { + pub info: FrameInfo, + /// Allocated plane widths (whole superblocks) = row strides. + pub strides: [usize; 3], + /// Rows per band of each plane. + pub band_h: [usize; 3], + /// Visible (cropped) plane sizes. + pub vis: [(usize, usize); 3], + bands: Box<[std::sync::OnceLock]>, + pools: std::sync::Arc, +} + +impl Frame { + pub fn new(info: FrameInfo, pools: std::sync::Arc) -> Frame { + let geo = plane_geometry(info.width, info.height, info.ss_x, info.ss_y); + let sb_rows = (info.height as usize).div_ceil(64); + Frame { + info, + strides: geo.map(|g| g.0), + band_h: [64, 64 >> info.ss_y as usize, 64 >> info.ss_y as usize], + vis: geo.map(|g| (g.2, g.3)), + bands: (0..sb_rows).map(|_| std::sync::OnceLock::new()).collect(), + pools, + } + } + + pub fn sb_rows(&self) -> usize { + self.bands.len() + } + + /// A band buffer for this frame's geometry (recycled memory, contents unspecified). + pub fn new_band(&self) -> Band { + Band { planes: std::array::from_fn(|p| self.pools.bands.take_any((self.band_h[p] + BAND_PAD) * self.strides[p], 0)) } + } + + /// Publish band `r` (ignored if already published). + pub fn publish(&self, r: usize, band: Band) { + if let Err(b) = self.bands[r].set(band) { + for p in b.planes { + self.pools.bands.put(p); + } + } + } + + pub fn is_published(&self, r: usize) -> bool { + self.bands[r].get().is_some() + } + + pub fn is_complete(&self) -> bool { + self.bands.iter().all(|b| b.get().is_some()) + } + + /// Band `r`, waiting until it has been published. + #[inline] + pub fn band(&self, r: usize) -> &Band { + #[cfg(feature = "threads")] + { + self.bands[r].wait() + } + #[cfg(not(feature = "threads"))] + { + // Bands are decoded in order before use; an unpublished one reads as empty. + static EMPTY: Band = Band { planes: [Vec::new(), Vec::new(), Vec::new()] }; + self.bands[r].get().unwrap_or(&EMPTY) + } + } + + /// Row `y` of plane `p` (all `strides[p]` samples), waiting until it is final. + #[inline] + pub fn row(&self, p: usize, y: usize) -> &[u16] { + let bh = self.band_h[p]; + let s = self.strides[p]; + let o = (y % bh + BAND_PAD) * s; + &self.band(y / bh).planes[p][o..o + s] + } + + /// Rows `y0..y1` of plane `p` as one slice with stride `strides[p]` when they lie in one band: + /// (band samples, offset of row y0). + #[inline] + pub fn span(&self, p: usize, y0: usize, y1: usize) -> Option<(&[u16], usize)> { + let bh = self.band_h[p]; + let r = y0 / bh; + if (y1 - 1) / bh != r { + return None; + } + Some((&self.band(r).planes[p], (y0 % bh + BAND_PAD) * self.strides[p])) + } + + /// A complete frame from full planes (tests). + #[cfg(test)] + pub fn from_planes(info: FrameInfo, planes: [&[u16]; 3]) -> Frame { + let f = Frame::new(info, Default::default()); + for r in 0..f.sb_rows() { + let mut b = f.new_band(); + for p in 0..3 { + let (s, bh) = (f.strides[p], f.band_h[p]); + for k in 0..bh { + let y = r * bh + k; + if let Some(src) = planes[p].get(y * s..y * s + s) { + b.planes[p][(k + BAND_PAD) * s..(k + BAND_PAD + 1) * s].copy_from_slice(src); + } + } + } + f.publish(r, b); + } + f + } +} + +impl Drop for Frame { + fn drop(&mut self) { + for b in std::mem::take(&mut self.bands).into_vec().into_iter().filter_map(|b| b.into_inner()) { + for p in b.planes { + self.pools.bands.put(p); + } + } + } +} + +/// Geometry of the frame planes: allocation sizes for (MiCols, MiRows) rounded to superblocks. +pub fn plane_geometry(width: u32, height: u32, ss_x: bool, ss_y: bool) -> [(usize, usize, usize, usize); 3] { + let sb_cols = (width as usize).div_ceil(64); + let sb_rows = (height as usize).div_ceil(64); + let (aw, ah) = (sb_cols * 64, sb_rows * 64); + let (sx, sy) = (ss_x as usize, ss_y as usize); + let cw = (width as usize + sx) >> sx; + let ch = (height as usize + sy) >> sy; + [(aw, ah, width as usize, height as usize), (aw >> sx, ah >> sy, cw, ch), (aw >> sx, ah >> sy, cw, ch)] +} + +/// Recycled buffers (strip / frame planes, mode info), so that steady-state decoding does not +/// allocate (and page-fault) megabytes per frame. +pub struct Pool { + bufs: std::sync::Mutex>>, + max: usize, +} + +impl Default for Pool { + fn default() -> Self { + Pool::with_max(32) + } +} + +impl Pool { + /// A pool keeping at most `max` buffers. + pub fn with_max(max: usize) -> Self { + Pool { bufs: Default::default(), max } + } +} + +impl Pool { + /// A buffer of `len` elements with unspecified contents (recycled memory when available; + /// `fill` only initialises fresh memory). + /// + /// Invariant for callers: every element that is read must have been written first. For the + /// sample planes and mode info grids this holds because every sample inside the mode info grid + /// is predicted before a later stage (loop filter, reference reads, intra edges) reads it, and + /// every mode info position is written by its tile before it is read. Reads are clamped to + /// that grid (intra edges to `max_x` / `max_y`, references to the visible size). A change that + /// reads outside the grid would see stale data from a recycled buffer. + pub fn take_any(&self, len: usize, fill: T) -> Vec { + let found = { + let mut b = self.bufs.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + // Best fit by capacity: the first fit handed large buffers to small requests (band + // scratch rows, chroma bands), and once truncated they never served a large one + // again, so large requests kept allocating fresh (zeroed) memory. + let mut best: Option<(usize, usize)> = None; + for (i, v) in b.iter().enumerate() { + let c = v.capacity(); + if c >= len && best.is_none_or(|(_, bc)| c < bc) { + best = Some((i, c)); + if c == len { + break; + } + } + } + best.map(|(i, _)| b.swap_remove(i)) + }; + match found { + Some(mut v) => { + // Contents are unspecified: only memory beyond the current length is written. + if v.len() >= len { + v.truncate(len); + } else { + v.resize(len, fill); + } + v + } + None => vec![fill; len], + } + } + + pub fn put(&self, v: Vec) { + if v.capacity() == 0 { + return; + } + let mut b = self.bufs.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if b.len() < self.max { + b.push(v); + } + } +} + +/// The decoder's buffer pools. +pub struct Pools { + pub samples: Pool, + /// Frame bands (many small buffers per frame). + pub bands: Pool, + pub mi: Pool, +} + +impl Default for Pools { + fn default() -> Self { + Pools { samples: Pool::default(), bands: Pool::with_max(1024), mi: Pool::default() } + } +} diff --git a/crates/video/src/vp9/decode/header.rs b/crates/video/src/vp9/decode/header.rs new file mode 100644 index 000000000..fccd25d52 --- /dev/null +++ b/crates/video/src/vp9/decode/header.rs @@ -0,0 +1,758 @@ +//! Uncompressed header (6.2) and compressed header (6.3) parsing, plus the state that persists +//! between frames (loop filter deltas, segmentation parameters, colour configuration). + +use crate::vp9::decode::boolcoder::BoolDecoder; +use crate::vp9::decode::error::{Error, Result, ensure}; +use crate::vp9::decode::probs::FrameContext; +use crate::vp9::decode::tables::{ + ALTREF_FRAME, GOLDEN_FRAME, INV_MAP_TABLE, LAST_FRAME, LITERAL_TO_TYPE, MAX_SEGMENTS, MAX_TILE_WIDTH_B64, MIN_TILE_WIDTH_B64, + SEGMENTATION_FEATURE_BITS, SEGMENTATION_FEATURE_SIGNED, SWITCHABLE, TX_MODE_SELECT, TX_MODE_TO_BIGGEST_TX_SIZE, +}; +use craft_media::bitstream::BitReader; + +pub const KEY_FRAME: u8 = 0; + +/// Loop filter parameters (6.2.8); the deltas persist across frames. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LoopFilterParams { + pub level: u8, + pub sharpness: u8, + pub delta_enabled: bool, + pub ref_deltas: [i8; 4], + pub mode_deltas: [i8; 2], +} + +/// Segmentation parameters (6.2.11); feature data persists across frames. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Segmentation { + pub enabled: bool, + pub update_map: bool, + pub tree_probs: [u8; 7], + pub pred_probs: [u8; 3], + pub temporal_update: bool, + pub abs_or_delta_update: bool, + pub feature_enabled: [[bool; 4]; 8], + pub feature_data: [[i16; 4]; 8], +} + +impl Segmentation { + #[inline] + pub fn feature_active(&self, segment_id: u8, feature: usize) -> bool { + self.enabled && self.feature_enabled[segment_id as usize][feature] + } +} + +/// Colour configuration (6.2.2); persists for inter frames. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ColorConfig { + pub bit_depth: u8, + pub color_space: u8, + pub color_range: bool, + pub subsampling_x: bool, + pub subsampling_y: bool, +} + +impl Default for ColorConfig { + fn default() -> Self { + ColorConfig { bit_depth: 8, color_space: 0, color_range: false, subsampling_x: true, subsampling_y: true } + } +} + +/// Everything read from the headers of one frame. +#[derive(Clone, Debug, Default)] +pub struct FrameHeader { + pub profile: u8, + pub show_existing_frame: bool, + pub frame_to_show_map_idx: u8, + pub frame_type: u8, + pub show_frame: bool, + pub error_resilient_mode: bool, + pub intra_only: bool, + pub reset_frame_context: u8, + pub color: ColorConfig, + pub refresh_frame_flags: u8, + pub ref_frame_idx: [u8; 3], + /// Indexed by reference frame (LAST_FRAME..ALTREF_FRAME); index 0 unused. + pub ref_frame_sign_bias: [bool; 4], + pub width: u32, + pub height: u32, + pub render_width: u32, + pub render_height: u32, + pub allow_high_precision_mv: bool, + pub interp_filter: u8, + pub refresh_frame_context: bool, + pub frame_parallel_decoding_mode: bool, + pub frame_context_idx: u8, + pub base_q_idx: u8, + pub delta_q_y_dc: i8, + pub delta_q_uv_dc: i8, + pub delta_q_uv_ac: i8, + pub lossless: bool, + pub tile_cols_log2: u32, + pub tile_rows_log2: u32, + pub header_size_in_bytes: u32, + /// Size of the uncompressed header in bytes. + pub uncompressed_size: usize, + // Compressed header. + pub tx_mode: u8, + pub reference_mode: u8, + pub comp_fixed_ref: i8, + pub comp_var_ref: [i8; 2], + // Derived. + pub mi_cols: u32, + pub mi_rows: u32, + pub sb64_cols: u32, + pub sb64_rows: u32, + pub frame_is_intra: bool, + /// The frame size was taken from reference `found_ref` (for statistics). + pub size_from_ref: bool, +} + +/// Reference slot information needed while parsing (frame_size_with_refs). +#[derive(Clone, Copy, Debug)] +pub struct RefInfo { + pub width: u32, + pub height: u32, +} + +/// State carried between frames that header parsing reads and updates. +pub struct HeaderState { + pub contexts: [FrameContext; 4], + pub lf: LoopFilterParams, + pub seg: Segmentation, + pub color: ColorConfig, + pub last_frame_type: u8, + pub frame_type: u8, + /// Set by setup_past_independence; cleared once consumed by the decoder (PrevSegmentIds reset). + pub reset_segment_map: bool, +} + +impl Default for HeaderState { + fn default() -> Self { + let fc = FrameContext::default(); + HeaderState { + contexts: [fc.clone(), fc.clone(), fc.clone(), fc], + lf: LoopFilterParams { ref_deltas: [1, 0, -1, -1], delta_enabled: true, ..Default::default() }, + seg: Segmentation::default(), + color: ColorConfig::default(), + last_frame_type: KEY_FRAME, + frame_type: KEY_FRAME, + reset_segment_map: false, + } + } +} + +fn read_s(r: &mut BitReader, bits: u32) -> Result { + let v = r.read_bits(bits)? as i32; + Ok(if r.read_flag()? { -v } else { v }) +} + +fn read_delta_q(r: &mut BitReader) -> Result { + Ok(if r.read_flag()? { read_s(r, 4)? as i8 } else { 0 }) +} + +fn read_prob(r: &mut BitReader) -> Result { + Ok(if r.read_flag()? { r.read_bits(8)? as u8 } else { 255 }) +} + +fn frame_sync_code(r: &mut BitReader) -> Result<()> { + let (a, b, c) = (r.read_bits(8)?, r.read_bits(8)?, r.read_bits(8)?); + ensure!(a == 0x49 && b == 0x83 && c == 0x42, "invalid frame sync code"); + Ok(()) +} + +fn color_config(r: &mut BitReader, profile: u8) -> Result { + let bit_depth = if profile >= 2 { if r.read_flag()? { 12 } else { 10 } } else { 8 }; + let color_space = r.read_bits(3)? as u8; + let mut c = ColorConfig { bit_depth, color_space, color_range: true, subsampling_x: true, subsampling_y: true }; + if color_space != 7 { + c.color_range = r.read_flag()?; + if profile == 1 || profile == 3 { + c.subsampling_x = r.read_flag()?; + c.subsampling_y = r.read_flag()?; + ensure!(!(c.subsampling_x && c.subsampling_y), "4:2:0 subsampling in profile {profile}"); + ensure!(!r.read_flag()?, "reserved bit set in color config"); + } + } else { + ensure!(profile == 1 || profile == 3, "RGB color space in profile {profile}"); + c.subsampling_x = false; + c.subsampling_y = false; + ensure!(!r.read_flag()?, "reserved bit set in color config"); + } + Ok(c) +} + +fn frame_size(r: &mut BitReader, h: &mut FrameHeader) -> Result<()> { + h.width = r.read_bits(16)? + 1; + h.height = r.read_bits(16)? + 1; + Ok(()) +} + +fn render_size(r: &mut BitReader, h: &mut FrameHeader) -> Result<()> { + if r.read_flag()? { + h.render_width = r.read_bits(16)? + 1; + h.render_height = r.read_bits(16)? + 1; + } else { + h.render_width = h.width; + h.render_height = h.height; + } + Ok(()) +} + +fn compute_image_size(h: &mut FrameHeader) { + h.mi_cols = (h.width + 7) >> 3; + h.mi_rows = (h.height + 7) >> 3; + h.sb64_cols = (h.mi_cols + 7) >> 3; + h.sb64_rows = (h.mi_rows + 7) >> 3; +} + +/// setup_past_independence (7.2): resets segmentation features, loop filter deltas and the +/// probabilities of `fc`. +fn setup_past_independence(st: &mut HeaderState, fc: &mut FrameContext) { + st.seg.feature_data = [[0; 4]; 8]; + st.seg.feature_enabled = [[false; 4]; 8]; + st.seg.abs_or_delta_update = false; + st.lf.delta_enabled = true; + st.lf.ref_deltas = [1, 0, -1, -1]; + st.lf.mode_deltas = [0, 0]; + st.reset_segment_map = true; + *fc = FrameContext::default(); +} + +/// Intra-only (non-key) frame header tail (6.2): sync code, colour config, refresh flags, sizes. +fn intra_only_header(r: &mut BitReader, h: &mut FrameHeader) -> Result<()> { + frame_sync_code(r)?; + h.color = if h.profile > 0 { + color_config(r, h.profile)? + } else { + ColorConfig { bit_depth: 8, color_space: 1, color_range: false, subsampling_x: true, subsampling_y: true } + }; + h.refresh_frame_flags = r.read_bits(8)? as u8; + frame_size(r, h)?; + render_size(r, h) +} + +/// frame_size_with_refs (6.2.5): the size is copied from the first flagged reference, or read explicitly. +fn frame_size_with_refs(r: &mut BitReader, h: &mut FrameHeader, refs: &[Option; 8]) -> Result<()> { + let mut found = false; + for i in 0..3 { + if r.read_flag()? { + let idx = h.ref_frame_idx[i] as usize; + let Some(info) = refs[idx] else { + return Err(Error::MissingReference(format!("frame size from empty slot {idx}"))); + }; + h.width = info.width; + h.height = info.height; + found = true; + break; + } + } + if !found { + frame_size(r, h)?; + } + h.size_from_ref = found; + Ok(()) +} + +/// Inter frame header tail (6.2): reference selection, sizes, motion vector precision and filter. +fn inter_frame_header(r: &mut BitReader, h: &mut FrameHeader, st: &HeaderState, refs: &[Option; 8]) -> Result<()> { + h.color = st.color; + h.refresh_frame_flags = r.read_bits(8)? as u8; + for i in 0..3 { + h.ref_frame_idx[i] = r.read_bits(3)? as u8; + h.ref_frame_sign_bias[LAST_FRAME as usize + i] = r.read_flag()?; + } + frame_size_with_refs(r, h, refs)?; + render_size(r, h)?; + h.allow_high_precision_mv = r.read_flag()?; + // read_interpolation_filter (6.2.7) + h.interp_filter = if r.read_flag()? { SWITCHABLE } else { LITERAL_TO_TYPE[r.read_bits(2)? as usize] }; + Ok(()) +} + +/// Resets the probability contexts for intra / error resilient frames (7.2). +fn reset_frame_contexts(st: &mut HeaderState, h: &mut FrameHeader) { + let mut fc = FrameContext::default(); + setup_past_independence(st, &mut fc); + if h.frame_type == KEY_FRAME || h.error_resilient_mode || h.reset_frame_context == 3 { + for c in st.contexts.iter_mut() { + *c = fc.clone(); + } + } else if h.reset_frame_context == 2 { + st.contexts[h.frame_context_idx as usize] = fc.clone(); + } + h.frame_context_idx = 0; +} + +/// loop_filter_params (6.2.8). +fn read_loop_filter_params(r: &mut BitReader, lf: &mut LoopFilterParams) -> Result<()> { + lf.level = r.read_bits(6)? as u8; + lf.sharpness = r.read_bits(3)? as u8; + lf.delta_enabled = r.read_flag()?; + if lf.delta_enabled && r.read_flag()? { + for i in 0..4 { + if r.read_flag()? { + lf.ref_deltas[i] = read_s(r, 6)? as i8; + } + } + for i in 0..2 { + if r.read_flag()? { + lf.mode_deltas[i] = read_s(r, 6)? as i8; + } + } + } + Ok(()) +} + +/// segmentation_params (6.2.11). +fn read_segmentation_params(r: &mut BitReader, seg: &mut Segmentation) -> Result<()> { + seg.enabled = r.read_flag()?; + seg.update_map = false; + seg.temporal_update = false; + if !seg.enabled { + return Ok(()); + } + seg.update_map = r.read_flag()?; + if seg.update_map { + for i in 0..7 { + seg.tree_probs[i] = read_prob(r)?; + } + seg.temporal_update = r.read_flag()?; + for i in 0..3 { + seg.pred_probs[i] = if seg.temporal_update { read_prob(r)? } else { 255 }; + } + } + if r.read_flag()? { + seg.abs_or_delta_update = r.read_flag()?; + for i in 0..MAX_SEGMENTS { + for j in 0..4 { + let mut value = 0i32; + let enabled = r.read_flag()?; + seg.feature_enabled[i][j] = enabled; + if enabled { + value = r.read_bits(SEGMENTATION_FEATURE_BITS[j])? as i32; + if SEGMENTATION_FEATURE_SIGNED[j] && r.read_flag()? { + value = -value; + } + } + seg.feature_data[i][j] = value as i16; + } + } + } + Ok(()) +} + +/// tile_info (6.2.13). +fn read_tile_info(r: &mut BitReader, h: &mut FrameHeader) -> Result<()> { + let mut min_log2 = 0; + while (MAX_TILE_WIDTH_B64 << min_log2) < h.sb64_cols { + min_log2 += 1; + } + let mut max_log2 = 1; + while (h.sb64_cols >> max_log2) >= MIN_TILE_WIDTH_B64 { + max_log2 += 1; + } + max_log2 -= 1; + h.tile_cols_log2 = min_log2; + while h.tile_cols_log2 < max_log2 { + if r.read_flag()? { + h.tile_cols_log2 += 1; + } else { + break; + } + } + h.tile_rows_log2 = r.read_bits(1)?; + if h.tile_rows_log2 == 1 { + h.tile_rows_log2 += r.read_bits(1)?; + } + Ok(()) +} + +/// Parse the uncompressed header (6.2). `refs` gives the sizes of the eight reference slots. +/// Updates the persistent state (probability contexts on reset, loop filter deltas, +/// segmentation, colour configuration). +pub fn parse_uncompressed(data: &[u8], st: &mut HeaderState, refs: &[Option; 8]) -> Result { + let mut r = BitReader::new(data); + let mut h = FrameHeader::default(); + ensure!(r.read_bits(2)? == 2, "invalid frame marker"); + let lo = r.read_bits(1)? as u8; + let hi = r.read_bits(1)? as u8; + h.profile = (hi << 1) | lo; + if h.profile == 3 { + ensure!(r.read_bits(1)? == 0, "reserved bit set after profile"); + } + h.show_existing_frame = r.read_flag()?; + if h.show_existing_frame { + h.frame_to_show_map_idx = r.read_bits(3)? as u8; + h.show_frame = true; + h.uncompressed_size = r.position().div_ceil(8); + return Ok(h); + } + st.last_frame_type = st.frame_type; + h.frame_type = r.read_bits(1)? as u8; + h.show_frame = r.read_flag()?; + h.error_resilient_mode = r.read_flag()?; + if h.frame_type == KEY_FRAME { + frame_sync_code(&mut r)?; + h.color = color_config(&mut r, h.profile)?; + frame_size(&mut r, &mut h)?; + render_size(&mut r, &mut h)?; + h.refresh_frame_flags = 0xff; + h.frame_is_intra = true; + } else { + h.intra_only = if h.show_frame { false } else { r.read_flag()? }; + h.frame_is_intra = h.intra_only; + h.reset_frame_context = if h.error_resilient_mode { 0 } else { r.read_bits(2)? as u8 }; + if h.intra_only { + intra_only_header(&mut r, &mut h)?; + } else { + inter_frame_header(&mut r, &mut h, st, refs)?; + } + } + compute_image_size(&mut h); + if !h.error_resilient_mode { + h.refresh_frame_context = r.read_flag()?; + h.frame_parallel_decoding_mode = r.read_flag()?; + } else { + h.refresh_frame_context = false; + h.frame_parallel_decoding_mode = true; + } + h.frame_context_idx = r.read_bits(2)? as u8; + if h.frame_is_intra || h.error_resilient_mode { + reset_frame_contexts(st, &mut h); + } + read_loop_filter_params(&mut r, &mut st.lf)?; + // quantization_params (6.2.9) + h.base_q_idx = r.read_bits(8)? as u8; + h.delta_q_y_dc = read_delta_q(&mut r)?; + h.delta_q_uv_dc = read_delta_q(&mut r)?; + h.delta_q_uv_ac = read_delta_q(&mut r)?; + h.lossless = h.base_q_idx == 0 && h.delta_q_y_dc == 0 && h.delta_q_uv_dc == 0 && h.delta_q_uv_ac == 0; + read_segmentation_params(&mut r, &mut st.seg)?; + read_tile_info(&mut r, &mut h)?; + h.header_size_in_bytes = r.read_bits(16)?; + // trailing_bits + h.uncompressed_size = r.position().div_ceil(8); + if h.frame_is_intra { + st.color = h.color; + } + st.frame_type = h.frame_type; + ensure!(h.header_size_in_bytes > 0, "empty compressed header"); + Ok(h) +} + +/// diff_update_prob (6.3.3). +fn diff_update_prob(bd: &mut BoolDecoder, prob: &mut u8) { + if bd.read_bool(252) { + let delta = decode_term_subexp(bd); + *prob = inv_remap_prob(delta, *prob); + } +} + +fn decode_term_subexp(bd: &mut BoolDecoder) -> usize { + if bd.read_literal(1) == 0 { + return bd.read_literal(4) as usize; + } + if bd.read_literal(1) == 0 { + return bd.read_literal(4) as usize + 16; + } + if bd.read_literal(1) == 0 { + return bd.read_literal(5) as usize + 32; + } + let v = bd.read_literal(7) as usize; + if v < 65 { + return v + 64; + } + let bit = bd.read_literal(1) as usize; + (v << 1) - 1 + bit +} + +fn inv_recenter_nonneg(v: i32, m: i32) -> i32 { + if v > 2 * m { + v + } else if v & 1 != 0 { + m - ((v + 1) >> 1) + } else { + m + (v >> 1) + } +} + +fn inv_remap_prob(delta: usize, prob: u8) -> u8 { + let v = INV_MAP_TABLE[delta.min(254)] as i32; + let m = prob as i32 - 1; + let r = if (m << 1) <= 255 { 1 + inv_recenter_nonneg(v, m) } else { 255 - inv_recenter_nonneg(v, 255 - 1 - m) }; + r.clamp(1, 255) as u8 +} + +/// update_mv_prob (6.3.17). +fn update_mv_prob(bd: &mut BoolDecoder, prob: &mut u8) { + if bd.read_bool(252) { + *prob = ((bd.read_literal(7) << 1) | 1) as u8; + } +} + +/// tx_mode_probs (6.3.2): per-size transform probability updates for `TX_MODE_SELECT`. +fn read_tx_mode_probs(bd: &mut BoolDecoder, fc: &mut FrameContext) { + for i in 0..2 { + diff_update_prob(bd, &mut fc.tx[1][i][0]); + } + for i in 0..2 { + for j in 0..2 { + diff_update_prob(bd, &mut fc.tx[2][i][j]); + } + } + for i in 0..2 { + for j in 0..3 { + diff_update_prob(bd, &mut fc.tx[3][i][j]); + } + } +} + +/// read_coef_probs (6.3.7). +fn read_coef_probs(bd: &mut BoolDecoder, h: &FrameHeader, fc: &mut FrameContext) { + let max_tx = TX_MODE_TO_BIGGEST_TX_SIZE[h.tx_mode as usize] as usize; + for tx in 0..=max_tx { + if bd.read_literal(1) == 1 { + for i in 0..2 { + for j in 0..2 { + for k in 0..6 { + let max_l = if k == 0 { 3 } else { 6 }; + for l in 0..max_l { + for m in 0..3 { + diff_update_prob(bd, &mut fc.coef[tx][i][j][k][l][m]); + } + } + } + } + } + } + } +} + +/// frame_reference_mode (6.3.12) including setup_compound_reference_mode (6.3.18). +fn read_frame_reference_mode(bd: &mut BoolDecoder, h: &mut FrameHeader) { + let bias = h.ref_frame_sign_bias; + let compound_allowed = bias[2] != bias[1] || bias[3] != bias[1]; + h.reference_mode = 0; + if !compound_allowed { + return; + } + if bd.read_literal(1) == 1 { + h.reference_mode = if bd.read_literal(1) == 1 { 2 } else { 1 }; + } + if bias[LAST_FRAME as usize] == bias[GOLDEN_FRAME as usize] { + h.comp_fixed_ref = ALTREF_FRAME; + h.comp_var_ref = [LAST_FRAME, GOLDEN_FRAME]; + } else if bias[LAST_FRAME as usize] == bias[ALTREF_FRAME as usize] { + h.comp_fixed_ref = GOLDEN_FRAME; + h.comp_var_ref = [LAST_FRAME, ALTREF_FRAME]; + } else { + h.comp_fixed_ref = LAST_FRAME; + h.comp_var_ref = [GOLDEN_FRAME, ALTREF_FRAME]; + } +} + +/// frame_reference_mode_probs (6.3.13). +fn read_frame_reference_mode_probs(bd: &mut BoolDecoder, reference_mode: u8, fc: &mut FrameContext) { + if reference_mode == 2 { + for i in 0..5 { + diff_update_prob(bd, &mut fc.comp_mode[i]); + } + } + if reference_mode != 1 { + for i in 0..5 { + diff_update_prob(bd, &mut fc.single_ref[i][0]); + diff_update_prob(bd, &mut fc.single_ref[i][1]); + } + } + if reference_mode != 0 { + for i in 0..5 { + diff_update_prob(bd, &mut fc.comp_ref[i]); + } + } +} + +/// mv_probs (6.3.16). +fn read_mv_probs(bd: &mut BoolDecoder, allow_high_precision_mv: bool, fc: &mut FrameContext) { + for j in 0..3 { + update_mv_prob(bd, &mut fc.mv_joint[j]); + } + for i in 0..2 { + update_mv_prob(bd, &mut fc.mv_sign[i]); + for j in 0..10 { + update_mv_prob(bd, &mut fc.mv_class[i][j]); + } + update_mv_prob(bd, &mut fc.mv_class0_bit[i]); + for j in 0..10 { + update_mv_prob(bd, &mut fc.mv_bits[i][j]); + } + } + for i in 0..2 { + for j in 0..2 { + for k in 0..3 { + update_mv_prob(bd, &mut fc.mv_class0_fr[i][j][k]); + } + } + for k in 0..3 { + update_mv_prob(bd, &mut fc.mv_fr[i][k]); + } + } + if allow_high_precision_mv { + for i in 0..2 { + update_mv_prob(bd, &mut fc.mv_class0_hp[i]); + update_mv_prob(bd, &mut fc.mv_hp[i]); + } + } +} + +/// Probability updates that only inter frames carry (6.3.8 to 6.3.16). +fn read_inter_frame_probs(bd: &mut BoolDecoder, h: &mut FrameHeader, fc: &mut FrameContext) { + for i in 0..7 { + for j in 0..3 { + diff_update_prob(bd, &mut fc.inter_mode[i][j]); + } + } + if h.interp_filter == SWITCHABLE { + for j in 0..4 { + for i in 0..2 { + diff_update_prob(bd, &mut fc.interp_filter[j][i]); + } + } + } + for i in 0..4 { + diff_update_prob(bd, &mut fc.is_inter[i]); + } + read_frame_reference_mode(bd, h); + read_frame_reference_mode_probs(bd, h.reference_mode, fc); + for i in 0..4 { + for j in 0..9 { + diff_update_prob(bd, &mut fc.y_mode[i][j]); + } + } + for i in 0..16 { + for j in 0..3 { + diff_update_prob(bd, &mut fc.partition[i][j]); + } + } + read_mv_probs(bd, h.allow_high_precision_mv, fc); +} + +/// Parse the compressed header (6.3) into `h` and the probabilities `fc`. +pub fn parse_compressed(data: &[u8], h: &mut FrameHeader, fc: &mut FrameContext) -> Result<()> { + let mut bd = BoolDecoder::new(data); + // read_tx_mode (6.3.1) + h.tx_mode = if h.lossless { + 0 + } else { + let mut m = bd.read_literal(2) as u8; + if m == 3 { + m += bd.read_literal(1) as u8; + } + m + }; + if h.tx_mode == TX_MODE_SELECT { + read_tx_mode_probs(&mut bd, fc); + } + read_coef_probs(&mut bd, h, fc); + for i in 0..3 { + diff_update_prob(&mut bd, &mut fc.skip[i]); + } + if !h.frame_is_intra { + read_inter_frame_probs(&mut bd, h, fc); + } + Ok(()) +} + +/// Superframe index parsing (Annex B): the frames of a chunk. +pub fn split_superframe(data: &[u8]) -> Vec<&[u8]> { + if let Some(&last) = data.last() + && last & 0xe0 == 0xc0 + { + let sz_bytes = ((last >> 3) & 3) as usize + 1; + let num = (last & 7) as usize + 1; + let index_size = 2 + num * sz_bytes; + if data.len() >= index_size && data[data.len() - index_size] == last { + let mut frames = Vec::with_capacity(num); + let mut pos = 0usize; + let mut p = data.len() - index_size + 1; + let end = data.len() - index_size; + for _ in 0..num { + let mut sz = 0usize; + for b in 0..sz_bytes { + sz |= (data[p + b] as usize) << (8 * b); + } + p += sz_bytes; + let Some(next) = pos.checked_add(sz).filter(|&next| next <= end) else { + frames.push(&data[pos..end]); + return frames; + }; + frames.push(&data[pos..next]); + pos = next; + } + return frames; + } + } + vec![data] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hostile_lengths_superframe_size_does_not_wrap() { + // #2801: the second frame's size overflowed usize after the first byte on wasm32. + let marker = 0xd9; // two frames, four bytes per size + let data = [0x88, 0x89, marker, 1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, marker]; + let result = std::panic::catch_unwind(|| split_superframe(&data)); + // Preserve the existing truncated-superframe policy: return the remaining payload. + assert_eq!(result.expect("superframe splitting must not panic"), vec![&data[..1], &data[1..2]]); + } + + #[test] + fn hostile_lengths_superframe_mutations_and_truncations() { + for width in 1..=4 { + let marker = 0xc1 | ((width - 1) << 3) as u8; + let mut data = vec![0x88, 0x89, marker]; + data.extend_from_slice(&1u32.to_le_bytes()[..width]); + data.extend_from_slice(&1u32.to_le_bytes()[..width]); + data.push(marker); + assert_eq!(split_superframe(&data), vec![&data[..1], &data[1..2]]); + for end in 0..=data.len() { + std::panic::catch_unwind(|| split_superframe(&data[..end])).expect("truncated superframe must not panic"); + } + for index in 0..data.len() { + for bit in 0..8 { + let mut mutated = data.clone(); + mutated[index] ^= 1 << bit; + std::panic::catch_unwind(|| split_superframe(&mutated)).expect("mutated superframe must not panic"); + } + } + } + } + + #[test] + fn superframe_index() { + // Two frames of 3 and 2 bytes, 1 byte per size. + let mut d = vec![1u8, 2, 3, 4, 5]; + let marker = 0xc0 | 1; // 2 frames, 1 byte sizes + d.extend([marker, 3, 2, marker]); + let f = split_superframe(&d); + assert_eq!(f, vec![&[1u8, 2, 3][..], &[4u8, 5][..]]); + // Not a superframe: last byte is a marker but the first index byte does not match. + let d2 = vec![9u8, 9, 0xc1]; + assert_eq!(split_superframe(&d2), vec![&d2[..]]); + } + + #[test] + fn inv_remap_is_bijective_per_prob() { + for p in 1..=255u8 { + let mut seen = std::collections::HashSet::new(); + for d in 0..254 { + seen.insert(inv_remap_prob(d, p)); + } + // Every other probability value is reachable. + assert!(seen.len() >= 253, "p {p}: {}", seen.len()); + } + } +} diff --git a/crates/video/src/vp9/decode/inter.rs b/crates/video/src/vp9/decode/inter.rs new file mode 100644 index 000000000..743f892a8 --- /dev/null +++ b/crates/video/src/vp9/decode/inter.rs @@ -0,0 +1,394 @@ +//! Block inter prediction process (8.5.2.4): separable 8-tap sub-sample interpolation with edge +//! clamping, for unscaled (step 16) and scaled references. + +use crate::vp9::decode::frame::Frame; +use crate::vp9::decode::tables::SUBPEL_FILTERS; + +/// Filter taps of `filter` (0..3) at sub-sample position `frac` (0..15). +#[inline(always)] +fn taps(filter: u8, frac: usize) -> &'static [i16] { + let o = (filter as usize * 16 + frac) * 8; + &SUBPEL_FILTERS[o..o + 8] +} + +/// Plane `plane` of a reference frame, valid for x <= last_x, y <= last_y. Rows are read through +/// [`Frame::row`] / [`Frame::span`], which wait until the reference has published them. +pub struct RefPlane<'a> { + pub frame: &'a Frame, + pub plane: usize, + pub last_x: i32, + pub last_y: i32, +} + +impl RefPlane<'_> { + #[inline] + fn row(&self, y: usize) -> &[u16] { + self.frame.row(self.plane, y) + } +} + +/// Geometry and filtering parameters of one motion-compensated block. +#[derive(Clone, Copy, Debug)] +pub struct McBlock { + /// Top-left sample position in 1/16 sample units of the reference. + pub x: i32, + pub y: i32, + /// Step per output sample in 1/16 units (16 = unscaled). + pub step_x: i32, + pub step_y: i32, + pub w: usize, + pub h: usize, + pub filter: u8, + pub bit_depth: u8, +} + +/// Predict a `w` x `h` block whose top-left sample is at (`x`, `y`) in 1/16 sample units of the +/// reference, stepping `step_x` / `step_y` (16 = unscaled). Output samples go to `out` (stride +/// `w`). `tmp` must hold at least `(h * step_y / 16 + 8) * w` entries, `win` at least +/// `(w + 7) * (h + 7)` (edge-clamped source window). +pub fn predict(r: &RefPlane, blk: &McBlock, out: &mut [u16], tmp: &mut [u16], win: &mut [u16]) { + if blk.step_x == 16 && blk.step_y == 16 { + predict_unscaled(r, blk, out, tmp, win); + } else { + predict_scaled(r, blk, out, tmp); + } +} + +/// Copy the (w + 7) x (h + 7) source window starting at (x0 - 3, y0 - 3) with edge clamping. +fn fetch_window(r: &RefPlane, x0: i32, y0: i32, w: usize, h: usize, win: &mut [u16]) -> usize { + let ws = w + 7; + for row in 0..h + 7 { + let yy = (y0 - 3 + row as i32).clamp(0, r.last_y) as usize; + let src = r.row(yy); + let dst = &mut win[row * ws..row * ws + ws]; + let xs = x0 - 3; + if xs >= 0 && xs + ws as i32 - 1 <= r.last_x { + dst.copy_from_slice(&src[xs as usize..xs as usize + ws]); + } else { + for (c, d) in dst.iter_mut().enumerate() { + *d = src[(xs + c as i32).clamp(0, r.last_x) as usize]; + } + } + } + ws +} + +fn predict_unscaled(r: &RefPlane, blk: &McBlock, out: &mut [u16], tmp: &mut [u16], win: &mut [u16]) { + let McBlock { x, y, w, h, filter, bit_depth, .. } = *blk; + let (x0, y0) = (x >> 4, y >> 4); + let (fx, fy) = ((x & 15) as usize, (y & 15) as usize); + let max = (1i32 << bit_depth) - 1; + let inside = x0 - 3 >= 0 && y0 - 3 >= 0 && x0 + w as i32 + 4 <= r.last_x && y0 + h as i32 + 4 <= r.last_y; + // Source view with origin at (x0 - 3, y0 - 3): the reference band itself when the window + // lies inside the picture and inside one band. + let span = if inside { r.frame.span(r.plane, (y0 - 3) as usize, (y0 + h as i32 + 5) as usize) } else { None }; + if inside && span.is_none() && predict_two_bands(r, x0, y0, w, h, fx, fy, filter, max, out, tmp) { + return; + } + let (src, ss, so): (&[u16], usize, usize) = match span { + Some((data, o)) => (data, r.frame.strides[r.plane], o + (x0 - 3) as usize), + None => { + let ws = fetch_window(r, x0, y0, w, h, win); + (&win[..], ws, 0) + } + }; + match (fx, fy) { + (0, 0) => { + for i in 0..h { + let s = so + (i + 3) * ss + 3; + out[i * w..i * w + w].copy_from_slice(&src[s..s + w]); + } + } + (_, 0) => { + let f = taps(filter, fx); + for i in 0..h { + filter_h(&src[so + (i + 3) * ss..], f, max, &mut out[i * w..i * w + w]); + } + } + (0, _) => { + let f = taps(filter, fy); + for i in 0..h { + filter_v(&src[so + i * ss + 3..], ss, f, max, &mut out[i * w..i * w + w]); + } + } + _ => { + let fh = taps(filter, fx); + let fv = taps(filter, fy); + for i in 0..h + 7 { + filter_h(&src[so + i * ss..], fh, max, &mut tmp[i * w..i * w + w]); + } + for i in 0..h { + filter_v(&tmp[i * w..], w, fv, max, &mut out[i * w..i * w + w]); + } + } + } +} + +/// [`predict_unscaled`] for a window inside the picture whose rows lie in two consecutive bands: +/// rows are read from the bands directly instead of being copied into a window first. False +/// (nothing written) if the window spans more than two bands. +#[allow(clippy::too_many_arguments)] +fn predict_two_bands( + r: &RefPlane, + x0: i32, + y0: i32, + w: usize, + h: usize, + fx: usize, + fy: usize, + filter: u8, + max: i32, + out: &mut [u16], + tmp: &mut [u16], +) -> bool { + let f = r.frame; + let p = r.plane; + let bh = f.band_h[p]; + let ss = f.strides[p]; + let (ya, yb) = ((y0 - 3) as usize, (y0 + h as i32 + 4) as usize + 1); + let split = (ya / bh + 1) * bh; + if split >= yb { + return false; + } + let (Some((d0, o0)), Some((d1, o1))) = (f.span(p, ya, split), f.span(p, split, yb)) else { return false }; + let xo = (x0 - 3) as usize; + let row = |i: usize| -> &[u16] { + let y = ya + i; + if y < split { &d0[o0 + i * ss + xo..] } else { &d1[o1 + (y - split) * ss + xo..] } + }; + match (fx, fy) { + (0, 0) => { + for i in 0..h { + out[i * w..i * w + w].copy_from_slice(&row(i + 3)[3..3 + w]); + } + } + (_, 0) => { + let fh = taps(filter, fx); + for i in 0..h { + filter_h(row(i + 3), fh, max, &mut out[i * w..i * w + w]); + } + } + (0, _) => { + let fv = taps(filter, fy); + for i in 0..h { + filter_v_rows(std::array::from_fn(|t| &row(i + t)[3..]), fv, max, &mut out[i * w..i * w + w]); + } + } + _ => { + let fh = taps(filter, fx); + let fv = taps(filter, fy); + for i in 0..h + 7 { + filter_h(row(i), fh, max, &mut tmp[i * w..i * w + w]); + } + for i in 0..h { + filter_v(&tmp[i * w..], w, fv, max, &mut out[i * w..i * w + w]); + } + } + } + true +} + +/// Horizontal 8-tap filter of one row: `o[j]` from `s[j..j + 8]`. The taps are the outer loop +/// so that the row vectorizes (same sums, same order of the exact integer additions). +#[inline(always)] +fn filter_h(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) { + match o.len() { + 4 => filter_h_n::<4>(s, f, max, o), + 8 => filter_h_n::<8>(s, f, max, o), + 16 => filter_h_n::<16>(s, f, max, o), + 32 => filter_h_n::<32>(s, f, max, o), + 64 => filter_h_n::<64>(s, f, max, o), + _ => { + for (j, d) in o.iter_mut().enumerate() { + let acc: i32 = (0..8).map(|t| f[t] as i32 * s[j + t] as i32).sum(); + *d = ((acc + 64) >> 7).clamp(0, max) as u16; + } + } + } +} + +#[inline(always)] +fn filter_h_n(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) { + let Some(o) = o.first_chunk_mut::() else { + return; + }; + if max == 255 { + let mut acc = [64u16; W]; + for t in 0..8 { + let c = f[t] as u16; + let Some(p) = s.get(t..).and_then(|s| s.first_chunk::()) else { + return; + }; + for j in 0..W { + acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j])); + } + } + narrow8(&acc, o); + return; + } + let mut acc = [64i32; W]; + for t in 0..8 { + let c = f[t] as i32; + let Some(p) = s.get(t..).and_then(|s| s.first_chunk::()) else { + return; + }; + for j in 0..W { + acc[j] += c * p[j] as i32; + } + } + for j in 0..W { + o[j] = (acc[j] >> 7).clamp(0, max) as u16; + } +} + +/// Round2(sum, 7) clipped to 8 bits, from 8-bit filter sums kept modulo 2^16 (8 lanes per +/// vector instead of 4). With 8-bit samples every sum of the VP9 filters (whose positive taps +/// add up to at most 182 and negative taps to at most -54) plus the rounding term 64 lies in +/// [-13706, 46474]: wrapped, the non-negative ones stay below 0xC000 and the negative ones +/// (which clip to 0) land above it. +#[inline(always)] +fn narrow8(acc: &[u16; W], o: &mut [u16; W]) { + for j in 0..W { + o[j] = if acc[j] < 0xC000 { (acc[j] >> 7).min(255) } else { 0 }; + } +} + +/// Vertical 8-tap filter of one row: `o[j]` from `s[j + t * ss]`, t = 0..8. +#[inline(always)] +fn filter_v(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) { + filter_v_rows(std::array::from_fn(|t| s.get(t * ss..).unwrap_or_default()), f, max, o) +} + +/// Vertical 8-tap filter of one row: `o[j]` from `rows[t][j]`, t = 0..8. +#[inline(always)] +fn filter_v_rows(rows: [&[u16]; 8], f: &[i16], max: i32, o: &mut [u16]) { + match o.len() { + 4 => filter_v_n::<4>(&rows, f, max, o), + 8 => filter_v_n::<8>(&rows, f, max, o), + 16 => filter_v_n::<16>(&rows, f, max, o), + 32 => filter_v_n::<32>(&rows, f, max, o), + 64 => filter_v_n::<64>(&rows, f, max, o), + _ => { + for (j, d) in o.iter_mut().enumerate() { + let acc: i32 = (0..8).map(|t| f[t] as i32 * rows[t][j] as i32).sum(); + *d = ((acc + 64) >> 7).clamp(0, max) as u16; + } + } + } +} + +#[inline(always)] +fn filter_v_n(rows: &[&[u16]; 8], f: &[i16], max: i32, o: &mut [u16]) { + let Some(o) = o.first_chunk_mut::() else { + return; + }; + if max == 255 { + let mut acc = [64u16; W]; + for t in 0..8 { + let c = f[t] as u16; + let Some(p) = rows[t].first_chunk::() else { + return; + }; + for j in 0..W { + acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j])); + } + } + narrow8(&acc, o); + return; + } + let mut acc = [64i32; W]; + for t in 0..8 { + let c = f[t] as i32; + let Some(p) = rows[t].first_chunk::() else { + return; + }; + for j in 0..W { + acc[j] += c * p[j] as i32; + } + } + for j in 0..W { + o[j] = (acc[j] >> 7).clamp(0, max) as u16; + } +} + +/// General process of 8.5.2.4 (any step). +fn predict_scaled(r: &RefPlane, blk: &McBlock, out: &mut [u16], tmp: &mut [u16]) { + let McBlock { x, y, step_x, step_y, w, h, filter, bit_depth } = *blk; + let max = (1i32 << bit_depth) - 1; + let ih = ((((h as i32 - 1) * step_y + 15) >> 4) + 8) as usize; + for row in 0..ih { + let yy = ((y >> 4) + row as i32 - 3).clamp(0, r.last_y) as usize; + let src = r.row(yy); + for c in 0..w { + let p = x + step_x * c as i32; + let f = taps(filter, (p & 15) as usize); + let mut acc = 0i32; + for t in 0..8 { + acc += f[t] as i32 * src[((p >> 4) + t as i32 - 3).clamp(0, r.last_x) as usize] as i32; + } + tmp[row * w + c] = ((acc + 64) >> 7).clamp(0, max) as u16; + } + } + for rr in 0..h { + let p = (y & 15) + step_y * rr as i32; + let f = taps(filter, (p & 15) as usize); + let base = (p >> 4) as usize; + for c in 0..w { + let mut acc = 0i32; + for t in 0..8 { + acc += f[t] as i32 * tmp[(base + t) * w + c] as i32; + } + out[rr * w + c] = ((acc + 64) >> 7).clamp(0, max) as u16; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scaled_path_with_unit_step_matches_unscaled() { + let (w, h) = (40usize, 30usize); + let data: Vec = (0..w * h).map(|i| ((i * 37 + (i / w) * 11) % 256) as u16).collect(); + let info = crate::vp9::decode::frame::FrameInfo { + width: w as u32, + height: h as u32, + ss_x: true, + ss_y: true, + bit_depth: 8, + color_space: 0, + color_range: false, + render_width: w as u32, + render_height: h as u32, + key: true, + intra_only: false, + }; + // Planes are allocated in whole superblocks: copy the test picture into a 64x64 plane. + let mut full = vec![0u16; 64 * 64]; + for y in 0..h { + full[y * 64..y * 64 + w].copy_from_slice(&data[y * w..y * w + w]); + } + let chroma = vec![0u16; 32 * 32]; + let frame = Frame::from_planes(info, [&full, &chroma, &chroma]); + let r = RefPlane { frame: &frame, plane: 0, last_x: w as i32 - 1, last_y: h as i32 - 1 }; + let mut tmp = vec![0u16; 80 * 80]; + for filter in 0..4u8 { + for &(x, y) in &[(0, 0), (5, 3), (16 * 7 + 5, 16 * 9 + 11), (-40, -3), (16 * 36 + 1, 16 * 25 + 15), (16 * 10, 16 * 10 + 8)] { + for &(bw, bh) in &[(4usize, 4usize), (8, 8), (16, 8)] { + let mut a = vec![0u16; bw * bh]; + let mut b = vec![0u16; bw * bh]; + predict_unscaled( + &r, + &McBlock { x, y, step_x: 16, step_y: 16, w: bw, h: bh, filter, bit_depth: 8 }, + &mut a, + &mut tmp, + &mut [0u16; 71 * 71], + ); + predict_scaled(&r, &McBlock { x, y, step_x: 16, step_y: 16, w: bw, h: bh, filter, bit_depth: 8 }, &mut b, &mut tmp); + assert_eq!(a, b, "filter {filter} pos ({x},{y}) {bw}x{bh}"); + } + } + } + } +} diff --git a/crates/video/src/vp9/decode/intra.rs b/crates/video/src/vp9/decode/intra.rs new file mode 100644 index 000000000..dcc540708 --- /dev/null +++ b/crates/video/src/vp9/decode/intra.rs @@ -0,0 +1,230 @@ +//! Intra prediction process (8.5.1). + +use crate::vp9::decode::tables::{D45_PRED, D63_PRED, D117_PRED, D135_PRED, D153_PRED, D207_PRED, H_PRED, TM_PRED, V_PRED}; + +/// Edge availability and geometry of one intra-predicted transform block. +pub struct IntraEdge { + pub have_left: bool, + pub have_above: bool, + pub not_on_right: bool, + pub tx_size: u8, + /// Last valid column / row of the plane (in plane coordinates). + pub max_x: usize, + pub max_y: usize, + pub bit_depth: u8, +} + +/// Neighbouring samples of a transform block: `above[0]` is the top-left corner, +/// `above[1..=2*size]` the above row and `left[..size]` the left column. +struct Neighbours { + above: [i32; 65], + left: [i32; 32], +} + +impl Neighbours { + /// aboveRow[i] for `i` in `-1..2*size`. + fn a(&self, i: isize) -> i32 { + self.above[(i + 1) as usize] + } +} + +type Block = [[i32; 32]; 32]; + +fn r1(v: i32) -> i32 { + (v + 1) >> 1 +} + +fn r2(v: i32) -> i32 { + (v + 2) >> 2 +} + +/// Gathers (and pads, per 8.5.1.1) the above row, top-left corner and left column. +fn gather_neighbours(buf: &[u16], stride: usize, x_off: usize, x: usize, y: usize, e: &IntraEdge) -> Neighbours { + let size = 4usize << e.tx_size; + let base = 1i32 << (e.bit_depth - 1); + let at = |xx: usize, yy: usize| yy * stride + xx - x_off; + // aboveRow[-1..2*size) stored at above[0..2*size+1); leftCol[0..size). + let mut n = Neighbours { above: [0i32; 65], left: [0i32; 32] }; + if !e.have_above { + n.above[1..=2 * size].fill(base - 1); + } else { + let row = (y - 1) * stride; + for i in 0..size { + n.above[1 + i] = buf[row + (x + i).min(e.max_x) - x_off] as i32; + } + if e.not_on_right && e.tx_size == 0 { + for i in size..2 * size { + n.above[1 + i] = buf[row + (x + i).min(e.max_x) - x_off] as i32; + } + } else { + let v = n.above[size]; + n.above[1 + size..=2 * size].fill(v); + } + } + n.above[0] = if e.have_above && e.have_left { + buf[at((x - 1).min(e.max_x), y - 1)] as i32 + } else if e.have_above { + base + 1 + } else { + base - 1 + }; + if e.have_left { + for i in 0..size { + n.left[i] = buf[at(x - 1, (y + i).min(e.max_y))] as i32; + } + } else { + n.left[..size].fill(base + 1); + } + n +} + +fn pred_d207(n: &Neighbours, size: usize) -> Block { + let left = &n.left; + let mut p = [[0i32; 32]; 32]; + for j in 0..size { + p[size - 1][j] = left[size - 1]; + } + for i in 0..size - 1 { + p[i][0] = r1(left[i] + left[i + 1]); + } + for i in 0..size.saturating_sub(2) { + p[i][1] = r2(left[i] + 2 * left[i + 1] + left[i + 2]); + } + p[size - 2][1] = r2(left[size - 2] + 3 * left[size - 1]); + for j in 2..size { + for i in (0..size - 1).rev() { + p[i][j] = p[i + 1][j - 2]; + } + } + p +} + +fn pred_d117(n: &Neighbours, size: usize) -> Block { + let (left, a) = (&n.left, |i: isize| n.a(i)); + let mut p = [[0i32; 32]; 32]; + for j in 0..size { + p[0][j] = r1(a(j as isize - 1) + a(j as isize)); + } + p[1][0] = r2(left[0] + 2 * a(-1) + a(0)); + for j in 1..size { + p[1][j] = r2(a(j as isize - 2) + 2 * a(j as isize - 1) + a(j as isize)); + } + p[2][0] = r2(a(-1) + 2 * left[0] + left[1]); + for i in 3..size { + p[i][0] = r2(left[i - 3] + 2 * left[i - 2] + left[i - 1]); + } + for i in 2..size { + for j in 1..size { + p[i][j] = p[i - 2][j - 1]; + } + } + p +} + +fn pred_d135(n: &Neighbours, size: usize) -> Block { + let (left, a) = (&n.left, |i: isize| n.a(i)); + let mut p = [[0i32; 32]; 32]; + p[0][0] = r2(left[0] + 2 * a(-1) + a(0)); + for j in 1..size { + p[0][j] = r2(a(j as isize - 2) + 2 * a(j as isize - 1) + a(j as isize)); + } + p[1][0] = r2(a(-1) + 2 * left[0] + left[1]); + for i in 2..size { + p[i][0] = r2(left[i - 2] + 2 * left[i - 1] + left[i]); + } + for i in 1..size { + for j in 1..size { + p[i][j] = p[i - 1][j - 1]; + } + } + p +} + +fn pred_d153(n: &Neighbours, size: usize) -> Block { + let (left, a) = (&n.left, |i: isize| n.a(i)); + let mut p = [[0i32; 32]; 32]; + p[0][0] = r1(left[0] + a(-1)); + for i in 1..size { + p[i][0] = r1(left[i - 1] + left[i]); + } + p[0][1] = r2(left[0] + 2 * a(-1) + a(0)); + p[1][1] = r2(a(-1) + 2 * left[0] + left[1]); + for i in 2..size { + p[i][1] = r2(left[i - 2] + 2 * left[i - 1] + left[i]); + } + for j in 2..size { + p[0][j] = r2(a(j as isize - 3) + 2 * a(j as isize - 2) + a(j as isize - 1)); + } + for i in 1..size { + for j in 2..size { + p[i][j] = p[i - 1][j - 2]; + } + } + p +} + +/// DC prediction value, depending on which edges are available. +fn dc_value(n: &Neighbours, e: &IntraEdge) -> i32 { + let log2 = e.tx_size as usize + 2; + let size = 1usize << log2; + let left = n.left[..size].iter().sum::(); + let above = n.above[1..=size].iter().sum::(); + match (e.have_left, e.have_above) { + (true, true) => (left + above + size as i32) >> (log2 + 1), + (true, false) => (left + (1 << (log2 - 1))) >> log2, + (false, true) => (above + (1 << (log2 - 1))) >> log2, + (false, false) => 1i32 << (e.bit_depth - 1), + } +} + +/// Predict a `4 << tx_size` square block at (`x`, `y`) of a plane buffer. `buf` is addressed as +/// `buf[y * stride + x - x_off]` (the buffer may be a tile-column strip starting at column `x_off`). +pub fn predict(buf: &mut [u16], stride: usize, x_off: usize, x: usize, y: usize, mode: u8, e: &IntraEdge) { + let size = 4usize << e.tx_size; + let n = gather_neighbours(buf, stride, x_off, x, y, e); + let a = |i: isize| n.a(i); + let max = (1i32 << e.bit_depth) - 1; + let o = y * stride + x - x_off; + let mut put = |i: usize, j: usize, v: i32| buf[o + i * stride + j] = v as u16; + let mut fill = |f: &dyn Fn(usize, usize) -> i32| { + for i in 0..size { + for j in 0..size { + put(i, j, f(i, j)); + } + } + }; + match mode { + V_PRED => fill(&|_, j| a(j as isize)), + H_PRED => fill(&|i, _| n.left[i]), + D207_PRED => { + let p = pred_d207(&n, size); + fill(&|i, j| p[i][j]); + } + D45_PRED => fill(&|i, j| { + let k = (i + j) as isize; + if i + j + 2 < 2 * size { r2(a(k) + 2 * a(k + 1) + a(k + 2)) } else { a(2 * size as isize - 1) } + }), + D63_PRED => fill(&|i, j| { + let k = (i / 2 + j) as isize; + if i & 1 == 1 { r2(a(k) + 2 * a(k + 1) + a(k + 2)) } else { r1(a(k) + a(k + 1)) } + }), + D117_PRED => { + let p = pred_d117(&n, size); + fill(&|i, j| p[i][j]); + } + D135_PRED => { + let p = pred_d135(&n, size); + fill(&|i, j| p[i][j]); + } + D153_PRED => { + let p = pred_d153(&n, size); + fill(&|i, j| p[i][j]); + } + TM_PRED => fill(&|i, j| (a(j as isize) + n.left[i] - a(-1)).clamp(0, max)), + _ => { + // DC_PRED + let v = dc_value(&n, e); + fill(&|_, _| v); + } + } +} diff --git a/crates/video/src/vp9/decode/loopfilter.rs b/crates/video/src/vp9/decode/loopfilter.rs new file mode 100644 index 000000000..c36398a88 --- /dev/null +++ b/crates/video/src/vp9/decode/loopfilter.rs @@ -0,0 +1,378 @@ +//! Loop filter process (8.8). + +use crate::vp9::decode::frame::{MiGrid, MiInfo}; +use crate::vp9::decode::header::{LoopFilterParams, Segmentation}; +use crate::vp9::decode::tables::{ + BLOCK_8X8, BLOCK_16X16, INTRA_FRAME, MAX_LOOP_FILTER, MAX_TXSIZE, NEARESTMV, NEARMV, NEWMV, NUM_8X8_HIGH, NUM_8X8_WIDE, SEG_LVL_ALT_L, + SS_SIZE_LOOKUP, TX_4X4, TX_8X8, TX_16X16, +}; + +/// Frame-level loop filter setup (8.8.1) plus the per-level limits of 8.8.4. +pub struct LfFrame { + /// LvlLookup[segment_id][ref][modeType]. + pub lvl: [[[u8; 2]; 4]; 8], + /// (limit, blimit, thresh) per filter level. + pub limits: [(i32, i32, i32); 64], + pub mi_rows: usize, + pub mi_cols: usize, + pub ss_x: usize, + pub ss_y: usize, + pub bit_depth: u8, +} + +impl LfFrame { + pub fn new(lf: &LoopFilterParams, seg: &Segmentation, mi_rows: usize, mi_cols: usize, ss_x: bool, ss_y: bool, bit_depth: u8) -> LfFrame { + let mut lvl = [[[0u8; 2]; 4]; 8]; + let n_shift = lf.level >> 5; + for (seg_id, l) in lvl.iter_mut().enumerate() { + let mut lvl_seg = lf.level as i32; + if seg.feature_active(seg_id as u8, SEG_LVL_ALT_L) { + let d = seg.feature_data[seg_id][SEG_LVL_ALT_L] as i32; + lvl_seg = if seg.abs_or_delta_update { d } else { d + lf.level as i32 }; + lvl_seg = lvl_seg.clamp(0, MAX_LOOP_FILTER); + } + if !lf.delta_enabled { + *l = [[lvl_seg as u8; 2]; 4]; + } else { + let intra = lvl_seg + ((lf.ref_deltas[0] as i32) << n_shift); + l[0][0] = intra.clamp(0, MAX_LOOP_FILTER) as u8; + l[0][1] = l[0][0]; + for rf in 1..4 { + for mode in 0..2 { + let v = lvl_seg + ((lf.ref_deltas[rf] as i32) << n_shift) + ((lf.mode_deltas[mode] as i32) << n_shift); + l[rf][mode] = v.clamp(0, MAX_LOOP_FILTER) as u8; + } + } + } + } + let mut limits = [(0, 0, 0); 64]; + let sh = lf.sharpness as i32; + let shift = if sh > 4 { + 2 + } else if sh > 0 { + 1 + } else { + 0 + }; + for (l, e) in limits.iter_mut().enumerate() { + let l = l as i32; + let limit = if sh > 0 { (l >> shift).clamp(1, 9 - sh) } else { (l >> shift).max(1) }; + *e = (limit, 2 * (l + 2) + limit, l >> 4); + } + LfFrame { lvl, limits, mi_rows, mi_cols, ss_x: ss_x as usize, ss_y: ss_y as usize, bit_depth } + } + + fn level(&self, mi: &MiInfo) -> u8 { + let mode_type = matches!(mi.y_mode, NEARESTMV | NEARMV | NEWMV) as usize; + let rf = mi.ref_frame[0].max(0) as usize; + self.lvl[mi.seg_id as usize & 7][rf][mode_type] + } +} + +/// A mutable view of one plane: sample (x, y) of the plane is `data[(y - oy) * stride + x - ox]`. +pub struct PlaneView<'a> { + pub data: &'a mut [u16], + pub stride: usize, + pub ox: usize, + pub oy: usize, +} + +/// Loop filter one superblock (all planes, both passes) at (`row`, `col`) in MI units (8.8.2). +pub fn filter_superblock(views: &mut [PlaneView; 3], mi: &MiGrid, f: &LfFrame, row: usize, col: usize) { + for (plane, view) in views.iter_mut().enumerate() { + for pass in 0..2 { + filter_sb_plane(view, mi, f, plane, pass, row, col); + } + } +} + +fn filter_sb_plane(v: &mut PlaneView, mi: &MiGrid, f: &LfFrame, plane: usize, pass: usize, row: usize, col: usize) { + let (sub_x, sub_y) = if plane > 0 { (f.ss_x, f.ss_y) } else { (0, 0) }; + let (sub, edge_len) = if pass == 0 { (sub_x, 64 >> sub_y) } else { (sub_y, 64 >> sub_x) }; + let (mi_rows, mi_cols) = (f.mi_rows, f.mi_cols); + // Step between the samples across the edge. + let across = if pass == 0 { 1isize } else { v.stride as isize }; + for edge in 0..(16 >> sub) { + let mut i = 0; + while i < edge_len { + // Group of 8 samples sharing loopRow / loopCol. + let (x, y) = if pass == 0 { + (col * 8 + edge * (4 << sub_x), row * 8 + (i << sub_y)) + } else { + (col * 8 + (i << sub_x), row * 8 + edge * (4 << sub_y)) + }; + // Skip groups entirely off screen (the varying coordinate is re-checked per sample). + if x >= 8 * mi_cols || y >= 8 * mi_rows || (pass == 0 && x == 0) || (pass == 1 && y == 0) { + i += 8; + continue; + } + let loop_col = ((x >> 3) >> sub_x) << sub_x; + let loop_row = ((y >> 3) >> sub_y) << sub_y; + let m = mi.at(loop_row, loop_col); + let tx_sz = if plane > 0 { + if m.sb_size < BLOCK_8X8 { 0 } else { m.tx_size.min(MAX_TXSIZE[SS_SIZE_LOOKUP[m.sb_size as usize][f.ss_x][f.ss_y].min(12) as usize]) } + } else { + m.tx_size + }; + let sb_size = if sub == 0 { m.sb_size } else { m.sb_size.max(BLOCK_16X16) } as usize; + let is_intra = m.ref_frame[0] <= INTRA_FRAME; + // Block sizes are powers of two, so the modulo is a mask. + let is_block_edge = + if pass == 0 { x & (8 * NUM_8X8_WIDE[sb_size] as usize - 1) == 0 } else { y & (8 * NUM_8X8_HIGH[sb_size] as usize - 1) == 0 }; + let tx_ok = edge & ((1 << tx_sz) - 1) == 0 && (is_intra || !m.skip); + if !is_block_edge && !tx_ok { + i += 8; + continue; + } + let lvl = f.level(m); + if lvl == 0 { + i += 8; + continue; + } + let is32 = edge % 8 == 0; + let base_size = if tx_sz == TX_4X4 && is32 { TX_8X8 } else { tx_sz.min(TX_16X16) }; + let filter_size = if base_size == TX_16X16 + && ((pass == 0 && sub_x == 1 && (x >> 3) == mi_cols - 1) || (pass == 1 && sub_y == 1 && (y >> 3) == mi_rows - 1)) + { + TX_8X8 + } else { + base_size + }; + let (limit, blimit, thresh) = f.limits[lvl as usize]; + // Lanes of the group (the 8 samples along the edge) inside the MI grid. + let n = + if pass == 0 { (8 * mi_rows - y).div_ceil(1 << sub_y) } else { (8 * mi_cols - x).div_ceil(1 << sub_x) }.min(LANES).min(edge_len - i); + // A transform edge is not filtered in the last odd chroma column (8.8.2). + let odd_last = pass == 1 && sub_x == 1 && mi_cols & 1 == 1 && edge & 1 == 1; + let mut apply = [false; LANES]; + for (l, a) in apply.iter_mut().enumerate().take(n) { + let is_tx_edge = tx_ok && !(odd_last && x + (l << sub_x) + 8 >= mi_cols * 8); + *a = is_block_edge || is_tx_edge; + } + if apply.iter().any(|&a| a) { + let px = x >> sub_x; + let py = y >> sub_y; + let pos = (py - v.oy) * v.stride + px - v.ox; + let along = if pass == 0 { v.stride } else { 1 }; + let lim = Limits { apply, limit, blimit, thresh, filter_size, bit_depth: f.bit_depth }; + let (d, st) = (&mut *v.data, across as usize); + match (f.bit_depth <= 10, filter_size == TX_16X16, pass == 1) { + (true, false, false) => lanes16::filter_group::<4, false>(d, pos, along, st, &lim), + (true, false, true) => lanes16::filter_group::<4, true>(d, pos, along, st, &lim), + (true, true, false) => lanes16::filter_group::<8, false>(d, pos, along, st, &lim), + (true, true, true) => lanes16::filter_group::<8, true>(d, pos, along, st, &lim), + (false, false, false) => lanes32::filter_group::<4, false>(d, pos, along, st, &lim), + (false, false, true) => lanes32::filter_group::<4, true>(d, pos, along, st, &lim), + (false, true, false) => lanes32::filter_group::<8, false>(d, pos, along, st, &lim), + (false, true, true) => lanes32::filter_group::<8, true>(d, pos, along, st, &lim), + } + } + i += 8; + } + } +} + +/// Samples along an edge filtered together. +const LANES: usize = 8; + +/// Per-group filter parameters. +struct Limits { + apply: [bool; LANES], + limit: i32, + blimit: i32, + thresh: i32, + filter_size: u8, + bit_depth: u8, +} + +/// The group filter, instantiated for 16-bit lanes (bit depths 8 and 10: every intermediate, +/// including the 16-tap sums of at most 16 * 1023 + 8, fits) and 32-bit lanes (12-bit). +macro_rules! group_filter { + ($m:ident, $t:ty) => { + mod $m { + use super::{LANES, Limits}; + use crate::vp9::decode::tables::*; + type T = $t; + + /// Sample filtering process (8.8.5) for the up to 8 lanes of a group whose `apply` flag is set. + /// Lane `l` has its q0 sample at `pos + l * along`; samples across the edge are `step` apart + /// (`ROWS`: `along` is 1, i.e. a horizontal edge). `R` samples on each side are read (8 for + /// 16x16 filters, else 4). Every lane is computed with every filter and the result chosen per + /// lane (8.8.5.1 decides which), so the arithmetic is branch-free and vectorizes; unmodified + /// samples are written back unchanged. + #[inline(always)] + pub(super) fn filter_group(d: &mut [u16], pos: usize, along: usize, step: usize, g: &Limits) { + let Limits { apply, limit, blimit, thresh, filter_size, bit_depth } = *g; + let (limit, blimit, thresh) = (limit as T, blimit as T, thresh as T); + // v[8 + k][l] = sample at offset k across the edge in lane l. + let mut v = [[0 as T; LANES]; 16]; + let start = pos - R * step; + if ROWS { + for k in 0..2 * R { + let row: &[u16; LANES] = d[start + k * step..start + k * step + LANES].try_into().expect("lanes"); + v[8 - R + k] = std::array::from_fn(|l| row[l] as T); + } + } else { + for l in 0..LANES { + let s = start + l * along; + let line: &[u16] = &d[s..s + 2 * R]; + for k in 0..2 * R { + v[8 - R + k][l] = line[k] as T; + } + } + } + let shift = bit_depth as u32 - 8; + let limit_bd = limit << shift; + let blimit_bd = blimit << shift; + let thresh_bd = thresh << shift; + let one = 1 << shift; + let (p3, p2, p1, p0, q0, q1, q2, q3) = (&v[4], &v[5], &v[6], &v[7], &v[8], &v[9], &v[10], &v[11]); + // Filter mask process (8.8.5.1). Masks are 0 / -1 per lane, selects are bitwise + // and clipping uses min / max (`clamp` asserts its bounds), so every lane loop + // below compiles to vector instructions. + let le = |a: T, b: T| -((a <= b) as T); + let sel = |m: T, a: T, b: T| (a & m) | (b & !m); + let mut mask = [0 as T; LANES]; + let mut hev = [0 as T; LANES]; + let mut flat = [0 as T; LANES]; + let mut flat2 = [0 as T; LANES]; + let can_flat = -((filter_size >= TX_8X8) as T); + let can_flat2 = R == 8 && filter_size >= TX_16X16; + let apply: [T; LANES] = apply.map(|a| -(a as T)); + for l in 0..LANES { + let d_p1p0 = (p1[l] - p0[l]).abs(); + let d_q1q0 = (q1[l] - q0[l]).abs(); + mask[l] = apply[l] + & le((p3[l] - p2[l]).abs(), limit_bd) + & le((p2[l] - p1[l]).abs(), limit_bd) + & le(d_p1p0, limit_bd) + & le(d_q1q0, limit_bd) + & le((q2[l] - q1[l]).abs(), limit_bd) + & le((q3[l] - q2[l]).abs(), limit_bd) + & le((p0[l] - q0[l]).abs() * 2 + ((p1[l] - q1[l]).abs() >> 1), blimit_bd); + hev[l] = !(le(d_p1p0, thresh_bd) & le(d_q1q0, thresh_bd)); + flat[l] = can_flat + & mask[l] + & le(d_p1p0, one) + & le(d_q1q0, one) + & le((p2[l] - p0[l]).abs(), one) + & le((q2[l] - q0[l]).abs(), one) + & le((p3[l] - p0[l]).abs(), one) + & le((q3[l] - q0[l]).abs(), one); + } + if mask.iter().fold(0, |a, &m| a | m) == 0 { + return; + } + if can_flat2 { + for l in 0..LANES { + flat2[l] = flat[l] + & le((v[0][l] - p0[l]).abs(), one) + & le((v[1][l] - p0[l]).abs(), one) + & le((v[2][l] - p0[l]).abs(), one) + & le((v[3][l] - p0[l]).abs(), one) + & le((v[12][l] - q0[l]).abs(), one) + & le((v[13][l] - q0[l]).abs(), one) + & le((v[14][l] - q0[l]).abs(), one) + & le((v[15][l] - q0[l]).abs(), one); + } + } + let any_flat = flat.iter().fold(0, |a, &f| a | f) != 0; + let any_flat2 = flat2.iter().fold(0, |a, &f| a | f) != 0; + // Results per position across the edge; start from the unfiltered samples. + let mut out = v; + // Narrow filter (8.8.5.2) for lanes that are masked but not flat. + { + let lo = -(1 << (bit_depth - 1)) as T; + let hi = ((1 << (bit_depth - 1)) - 1) as T; + let c = |x: T| x.max(lo).min(hi); + let off: T = 0x80 << shift; + for l in 0..LANES { + let (ps1, ps0, qs0, qs1) = (p1[l] - off, p0[l] - off, q0[l] - off, q1[l] - off); + let filter = c((c(ps1 - qs1) & hev[l]) + 3 * (qs0 - ps0)); + let filter1 = c(filter + 4) >> 3; + let filter2 = c(filter + 3) >> 3; + let f = (filter1 + 1) >> 1; + let narrow = mask[l] & !flat[l]; + let outer = narrow & !hev[l]; + out[8][l] = sel(narrow, c(qs0 - filter1) + off, out[8][l]); + out[7][l] = sel(narrow, c(ps0 + filter2) + off, out[7][l]); + out[9][l] = sel(outer, c(qs1 - f) + off, out[9][l]); + out[6][l] = sel(outer, c(ps1 + f) + off, out[6][l]); + } + } + // Wide filter (8.8.5.3) with 8 taps for flat lanes (p2..q2) ... + if any_flat { + let w8 = wide_filter::<3>(&v); + for k in 5..11 { + for l in 0..LANES { + out[k][l] = sel(flat[l] & !flat2[l], w8[k][l], out[k][l]); + } + } + } + // ... and 16 taps for flat2 lanes (p6..q6). + if any_flat2 { + let w16 = wide_filter::<4>(&v); + for k in 1..15 { + for l in 0..LANES { + out[k][l] = sel(flat2[l], w16[k][l], out[k][l]); + } + } + } + let (lo, hi) = if any_flat2 { + (1, 15) + } else if any_flat { + (5, 11) + } else { + (6, 10) + }; + if ROWS { + for k in lo..hi { + let o = start + (k + R - 8) * step; // k >= 8 - R + let row: &mut [u16; LANES] = (&mut d[o..o + LANES]).try_into().expect("lanes"); + for l in 0..LANES { + row[l] = out[k][l] as u16; + } + } + } else { + // Whole lines: unmodified samples are rewritten with their own value. + for l in 0..LANES { + let s = start + l * along; + let line: &mut [u16] = &mut d[s..s + 2 * R]; + for k in 0..2 * R { + line[k] = out[8 - R + k][l] as u16; + } + } + } + } + + /// Wide filter process (8.8.5.3) with 2^LOG2 taps on gathered lanes (positions 8 - 2^(LOG2-1) + /// .. 8 + 2^(LOG2-1) - 2 are produced); the sum over j of the specification is computed as a + /// sliding window over the clamped indices (identical results). + #[inline(always)] + fn wide_filter(v: &[[T; LANES]; 16]) -> [[T; LANES]; 16] { + let n = (1isize << (LOG2 - 1)) - 1; + let s = |k: isize| &v[(k.clamp(-(n + 1), n) + 8) as usize]; + let mut out = [[0 as T; LANES]; 16]; + let mut sum = [0 as T; LANES]; + for j in -n..=n { + let a = s(-n + j); + for l in 0..LANES { + sum[l] += a[l]; + } + } + let round: T = 1 << (LOG2 - 1); + for i in -n..n { + let (cur, add, sub) = (s(i), s(i + 1 + n), s(i - n)); + let o = &mut out[(i + 8) as usize]; + for l in 0..LANES { + o[l] = (sum[l] + cur[l] + round) >> LOG2; + sum[l] += add[l] - sub[l]; + } + } + out + } + } + }; +} +group_filter!(lanes16, i16); +group_filter!(lanes32, i32); diff --git a/crates/video/src/vp9/decode/mod.rs b/crates/video/src/vp9/decode/mod.rs new file mode 100644 index 000000000..2e00b4bf3 --- /dev/null +++ b/crates/video/src/vp9/decode/mod.rs @@ -0,0 +1,169 @@ +//! Clean-room pure-Rust VP9 decoder, implemented from the public VP9 Bitstream & Decoding Process +//! Specification (v0.6, WebM project). +//! +//! Supported: profiles 0-3 (8 / 10 / 12-bit; 4:2:0, 4:2:2, 4:4:0, 4:4:4), all coding tools: +//! boolean decoder, forward and backward probability adaptation, segmentation, loop filter with +//! deltas, all intra modes, inter prediction (8-tap regular / smooth / sharp and bilinear, +//! scaled references), compound prediction, motion vector prediction, DCT / ADST 4..32 and the +//! Walsh-Hadamard lossless transform, tiles (tile columns decode in parallel), superframes, +//! hidden frames, show_existing_frame, intra-only frames, reference frame resizing and error +//! resilient / frame parallel modes. +//! +//! ```no_run +//! let mut dec = craft_video::vp9::decode::Decoder::new(); +//! let chunk: Vec = Vec::new(); // one IVF / WebM frame +//! for pic in dec.decode(&chunk, 0).unwrap() { +//! println!("{}x{} {}-bit", pic.width, pic.height, pic.bit_depth); +//! } +//! ``` + +#![allow(clippy::needless_range_loop, clippy::type_complexity)] + +mod boolcoder; +mod decoder; +mod error; +mod frame; +mod header; +mod inter; +mod intra; +mod loopfilter; +mod probs; +#[rustfmt::skip] +#[allow(dead_code)] +mod spec_tables; +mod tables; +mod tile; +mod transform; +#[rustfmt::skip] +#[allow(dead_code)] +mod transform_gen; + +pub use decoder::{DecodeStats, Decoder}; + +/// Stream parameters read from a key frame's uncompressed header (see [`keyframe_info`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct KeyframeInfo { + pub profile: u8, + pub width: u32, + pub height: u32, + pub render_width: u32, + pub render_height: u32, + pub bit_depth: u32, + pub subsampling_x: bool, + pub subsampling_y: bool, + pub color: ColorInfo, +} + +/// If the first frame of `chunk` (one container sample: a frame or a superframe) is a key frame, +/// i.e. a random access point, its stream parameters. Intra-only frames and show_existing_frame +/// are not random access points. +pub fn keyframe_info(chunk: &[u8]) -> Option { + let first = header::split_superframe(chunk).into_iter().find(|f| !f.is_empty())?; + let mut st = header::HeaderState::default(); + let h = header::parse_uncompressed(first, &mut st, &[None; 8]).ok()?; + if h.show_existing_frame || h.frame_type != header::KEY_FRAME { + return None; + } + Some(KeyframeInfo { + profile: h.profile, + width: h.width, + height: h.height, + render_width: h.render_width, + render_height: h.render_height, + bit_depth: h.color.bit_depth as u32, + subsampling_x: h.color.subsampling_x, + subsampling_y: h.color.subsampling_y, + color: ColorInfo { color_space: h.color.color_space, full_range: h.color.color_range }, + }) +} + +/// Whether `chunk` starts with a key frame (decoding can start there). +pub fn is_keyframe(chunk: &[u8]) -> bool { + keyframe_info(chunk).is_some() +} +pub use error::{Error, Result}; + +/// Colour information from the uncompressed header. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ColorInfo { + /// `color_space` (0 unknown, 1 BT.601, 2 BT.709, 3 SMPTE-170, 4 SMPTE-240, 5 BT.2020, + /// 6 reserved, 7 sRGB). + pub color_space: u8, + /// `color_range`: full swing when true, studio swing otherwise. + pub full_range: bool, +} + +/// Samples of one plane: 8-bit streams produce `U8`, higher bit depths `U16`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Plane { + U8(Vec), + U16(Vec), +} + +impl Plane { + pub fn len(&self) -> usize { + match self { + Plane::U8(v) => v.len(), + Plane::U16(v) => v.len(), + } + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn as_u8(&self) -> Option<&[u8]> { + match self { + Plane::U8(v) => Some(v), + Plane::U16(_) => None, + } + } + pub fn as_u16(&self) -> Option<&[u16]> { + match self { + Plane::U8(_) => None, + Plane::U16(v) => Some(v), + } + } + /// Sample `i` widened to u16. + pub fn get(&self, i: usize) -> u16 { + match self { + Plane::U8(v) => v[i] as u16, + Plane::U16(v) => v[i], + } + } + /// Raw bytes as written by ffmpeg `rawvideo` (8-bit, or 16-bit little endian). + pub fn to_le_bytes(&self) -> Vec { + match self { + Plane::U8(v) => v.clone(), + Plane::U16(v) => v.iter().flat_map(|s| s.to_le_bytes()).collect(), + } + } +} + +/// A decoded, cropped planar picture. +#[derive(Clone, Debug)] +pub struct Picture { + /// Frame width / height (decoded size). + pub width: u32, + pub height: u32, + pub chroma_width: u32, + pub chroma_height: u32, + pub bit_depth: u32, + pub subsampling_x: bool, + pub subsampling_y: bool, + pub y: Plane, + pub u: Plane, + pub v: Plane, + /// Strides in samples. + pub y_stride: usize, + pub uv_stride: usize, + /// `pts` passed to [`Decoder::decode`] with the chunk that produced this picture. + pub pts: i64, + pub key: bool, + pub intra_only: bool, + pub color: ColorInfo, + /// Intended display size (render_size); has no effect on decoding. + pub render_width: u32, + pub render_height: u32, + /// Decoded in draft mode without the loop filter ([`Decoder::set_draft`]): approximate, for + /// reduced-resolution playback only. Never set without draft mode. + pub draft: bool, +} diff --git a/crates/video/src/vp9/decode/probs.rs b/crates/video/src/vp9/decode/probs.rs new file mode 100644 index 000000000..fc23ceb4e --- /dev/null +++ b/crates/video/src/vp9/decode/probs.rs @@ -0,0 +1,322 @@ +//! Probability tables (a "frame context"), syntax element counters and the backward adaptation +//! processes of section 8.4. + +use crate::vp9::decode::tables::{ + BINARY_TREE, COUNT_SAT, DEFAULT_COEF_PROBS, DEFAULT_COMP_MODE_PROB, DEFAULT_COMP_REF_PROB, DEFAULT_INTER_MODE_PROBS, DEFAULT_INTERP_FILTER_PROBS, + DEFAULT_IS_INTER_PROB, DEFAULT_MV_BITS_PROB, DEFAULT_MV_CLASS_PROBS, DEFAULT_MV_CLASS0_BIT_PROB, DEFAULT_MV_CLASS0_FR_PROBS, + DEFAULT_MV_CLASS0_HP_PROB, DEFAULT_MV_FR_PROBS, DEFAULT_MV_HP_PROB, DEFAULT_MV_JOINT_PROBS, DEFAULT_MV_SIGN_PROB, DEFAULT_PARTITION_PROBS, + DEFAULT_SINGLE_REF_PROB, DEFAULT_SKIP_PROB, DEFAULT_TX_PROBS, DEFAULT_UV_MODE_PROBS, DEFAULT_Y_MODE_PROBS, INTER_MODE_TREE, INTERP_FILTER_TREE, + INTRA_MODE_TREE, MAX_UPDATE_FACTOR, MV_CLASS_TREE, MV_FR_TREE, MV_JOINT_TREE, PARTITION_TREE, SMALL_TOKEN_TREE, TX_SIZE_8_TREE, TX_SIZE_16_TREE, + TX_SIZE_32_TREE, +}; + +/// coef_probs[txSz][plane > 0][is_inter][band][ctx][node]. +pub type CoefProbs = [[[[[[u8; 3]; 6]; 6]; 2]; 2]; 4]; + +/// All adaptive probabilities of one frame context (10.5). +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct FrameContext { + /// tx_probs[maxTxSize][ctx][node] (index 0 unused). + pub tx: [[[u8; 3]; 2]; 4], + pub coef: CoefProbs, + pub skip: [u8; 3], + pub inter_mode: [[u8; 3]; 7], + pub interp_filter: [[u8; 2]; 4], + pub is_inter: [u8; 4], + pub comp_mode: [u8; 5], + pub single_ref: [[u8; 2]; 5], + pub comp_ref: [u8; 5], + pub y_mode: [[u8; 9]; 4], + pub uv_mode: [[u8; 9]; 10], + pub partition: [[u8; 3]; 16], + pub mv_joint: [u8; 3], + pub mv_sign: [u8; 2], + pub mv_class: [[u8; 10]; 2], + pub mv_class0_bit: [u8; 2], + pub mv_bits: [[u8; 10]; 2], + pub mv_class0_fr: [[[u8; 3]; 2]; 2], + pub mv_fr: [[u8; 3]; 2], + pub mv_class0_hp: [u8; 2], + pub mv_hp: [u8; 2], +} + +fn arr(s: &[u8]) -> [u8; N] { + s.first_chunk::().copied().unwrap_or([128; N]) +} + +impl Default for FrameContext { + fn default() -> Self { + let mut coef = [[[[[[0u8; 3]; 6]; 6]; 2]; 2]; 4]; + let mut k = 0; + for t in coef.iter_mut() { + for i in t.iter_mut() { + for j in i.iter_mut() { + for b in j.iter_mut() { + for c in b.iter_mut() { + *c = arr(&DEFAULT_COEF_PROBS[k..k + 3]); + k += 3; + } + } + } + } + } + let mut tx = [[[0u8; 3]; 2]; 4]; + for (m, t) in tx.iter_mut().enumerate() { + for (c, p) in t.iter_mut().enumerate() { + *p = arr(&DEFAULT_TX_PROBS[(m * 2 + c) * 3..(m * 2 + c) * 3 + 3]); + } + } + FrameContext { + tx, + coef, + skip: DEFAULT_SKIP_PROB, + inter_mode: std::array::from_fn(|i| arr(&DEFAULT_INTER_MODE_PROBS[i * 3..i * 3 + 3])), + interp_filter: std::array::from_fn(|i| arr(&DEFAULT_INTERP_FILTER_PROBS[i * 2..i * 2 + 2])), + is_inter: DEFAULT_IS_INTER_PROB, + comp_mode: DEFAULT_COMP_MODE_PROB, + single_ref: std::array::from_fn(|i| arr(&DEFAULT_SINGLE_REF_PROB[i * 2..i * 2 + 2])), + comp_ref: DEFAULT_COMP_REF_PROB, + y_mode: std::array::from_fn(|i| arr(&DEFAULT_Y_MODE_PROBS[i * 9..i * 9 + 9])), + uv_mode: std::array::from_fn(|i| arr(&DEFAULT_UV_MODE_PROBS[i * 9..i * 9 + 9])), + partition: std::array::from_fn(|i| arr(&DEFAULT_PARTITION_PROBS[i * 3..i * 3 + 3])), + mv_joint: DEFAULT_MV_JOINT_PROBS, + mv_sign: DEFAULT_MV_SIGN_PROB, + mv_class: std::array::from_fn(|i| arr(&DEFAULT_MV_CLASS_PROBS[i * 10..i * 10 + 10])), + mv_class0_bit: DEFAULT_MV_CLASS0_BIT_PROB, + mv_bits: std::array::from_fn(|i| arr(&DEFAULT_MV_BITS_PROB[i * 10..i * 10 + 10])), + mv_class0_fr: std::array::from_fn(|i| std::array::from_fn(|j| arr(&DEFAULT_MV_CLASS0_FR_PROBS[(i * 2 + j) * 3..(i * 2 + j) * 3 + 3]))), + mv_fr: std::array::from_fn(|i| arr(&DEFAULT_MV_FR_PROBS[i * 3..i * 3 + 3])), + mv_class0_hp: DEFAULT_MV_CLASS0_HP_PROB, + mv_hp: DEFAULT_MV_HP_PROB, + } + } +} + +/// Syntax element counters (8.3). +#[derive(Clone, Default)] +pub struct Counts { + pub intra_mode: [[u32; 10]; 4], + pub uv_mode: [[u32; 10]; 10], + pub partition: [[u32; 4]; 16], + pub interp_filter: [[u32; 3]; 4], + pub inter_mode: [[u32; 4]; 7], + /// tx_size[maxTxSize][ctx][value]. + pub tx: [[[u32; 4]; 2]; 4], + pub is_inter: [[u32; 2]; 4], + pub comp_mode: [[u32; 2]; 5], + pub single_ref: [[[u32; 2]; 2]; 5], + pub comp_ref: [[u32; 2]; 5], + pub skip: [[u32; 2]; 3], + pub mv_joint: [u32; 4], + pub mv_sign: [[u32; 2]; 2], + pub mv_class: [[u32; 11]; 2], + pub mv_class0_bit: [[u32; 2]; 2], + pub mv_class0_fr: [[[u32; 4]; 2]; 2], + pub mv_class0_hp: [[u32; 2]; 2], + pub mv_bits: [[[u32; 2]; 10]; 2], + pub mv_fr: [[u32; 4]; 2], + pub mv_hp: [[u32; 2]; 2], + /// token[txSz][plane>0][is_inter][band][ctx][min(2, token)]. + pub token: [[[[[[u32; 3]; 6]; 6]; 2]; 2]; 4], + /// more_coefs[txSz][plane>0][is_inter][band][ctx][value]. + pub more_coefs: [[[[[[u32; 2]; 6]; 6]; 2]; 2]; 4], +} + +fn add_slice(a: &mut [u32], b: &[u32]) { + for (x, y) in a.iter_mut().zip(b) { + *x += y; + } +} + +macro_rules! add_nested { + ($a:expr, $b:expr, 1) => { + add_slice(&mut $a[..], &$b[..]) + }; + ($a:expr, $b:expr, 2) => { + for (x, y) in $a.iter_mut().zip($b.iter()) { + add_slice(&mut x[..], &y[..]); + } + }; + ($a:expr, $b:expr, 3) => { + for (x, y) in $a.iter_mut().zip($b.iter()) { + add_nested!(x, y, 2); + } + }; + ($a:expr, $b:expr, 6) => { + for (x, y) in $a.iter_mut().zip($b.iter()) { + for (x, y) in x.iter_mut().zip(y.iter()) { + for (x, y) in x.iter_mut().zip(y.iter()) { + for (x, y) in x.iter_mut().zip(y.iter()) { + add_nested!(x, y, 2); + } + } + } + } + }; +} + +impl Counts { + /// Accumulate the counts of another tile. + pub fn add(&mut self, o: &Counts) { + add_nested!(self.intra_mode, o.intra_mode, 2); + add_nested!(self.uv_mode, o.uv_mode, 2); + add_nested!(self.partition, o.partition, 2); + add_nested!(self.interp_filter, o.interp_filter, 2); + add_nested!(self.inter_mode, o.inter_mode, 2); + add_nested!(self.tx, o.tx, 3); + add_nested!(self.is_inter, o.is_inter, 2); + add_nested!(self.comp_mode, o.comp_mode, 2); + add_nested!(self.single_ref, o.single_ref, 3); + add_nested!(self.comp_ref, o.comp_ref, 2); + add_nested!(self.skip, o.skip, 2); + add_nested!(self.mv_joint, o.mv_joint, 1); + add_nested!(self.mv_sign, o.mv_sign, 2); + add_nested!(self.mv_class, o.mv_class, 2); + add_nested!(self.mv_class0_bit, o.mv_class0_bit, 2); + add_nested!(self.mv_class0_fr, o.mv_class0_fr, 3); + add_nested!(self.mv_class0_hp, o.mv_class0_hp, 2); + add_nested!(self.mv_bits, o.mv_bits, 3); + add_nested!(self.mv_fr, o.mv_fr, 2); + add_nested!(self.mv_hp, o.mv_hp, 2); + self.add_coef_counts(o); + } + + /// Accumulate the per-coefficient token and more_coefs counts of another tile. + fn add_coef_counts(&mut self, o: &Counts) { + add_nested!(self.token, o.token, 6); + add_nested!(self.more_coefs, o.more_coefs, 6); + } +} + +/// merge_prob (8.4.1). +fn merge_prob(pre: u8, ct0: u32, ct1: u32, count_sat: u32, max_update_factor: u32) -> u8 { + let den = ct0 + ct1; + let prob = if den == 0 { 128 } else { ((ct0 as u64 * 256 + (den as u64 >> 1)) / den as u64).clamp(1, 255) as u32 }; + let count = den.min(count_sat); + let factor = max_update_factor * count / count_sat; + ((pre as u32 * (256 - factor) + prob * factor + 128) >> 8) as u8 +} + +/// merge_probs (8.4.2). +fn merge_probs(tree: &[i8], i: usize, probs: &mut [u8], counts: &[u32], count_sat: u32, max_update_factor: u32) -> u32 { + let s = tree[i]; + let left = if s <= 0 { counts[(-s) as usize] } else { merge_probs(tree, s as usize, probs, counts, count_sat, max_update_factor) }; + let r = tree[i + 1]; + let right = if r <= 0 { counts[(-r) as usize] } else { merge_probs(tree, r as usize, probs, counts, count_sat, max_update_factor) }; + probs[i >> 1] = merge_prob(probs[i >> 1], left, right, count_sat, max_update_factor); + left + right +} + +fn adapt_probs(tree: &[i8], probs: &mut [u8], counts: &[u32]) { + merge_probs(tree, 0, probs, counts, COUNT_SAT, MAX_UPDATE_FACTOR); +} + +fn adapt_prob(prob: &mut u8, counts: &[u32; 2]) { + *prob = merge_prob(*prob, counts[0], counts[1], COUNT_SAT, MAX_UPDATE_FACTOR); +} + +/// Coefficient probability adaptation (8.4.3). `fc` holds the probabilities loaded from the +/// saved frame context. +pub fn adapt_coef_probs(fc: &mut FrameContext, counts: &Counts, frame_is_intra: bool, last_frame_was_key: bool) { + let update_factor = if frame_is_intra { + 112 + } else if last_frame_was_key { + 128 + } else { + 112 + }; + for t in 0..4 { + for i in 0..2 { + for j in 0..2 { + for k in 0..6 { + let max_l = if k == 0 { 3 } else { 6 }; + for l in 0..max_l { + let p = &mut fc.coef[t][i][j][k][l]; + merge_probs(&SMALL_TOKEN_TREE, 2, p, &counts.token[t][i][j][k][l], 24, update_factor); + merge_probs(&BINARY_TREE, 0, p, &counts.more_coefs[t][i][j][k][l], 24, update_factor); + } + } + } + } + } +} + +/// Non-coefficient probability adaptation (8.4.4). +pub fn adapt_noncoef_probs(fc: &mut FrameContext, c: &Counts, interp_switchable: bool, tx_select: bool, allow_hp: bool) { + for i in 0..4 { + adapt_prob(&mut fc.is_inter[i], &c.is_inter[i]); + } + for i in 0..5 { + adapt_prob(&mut fc.comp_mode[i], &c.comp_mode[i]); + adapt_prob(&mut fc.comp_ref[i], &c.comp_ref[i]); + for j in 0..2 { + adapt_prob(&mut fc.single_ref[i][j], &c.single_ref[i][j]); + } + } + for i in 0..7 { + adapt_probs(&INTER_MODE_TREE, &mut fc.inter_mode[i], &c.inter_mode[i]); + } + for i in 0..4 { + adapt_probs(&INTRA_MODE_TREE, &mut fc.y_mode[i], &c.intra_mode[i]); + } + for i in 0..10 { + adapt_probs(&INTRA_MODE_TREE, &mut fc.uv_mode[i], &c.uv_mode[i]); + } + for i in 0..16 { + adapt_probs(&PARTITION_TREE, &mut fc.partition[i], &c.partition[i]); + } + for i in 0..3 { + adapt_prob(&mut fc.skip[i], &c.skip[i]); + } + if interp_switchable { + for i in 0..4 { + adapt_probs(&INTERP_FILTER_TREE, &mut fc.interp_filter[i], &c.interp_filter[i]); + } + } + if tx_select { + for i in 0..2 { + adapt_probs(&TX_SIZE_8_TREE, &mut fc.tx[1][i], &c.tx[1][i]); + adapt_probs(&TX_SIZE_16_TREE, &mut fc.tx[2][i], &c.tx[2][i]); + adapt_probs(&TX_SIZE_32_TREE, &mut fc.tx[3][i], &c.tx[3][i]); + } + } + adapt_probs(&MV_JOINT_TREE, &mut fc.mv_joint, &c.mv_joint); + for i in 0..2 { + adapt_prob(&mut fc.mv_sign[i], &c.mv_sign[i]); + adapt_probs(&MV_CLASS_TREE, &mut fc.mv_class[i], &c.mv_class[i]); + adapt_prob(&mut fc.mv_class0_bit[i], &c.mv_class0_bit[i]); + for j in 0..10 { + adapt_prob(&mut fc.mv_bits[i][j], &c.mv_bits[i][j]); + } + for j in 0..2 { + adapt_probs(&MV_FR_TREE, &mut fc.mv_class0_fr[i][j], &c.mv_class0_fr[i][j]); + } + adapt_probs(&MV_FR_TREE, &mut fc.mv_fr[i], &c.mv_fr[i]); + if allow_hp { + adapt_prob(&mut fc.mv_class0_hp[i], &c.mv_class0_hp[i]); + adapt_prob(&mut fc.mv_hp[i], &c.mv_hp[i]); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merge_prob_identity_without_counts() { + for p in 1..=255u8 { + assert_eq!(merge_prob(p, 0, 0, 20, 128), p); + } + // Saturated counts move halfway towards the observed probability. + assert_eq!(merge_prob(128, 20, 0, 20, 128), 192); + } + + #[test] + fn defaults_load() { + let fc = FrameContext::default(); + assert_eq!(fc.coef[0][0][0][0][0], [195, 29, 183]); + assert_eq!(fc.tx[3][1], [5, 52, 13]); + assert_eq!(fc.mv_class0_fr[1][1], [96, 112, 64]); + } +} diff --git a/crates/video/src/vp9/decode/spec_tables.rs b/crates/video/src/vp9/decode/spec_tables.rs new file mode 100644 index 000000000..686d64fcf --- /dev/null +++ b/crates/video/src/vp9/decode/spec_tables.rs @@ -0,0 +1,1427 @@ +// Generated by tools/extract_tables.py from the VP9 Bitstream & Decoding Process Specification +// v0.6 (section numbers refer to that document). Do not edit by hand. + +pub const INV_MAP_TABLE: [u8; 255] = [ + 7, 20, 33, 46, 59, 72, 85, 98, 111, 124, 137, 150, 163, 176, 189, 202, + 215, 228, 241, 254, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 48, + 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 60, 61, 62, 63, 64, 65, + 66, 67, 68, 69, 70, 71, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, + 83, 84, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 99, 100, + 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 112, 113, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 135, 136, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 151, 152, + 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 164, 165, 166, 167, 168, 169, + 170, 171, 172, 173, 174, 175, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, + 187, 188, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 203, 204, + 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 216, 217, 218, 219, 220, 221, + 222, 223, 224, 225, 226, 227, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, + 239, 240, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 253, +]; + +pub const SUBPEL_FILTERS: [i16; 512] = [ + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 1, -5, 126, 8, -3, 1, 0, + -1, 3, -10, 122, 18, -6, 2, 0, + -1, 4, -13, 118, 27, -9, 3, -1, + -1, 4, -16, 112, 37, -11, 4, -1, + -1, 5, -18, 105, 48, -14, 4, -1, + -1, 5, -19, 97, 58, -16, 5, -1, + -1, 6, -19, 88, 68, -18, 5, -1, + -1, 6, -19, 78, 78, -19, 6, -1, + -1, 5, -18, 68, 88, -19, 6, -1, + -1, 5, -16, 58, 97, -19, 5, -1, + -1, 4, -14, 48, 105, -18, 5, -1, + -1, 4, -11, 37, 112, -16, 4, -1, + -1, 3, -9, 27, 118, -13, 4, -1, + 0, 2, -6, 18, 122, -10, 3, -1, + 0, 1, -3, 8, 126, -5, 1, 0, + 0, 0, 0, 128, 0, 0, 0, 0, + -3, -1, 32, 64, 38, 1, -3, 0, + -2, -2, 29, 63, 41, 2, -3, 0, + -2, -2, 26, 63, 43, 4, -4, 0, + -2, -3, 24, 62, 46, 5, -4, 0, + -2, -3, 21, 60, 49, 7, -4, 0, + -1, -4, 18, 59, 51, 9, -4, 0, + -1, -4, 16, 57, 53, 12, -4, -1, + -1, -4, 14, 55, 55, 14, -4, -1, + -1, -4, 12, 53, 57, 16, -4, -1, + 0, -4, 9, 51, 59, 18, -4, -1, + 0, -4, 7, 49, 60, 21, -3, -2, + 0, -4, 5, 46, 62, 24, -3, -2, + 0, -4, 4, 43, 63, 26, -2, -2, + 0, -3, 2, 41, 63, 29, -2, -2, + 0, -3, 1, 38, 64, 32, -1, -3, + 0, 0, 0, 128, 0, 0, 0, 0, + -1, 3, -7, 127, 8, -3, 1, 0, + -2, 5, -13, 125, 17, -6, 3, -1, + -3, 7, -17, 121, 27, -10, 5, -2, + -4, 9, -20, 115, 37, -13, 6, -2, + -4, 10, -23, 108, 48, -16, 8, -3, + -4, 10, -24, 100, 59, -19, 9, -3, + -4, 11, -24, 90, 70, -21, 10, -4, + -4, 11, -23, 80, 80, -23, 11, -4, + -4, 10, -21, 70, 90, -24, 11, -4, + -3, 9, -19, 59, 100, -24, 10, -4, + -3, 8, -16, 48, 108, -23, 10, -4, + -2, 6, -13, 37, 115, -20, 9, -4, + -2, 5, -10, 27, 121, -17, 7, -3, + -1, 3, -6, 17, 125, -13, 5, -2, + 0, 1, -3, 8, 127, -7, 3, -1, + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 0, 0, 120, 8, 0, 0, 0, + 0, 0, 0, 112, 16, 0, 0, 0, + 0, 0, 0, 104, 24, 0, 0, 0, + 0, 0, 0, 96, 32, 0, 0, 0, + 0, 0, 0, 88, 40, 0, 0, 0, + 0, 0, 0, 80, 48, 0, 0, 0, + 0, 0, 0, 72, 56, 0, 0, 0, + 0, 0, 0, 64, 64, 0, 0, 0, + 0, 0, 0, 56, 72, 0, 0, 0, + 0, 0, 0, 48, 80, 0, 0, 0, + 0, 0, 0, 40, 88, 0, 0, 0, + 0, 0, 0, 32, 96, 0, 0, 0, + 0, 0, 0, 24, 104, 0, 0, 0, + 0, 0, 0, 16, 112, 0, 0, 0, + 0, 0, 0, 8, 120, 0, 0, 0, +]; + +pub const DC_QLOOKUP: [i32; 768] = [ + 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, + 20, 21, 22, 23, 24, 25, 26, 26, 27, 28, 29, 30, 31, 32, 32, 33, + 34, 35, 36, 37, 38, 38, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, + 48, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 57, 58, 59, 60, + 61, 62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, 71, 72, 73, + 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, 82, 83, 84, 85, 85, + 87, 88, 90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, + 111, 113, 114, 116, 117, 118, 120, 121, 123, 125, 127, 129, 131, 134, 136, 138, + 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 161, 164, 166, 169, 172, 174, + 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208, 211, 214, 217, 220, + 223, 226, 230, 233, 237, 240, 243, 247, 250, 253, 257, 261, 265, 269, 272, 276, + 280, 284, 288, 292, 296, 300, 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, + 349, 354, 359, 364, 369, 374, 379, 384, 389, 395, 400, 406, 411, 417, 423, 429, + 435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, 522, 530, 539, 549, + 559, 569, 579, 590, 602, 614, 626, 640, 654, 668, 684, 700, 717, 736, 755, 775, + 796, 819, 843, 869, 896, 925, 955, 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336, + 4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, + 50, 53, 57, 60, 64, 68, 71, 75, 78, 82, 86, 90, 93, 97, 101, 105, + 109, 113, 116, 120, 124, 128, 132, 136, 140, 143, 147, 151, 155, 159, 163, 166, + 170, 174, 178, 182, 185, 189, 193, 197, 200, 204, 208, 212, 215, 219, 223, 226, + 230, 233, 237, 241, 244, 248, 251, 255, 259, 262, 266, 269, 273, 276, 280, 283, + 287, 290, 293, 297, 300, 304, 307, 310, 314, 317, 321, 324, 327, 331, 334, 337, + 343, 350, 356, 362, 369, 375, 381, 387, 394, 400, 406, 412, 418, 424, 430, 436, + 442, 448, 454, 460, 466, 472, 478, 484, 490, 499, 507, 516, 525, 533, 542, 550, + 559, 567, 576, 584, 592, 601, 609, 617, 625, 634, 644, 655, 666, 676, 687, 698, + 708, 718, 729, 739, 749, 759, 770, 782, 795, 807, 819, 831, 844, 856, 868, 880, + 891, 906, 920, 933, 947, 961, 975, 988, 1001, 1015, 1030, 1045, 1061, 1076, 1090, 1105, + 1120, 1137, 1153, 1170, 1186, 1202, 1218, 1236, 1253, 1271, 1288, 1306, 1323, 1342, 1361, 1379, + 1398, 1416, 1436, 1456, 1476, 1496, 1516, 1537, 1559, 1580, 1601, 1624, 1647, 1670, 1692, 1717, + 1741, 1766, 1791, 1817, 1844, 1871, 1900, 1929, 1958, 1990, 2021, 2054, 2088, 2123, 2159, 2197, + 2236, 2276, 2319, 2363, 2410, 2458, 2508, 2561, 2616, 2675, 2737, 2802, 2871, 2944, 3020, 3102, + 3188, 3280, 3375, 3478, 3586, 3702, 3823, 3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, 5347, + 4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153, + 166, 180, 194, 208, 222, 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390, + 405, 421, 437, 453, 469, 484, 500, 516, 532, 548, 564, 580, 596, 611, 627, 643, + 659, 674, 690, 706, 721, 737, 752, 768, 783, 798, 814, 829, 844, 859, 874, 889, + 904, 919, 934, 949, 964, 978, 993, 1008, 1022, 1037, 1051, 1065, 1080, 1094, 1108, 1122, + 1136, 1151, 1165, 1179, 1192, 1206, 1220, 1234, 1248, 1261, 1275, 1288, 1302, 1315, 1329, 1342, + 1368, 1393, 1419, 1444, 1469, 1494, 1519, 1544, 1569, 1594, 1618, 1643, 1668, 1692, 1717, 1741, + 1765, 1789, 1814, 1838, 1862, 1885, 1909, 1933, 1957, 1992, 2027, 2061, 2096, 2130, 2165, 2199, + 2233, 2267, 2300, 2334, 2367, 2400, 2434, 2467, 2499, 2532, 2575, 2618, 2661, 2704, 2746, 2788, + 2830, 2872, 2913, 2954, 2995, 3036, 3076, 3127, 3177, 3226, 3275, 3324, 3373, 3421, 3469, 3517, + 3565, 3621, 3677, 3733, 3788, 3843, 3897, 3951, 4005, 4058, 4119, 4181, 4241, 4301, 4361, 4420, + 4479, 4546, 4612, 4677, 4742, 4807, 4871, 4942, 5013, 5083, 5153, 5222, 5291, 5367, 5442, 5517, + 5591, 5665, 5745, 5825, 5905, 5984, 6063, 6149, 6234, 6319, 6404, 6495, 6587, 6678, 6769, 6867, + 6966, 7064, 7163, 7269, 7376, 7483, 7599, 7715, 7832, 7958, 8085, 8214, 8352, 8492, 8635, 8788, + 8945, 9104, 9275, 9450, 9639, 9832, 10031, 10245, 10465, 10702, 10946, 11210, 11482, 11776, 12081, 12409, + 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387, +]; + +pub const AC_QLOOKUP: [i32; 768] = [ + 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, + 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, + 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, + 136, 138, 140, 142, 144, 146, 148, 150, 152, 155, 158, 161, 164, 167, 170, 173, + 176, 179, 182, 185, 188, 191, 194, 197, 200, 203, 207, 211, 215, 219, 223, 227, + 231, 235, 239, 243, 247, 251, 255, 260, 265, 270, 275, 280, 285, 290, 295, 300, + 305, 311, 317, 323, 329, 335, 341, 347, 353, 359, 366, 373, 380, 387, 394, 401, + 408, 416, 424, 432, 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, 530, 540, + 550, 560, 571, 582, 593, 604, 615, 627, 639, 651, 663, 676, 689, 702, 715, 729, + 743, 757, 771, 786, 801, 816, 832, 848, 864, 881, 898, 915, 933, 951, 969, 988, + 1007, 1026, 1046, 1066, 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, + 1369, 1396, 1423, 1451, 1479, 1508, 1537, 1567, 1597, 1628, 1660, 1692, 1725, 1759, 1793, 1828, + 4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, + 55, 59, 63, 67, 71, 75, 79, 83, 88, 92, 96, 100, 105, 109, 114, 118, + 122, 127, 131, 136, 140, 145, 149, 154, 158, 163, 168, 172, 177, 181, 186, 190, + 195, 199, 204, 208, 213, 217, 222, 226, 231, 235, 240, 244, 249, 253, 258, 262, + 267, 271, 275, 280, 284, 289, 293, 297, 302, 306, 311, 315, 319, 324, 328, 332, + 337, 341, 345, 349, 354, 358, 362, 367, 371, 375, 379, 384, 388, 392, 396, 401, + 409, 417, 425, 433, 441, 449, 458, 466, 474, 482, 490, 498, 506, 514, 523, 531, + 539, 547, 555, 563, 571, 579, 588, 596, 604, 616, 628, 640, 652, 664, 676, 688, + 700, 713, 725, 737, 749, 761, 773, 785, 797, 809, 825, 841, 857, 873, 889, 905, + 922, 938, 954, 970, 986, 1002, 1018, 1038, 1058, 1078, 1098, 1118, 1138, 1158, 1178, 1198, + 1218, 1242, 1266, 1290, 1314, 1338, 1362, 1386, 1411, 1435, 1463, 1491, 1519, 1547, 1575, 1603, + 1631, 1663, 1695, 1727, 1759, 1791, 1823, 1859, 1895, 1931, 1967, 2003, 2039, 2079, 2119, 2159, + 2199, 2239, 2283, 2327, 2371, 2415, 2459, 2507, 2555, 2603, 2651, 2703, 2755, 2807, 2859, 2915, + 2971, 3027, 3083, 3143, 3203, 3263, 3327, 3391, 3455, 3523, 3591, 3659, 3731, 3803, 3876, 3952, + 4028, 4104, 4184, 4264, 4348, 4432, 4516, 4604, 4692, 4784, 4876, 4972, 5068, 5168, 5268, 5372, + 5476, 5584, 5692, 5804, 5916, 6032, 6148, 6268, 6388, 6512, 6640, 6768, 6900, 7036, 7172, 7312, + 4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168, + 183, 199, 214, 230, 247, 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438, + 456, 475, 493, 511, 530, 548, 567, 586, 604, 623, 642, 660, 679, 698, 716, 735, + 753, 772, 791, 809, 828, 846, 865, 884, 902, 920, 939, 957, 976, 994, 1012, 1030, + 1049, 1067, 1085, 1103, 1121, 1139, 1157, 1175, 1193, 1211, 1229, 1246, 1264, 1282, 1299, 1317, + 1335, 1352, 1370, 1387, 1405, 1422, 1440, 1457, 1474, 1491, 1509, 1526, 1543, 1560, 1577, 1595, + 1627, 1660, 1693, 1725, 1758, 1791, 1824, 1856, 1889, 1922, 1954, 1987, 2020, 2052, 2085, 2118, + 2150, 2183, 2216, 2248, 2281, 2313, 2346, 2378, 2411, 2459, 2508, 2556, 2605, 2653, 2701, 2750, + 2798, 2847, 2895, 2943, 2992, 3040, 3088, 3137, 3185, 3234, 3298, 3362, 3426, 3491, 3555, 3619, + 3684, 3748, 3812, 3876, 3941, 4005, 4069, 4149, 4230, 4310, 4390, 4470, 4550, 4631, 4711, 4791, + 4871, 4967, 5064, 5160, 5256, 5352, 5448, 5544, 5641, 5737, 5849, 5961, 6073, 6185, 6297, 6410, + 6522, 6650, 6778, 6906, 7034, 7162, 7290, 7435, 7579, 7723, 7867, 8011, 8155, 8315, 8475, 8635, + 8795, 8956, 9132, 9308, 9484, 9660, 9836, 10028, 10220, 10412, 10604, 10812, 11020, 11228, 11437, 11661, + 11885, 12109, 12333, 12573, 12813, 13053, 13309, 13565, 13821, 14093, 14365, 14637, 14925, 15213, 15502, 15806, + 16110, 16414, 16734, 17054, 17390, 17726, 18062, 18414, 18766, 19134, 19502, 19886, 20270, 20670, 21070, 21486, + 21902, 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, 27599, 28143, 28687, 29247, +]; + +pub const COS64_LOOKUP: [i32; 33] = [ + 16384, 16364, 16305, 16207, 16069, 15893, 15679, 15426, 15137, 14811, 14449, 14053, 13623, 13160, 12665, 12140, + 11585, 11003, 10394, 9760, 9102, 8423, 7723, 7005, 6270, 5520, 4756, 3981, 3196, 2404, 1606, 804, + 0, +]; + +pub const DEFAULT_SCAN_4X4: [u16; 16] = [ + 0, 4, 1, 5, 8, 2, 12, 9, 3, 6, 13, 10, 7, 14, 11, 15, +]; + +pub const COL_SCAN_4X4: [u16; 16] = [ + 0, 4, 8, 1, 12, 5, 9, 2, 13, 6, 10, 3, 7, 14, 11, 15, +]; + +pub const ROW_SCAN_4X4: [u16; 16] = [ + 0, 1, 4, 2, 5, 3, 6, 8, 9, 7, 12, 10, 13, 11, 14, 15, +]; + +pub const DEFAULT_SCAN_8X8: [u16; 64] = [ + 0, 8, 1, 16, 9, 2, 17, 24, 10, 3, 18, 25, 32, 11, 4, 26, + 33, 19, 40, 12, 34, 27, 5, 41, 20, 48, 13, 35, 42, 28, 21, 6, + 49, 56, 36, 43, 29, 7, 14, 50, 57, 44, 22, 37, 15, 51, 58, 30, + 45, 23, 52, 59, 38, 31, 60, 53, 46, 39, 61, 54, 47, 62, 55, 63, +]; + +pub const COL_SCAN_8X8: [u16; 64] = [ + 0, 8, 16, 1, 24, 9, 32, 17, 2, 40, 25, 10, 33, 18, 48, 3, + 26, 41, 11, 56, 19, 34, 4, 49, 27, 42, 12, 35, 20, 57, 50, 28, + 5, 43, 13, 36, 58, 51, 21, 44, 6, 29, 59, 37, 14, 52, 22, 7, + 45, 60, 30, 15, 38, 53, 23, 46, 31, 61, 39, 54, 47, 62, 55, 63, +]; + +pub const ROW_SCAN_8X8: [u16; 64] = [ + 0, 1, 2, 8, 9, 3, 16, 10, 4, 17, 11, 24, 5, 18, 25, 12, + 19, 26, 32, 6, 13, 20, 33, 27, 7, 34, 40, 21, 28, 41, 14, 35, + 48, 42, 29, 36, 49, 22, 43, 15, 56, 37, 50, 44, 30, 57, 23, 51, + 58, 45, 38, 52, 31, 59, 53, 46, 60, 39, 61, 47, 54, 55, 62, 63, +]; + +pub const DEFAULT_SCAN_16X16: [u16; 256] = [ + 0, 16, 1, 32, 17, 2, 48, 33, 18, 3, 64, 34, 49, 19, 65, 80, + 50, 4, 35, 66, 20, 81, 96, 51, 5, 36, 82, 97, 67, 112, 21, 52, + 98, 37, 83, 113, 6, 68, 128, 53, 22, 99, 114, 84, 7, 129, 38, 69, + 100, 115, 144, 130, 85, 54, 23, 8, 145, 39, 70, 116, 101, 131, 160, 146, + 55, 86, 24, 71, 132, 117, 161, 40, 9, 102, 147, 176, 162, 87, 56, 25, + 133, 118, 177, 148, 72, 103, 41, 163, 10, 192, 178, 88, 57, 134, 149, 119, + 26, 164, 73, 104, 193, 42, 179, 208, 11, 135, 89, 165, 120, 150, 58, 194, + 180, 27, 74, 209, 105, 151, 136, 43, 90, 224, 166, 195, 181, 121, 210, 59, + 12, 152, 106, 167, 196, 75, 137, 225, 211, 240, 182, 122, 91, 28, 197, 13, + 226, 168, 183, 153, 44, 212, 138, 107, 241, 60, 29, 123, 198, 184, 227, 169, + 242, 76, 213, 154, 45, 92, 14, 199, 139, 61, 228, 214, 170, 185, 243, 108, + 77, 155, 30, 15, 200, 229, 124, 215, 244, 93, 46, 186, 171, 201, 109, 140, + 230, 62, 216, 245, 31, 125, 78, 156, 231, 47, 187, 202, 217, 94, 246, 141, + 63, 232, 172, 110, 247, 157, 79, 218, 203, 126, 233, 188, 248, 95, 173, 142, + 219, 111, 249, 234, 158, 127, 189, 204, 250, 235, 143, 174, 220, 205, 159, 251, + 190, 221, 175, 236, 237, 191, 206, 252, 222, 253, 207, 238, 223, 254, 239, 255, +]; + +pub const COL_SCAN_16X16: [u16; 256] = [ + 0, 16, 32, 48, 1, 64, 17, 80, 33, 96, 49, 2, 65, 112, 18, 81, + 34, 128, 50, 97, 3, 66, 144, 19, 113, 35, 82, 160, 98, 51, 129, 4, + 67, 176, 20, 114, 145, 83, 36, 99, 130, 52, 192, 5, 161, 68, 115, 21, + 146, 84, 208, 177, 37, 131, 100, 53, 162, 224, 69, 6, 116, 193, 147, 85, + 22, 240, 132, 38, 178, 101, 163, 54, 209, 117, 70, 7, 148, 194, 86, 179, + 225, 23, 133, 39, 164, 8, 102, 210, 241, 55, 195, 118, 149, 71, 180, 24, + 87, 226, 134, 165, 211, 40, 103, 56, 72, 150, 196, 242, 119, 9, 181, 227, + 88, 166, 25, 135, 41, 104, 212, 57, 151, 197, 120, 73, 243, 182, 136, 167, + 213, 89, 10, 228, 105, 152, 198, 26, 42, 121, 183, 244, 168, 58, 137, 229, + 74, 214, 90, 153, 199, 184, 11, 106, 245, 27, 122, 230, 169, 43, 215, 59, + 200, 138, 185, 246, 75, 12, 91, 154, 216, 231, 107, 28, 44, 201, 123, 170, + 60, 247, 232, 76, 139, 13, 92, 217, 186, 248, 155, 108, 29, 124, 45, 202, + 233, 171, 61, 14, 77, 140, 15, 249, 93, 30, 187, 156, 218, 46, 109, 125, + 62, 172, 78, 203, 31, 141, 234, 94, 47, 188, 63, 157, 110, 250, 219, 79, + 126, 204, 173, 142, 95, 189, 111, 235, 158, 220, 251, 127, 174, 143, 205, 236, + 159, 190, 221, 252, 175, 206, 237, 191, 253, 222, 238, 207, 254, 223, 239, 255, +]; + +pub const ROW_SCAN_16X16: [u16; 256] = [ + 0, 1, 2, 16, 3, 17, 4, 18, 32, 5, 33, 19, 6, 34, 48, 20, + 49, 7, 35, 21, 50, 64, 8, 36, 65, 22, 51, 37, 80, 9, 66, 52, + 23, 38, 81, 67, 10, 53, 24, 82, 68, 96, 39, 11, 54, 83, 97, 69, + 25, 98, 84, 40, 112, 55, 12, 70, 99, 113, 85, 26, 41, 56, 114, 100, + 13, 71, 128, 86, 27, 115, 101, 129, 42, 57, 72, 116, 14, 87, 130, 102, + 144, 73, 131, 117, 28, 58, 15, 88, 43, 145, 103, 132, 146, 118, 74, 160, + 89, 133, 104, 29, 59, 147, 119, 44, 161, 148, 90, 105, 134, 162, 120, 176, + 75, 135, 149, 30, 60, 163, 177, 45, 121, 91, 106, 164, 178, 150, 192, 136, + 165, 179, 31, 151, 193, 76, 122, 61, 137, 194, 107, 152, 180, 208, 46, 166, + 167, 195, 92, 181, 138, 209, 123, 153, 224, 196, 77, 168, 210, 182, 240, 108, + 197, 62, 154, 225, 183, 169, 211, 47, 139, 93, 184, 226, 212, 241, 198, 170, + 124, 155, 199, 78, 213, 185, 109, 227, 200, 63, 228, 242, 140, 214, 171, 186, + 156, 229, 243, 125, 94, 201, 244, 215, 216, 230, 141, 187, 202, 79, 172, 110, + 157, 245, 217, 231, 95, 246, 232, 126, 203, 247, 233, 173, 218, 142, 111, 158, + 188, 248, 127, 234, 219, 249, 189, 204, 143, 174, 159, 250, 235, 205, 220, 175, + 190, 251, 221, 191, 206, 236, 207, 237, 252, 222, 253, 223, 238, 239, 254, 255, +]; + +pub const DEFAULT_SCAN_32X32: [u16; 1024] = [ + 0, 32, 1, 64, 33, 2, 96, 65, 34, 128, 3, 97, 66, 160, 129, 35, + 98, 4, 67, 130, 161, 192, 36, 99, 224, 5, 162, 193, 68, 131, 37, 100, + 225, 194, 256, 163, 69, 132, 6, 226, 257, 288, 195, 101, 164, 38, 258, 7, + 227, 289, 133, 320, 70, 196, 165, 290, 259, 228, 39, 321, 102, 352, 8, 197, + 71, 134, 322, 291, 260, 353, 384, 229, 166, 103, 40, 354, 323, 292, 135, 385, + 198, 261, 72, 9, 416, 167, 386, 355, 230, 324, 104, 293, 41, 417, 199, 136, + 262, 387, 448, 325, 356, 10, 73, 418, 231, 168, 449, 294, 388, 105, 419, 263, + 42, 200, 357, 450, 137, 480, 74, 326, 232, 11, 389, 169, 295, 420, 106, 451, + 481, 358, 264, 327, 201, 43, 138, 512, 482, 390, 296, 233, 170, 421, 75, 452, + 359, 12, 513, 265, 483, 328, 107, 202, 514, 544, 422, 391, 453, 139, 44, 234, + 484, 297, 360, 171, 76, 515, 545, 266, 329, 454, 13, 423, 203, 108, 546, 485, + 576, 298, 235, 140, 361, 330, 172, 547, 45, 455, 267, 577, 486, 77, 204, 362, + 608, 14, 299, 578, 109, 236, 487, 609, 331, 141, 579, 46, 15, 173, 610, 363, + 78, 205, 16, 110, 237, 611, 142, 47, 174, 79, 206, 17, 111, 238, 48, 143, + 80, 175, 112, 207, 49, 18, 239, 81, 113, 19, 50, 82, 114, 51, 83, 115, + 640, 516, 392, 268, 144, 20, 672, 641, 548, 517, 424, 393, 300, 269, 176, 145, + 52, 21, 704, 673, 642, 580, 549, 518, 456, 425, 394, 332, 301, 270, 208, 177, + 146, 84, 53, 22, 736, 705, 674, 643, 612, 581, 550, 519, 488, 457, 426, 395, + 364, 333, 302, 271, 240, 209, 178, 147, 116, 85, 54, 23, 737, 706, 675, 613, + 582, 551, 489, 458, 427, 365, 334, 303, 241, 210, 179, 117, 86, 55, 738, 707, + 614, 583, 490, 459, 366, 335, 242, 211, 118, 87, 739, 615, 491, 367, 243, 119, + 768, 644, 520, 396, 272, 148, 24, 800, 769, 676, 645, 552, 521, 428, 397, 304, + 273, 180, 149, 56, 25, 832, 801, 770, 708, 677, 646, 584, 553, 522, 460, 429, + 398, 336, 305, 274, 212, 181, 150, 88, 57, 26, 864, 833, 802, 771, 740, 709, + 678, 647, 616, 585, 554, 523, 492, 461, 430, 399, 368, 337, 306, 275, 244, 213, + 182, 151, 120, 89, 58, 27, 865, 834, 803, 741, 710, 679, 617, 586, 555, 493, + 462, 431, 369, 338, 307, 245, 214, 183, 121, 90, 59, 866, 835, 742, 711, 618, + 587, 494, 463, 370, 339, 246, 215, 122, 91, 867, 743, 619, 495, 371, 247, 123, + 896, 772, 648, 524, 400, 276, 152, 28, 928, 897, 804, 773, 680, 649, 556, 525, + 432, 401, 308, 277, 184, 153, 60, 29, 960, 929, 898, 836, 805, 774, 712, 681, + 650, 588, 557, 526, 464, 433, 402, 340, 309, 278, 216, 185, 154, 92, 61, 30, + 992, 961, 930, 899, 868, 837, 806, 775, 744, 713, 682, 651, 620, 589, 558, 527, + 496, 465, 434, 403, 372, 341, 310, 279, 248, 217, 186, 155, 124, 93, 62, 31, + 993, 962, 931, 869, 838, 807, 745, 714, 683, 621, 590, 559, 497, 466, 435, 373, + 342, 311, 249, 218, 187, 125, 94, 63, 994, 963, 870, 839, 746, 715, 622, 591, + 498, 467, 374, 343, 250, 219, 126, 95, 995, 871, 747, 623, 499, 375, 251, 127, + 900, 776, 652, 528, 404, 280, 156, 932, 901, 808, 777, 684, 653, 560, 529, 436, + 405, 312, 281, 188, 157, 964, 933, 902, 840, 809, 778, 716, 685, 654, 592, 561, + 530, 468, 437, 406, 344, 313, 282, 220, 189, 158, 996, 965, 934, 903, 872, 841, + 810, 779, 748, 717, 686, 655, 624, 593, 562, 531, 500, 469, 438, 407, 376, 345, + 314, 283, 252, 221, 190, 159, 997, 966, 935, 873, 842, 811, 749, 718, 687, 625, + 594, 563, 501, 470, 439, 377, 346, 315, 253, 222, 191, 998, 967, 874, 843, 750, + 719, 626, 595, 502, 471, 378, 347, 254, 223, 999, 875, 751, 627, 503, 379, 255, + 904, 780, 656, 532, 408, 284, 936, 905, 812, 781, 688, 657, 564, 533, 440, 409, + 316, 285, 968, 937, 906, 844, 813, 782, 720, 689, 658, 596, 565, 534, 472, 441, + 410, 348, 317, 286, 1000, 969, 938, 907, 876, 845, 814, 783, 752, 721, 690, 659, + 628, 597, 566, 535, 504, 473, 442, 411, 380, 349, 318, 287, 1001, 970, 939, 877, + 846, 815, 753, 722, 691, 629, 598, 567, 505, 474, 443, 381, 350, 319, 1002, 971, + 878, 847, 754, 723, 630, 599, 506, 475, 382, 351, 1003, 879, 755, 631, 507, 383, + 908, 784, 660, 536, 412, 940, 909, 816, 785, 692, 661, 568, 537, 444, 413, 972, + 941, 910, 848, 817, 786, 724, 693, 662, 600, 569, 538, 476, 445, 414, 1004, 973, + 942, 911, 880, 849, 818, 787, 756, 725, 694, 663, 632, 601, 570, 539, 508, 477, + 446, 415, 1005, 974, 943, 881, 850, 819, 757, 726, 695, 633, 602, 571, 509, 478, + 447, 1006, 975, 882, 851, 758, 727, 634, 603, 510, 479, 1007, 883, 759, 635, 511, + 912, 788, 664, 540, 944, 913, 820, 789, 696, 665, 572, 541, 976, 945, 914, 852, + 821, 790, 728, 697, 666, 604, 573, 542, 1008, 977, 946, 915, 884, 853, 822, 791, + 760, 729, 698, 667, 636, 605, 574, 543, 1009, 978, 947, 885, 854, 823, 761, 730, + 699, 637, 606, 575, 1010, 979, 886, 855, 762, 731, 638, 607, 1011, 887, 763, 639, + 916, 792, 668, 948, 917, 824, 793, 700, 669, 980, 949, 918, 856, 825, 794, 732, + 701, 670, 1012, 981, 950, 919, 888, 857, 826, 795, 764, 733, 702, 671, 1013, 982, + 951, 889, 858, 827, 765, 734, 703, 1014, 983, 890, 859, 766, 735, 1015, 891, 767, + 920, 796, 952, 921, 828, 797, 984, 953, 922, 860, 829, 798, 1016, 985, 954, 923, + 892, 861, 830, 799, 1017, 986, 955, 893, 862, 831, 1018, 987, 894, 863, 1019, 895, + 924, 956, 925, 988, 957, 926, 1020, 989, 958, 927, 1021, 990, 959, 1022, 991, 1023, +]; + +pub const COEFBAND_4X4: [u8; 16] = [ + 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, +]; + +pub const COEFBAND_8X8PLUS: [u8; 1024] = [ + 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, +]; + +pub const ENERGY_CLASS: [u8; 12] = [ + 0, 1, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5, +]; + +pub const PARETO_TABLE: [u8; 1024] = [ + 3, 86, 128, 6, 86, 23, 88, 29, + 9, 86, 129, 17, 88, 61, 94, 76, + 15, 87, 129, 28, 89, 93, 100, 110, + 20, 88, 130, 38, 91, 118, 106, 136, + 26, 89, 131, 48, 92, 139, 111, 156, + 31, 90, 131, 58, 94, 156, 117, 171, + 37, 90, 132, 66, 95, 171, 122, 184, + 42, 91, 132, 75, 97, 183, 127, 194, + 47, 92, 133, 83, 98, 193, 132, 202, + 52, 93, 133, 90, 100, 201, 137, 208, + 57, 94, 134, 98, 101, 208, 142, 214, + 62, 94, 135, 105, 103, 214, 146, 218, + 66, 95, 135, 111, 104, 219, 151, 222, + 71, 96, 136, 117, 106, 224, 155, 225, + 76, 97, 136, 123, 107, 227, 159, 228, + 80, 98, 137, 129, 109, 231, 162, 231, + 84, 98, 138, 134, 110, 234, 166, 233, + 89, 99, 138, 140, 112, 236, 170, 235, + 93, 100, 139, 145, 113, 238, 173, 236, + 97, 101, 140, 149, 115, 240, 176, 238, + 101, 102, 140, 154, 116, 242, 179, 239, + 105, 103, 141, 158, 118, 243, 182, 240, + 109, 104, 141, 162, 119, 244, 185, 241, + 113, 104, 142, 166, 120, 245, 187, 242, + 116, 105, 143, 170, 122, 246, 190, 243, + 120, 106, 143, 173, 123, 247, 192, 244, + 123, 107, 144, 177, 125, 248, 195, 244, + 127, 108, 145, 180, 126, 249, 197, 245, + 130, 109, 145, 183, 128, 249, 199, 245, + 134, 110, 146, 186, 129, 250, 201, 246, + 137, 111, 147, 189, 131, 251, 203, 246, + 140, 112, 147, 192, 132, 251, 205, 247, + 143, 113, 148, 194, 133, 251, 207, 247, + 146, 114, 149, 197, 135, 252, 208, 248, + 149, 115, 149, 199, 136, 252, 210, 248, + 152, 115, 150, 201, 138, 252, 211, 248, + 155, 116, 151, 204, 139, 253, 213, 249, + 158, 117, 151, 206, 140, 253, 214, 249, + 161, 118, 152, 208, 142, 253, 216, 249, + 163, 119, 153, 210, 143, 253, 217, 249, + 166, 120, 153, 212, 144, 254, 218, 250, + 168, 121, 154, 213, 146, 254, 220, 250, + 171, 122, 155, 215, 147, 254, 221, 250, + 173, 123, 155, 217, 148, 254, 222, 250, + 176, 124, 156, 218, 150, 254, 223, 250, + 178, 125, 157, 220, 151, 254, 224, 251, + 180, 126, 157, 221, 152, 254, 225, 251, + 183, 127, 158, 222, 153, 254, 226, 251, + 185, 128, 159, 224, 155, 255, 227, 251, + 187, 129, 160, 225, 156, 255, 228, 251, + 189, 131, 160, 226, 157, 255, 228, 251, + 191, 132, 161, 227, 159, 255, 229, 251, + 193, 133, 162, 228, 160, 255, 230, 252, + 195, 134, 163, 230, 161, 255, 231, 252, + 197, 135, 163, 231, 162, 255, 231, 252, + 199, 136, 164, 232, 163, 255, 232, 252, + 201, 137, 165, 233, 165, 255, 233, 252, + 202, 138, 166, 233, 166, 255, 233, 252, + 204, 139, 166, 234, 167, 255, 234, 252, + 206, 140, 167, 235, 168, 255, 235, 252, + 207, 141, 168, 236, 169, 255, 235, 252, + 209, 142, 169, 237, 171, 255, 236, 252, + 210, 144, 169, 237, 172, 255, 236, 252, + 212, 145, 170, 238, 173, 255, 237, 252, + 214, 146, 171, 239, 174, 255, 237, 253, + 215, 147, 172, 240, 175, 255, 238, 253, + 216, 148, 173, 240, 176, 255, 238, 253, + 218, 149, 173, 241, 177, 255, 239, 253, + 219, 150, 174, 241, 179, 255, 239, 253, + 220, 152, 175, 242, 180, 255, 240, 253, + 222, 153, 176, 242, 181, 255, 240, 253, + 223, 154, 177, 243, 182, 255, 240, 253, + 224, 155, 178, 244, 183, 255, 241, 253, + 225, 156, 178, 244, 184, 255, 241, 253, + 226, 158, 179, 244, 185, 255, 242, 253, + 228, 159, 180, 245, 186, 255, 242, 253, + 229, 160, 181, 245, 187, 255, 242, 253, + 230, 161, 182, 246, 188, 255, 243, 253, + 231, 163, 183, 246, 189, 255, 243, 253, + 232, 164, 184, 247, 190, 255, 243, 253, + 233, 165, 185, 247, 191, 255, 244, 253, + 234, 166, 185, 247, 192, 255, 244, 253, + 235, 168, 186, 248, 193, 255, 244, 253, + 236, 169, 187, 248, 194, 255, 244, 253, + 236, 170, 188, 248, 195, 255, 245, 253, + 237, 171, 189, 249, 196, 255, 245, 254, + 238, 173, 190, 249, 197, 255, 245, 254, + 239, 174, 191, 249, 198, 255, 245, 254, + 240, 175, 192, 249, 199, 255, 246, 254, + 240, 177, 193, 250, 200, 255, 246, 254, + 241, 178, 194, 250, 201, 255, 246, 254, + 242, 179, 195, 250, 202, 255, 246, 254, + 242, 181, 196, 250, 203, 255, 247, 254, + 243, 182, 197, 251, 204, 255, 247, 254, + 244, 184, 198, 251, 205, 255, 247, 254, + 244, 185, 199, 251, 206, 255, 247, 254, + 245, 186, 200, 251, 207, 255, 247, 254, + 246, 188, 201, 252, 207, 255, 248, 254, + 246, 189, 202, 252, 208, 255, 248, 254, + 247, 191, 203, 252, 209, 255, 248, 254, + 247, 192, 204, 252, 210, 255, 248, 254, + 248, 194, 205, 252, 211, 255, 248, 254, + 248, 195, 206, 252, 212, 255, 249, 254, + 249, 197, 207, 253, 213, 255, 249, 254, + 249, 198, 208, 253, 214, 255, 249, 254, + 250, 200, 210, 253, 215, 255, 249, 254, + 250, 201, 211, 253, 215, 255, 249, 254, + 250, 203, 212, 253, 216, 255, 249, 254, + 251, 204, 213, 253, 217, 255, 250, 254, + 251, 206, 214, 254, 218, 255, 250, 254, + 252, 207, 216, 254, 219, 255, 250, 254, + 252, 209, 217, 254, 220, 255, 250, 254, + 252, 211, 218, 254, 221, 255, 250, 254, + 253, 213, 219, 254, 222, 255, 250, 254, + 253, 214, 221, 254, 223, 255, 250, 254, + 253, 216, 222, 254, 224, 255, 251, 254, + 253, 218, 224, 254, 225, 255, 251, 254, + 254, 220, 225, 254, 225, 255, 251, 254, + 254, 222, 227, 255, 226, 255, 251, 254, + 254, 224, 228, 255, 227, 255, 251, 254, + 254, 226, 230, 255, 228, 255, 251, 254, + 255, 228, 231, 255, 230, 255, 251, 254, + 255, 230, 233, 255, 231, 255, 252, 254, + 255, 232, 235, 255, 232, 255, 252, 254, + 255, 235, 237, 255, 233, 255, 252, 254, + 255, 238, 240, 255, 235, 255, 252, 255, + 255, 241, 243, 255, 236, 255, 252, 254, + 255, 246, 247, 255, 239, 255, 253, 255, +]; + +pub const MV_REF_BLOCKS: [i8; 208] = [ + -1, 0, 0, -1, -1, -1, -2, 0, 0, -2, -2, -1, -1, -2, -2, -2, + -1, 0, 0, -1, -1, -1, -2, 0, 0, -2, -2, -1, -1, -2, -2, -2, + -1, 0, 0, -1, -1, -1, -2, 0, 0, -2, -2, -1, -1, -2, -2, -2, + -1, 0, 0, -1, -1, -1, -2, 0, 0, -2, -2, -1, -1, -2, -2, -2, + 0, -1, -1, 0, 1, -1, -1, -1, 0, -2, -2, 0, -2, -1, -1, -2, + -1, 0, 0, -1, -1, 1, -1, -1, -2, 0, 0, -2, -1, -2, -2, -1, + -1, 0, 0, -1, -1, 1, 1, -1, -1, -1, -3, 0, 0, -3, -3, -3, + 0, -1, -1, 0, 2, -1, -1, -1, -1, 1, 0, -3, -3, 0, -3, -3, + -1, 0, 0, -1, -1, 2, -1, -1, 1, -1, -3, 0, 0, -3, -3, -3, + -1, 1, 1, -1, -1, 2, 2, -1, -1, -1, -3, 0, 0, -3, -3, -3, + 0, -1, -1, 0, 4, -1, -1, 2, -1, -1, 0, -3, -3, 0, 2, -1, + -1, 0, 0, -1, -1, 4, 2, -1, -1, -1, -3, 0, 0, -3, -1, 2, + -1, 3, 3, -1, -1, 4, 4, -1, -1, -1, -1, 0, 0, -1, -1, 6, +]; + +pub const MODE_2_COUNTER: [u8; 14] = [ + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 3, 1, +]; + +pub const KF_PARTITION_PROBS: [u8; 48] = [ + 158, 97, 94, + 93, 24, 99, + 85, 119, 44, + 62, 59, 67, + 149, 53, 53, + 94, 20, 48, + 83, 53, 24, + 52, 18, 18, + 150, 40, 39, + 78, 12, 26, + 67, 33, 11, + 24, 7, 5, + 174, 35, 49, + 68, 11, 27, + 57, 15, 9, + 12, 3, 3, +]; + +pub const KF_Y_MODE_PROBS: [u8; 900] = [ + 137, 30, 42, 148, 151, 207, 70, 52, 91, + 92, 45, 102, 136, 116, 180, 74, 90, 100, + 73, 32, 19, 187, 222, 215, 46, 34, 100, + 91, 30, 32, 116, 121, 186, 93, 86, 94, + 72, 35, 36, 149, 68, 206, 68, 63, 105, + 73, 31, 28, 138, 57, 124, 55, 122, 151, + 67, 23, 21, 140, 126, 197, 40, 37, 171, + 86, 27, 28, 128, 154, 212, 45, 43, 53, + 74, 32, 27, 107, 86, 160, 63, 134, 102, + 59, 67, 44, 140, 161, 202, 78, 67, 119, + 63, 36, 126, 146, 123, 158, 60, 90, 96, + 43, 46, 168, 134, 107, 128, 69, 142, 92, + 44, 29, 68, 159, 201, 177, 50, 57, 77, + 58, 38, 76, 114, 97, 172, 78, 133, 92, + 46, 41, 76, 140, 63, 184, 69, 112, 57, + 38, 32, 85, 140, 46, 112, 54, 151, 133, + 39, 27, 61, 131, 110, 175, 44, 75, 136, + 52, 30, 74, 113, 130, 175, 51, 64, 58, + 47, 35, 80, 100, 74, 143, 64, 163, 74, + 36, 61, 116, 114, 128, 162, 80, 125, 82, + 82, 26, 26, 171, 208, 204, 44, 32, 105, + 55, 44, 68, 166, 179, 192, 57, 57, 108, + 42, 26, 11, 199, 241, 228, 23, 15, 85, + 68, 42, 19, 131, 160, 199, 55, 52, 83, + 58, 50, 25, 139, 115, 232, 39, 52, 118, + 50, 35, 33, 153, 104, 162, 64, 59, 131, + 44, 24, 16, 150, 177, 202, 33, 19, 156, + 55, 27, 12, 153, 203, 218, 26, 27, 49, + 53, 49, 21, 110, 116, 168, 59, 80, 76, + 38, 72, 19, 168, 203, 212, 50, 50, 107, + 103, 26, 36, 129, 132, 201, 83, 80, 93, + 59, 38, 83, 112, 103, 162, 98, 136, 90, + 62, 30, 23, 158, 200, 207, 59, 57, 50, + 67, 30, 29, 84, 86, 191, 102, 91, 59, + 60, 32, 33, 112, 71, 220, 64, 89, 104, + 53, 26, 34, 130, 56, 149, 84, 120, 103, + 53, 21, 23, 133, 109, 210, 56, 77, 172, + 77, 19, 29, 112, 142, 228, 55, 66, 36, + 61, 29, 29, 93, 97, 165, 83, 175, 162, + 47, 47, 43, 114, 137, 181, 100, 99, 95, + 69, 23, 29, 128, 83, 199, 46, 44, 101, + 53, 40, 55, 139, 69, 183, 61, 80, 110, + 40, 29, 19, 161, 180, 207, 43, 24, 91, + 60, 34, 19, 105, 61, 198, 53, 64, 89, + 52, 31, 22, 158, 40, 209, 58, 62, 89, + 44, 31, 29, 147, 46, 158, 56, 102, 198, + 35, 19, 12, 135, 87, 209, 41, 45, 167, + 55, 25, 21, 118, 95, 215, 38, 39, 66, + 51, 38, 25, 113, 58, 164, 70, 93, 97, + 47, 54, 34, 146, 108, 203, 72, 103, 151, + 64, 19, 37, 156, 66, 138, 49, 95, 133, + 46, 27, 80, 150, 55, 124, 55, 121, 135, + 36, 23, 27, 165, 149, 166, 54, 64, 118, + 53, 21, 36, 131, 63, 163, 60, 109, 81, + 40, 26, 35, 154, 40, 185, 51, 97, 123, + 35, 19, 34, 179, 19, 97, 48, 129, 124, + 36, 20, 26, 136, 62, 164, 33, 77, 154, + 45, 18, 32, 130, 90, 157, 40, 79, 91, + 45, 26, 28, 129, 45, 129, 49, 147, 123, + 38, 44, 51, 136, 74, 162, 57, 97, 121, + 75, 17, 22, 136, 138, 185, 32, 34, 166, + 56, 39, 58, 133, 117, 173, 48, 53, 187, + 35, 21, 12, 161, 212, 207, 20, 23, 145, + 56, 29, 19, 117, 109, 181, 55, 68, 112, + 47, 29, 17, 153, 64, 220, 59, 51, 114, + 46, 16, 24, 136, 76, 147, 41, 64, 172, + 34, 17, 11, 108, 152, 187, 13, 15, 209, + 51, 24, 14, 115, 133, 209, 32, 26, 104, + 55, 30, 18, 122, 79, 179, 44, 88, 116, + 37, 49, 25, 129, 168, 164, 41, 54, 148, + 82, 22, 32, 127, 143, 213, 39, 41, 70, + 62, 44, 61, 123, 105, 189, 48, 57, 64, + 47, 25, 17, 175, 222, 220, 24, 30, 86, + 68, 36, 17, 106, 102, 206, 59, 74, 74, + 57, 39, 23, 151, 68, 216, 55, 63, 58, + 49, 30, 35, 141, 70, 168, 82, 40, 115, + 51, 25, 15, 136, 129, 202, 38, 35, 139, + 68, 26, 16, 111, 141, 215, 29, 28, 28, + 59, 39, 19, 114, 75, 180, 77, 104, 42, + 40, 61, 26, 126, 152, 206, 61, 59, 93, + 78, 23, 39, 111, 117, 170, 74, 124, 94, + 48, 34, 86, 101, 92, 146, 78, 179, 134, + 47, 22, 24, 138, 187, 178, 68, 69, 59, + 56, 25, 33, 105, 112, 187, 95, 177, 129, + 48, 31, 27, 114, 63, 183, 82, 116, 56, + 43, 28, 37, 121, 63, 123, 61, 192, 169, + 42, 17, 24, 109, 97, 177, 56, 76, 122, + 58, 18, 28, 105, 139, 182, 70, 92, 63, + 46, 23, 32, 74, 86, 150, 67, 183, 88, + 36, 38, 48, 92, 122, 165, 88, 137, 91, + 65, 70, 60, 155, 159, 199, 61, 60, 81, + 44, 78, 115, 132, 119, 173, 71, 112, 93, + 39, 38, 21, 184, 227, 206, 42, 32, 64, + 58, 47, 36, 124, 137, 193, 80, 82, 78, + 49, 50, 35, 144, 95, 205, 63, 78, 59, + 41, 53, 52, 148, 71, 142, 65, 128, 51, + 40, 36, 28, 143, 143, 202, 40, 55, 137, + 52, 34, 29, 129, 183, 227, 42, 35, 43, + 42, 44, 44, 104, 105, 164, 64, 130, 80, + 43, 81, 53, 140, 169, 204, 68, 84, 72, +]; + +pub const KF_UV_MODE_PROBS: [u8; 90] = [ + 144, 11, 54, 157, 195, 130, 46, 58, 108, + 118, 15, 123, 148, 131, 101, 44, 93, 131, + 113, 12, 23, 188, 226, 142, 26, 32, 125, + 120, 11, 50, 123, 163, 135, 64, 77, 103, + 113, 9, 36, 155, 111, 157, 32, 44, 161, + 116, 9, 55, 176, 76, 96, 37, 61, 149, + 115, 9, 28, 141, 161, 167, 21, 25, 193, + 120, 12, 32, 145, 195, 142, 32, 38, 86, + 116, 12, 64, 120, 140, 125, 49, 115, 121, + 102, 19, 66, 162, 182, 122, 35, 59, 128, +]; + +pub const DEFAULT_PARTITION_PROBS: [u8; 48] = [ + 199, 122, 141, + 147, 63, 159, + 148, 133, 118, + 121, 104, 114, + 174, 73, 87, + 92, 41, 83, + 82, 99, 50, + 53, 39, 39, + 177, 58, 59, + 68, 26, 63, + 52, 79, 25, + 17, 14, 12, + 222, 34, 30, + 72, 16, 44, + 58, 32, 12, + 10, 7, 6, +]; + +pub const DEFAULT_Y_MODE_PROBS: [u8; 36] = [ + 65, 32, 18, 144, 162, 194, 41, 51, 98, + 132, 68, 18, 165, 217, 196, 45, 40, 78, + 173, 80, 19, 176, 240, 193, 64, 35, 46, + 221, 135, 38, 194, 248, 121, 96, 85, 29, +]; + +pub const DEFAULT_UV_MODE_PROBS: [u8; 90] = [ + 120, 7, 76, 176, 208, 126, 28, 54, 103, + 48, 12, 154, 155, 139, 90, 34, 117, 119, + 67, 6, 25, 204, 243, 158, 13, 21, 96, + 97, 5, 44, 131, 176, 139, 48, 68, 97, + 83, 5, 42, 156, 111, 152, 26, 49, 152, + 80, 5, 58, 178, 74, 83, 33, 62, 145, + 86, 5, 32, 154, 192, 168, 14, 22, 163, + 85, 5, 32, 156, 216, 148, 19, 29, 73, + 77, 7, 64, 116, 132, 122, 37, 126, 120, + 101, 21, 107, 181, 192, 103, 19, 67, 125, +]; + +pub const DEFAULT_SKIP_PROB: [u8; 3] = [ + 192, 128, 64, +]; + +pub const DEFAULT_IS_INTER_PROB: [u8; 4] = [ + 9, 102, 187, 225, +]; + +pub const DEFAULT_COMP_MODE_PROB: [u8; 5] = [ + 239, 183, 119, 96, 41, +]; + +pub const DEFAULT_COMP_REF_PROB: [u8; 5] = [ + 50, 126, 123, 221, 226, +]; + +pub const DEFAULT_SINGLE_REF_PROB: [u8; 10] = [ + 33, 16, + 77, 74, + 142, 142, + 172, 170, + 238, 247, +]; + +pub const DEFAULT_MV_SIGN_PROB: [u8; 2] = [ + 128, 128, +]; + +pub const DEFAULT_MV_BITS_PROB: [u8; 20] = [ + 136, 140, 148, 160, 176, 192, 224, 234, 234, 240, + 136, 140, 148, 160, 176, 192, 224, 234, 234, 240, +]; + +pub const DEFAULT_MV_CLASS0_BIT_PROB: [u8; 2] = [ + 216, 208, +]; + +pub const DEFAULT_TX_PROBS: [u8; 24] = [ + 0, 0, 0, + 0, 0, 0, + 100, 0, 0, + 66, 0, 0, + 20, 152, 0, + 15, 101, 0, + 3, 136, 37, + 5, 52, 13, +]; + +pub const DEFAULT_INTER_MODE_PROBS: [u8; 21] = [ + 2, 173, 34, + 7, 145, 85, + 7, 166, 63, + 7, 94, 66, + 8, 64, 46, + 17, 81, 31, + 25, 29, 30, +]; + +pub const DEFAULT_INTERP_FILTER_PROBS: [u8; 8] = [ + 235, 162, + 36, 255, + 34, 3, + 149, 144, +]; + +pub const DEFAULT_MV_JOINT_PROBS: [u8; 3] = [ + 32, 64, 96, +]; + +pub const DEFAULT_MV_CLASS_PROBS: [u8; 20] = [ + 224, 144, 192, 168, 192, 176, 192, 198, 198, 245, + 216, 128, 176, 160, 176, 176, 192, 198, 198, 208, +]; + +pub const DEFAULT_MV_CLASS0_FR_PROBS: [u8; 12] = [ + 128, 128, 64, + 96, 112, 64, + 128, 128, 64, + 96, 112, 64, +]; + +pub const DEFAULT_MV_CLASS0_HP_PROB: [u8; 2] = [ + 160, 160, +]; + +pub const DEFAULT_MV_FR_PROBS: [u8; 6] = [ + 64, 96, 64, + 64, 96, 64, +]; + +pub const DEFAULT_MV_HP_PROB: [u8; 2] = [ + 128, 128, +]; + +pub const DEFAULT_COEF_PROBS: [u8; 1728] = [ + 195, 29, 183, + 84, 49, 136, + 8, 42, 71, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 31, 107, 169, + 35, 99, 159, + 17, 82, 140, + 8, 66, 114, + 2, 44, 76, + 1, 19, 32, + 40, 132, 201, + 29, 114, 187, + 13, 91, 157, + 7, 75, 127, + 3, 58, 95, + 1, 28, 47, + 69, 142, 221, + 42, 122, 201, + 15, 91, 159, + 6, 67, 121, + 1, 42, 77, + 1, 17, 31, + 102, 148, 228, + 67, 117, 204, + 17, 82, 154, + 6, 59, 114, + 2, 39, 75, + 1, 15, 29, + 156, 57, 233, + 119, 57, 212, + 58, 48, 163, + 29, 40, 124, + 12, 30, 81, + 3, 12, 31, + 191, 107, 226, + 124, 117, 204, + 25, 99, 155, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 29, 148, 210, + 37, 126, 194, + 8, 93, 157, + 2, 68, 118, + 1, 39, 69, + 1, 17, 33, + 41, 151, 213, + 27, 123, 193, + 3, 82, 144, + 1, 58, 105, + 1, 32, 60, + 1, 13, 26, + 59, 159, 220, + 23, 126, 198, + 4, 88, 151, + 1, 66, 114, + 1, 38, 71, + 1, 18, 34, + 114, 136, 232, + 51, 114, 207, + 11, 83, 155, + 3, 56, 105, + 1, 33, 65, + 1, 17, 34, + 149, 65, 234, + 121, 57, 215, + 61, 49, 166, + 28, 36, 114, + 12, 25, 76, + 3, 16, 42, + 214, 49, 220, + 132, 63, 188, + 42, 65, 137, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 85, 137, 221, + 104, 131, 216, + 49, 111, 192, + 21, 87, 155, + 2, 49, 87, + 1, 16, 28, + 89, 163, 230, + 90, 137, 220, + 29, 100, 183, + 10, 70, 135, + 2, 42, 81, + 1, 17, 33, + 108, 167, 237, + 55, 133, 222, + 15, 97, 179, + 4, 72, 135, + 1, 45, 85, + 1, 19, 38, + 124, 146, 240, + 66, 124, 224, + 17, 88, 175, + 4, 58, 122, + 1, 36, 75, + 1, 18, 37, + 141, 79, 241, + 126, 70, 227, + 66, 58, 182, + 30, 44, 136, + 12, 34, 96, + 2, 20, 47, + 229, 99, 249, + 143, 111, 235, + 46, 109, 192, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 82, 158, 236, + 94, 146, 224, + 25, 117, 191, + 9, 87, 149, + 3, 56, 99, + 1, 33, 57, + 83, 167, 237, + 68, 145, 222, + 10, 103, 177, + 2, 72, 131, + 1, 41, 79, + 1, 20, 39, + 99, 167, 239, + 47, 141, 224, + 10, 104, 178, + 2, 73, 133, + 1, 44, 85, + 1, 22, 47, + 127, 145, 243, + 71, 129, 228, + 17, 93, 177, + 3, 61, 124, + 1, 41, 84, + 1, 21, 52, + 157, 78, 244, + 140, 72, 231, + 69, 58, 184, + 31, 44, 137, + 14, 38, 105, + 8, 23, 61, + 125, 34, 187, + 52, 41, 133, + 6, 31, 56, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 37, 109, 153, + 51, 102, 147, + 23, 87, 128, + 8, 67, 101, + 1, 41, 63, + 1, 19, 29, + 31, 154, 185, + 17, 127, 175, + 6, 96, 145, + 2, 73, 114, + 1, 51, 82, + 1, 28, 45, + 23, 163, 200, + 10, 131, 185, + 2, 93, 148, + 1, 67, 111, + 1, 41, 69, + 1, 14, 24, + 29, 176, 217, + 12, 145, 201, + 3, 101, 156, + 1, 69, 111, + 1, 39, 63, + 1, 14, 23, + 57, 192, 233, + 25, 154, 215, + 6, 109, 167, + 3, 78, 118, + 1, 48, 69, + 1, 21, 29, + 202, 105, 245, + 108, 106, 216, + 18, 90, 144, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 33, 172, 219, + 64, 149, 206, + 14, 117, 177, + 5, 90, 141, + 2, 61, 95, + 1, 37, 57, + 33, 179, 220, + 11, 140, 198, + 1, 89, 148, + 1, 60, 104, + 1, 33, 57, + 1, 12, 21, + 30, 181, 221, + 8, 141, 198, + 1, 87, 145, + 1, 58, 100, + 1, 31, 55, + 1, 12, 20, + 32, 186, 224, + 7, 142, 198, + 1, 86, 143, + 1, 58, 100, + 1, 31, 55, + 1, 12, 22, + 57, 192, 227, + 20, 143, 204, + 3, 96, 154, + 1, 68, 112, + 1, 42, 69, + 1, 19, 32, + 212, 35, 215, + 113, 47, 169, + 29, 48, 105, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 74, 129, 203, + 106, 120, 203, + 49, 107, 178, + 19, 84, 144, + 4, 50, 84, + 1, 15, 25, + 71, 172, 217, + 44, 141, 209, + 15, 102, 173, + 6, 76, 133, + 2, 51, 89, + 1, 24, 42, + 64, 185, 231, + 31, 148, 216, + 8, 103, 175, + 3, 74, 131, + 1, 46, 81, + 1, 18, 30, + 65, 196, 235, + 25, 157, 221, + 5, 105, 174, + 1, 67, 120, + 1, 38, 69, + 1, 15, 30, + 65, 204, 238, + 30, 156, 224, + 7, 107, 177, + 2, 70, 124, + 1, 42, 73, + 1, 18, 34, + 225, 86, 251, + 144, 104, 235, + 42, 99, 181, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 85, 175, 239, + 112, 165, 229, + 29, 136, 200, + 12, 103, 162, + 6, 77, 123, + 2, 53, 84, + 75, 183, 239, + 30, 155, 221, + 3, 106, 171, + 1, 74, 128, + 1, 44, 76, + 1, 17, 28, + 73, 185, 240, + 27, 159, 222, + 2, 107, 172, + 1, 75, 127, + 1, 42, 73, + 1, 17, 29, + 62, 190, 238, + 21, 159, 222, + 2, 107, 172, + 1, 72, 122, + 1, 40, 71, + 1, 18, 32, + 61, 199, 240, + 27, 161, 226, + 4, 113, 180, + 1, 76, 129, + 1, 46, 80, + 1, 23, 41, + 7, 27, 153, + 5, 30, 95, + 1, 16, 30, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 50, 75, 127, + 57, 75, 124, + 27, 67, 108, + 10, 54, 86, + 1, 33, 52, + 1, 12, 18, + 43, 125, 151, + 26, 108, 148, + 7, 83, 122, + 2, 59, 89, + 1, 38, 60, + 1, 17, 27, + 23, 144, 163, + 13, 112, 154, + 2, 75, 117, + 1, 50, 81, + 1, 31, 51, + 1, 14, 23, + 18, 162, 185, + 6, 123, 171, + 1, 78, 125, + 1, 51, 86, + 1, 31, 54, + 1, 14, 23, + 15, 199, 227, + 3, 150, 204, + 1, 91, 146, + 1, 55, 95, + 1, 30, 53, + 1, 11, 20, + 19, 55, 240, + 19, 59, 196, + 3, 52, 105, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 41, 166, 207, + 104, 153, 199, + 31, 123, 181, + 14, 101, 152, + 5, 72, 106, + 1, 36, 52, + 35, 176, 211, + 12, 131, 190, + 2, 88, 144, + 1, 60, 101, + 1, 36, 60, + 1, 16, 28, + 28, 183, 213, + 8, 134, 191, + 1, 86, 142, + 1, 56, 96, + 1, 30, 53, + 1, 12, 20, + 20, 190, 215, + 4, 135, 192, + 1, 84, 139, + 1, 53, 91, + 1, 28, 49, + 1, 11, 20, + 13, 196, 216, + 2, 137, 192, + 1, 86, 143, + 1, 57, 99, + 1, 32, 56, + 1, 13, 24, + 211, 29, 217, + 96, 47, 156, + 22, 43, 87, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 78, 120, 193, + 111, 116, 186, + 46, 102, 164, + 15, 80, 128, + 2, 49, 76, + 1, 18, 28, + 71, 161, 203, + 42, 132, 192, + 10, 98, 150, + 3, 69, 109, + 1, 44, 70, + 1, 18, 29, + 57, 186, 211, + 30, 140, 196, + 4, 93, 146, + 1, 62, 102, + 1, 38, 65, + 1, 16, 27, + 47, 199, 217, + 14, 145, 196, + 1, 88, 142, + 1, 57, 98, + 1, 36, 62, + 1, 15, 26, + 26, 219, 229, + 5, 155, 207, + 1, 94, 151, + 1, 60, 104, + 1, 36, 62, + 1, 16, 28, + 233, 29, 248, + 146, 47, 220, + 43, 52, 140, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 100, 163, 232, + 179, 161, 222, + 63, 142, 204, + 37, 113, 174, + 26, 89, 137, + 18, 68, 97, + 85, 181, 230, + 32, 146, 209, + 7, 100, 164, + 3, 71, 121, + 1, 45, 77, + 1, 18, 30, + 65, 187, 230, + 20, 148, 207, + 2, 97, 159, + 1, 68, 116, + 1, 40, 70, + 1, 14, 29, + 40, 194, 227, + 8, 147, 204, + 1, 94, 155, + 1, 65, 112, + 1, 39, 66, + 1, 14, 26, + 16, 208, 228, + 3, 151, 207, + 1, 98, 160, + 1, 67, 117, + 1, 41, 74, + 1, 17, 31, + 17, 38, 140, + 7, 34, 80, + 1, 17, 29, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 37, 75, 128, + 41, 76, 128, + 26, 66, 116, + 12, 52, 94, + 2, 32, 55, + 1, 10, 16, + 50, 127, 154, + 37, 109, 152, + 16, 82, 121, + 5, 59, 85, + 1, 35, 54, + 1, 13, 20, + 40, 142, 167, + 17, 110, 157, + 2, 71, 112, + 1, 44, 72, + 1, 27, 45, + 1, 11, 17, + 30, 175, 188, + 9, 124, 169, + 1, 74, 116, + 1, 48, 78, + 1, 30, 49, + 1, 11, 18, + 10, 222, 223, + 2, 150, 194, + 1, 83, 128, + 1, 48, 79, + 1, 27, 45, + 1, 11, 17, + 36, 41, 235, + 29, 36, 193, + 10, 27, 111, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 85, 165, 222, + 177, 162, 215, + 110, 135, 195, + 57, 113, 168, + 23, 83, 120, + 10, 49, 61, + 85, 190, 223, + 36, 139, 200, + 5, 90, 146, + 1, 60, 103, + 1, 38, 65, + 1, 18, 30, + 72, 202, 223, + 23, 141, 199, + 2, 86, 140, + 1, 56, 97, + 1, 36, 61, + 1, 16, 27, + 55, 218, 225, + 13, 145, 200, + 1, 86, 141, + 1, 57, 99, + 1, 35, 61, + 1, 13, 22, + 15, 235, 212, + 1, 132, 184, + 1, 84, 139, + 1, 57, 97, + 1, 34, 56, + 1, 14, 23, + 181, 21, 201, + 61, 37, 123, + 10, 38, 71, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 47, 106, 172, + 95, 104, 173, + 42, 93, 159, + 18, 77, 131, + 4, 50, 81, + 1, 17, 23, + 62, 147, 199, + 44, 130, 189, + 28, 102, 154, + 18, 75, 115, + 2, 44, 65, + 1, 12, 19, + 55, 153, 210, + 24, 130, 194, + 3, 93, 146, + 1, 61, 97, + 1, 31, 50, + 1, 10, 16, + 49, 186, 223, + 17, 148, 204, + 1, 96, 142, + 1, 53, 83, + 1, 26, 44, + 1, 11, 17, + 13, 217, 212, + 2, 136, 180, + 1, 78, 124, + 1, 50, 83, + 1, 29, 49, + 1, 14, 23, + 197, 13, 247, + 82, 17, 222, + 25, 17, 162, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 126, 186, 247, + 234, 191, 243, + 176, 177, 234, + 104, 158, 220, + 66, 128, 186, + 55, 90, 137, + 111, 197, 242, + 46, 158, 219, + 9, 104, 171, + 2, 65, 125, + 1, 44, 80, + 1, 17, 91, + 104, 208, 245, + 39, 168, 224, + 3, 109, 162, + 1, 79, 124, + 1, 50, 102, + 1, 43, 102, + 84, 220, 246, + 31, 177, 231, + 2, 115, 180, + 1, 79, 134, + 1, 55, 77, + 1, 60, 79, + 43, 243, 240, + 8, 180, 217, + 1, 115, 166, + 1, 84, 121, + 1, 51, 67, + 1, 16, 6, +]; + diff --git a/crates/video/src/vp9/decode/tables.rs b/crates/video/src/vp9/decode/tables.rs new file mode 100644 index 000000000..6aa6394a6 --- /dev/null +++ b/crates/video/src/vp9/decode/tables.rs @@ -0,0 +1,288 @@ +//! Constants, small lookup tables and decode trees of the VP9 specification (sections 3, 6, 9.3 +//! and 10.2). The large tables are extracted from the specification text into `spec_tables`. + +pub use crate::vp9::decode::spec_tables::*; + +// Block sizes (7.4.3). +pub const BLOCK_8X8: u8 = 3; +pub const BLOCK_16X16: u8 = 6; +pub const BLOCK_64X64: u8 = 12; +pub const BLOCK_INVALID: u8 = 14; + +// Transform sizes / modes. +pub const TX_4X4: u8 = 0; +pub const TX_8X8: u8 = 1; +pub const TX_16X16: u8 = 2; +pub const TX_32X32: u8 = 3; +pub const TX_MODE_SELECT: u8 = 4; + +// Prediction modes (7.4.5, 7.4.11). +pub const DC_PRED: u8 = 0; +pub const V_PRED: u8 = 1; +pub const H_PRED: u8 = 2; +pub const D45_PRED: u8 = 3; +pub const D135_PRED: u8 = 4; +pub const D117_PRED: u8 = 5; +pub const D153_PRED: u8 = 6; +pub const D207_PRED: u8 = 7; +pub const D63_PRED: u8 = 8; +pub const TM_PRED: u8 = 9; +pub const NEARESTMV: u8 = 10; +pub const NEARMV: u8 = 11; +pub const ZEROMV: u8 = 12; +pub const NEWMV: u8 = 13; + +// Reference frames. +pub const NONE: i8 = -1; +pub const INTRA_FRAME: i8 = 0; +pub const LAST_FRAME: i8 = 1; +pub const GOLDEN_FRAME: i8 = 2; +pub const ALTREF_FRAME: i8 = 3; + +// Transform types. +pub const DCT_DCT: u8 = 0; +pub const ADST_DCT: u8 = 1; +pub const DCT_ADST: u8 = 2; +pub const ADST_ADST: u8 = 3; + +// Interpolation filters. +pub const SWITCHABLE: u8 = 4; + +// Segmentation features. +pub const SEG_LVL_ALT_Q: usize = 0; +pub const SEG_LVL_ALT_L: usize = 1; +pub const SEG_LVL_REF_FRAME: usize = 2; +pub const SEG_LVL_SKIP: usize = 3; +pub const MAX_SEGMENTS: usize = 8; + +pub const MAX_LOOP_FILTER: i32 = 63; +pub const MIN_TILE_WIDTH_B64: u32 = 4; +pub const MAX_TILE_WIDTH_B64: u32 = 64; +pub const COMPANDED_MVREF_THRESH: i32 = 8; +pub const MV_BORDER: i32 = 128; +pub const INTERP_EXTEND: i32 = 4; +pub const BORDERINPIXELS: i32 = 160; +pub const COUNT_SAT: u32 = 20; +pub const MAX_UPDATE_FACTOR: u32 = 128; + +pub const B_WIDTH_LOG2: [u8; 13] = [0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4]; +pub const B_HEIGHT_LOG2: [u8; 13] = [0, 1, 0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4]; +pub const NUM_4X4_WIDE: [u8; 13] = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16]; +pub const NUM_4X4_HIGH: [u8; 13] = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16]; +pub const MI_WIDTH_LOG2: [u8; 13] = [0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3]; +pub const NUM_8X8_WIDE: [u8; 13] = [1, 1, 1, 1, 1, 2, 2, 2, 4, 4, 4, 8, 8]; +pub const NUM_8X8_HIGH: [u8; 13] = [1, 1, 1, 1, 2, 1, 2, 4, 2, 4, 8, 4, 8]; +pub const SIZE_GROUP: [u8; 13] = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3]; +pub const MAX_TXSIZE: [u8; 13] = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3]; +pub const TX_MODE_TO_BIGGEST_TX_SIZE: [u8; 5] = [0, 1, 2, 3, 3]; + +/// subsize_lookup[partition][bsize] (10.2). +pub const SUBSIZE_LOOKUP: [[u8; 13]; 4] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + [14, 14, 14, 2, 14, 14, 5, 14, 14, 8, 14, 14, 11], + [14, 14, 14, 1, 14, 14, 4, 14, 14, 7, 14, 14, 10], + [14, 14, 14, 0, 14, 14, 3, 14, 14, 6, 14, 14, 9], +]; + +/// ss_size_lookup[bsize][subsampling_x][subsampling_y] (6.4.23). +pub const SS_SIZE_LOOKUP: [[[u8; 2]; 2]; 13] = [ + [[0, 14], [14, 14]], + [[1, 0], [14, 14]], + [[2, 14], [0, 14]], + [[3, 2], [1, 0]], + [[4, 3], [14, 1]], + [[5, 14], [3, 2]], + [[6, 5], [4, 3]], + [[7, 6], [14, 4]], + [[8, 14], [6, 5]], + [[9, 8], [7, 6]], + [[10, 9], [14, 7]], + [[11, 14], [9, 8]], + [[12, 11], [10, 9]], +]; + +/// mode2txfm_map (10.2). +pub const MODE2TXFM: [u8; 14] = + [DCT_DCT, ADST_DCT, DCT_ADST, DCT_DCT, ADST_ADST, ADST_DCT, DCT_ADST, DCT_ADST, ADST_DCT, ADST_ADST, DCT_DCT, DCT_DCT, DCT_DCT, DCT_DCT]; + +/// counter_to_context (6.5.1); INVALID_CASE entries are 9. +pub const COUNTER_TO_CONTEXT: [u8; 19] = [2, 3, 4, 1, 3, 9, 0, 9, 9, 5, 5, 9, 5, 9, 9, 9, 9, 9, 6]; + +/// idx_n_column_to_subblock (6.5.11). +pub const IDX_N_COLUMN_TO_SUBBLOCK: [[u8; 2]; 4] = [[1, 2], [1, 3], [3, 2], [3, 3]]; + +/// literal_to_type (6.2.7). +pub const LITERAL_TO_TYPE: [u8; 4] = [1, 0, 2, 3]; + +pub const SEGMENTATION_FEATURE_BITS: [u32; 4] = [8, 6, 2, 0]; +pub const SEGMENTATION_FEATURE_SIGNED: [bool; 4] = [true, true, false, false]; + +/// extra_bits[token] = (cat, numExtra, base) (6.4.26). +pub const EXTRA_BITS: [(u8, u8, u16); 11] = + [(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3), (0, 0, 4), (1, 1, 5), (2, 2, 7), (3, 3, 11), (4, 4, 19), (5, 5, 35), (6, 14, 67)]; + +/// cat_probs (6.4.26). +pub const CAT_PROBS: [&[u8]; 7] = [ + &[], + &[159], + &[165, 145], + &[173, 148, 140], + &[176, 155, 140, 135], + &[180, 157, 141, 134, 130], + &[254, 254, 254, 252, 249, 243, 230, 196, 177, 153, 140, 133, 130, 129], +]; + +// Decode trees (9.3.1); leaves are stored negated. +pub const PARTITION_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3]; +pub const INTRA_MODE_TREE: [i8; 18] = [ + -(DC_PRED as i8), + 2, + -(TM_PRED as i8), + 4, + -(V_PRED as i8), + 6, + 8, + 12, + -(H_PRED as i8), + 10, + -(D135_PRED as i8), + -(D117_PRED as i8), + -(D45_PRED as i8), + 14, + -(D63_PRED as i8), + 16, + -(D153_PRED as i8), + -(D207_PRED as i8), +]; +pub const SEGMENT_TREE: [i8; 14] = [2, 4, 6, 8, 10, 12, 0, -1, -2, -3, -4, -5, -6, -7]; +pub const TX_SIZE_32_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3]; +pub const TX_SIZE_16_TREE: [i8; 4] = [0, 2, -1, -2]; +pub const TX_SIZE_8_TREE: [i8; 2] = [0, -1]; +/// inter_mode_tree: values are inter_mode = y_mode - NEARESTMV. +pub const INTER_MODE_TREE: [i8; 6] = [-2, 2, 0, 4, -1, -3]; +pub const INTERP_FILTER_TREE: [i8; 4] = [0, 2, -1, -2]; +pub const MV_JOINT_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3]; +pub const MV_CLASS_TREE: [i8; 20] = [0, 2, -1, 4, 6, 8, -2, -3, 10, 12, -4, -5, -6, 14, 16, 18, -7, -8, -9, -10]; +pub const MV_FR_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3]; +/// small_token_tree used by coefficient probability adaptation (8.4.3). +pub const SMALL_TOKEN_TREE: [i8; 6] = [0, 0, 0, 4, -1, -2]; +pub const BINARY_TREE: [i8; 2] = [0, -1]; + +/// Probabilities of the full token tree for each model probability (9.3.2 pareto): row +/// `p - 1` holds the probabilities of token-tree nodes 2..9 when `coef_probs[..][2] == p`. +pub fn pareto_full() -> &'static [[u8; 8]; 255] { + use std::sync::OnceLock; + static T: OnceLock<[[u8; 8]; 255]> = OnceLock::new(); + T.get_or_init(|| { + let mut t = [[0u8; 8]; 255]; + for p in 1..=255usize { + let x = (p - 1) / 2; + for n in 0..8 { + t[p - 1][n] = if p & 1 == 1 { + PARETO_TABLE[x * 8 + n] + } else { + ((PARETO_TABLE[x * 8 + n] as u32 + PARETO_TABLE[(x + 1) * 8 + n] as u32) >> 1) as u8 + }; + } + } + t + }) +} + +/// Scan order for a transform size and type (6.4.25). +pub fn scan(tx_size: u8, tx_type: u8) -> &'static [u16] { + match (tx_size, tx_type) { + (0, ADST_DCT) => &ROW_SCAN_4X4, + (0, DCT_ADST) => &COL_SCAN_4X4, + (0, _) => &DEFAULT_SCAN_4X4, + (1, ADST_DCT) => &ROW_SCAN_8X8, + (1, DCT_ADST) => &COL_SCAN_8X8, + (1, _) => &DEFAULT_SCAN_8X8, + (2, ADST_DCT) => &ROW_SCAN_16X16, + (2, DCT_ADST) => &COL_SCAN_16X16, + (2, _) => &DEFAULT_SCAN_16X16, + _ => &DEFAULT_SCAN_32X32, + } +} + +/// Coefficient context neighbours (9.3.2, `nb`) for every scan position of a transform size and +/// type: `[c] = (nb0, nb1)` positions in the block. +pub fn neighbors(tx_size: u8, tx_type: u8) -> &'static [(u16, u16)] { + use std::sync::OnceLock; + static T: OnceLock>> = OnceLock::new(); + let all = T.get_or_init(|| { + let mut v = Vec::new(); + for tx in 0..4u8 { + for ty in 0..4u8 { + let sc = scan(tx, ty); + let n = 4usize << tx; + let mut nb = vec![(0u16, 0u16); sc.len()]; + for (c, &pos) in sc.iter().enumerate().skip(1) { + let pos = pos as usize; + let (i, j) = (pos / n, pos % n); + nb[c] = if i > 0 && j > 0 { + let a = ((i - 1) * n + j) as u16; + let a2 = (i * n + j - 1) as u16; + // Only the 4x4..16x16 scans depend on the type; 32x32 is always DCT_DCT. + let ty = if tx == 3 { DCT_DCT } else { ty }; + match ty { + DCT_ADST => (a, a), + ADST_DCT => (a2, a2), + _ => (a, a2), + } + } else if i > 0 { + let a = ((i - 1) * n + j) as u16; + (a, a) + } else { + let a = (i * n + j - 1) as u16; + (a, a) + }; + } + v.push(nb); + } + } + v + }); + &all[(tx_size * 4 + tx_type) as usize] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn trees_are_consistent() { + // Every leaf value appears exactly once. + fn leaves(t: &[i8]) -> Vec { + let mut v: Vec = + t.iter().enumerate().filter(|(i, x)| **x <= 0 && !(*i == 0 && t.len() > 2 && t[0] == 0 && t[1] == 0)).map(|(_, x)| -x).collect(); + v.sort(); + v + } + assert_eq!(leaves(&INTRA_MODE_TREE), (0..10).collect::>()); + assert_eq!(leaves(&MV_CLASS_TREE), (0..11).collect::>()); + assert_eq!(leaves(&PARTITION_TREE), (0..4).collect::>()); + assert_eq!(leaves(&INTER_MODE_TREE), (0..4).collect::>()); + } + + #[test] + fn pareto_rows() { + let p = pareto_full(); + assert_eq!(p[0], [3, 86, 128, 6, 86, 23, 88, 29]); + assert_eq!(p[254][..], PARETO_TABLE[127 * 8..128 * 8]); + } + + #[test] + fn default_coef_probs_unused_are_zero() { + for (k, chunk) in DEFAULT_COEF_PROBS.chunks(3 * 6).enumerate() { + let band = k % 6; + for (ctx, p) in chunk.chunks(3).enumerate() { + if band == 0 && ctx >= 3 { + assert_eq!(p, [0, 0, 0]); + } else { + assert!(p.iter().all(|&x| x > 0), "band {band} ctx {ctx}"); + } + } + } + } +} diff --git a/crates/video/src/vp9/decode/tile.rs b/crates/video/src/vp9/decode/tile.rs new file mode 100644 index 000000000..ab10ce6cc --- /dev/null +++ b/crates/video/src/vp9/decode/tile.rs @@ -0,0 +1,1097 @@ +//! Tile decoding: partition trees, mode info (6.4), and block reconstruction. One `TileDecoder` +//! decodes all tiles of one tile column (tile columns are independent of each other; tile rows +//! of a column are decoded in order). + +use crate::vp9::decode::boolcoder::BoolDecoder; +use crate::vp9::decode::frame::{Frame, MiGrid, MiInfo, Mv}; +use crate::vp9::decode::header::{FrameHeader, Segmentation}; +use crate::vp9::decode::probs::{Counts, FrameContext}; +use crate::vp9::decode::tables::{ + ALTREF_FRAME, B_HEIGHT_LOG2, B_WIDTH_LOG2, BLOCK_8X8, BLOCK_64X64, BLOCK_INVALID, BORDERINPIXELS, COMPANDED_MVREF_THRESH, COUNTER_TO_CONTEXT, + DC_PRED, GOLDEN_FRAME, IDX_N_COLUMN_TO_SUBBLOCK, INTER_MODE_TREE, INTERP_EXTEND, INTERP_FILTER_TREE, INTRA_FRAME, INTRA_MODE_TREE, + KF_PARTITION_PROBS, KF_UV_MODE_PROBS, KF_Y_MODE_PROBS, LAST_FRAME, MAX_TXSIZE, MI_WIDTH_LOG2, MODE_2_COUNTER, MV_BORDER, MV_CLASS_TREE, + MV_FR_TREE, MV_JOINT_TREE, MV_REF_BLOCKS, NEARESTMV, NEARMV, NEWMV, NONE, NUM_4X4_HIGH, NUM_4X4_WIDE, NUM_8X8_HIGH, NUM_8X8_WIDE, PARTITION_TREE, + SEG_LVL_REF_FRAME, SEG_LVL_SKIP, SEGMENT_TREE, SIZE_GROUP, SS_SIZE_LOOKUP, SUBSIZE_LOOKUP, SWITCHABLE, TX_MODE_SELECT, + TX_MODE_TO_BIGGEST_TX_SIZE, TX_SIZE_8_TREE, TX_SIZE_16_TREE, TX_SIZE_32_TREE, ZEROMV, +}; + +mod recon; + +/// A reference frame as used by the current frame, with its scale factors (8.5.2.3). +pub struct RefUse<'a> { + pub frame: &'a Frame, + pub x_scale: i32, + pub y_scale: i32, + pub x_step: i32, + pub y_step: i32, +} + +/// Read-only data shared by all tile columns of a frame. +pub struct FrameShared<'a> { + pub h: &'a FrameHeader, + pub fc: &'a FrameContext, + pub seg: &'a Segmentation, + /// PrevSegmentIds (MiRows x MiCols). + pub prev_seg_ids: &'a [u8], + /// Mode info of the previous frame when UsePrevFrameMvs is 1. + pub prev_mi: Option<&'a MiGrid>, + /// References for LAST, GOLDEN, ALTREF. + pub refs: [Option>; 3], + /// Quantizers per segment: [segment][plane > 0][dc = 0 / ac = 1]. + pub seg_q: [[[i32; 2]; 2]; 8], + /// Whether syntax elements need to be counted (backward adaptation enabled). + pub counting: bool, + pub pools: &'a crate::vp9::decode::frame::Pools, +} + +/// Output of one tile column: samples, mode info and segment ids of its columns. +pub struct Strip { + pub mi_col_start: usize, + pub mi_col_end: usize, + /// Mode info, (mi_col_end - mi_col_start) x MiRows. + pub mi: Vec, + pub seg_ids: Vec, + pub planes: [Vec; 3], + pub strides: [usize; 3], + /// Plane x coordinate of the first column of each plane buffer. + pub x_off: [usize; 3], + pub counts: Box, + /// Statistics. + pub compound_blocks: u64, + pub scaled_blocks: u64, + pub intra_blocks: u64, + pub inter_blocks: u64, +} + +/// Per-block state (the variables of 6.4.4 - 6.4.20). +#[derive(Clone, Copy, Default)] +struct Blk { + r: usize, + c: usize, + size: u8, + avail_u: bool, + avail_l: bool, + seg_id: u8, + skip: bool, + tx_size: u8, + is_inter: bool, + y_mode: u8, + sub_modes: [u8; 4], + uv_mode: u8, + ref_frame: [i8; 2], + interp_filter: u8, + block_mvs: [[Mv; 4]; 2], + left_ref: [i8; 2], + above_ref: [i8; 2], + left_intra: bool, + above_intra: bool, + left_single: bool, + above_single: bool, + eob_total: u32, +} + +pub struct TileDecoder<'a> { + s: &'a FrameShared<'a>, + h: &'a FrameHeader, + fc: &'a FrameContext, + pub strip: Strip, + mi_cols: usize, + mi_rows: usize, + ss_x: usize, + ss_y: usize, + bit_depth: u8, + // Contexts (indexed by absolute position). + above_nonzero: [Vec; 3], + left_nonzero: [Vec; 3], + above_partition: Vec, + left_partition: Vec, + above_seg_pred: Vec, + left_seg_pred: Vec, + // MV prediction state. + ref_list_mv: [Mv; 2], + ref_mv_count: usize, + mode_context: [u8; 4], + nearest_mv: [Mv; 2], + near_mv: [Mv; 2], + best_mv: [Mv; 2], + // Scratch. + coefs: Vec, + token_cache: Vec, + pred: Vec, + pred2: Vec, + mc_tmp: Vec, + mc_win: Vec, + /// First error met while decoding (e.g. a block referencing an unusable reference frame). + pub error: Option, +} + +impl<'a> TileDecoder<'a> { + pub fn new(s: &'a FrameShared<'a>, mi_col_start: usize, mi_col_end: usize) -> Self { + let h = s.h; + let (mi_cols, mi_rows) = (h.mi_cols as usize, h.mi_rows as usize); + let (ss_x, ss_y) = (h.color.subsampling_x as usize, h.color.subsampling_y as usize); + let sb_rows = h.sb64_rows as usize; + let sb_cols = h.sb64_cols as usize; + let strip_w = (mi_col_end - mi_col_start).div_ceil(8) * 64; + let strip_h = sb_rows * 64; + let x0 = mi_col_start * 8; + let uv_len = (strip_w >> ss_x) * (strip_h >> ss_y); + // Every sample a later stage reads (inside the mode info grid) is predicted before use, so + // recycled planes need no clearing. + let planes = [s.pools.samples.take_any(strip_w * strip_h, 0), s.pools.samples.take_any(uv_len, 0), s.pools.samples.take_any(uv_len, 0)]; + let mi_w = mi_col_end - mi_col_start; + let strip = Strip { + mi_col_start, + mi_col_end, + // Written for every position of the grid before it is read. + mi: s.pools.mi.take_any(mi_w * mi_rows, MiInfo::default()), + seg_ids: vec![0; mi_w * mi_rows], + planes, + strides: [strip_w, strip_w >> ss_x, strip_w >> ss_x], + x_off: [x0, x0 >> ss_x, x0 >> ss_x], + counts: Box::default(), + compound_blocks: 0, + scaled_blocks: 0, + intra_blocks: 0, + inter_blocks: 0, + }; + let an = sb_cols * 16 + 16; + let ln = sb_rows * 16 + 16; + TileDecoder { + s, + h, + fc: s.fc, + strip, + mi_cols, + mi_rows, + ss_x, + ss_y, + bit_depth: h.color.bit_depth, + above_nonzero: [vec![0; an], vec![0; an], vec![0; an]], + left_nonzero: [vec![0; ln], vec![0; ln], vec![0; ln]], + above_partition: vec![0; sb_cols * 8 + 8], + left_partition: vec![0; sb_rows * 8 + 8], + above_seg_pred: vec![0; sb_cols * 8 + 8], + left_seg_pred: vec![0; sb_rows * 8 + 8], + ref_list_mv: [Mv::ZERO; 2], + ref_mv_count: 0, + mode_context: [0; 4], + nearest_mv: [Mv::ZERO; 2], + near_mv: [Mv::ZERO; 2], + best_mv: [Mv::ZERO; 2], + coefs: vec![0; 1024], + token_cache: vec![0; 1024], + pred: vec![0; 64 * 64], + pred2: vec![0; 64 * 64], + // Scaled prediction reads up to 5x the block height (8.5.2.3), unscaled h + 7 rows. + mc_tmp: vec![0; if s.refs.iter().flatten().any(|r| r.x_scale != 1 << 14 || r.y_scale != 1 << 14) { 64 * (64 * 5 + 16) } else { 64 * 71 }], + mc_win: vec![0; 71 * 71], + error: None, + } + } + + #[inline] + fn mi_at(&self, r: usize, c: usize) -> &MiInfo { + &self.strip.mi[r * (self.strip.mi_col_end - self.strip.mi_col_start) + c - self.strip.mi_col_start] + } + + /// decode_tile (6.4.2) for one tile of this column. Returns false if the data ran out badly. + pub fn decode_tile(&mut self, data: &[u8], mi_row_start: usize, mi_row_end: usize) -> bool { + let mut bd = BoolDecoder::new(data); + let (cs, ce) = (self.strip.mi_col_start, self.strip.mi_col_end); + for r in (mi_row_start..mi_row_end).step_by(8) { + // clear_left_context + for p in 0..3 { + let sy = if p > 0 { self.ss_y } else { 0 }; + let s = (r * 2) >> sy; + let e = (s + (16 >> sy)).min(self.left_nonzero[p].len()); + self.left_nonzero[p][s..e].fill(0); + } + self.left_partition[r..r + 8].fill(0); + self.left_seg_pred[r..r + 8].fill(0); + for c in (cs..ce).step_by(8) { + self.decode_partition(&mut bd, r, c, BLOCK_64X64); + } + } + true + } + + fn decode_partition(&mut self, bd: &mut BoolDecoder, r: usize, c: usize, bsize: u8) { + if r >= self.mi_rows || c >= self.mi_cols { + return; + } + let num8x8 = NUM_8X8_WIDE[bsize as usize] as usize; + let half = num8x8 >> 1; + let has_rows = (r + half) < self.mi_rows; + let has_cols = (c + half) < self.mi_cols; + let bsl = MI_WIDTH_LOG2[bsize as usize] as usize; + let boffset = 3 - bsl; + let mut above = 0u8; + let mut left = 0u8; + for i in 0..num8x8 { + above |= self.above_partition[c + i]; + left |= self.left_partition[r + i]; + } + let above = (above >> boffset) & 1; + let left = (left >> boffset) & 1; + let ctx = bsl * 4 + left as usize * 2 + above as usize; + let probs: &[u8] = if self.h.frame_is_intra { &KF_PARTITION_PROBS[ctx * 3..ctx * 3 + 3] } else { &self.fc.partition[ctx] }; + let partition = if has_rows && has_cols { + bd.read_tree(&PARTITION_TREE, probs) + } else if has_cols { + if bd.read_bool(probs[1]) { 3 } else { 1 } + } else if has_rows { + if bd.read_bool(probs[2]) { 3 } else { 2 } + } else { + 3 + }; + if self.s.counting { + self.strip.counts.partition[ctx][partition as usize] += 1; + } + let subsize = SUBSIZE_LOOKUP[partition as usize][bsize as usize]; + if subsize < BLOCK_8X8 || partition == 0 { + self.decode_block(bd, r, c, subsize); + } else if partition == 1 { + self.decode_block(bd, r, c, subsize); + if has_rows { + self.decode_block(bd, r + half, c, subsize); + } + } else if partition == 2 { + self.decode_block(bd, r, c, subsize); + if has_cols { + self.decode_block(bd, r, c + half, subsize); + } + } else { + self.decode_partition(bd, r, c, subsize); + self.decode_partition(bd, r, c + half, subsize); + self.decode_partition(bd, r + half, c, subsize); + self.decode_partition(bd, r + half, c + half, subsize); + } + if bsize == BLOCK_8X8 || partition != 3 { + let a = 15 >> B_WIDTH_LOG2[subsize as usize]; + let l = 15 >> B_HEIGHT_LOG2[subsize as usize]; + self.above_partition[c..c + num8x8].fill(a); + self.left_partition[r..r + num8x8].fill(l); + } + } + + fn decode_block(&mut self, bd: &mut BoolDecoder, r: usize, c: usize, size: u8) { + let mut b = Blk { r, c, size, avail_u: r > 0, avail_l: c > self.strip.mi_col_start, ..Default::default() }; + // Chroma block size must be valid (7.4.3); treat violations as a damaged stream. + if size >= BLOCK_8X8 && SS_SIZE_LOOKUP[size as usize][self.ss_x][self.ss_y] == BLOCK_INVALID { + b.size = BLOCK_8X8; + } + if self.h.frame_is_intra { + self.intra_frame_mode_info(bd, &mut b); + } else { + self.inter_frame_mode_info(bd, &mut b); + } + if b.is_inter { + self.strip.inter_blocks += 1; + } else { + self.strip.intra_blocks += 1; + } + self.residual(bd, &mut b); + if b.is_inter && b.size >= BLOCK_8X8 && b.eob_total == 0 { + b.skip = true; + } + let info = MiInfo { + sb_size: b.size, + skip: b.skip, + tx_size: b.tx_size, + y_mode: b.y_mode, + sub_modes: b.sub_modes, + seg_id: b.seg_id, + ref_frame: b.ref_frame, + interp_filter: b.interp_filter, + mv: b.block_mvs, + }; + let bh = NUM_8X8_HIGH[b.size as usize] as usize; + let bw = NUM_8X8_WIDE[b.size as usize] as usize; + let mi_w = self.strip.mi_col_end - self.strip.mi_col_start; + let (y1, x1) = ((r + bh).min(self.mi_rows), (c + bw).min(self.strip.mi_col_end)); + for y in r..y1 { + let row = y * mi_w; + for x in c..x1 { + self.strip.mi[row + x - self.strip.mi_col_start] = info; + self.strip.seg_ids[row + x - self.strip.mi_col_start] = b.seg_id; + } + } + } + + // ---------------------------------------------------------------- mode info (6.4.5 - 6.4.20) + + fn intra_frame_mode_info(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + // intra_segment_id + b.seg_id = if self.s.seg.enabled && self.s.seg.update_map { bd.read_tree(&SEGMENT_TREE, &self.s.seg.tree_probs) } else { 0 }; + self.read_skip(bd, b); + self.read_tx_size(bd, b, true); + b.ref_frame = [INTRA_FRAME, NONE]; + b.is_inter = false; + let above_mi = if b.avail_u { Some(*self.mi_at(b.r - 1, b.c)) } else { None }; + let left_mi = if b.avail_l { Some(*self.mi_at(b.r, b.c - 1)) } else { None }; + if b.size >= BLOCK_8X8 { + let above = above_mi.map_or(DC_PRED, |m| m.sub_modes[2]); + let left = left_mi.map_or(DC_PRED, |m| m.sub_modes[1]); + let o = (above as usize * 10 + left as usize) * 9; + let mode = bd.read_tree(&INTRA_MODE_TREE, &KF_Y_MODE_PROBS[o..o + 9]); + b.y_mode = mode; + b.sub_modes = [mode; 4]; + } else { + let n4w = NUM_4X4_WIDE[b.size as usize] as usize; + let n4h = NUM_4X4_HIGH[b.size as usize] as usize; + let mut mode = 0; + let mut idy = 0; + while idy < 2 { + let mut idx = 0; + while idx < 2 { + let above = if idy > 0 { b.sub_modes[idx] } else { above_mi.map_or(DC_PRED, |m| m.sub_modes[2 + idx]) }; + let left = if idx > 0 { b.sub_modes[idy * 2] } else { left_mi.map_or(DC_PRED, |m| m.sub_modes[1 + idy * 2]) }; + let o = (above as usize * 10 + left as usize) * 9; + mode = bd.read_tree(&INTRA_MODE_TREE, &KF_Y_MODE_PROBS[o..o + 9]); + for y2 in 0..n4h { + for x2 in 0..n4w { + b.sub_modes[(idy + y2) * 2 + idx + x2] = mode; + } + } + idx += n4w; + } + idy += n4h; + } + b.y_mode = mode; + } + let o = b.y_mode as usize * 9; + b.uv_mode = bd.read_tree(&INTRA_MODE_TREE, &KF_UV_MODE_PROBS[o..o + 9]); + } + + fn read_skip(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + if self.s.seg.feature_active(b.seg_id, SEG_LVL_SKIP) { + b.skip = true; + } else { + let mut ctx = 0; + if b.avail_u { + ctx += self.mi_at(b.r - 1, b.c).skip as usize; + } + if b.avail_l { + ctx += self.mi_at(b.r, b.c - 1).skip as usize; + } + b.skip = bd.read_bool(self.fc.skip[ctx]); + if self.s.counting { + self.strip.counts.skip[ctx][b.skip as usize] += 1; + } + } + } + + fn read_tx_size(&mut self, bd: &mut BoolDecoder, b: &mut Blk, allow_select: bool) { + let max_tx = MAX_TXSIZE[b.size as usize]; + if allow_select && self.h.tx_mode == TX_MODE_SELECT && b.size >= BLOCK_8X8 { + let mut above = max_tx; + let mut left = max_tx; + if b.avail_u { + let m = self.mi_at(b.r - 1, b.c); + if !m.skip { + above = m.tx_size; + } + } + if b.avail_l { + let m = self.mi_at(b.r, b.c - 1); + if !m.skip { + left = m.tx_size; + } + } + if !b.avail_l { + left = above; + } + if !b.avail_u { + above = left; + } + let ctx = ((above + left) > max_tx) as usize; + let tree: &[i8] = match max_tx { + 3 => &TX_SIZE_32_TREE, + 2 => &TX_SIZE_16_TREE, + _ => &TX_SIZE_8_TREE, + }; + b.tx_size = bd.read_tree(tree, &self.fc.tx[max_tx as usize][ctx]); + if self.s.counting { + self.strip.counts.tx[max_tx as usize][ctx][b.tx_size as usize] += 1; + } + } else { + b.tx_size = max_tx.min(TX_MODE_TO_BIGGEST_TX_SIZE[self.h.tx_mode as usize]); + } + } + + fn inter_frame_mode_info(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + b.left_ref = if b.avail_l { self.mi_at(b.r, b.c - 1).ref_frame } else { [INTRA_FRAME, NONE] }; + b.above_ref = if b.avail_u { self.mi_at(b.r - 1, b.c).ref_frame } else { [INTRA_FRAME, NONE] }; + b.left_intra = b.left_ref[0] <= INTRA_FRAME; + b.above_intra = b.above_ref[0] <= INTRA_FRAME; + b.left_single = b.left_ref[1] <= NONE; + b.above_single = b.above_ref[1] <= NONE; + self.inter_segment_id(bd, b); + self.read_skip(bd, b); + self.read_is_inter(bd, b); + let allow = !b.skip || !b.is_inter; + self.read_tx_size(bd, b, allow); + if b.is_inter { + self.inter_block_mode_info(bd, b); + } else { + self.intra_block_mode_info(bd, b); + } + } + + fn get_segment_id(&self, b: &Blk) -> u8 { + let bw = NUM_8X8_WIDE[b.size as usize] as usize; + let bh = NUM_8X8_HIGH[b.size as usize] as usize; + let xmis = (self.mi_cols - b.c).min(bw); + let ymis = (self.mi_rows - b.r).min(bh); + let mut seg = 7; + let prev = self.s.prev_seg_ids; + if prev.is_empty() { + return 0; + } + for y in 0..ymis { + for x in 0..xmis { + seg = seg.min(prev[(b.r + y) * self.mi_cols + b.c + x]); + } + } + seg + } + + fn inter_segment_id(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + let seg = self.s.seg; + if !seg.enabled { + b.seg_id = 0; + return; + } + let predicted = self.get_segment_id(b); + if !seg.update_map { + b.seg_id = predicted; + return; + } + if seg.temporal_update { + let ctx = (self.left_seg_pred[b.r] + self.above_seg_pred[b.c]) as usize; + let pred_flag = bd.read_bool(seg.pred_probs[ctx]); + b.seg_id = if pred_flag { predicted } else { bd.read_tree(&SEGMENT_TREE, &seg.tree_probs) }; + let bw = NUM_8X8_WIDE[b.size as usize] as usize; + let bh = NUM_8X8_HIGH[b.size as usize] as usize; + self.above_seg_pred[b.c..b.c + bw].fill(pred_flag as u8); + self.left_seg_pred[b.r..b.r + bh].fill(pred_flag as u8); + } else { + b.seg_id = bd.read_tree(&SEGMENT_TREE, &seg.tree_probs); + } + } + + fn read_is_inter(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + if self.s.seg.feature_active(b.seg_id, SEG_LVL_REF_FRAME) { + b.is_inter = self.s.seg.feature_data[b.seg_id as usize][SEG_LVL_REF_FRAME] != INTRA_FRAME as i16; + } else { + let ctx = if b.avail_u && b.avail_l { + if b.left_intra && b.above_intra { 3 } else { (b.left_intra || b.above_intra) as usize } + } else if b.avail_u || b.avail_l { + 2 * (if b.avail_u { b.above_intra } else { b.left_intra }) as usize + } else { + 0 + }; + b.is_inter = bd.read_bool(self.fc.is_inter[ctx]); + if self.s.counting { + self.strip.counts.is_inter[ctx][b.is_inter as usize] += 1; + } + } + } + + fn intra_block_mode_info(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + b.ref_frame = [INTRA_FRAME, NONE]; + if b.size >= BLOCK_8X8 { + let ctx = SIZE_GROUP[b.size as usize] as usize; + let mode = bd.read_tree(&INTRA_MODE_TREE, &self.fc.y_mode[ctx]); + if self.s.counting { + self.strip.counts.intra_mode[ctx][mode as usize] += 1; + } + b.y_mode = mode; + b.sub_modes = [mode; 4]; + } else { + let n4w = NUM_4X4_WIDE[b.size as usize] as usize; + let n4h = NUM_4X4_HIGH[b.size as usize] as usize; + let mut mode = 0; + let mut idy = 0; + while idy < 2 { + let mut idx = 0; + while idx < 2 { + mode = bd.read_tree(&INTRA_MODE_TREE, &self.fc.y_mode[0]); + if self.s.counting { + self.strip.counts.intra_mode[0][mode as usize] += 1; + } + for y2 in 0..n4h { + for x2 in 0..n4w { + b.sub_modes[(idy + y2) * 2 + idx + x2] = mode; + } + } + idx += n4w; + } + idy += n4h; + } + b.y_mode = mode; + } + b.uv_mode = bd.read_tree(&INTRA_MODE_TREE, &self.fc.uv_mode[b.y_mode as usize]); + if self.s.counting { + self.strip.counts.uv_mode[b.y_mode as usize][b.uv_mode as usize] += 1; + } + } + + fn inter_block_mode_info(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + self.read_ref_frames(bd, b); + let is_compound = b.ref_frame[1] > INTRA_FRAME; + if is_compound { + self.strip.compound_blocks += 1; + } + for j in 0..1 + is_compound as usize { + let rf = b.ref_frame[j]; + self.find_mv_refs(b, rf, -1); + self.find_best_ref_mvs(b, j); + } + b.sub_modes = [DC_PRED; 4]; + if self.s.seg.feature_active(b.seg_id, SEG_LVL_SKIP) { + b.y_mode = ZEROMV; + } else if b.size >= BLOCK_8X8 { + let ctx = self.mode_context[b.ref_frame[0] as usize] as usize; + let inter_mode = bd.read_tree(&INTER_MODE_TREE, &self.fc.inter_mode[ctx.min(6)]); + if self.s.counting { + self.strip.counts.inter_mode[ctx.min(6)][inter_mode as usize] += 1; + } + b.y_mode = NEARESTMV + inter_mode; + } + b.interp_filter = if self.h.interp_filter == SWITCHABLE { + let left = if b.avail_l && b.left_ref[0] > INTRA_FRAME { self.mi_at(b.r, b.c - 1).interp_filter } else { 3 }; + let above = if b.avail_u && b.above_ref[0] > INTRA_FRAME { self.mi_at(b.r - 1, b.c).interp_filter } else { 3 }; + let ctx = if left == above { + left + } else if left == 3 && above != 3 { + above + } else if left != 3 && above == 3 { + left + } else { + 3 + } as usize; + let f = bd.read_tree(&INTERP_FILTER_TREE, &self.fc.interp_filter[ctx]); + if self.s.counting { + self.strip.counts.interp_filter[ctx][f as usize] += 1; + } + f + } else { + self.h.interp_filter + }; + b.block_mvs = [[Mv::ZERO; 4]; 2]; + if b.size < BLOCK_8X8 { + let n4w = NUM_4X4_WIDE[b.size as usize] as usize; + let n4h = NUM_4X4_HIGH[b.size as usize] as usize; + let mut idy = 0; + while idy < 2 { + let mut idx = 0; + while idx < 2 { + let ctx = self.mode_context[b.ref_frame[0] as usize] as usize; + let inter_mode = bd.read_tree(&INTER_MODE_TREE, &self.fc.inter_mode[ctx.min(6)]); + if self.s.counting { + self.strip.counts.inter_mode[ctx.min(6)][inter_mode as usize] += 1; + } + b.y_mode = NEARESTMV + inter_mode; + if b.y_mode == NEARESTMV || b.y_mode == NEARMV { + for j in 0..1 + is_compound as usize { + self.append_sub8x8_mvs(b, (idy * 2 + idx) as i32, j); + } + } + let mv = self.assign_mv(bd, b, is_compound); + for y2 in 0..n4h { + for x2 in 0..n4w { + let blk = (idy + y2) * 2 + idx + x2; + for l in 0..1 + is_compound as usize { + b.block_mvs[l][blk] = mv[l]; + } + } + } + idx += n4w; + } + idy += n4h; + } + } else { + let mv = self.assign_mv(bd, b, is_compound); + for l in 0..1 + is_compound as usize { + b.block_mvs[l] = [mv[l]; 4]; + } + } + } + + fn read_ref_frames(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + let seg = self.s.seg; + if seg.feature_active(b.seg_id, SEG_LVL_REF_FRAME) { + b.ref_frame = [(seg.feature_data[b.seg_id as usize][SEG_LVL_REF_FRAME] as i8).clamp(LAST_FRAME, ALTREF_FRAME), NONE]; + return; + } + let h = self.h; + let comp_mode = if h.reference_mode == 2 { + let ctx = self.comp_mode_ctx(b); + let v = bd.read_bool(self.fc.comp_mode[ctx]); + if self.s.counting { + self.strip.counts.comp_mode[ctx][v as usize] += 1; + } + v as u8 + } else { + h.reference_mode + }; + if comp_mode == 1 { + let idx = h.ref_frame_sign_bias[h.comp_fixed_ref as usize] as usize; + let ctx = self.comp_ref_ctx(b); + let comp_ref = bd.read_bool(self.fc.comp_ref[ctx]); + if self.s.counting { + self.strip.counts.comp_ref[ctx][comp_ref as usize] += 1; + } + b.ref_frame[idx] = h.comp_fixed_ref; + b.ref_frame[1 - idx] = h.comp_var_ref[comp_ref as usize]; + } else { + let ctx = self.single_ref_p1_ctx(b); + let p1 = bd.read_bool(self.fc.single_ref[ctx][0]); + if self.s.counting { + self.strip.counts.single_ref[ctx][0][p1 as usize] += 1; + } + b.ref_frame[0] = if p1 { + let ctx2 = self.single_ref_p2_ctx(b); + let p2 = bd.read_bool(self.fc.single_ref[ctx2][1]); + if self.s.counting { + self.strip.counts.single_ref[ctx2][1][p2 as usize] += 1; + } + if p2 { ALTREF_FRAME } else { GOLDEN_FRAME } + } else { + LAST_FRAME + }; + b.ref_frame[1] = NONE; + } + } + + fn comp_mode_ctx(&self, b: &Blk) -> usize { + let fixed = self.h.comp_fixed_ref; + (if b.avail_u && b.avail_l { + if b.above_single && b.left_single { + ((b.above_ref[0] == fixed) ^ (b.left_ref[0] == fixed)) as u8 + } else if b.above_single { + 2 + (b.above_ref[0] == fixed || b.above_intra) as u8 + } else if b.left_single { + 2 + (b.left_ref[0] == fixed || b.left_intra) as u8 + } else { + 4 + } + } else if b.avail_u { + if b.above_single { (b.above_ref[0] == fixed) as u8 } else { 3 } + } else if b.avail_l { + if b.left_single { (b.left_ref[0] == fixed) as u8 } else { 3 } + } else { + 1 + }) as usize + } + + fn comp_ref_ctx(&self, b: &Blk) -> usize { + let h = self.h; + let fix_ref_idx = h.ref_frame_sign_bias[h.comp_fixed_ref as usize] as usize; + let var_ref_idx = 1 - fix_ref_idx; + let var0 = h.comp_var_ref[0]; + let var1 = h.comp_var_ref[1]; + let fixed = h.comp_fixed_ref; + (if b.avail_u && b.avail_l { + if b.above_intra && b.left_intra { + 2 + } else if b.left_intra { + if b.above_single { 1 + 2 * (b.above_ref[0] != var1) as u8 } else { 1 + 2 * (b.above_ref[var_ref_idx] != var1) as u8 } + } else if b.above_intra { + if b.left_single { 1 + 2 * (b.left_ref[0] != var1) as u8 } else { 1 + 2 * (b.left_ref[var_ref_idx] != var1) as u8 } + } else { + let vrfa = if b.above_single { b.above_ref[0] } else { b.above_ref[var_ref_idx] }; + let vrfl = if b.left_single { b.left_ref[0] } else { b.left_ref[var_ref_idx] }; + if vrfa == vrfl && var1 == vrfa { + 0 + } else if b.left_single && b.above_single { + if (vrfa == fixed && vrfl == var0) || (vrfl == fixed && vrfa == var0) { + 4 + } else if vrfa == vrfl { + 3 + } else { + 1 + } + } else if b.left_single || b.above_single { + let vrfc = if b.left_single { vrfa } else { vrfl }; + let rfs = if b.above_single { vrfa } else { vrfl }; + if vrfc == var1 && rfs != var1 { + 1 + } else if rfs == var1 && vrfc != var1 { + 2 + } else { + 4 + } + } else if vrfa == vrfl { + 4 + } else { + 2 + } + } + } else if b.avail_u { + if b.above_intra { + 2 + } else if b.above_single { + 3 * (b.above_ref[0] != var1) as u8 + } else { + 4 * (b.above_ref[var_ref_idx] != var1) as u8 + } + } else if b.avail_l { + if b.left_intra { + 2 + } else if b.left_single { + 3 * (b.left_ref[0] != var1) as u8 + } else { + 4 * (b.left_ref[var_ref_idx] != var1) as u8 + } + } else { + 2 + }) as usize + } + + fn single_ref_p1_ctx(&self, b: &Blk) -> usize { + let (a, l) = (b.above_ref, b.left_ref); + (if b.avail_u && b.avail_l { + if b.above_intra && b.left_intra { + 2 + } else if b.left_intra { + if b.above_single { 4 * (a[0] == LAST_FRAME) as u8 } else { 1 + (a[0] == LAST_FRAME || a[1] == LAST_FRAME) as u8 } + } else if b.above_intra { + if b.left_single { 4 * (l[0] == LAST_FRAME) as u8 } else { 1 + (l[0] == LAST_FRAME || l[1] == LAST_FRAME) as u8 } + } else if b.above_single && b.left_single { + 2 * (a[0] == LAST_FRAME) as u8 + 2 * (l[0] == LAST_FRAME) as u8 + } else if !b.above_single && !b.left_single { + 1 + (a[0] == LAST_FRAME || a[1] == LAST_FRAME || l[0] == LAST_FRAME || l[1] == LAST_FRAME) as u8 + } else { + let rfs = if b.above_single { a[0] } else { l[0] }; + let crf1 = if b.above_single { l[0] } else { a[0] }; + let crf2 = if b.above_single { l[1] } else { a[1] }; + if rfs == LAST_FRAME { + 3 + (crf1 == LAST_FRAME || crf2 == LAST_FRAME) as u8 + } else { + (crf1 == LAST_FRAME || crf2 == LAST_FRAME) as u8 + } + } + } else if b.avail_u { + if b.above_intra { + 2 + } else if b.above_single { + 4 * (a[0] == LAST_FRAME) as u8 + } else { + 1 + (a[0] == LAST_FRAME || a[1] == LAST_FRAME) as u8 + } + } else if b.avail_l { + if b.left_intra { + 2 + } else if b.left_single { + 4 * (l[0] == LAST_FRAME) as u8 + } else { + 1 + (l[0] == LAST_FRAME || l[1] == LAST_FRAME) as u8 + } + } else { + 2 + }) as usize + } + + fn single_ref_p2_ctx(&self, b: &Blk) -> usize { + let (a, l) = (b.above_ref, b.left_ref); + const G: i8 = GOLDEN_FRAME; + const L: i8 = LAST_FRAME; + (if b.avail_u && b.avail_l { + if b.above_intra && b.left_intra { + 2 + } else if b.left_intra { + if b.above_single { if a[0] == L { 3 } else { 4 * (a[0] == G) as u8 } } else { 1 + 2 * (a[0] == G || a[1] == G) as u8 } + } else if b.above_intra { + if b.left_single { if l[0] == L { 3 } else { 4 * (l[0] == G) as u8 } } else { 1 + 2 * (l[0] == G || l[1] == G) as u8 } + } else if b.above_single && b.left_single { + if a[0] == L && l[0] == L { + 3 + } else if a[0] == L { + 4 * (l[0] == G) as u8 + } else if l[0] == L { + 4 * (a[0] == G) as u8 + } else { + 2 * (a[0] == G) as u8 + 2 * (l[0] == G) as u8 + } + } else if !b.above_single && !b.left_single { + if a[0] == l[0] && a[1] == l[1] { 3 * (a[0] == G || a[1] == G) as u8 } else { 2 } + } else { + let rfs = if b.above_single { a[0] } else { l[0] }; + let crf1 = if b.above_single { l[0] } else { a[0] }; + let crf2 = if b.above_single { l[1] } else { a[1] }; + if rfs == G { + 3 + (crf1 == G || crf2 == G) as u8 + } else if rfs == ALTREF_FRAME { + (crf1 == G || crf2 == G) as u8 + } else { + 1 + 2 * (crf1 == G || crf2 == G) as u8 + } + } + } else if b.avail_u { + if b.above_intra || (a[0] == L && b.above_single) { + 2 + } else if b.above_single { + 4 * (a[0] == G) as u8 + } else { + 3 * (a[0] == G || a[1] == G) as u8 + } + } else if b.avail_l { + if b.left_intra || (l[0] == L && b.left_single) { + 2 + } else if b.left_single { + 4 * (l[0] == G) as u8 + } else { + 3 * (l[0] == G || l[1] == G) as u8 + } + } else { + 2 + }) as usize + } + + /// assign_mv (6.4.18): returns Mv[0..2]. + fn assign_mv(&mut self, bd: &mut BoolDecoder, b: &Blk, is_compound: bool) -> [Mv; 2] { + let mut mv = [Mv::ZERO; 2]; + for i in 0..1 + is_compound as usize { + mv[i] = match b.y_mode { + NEWMV => self.read_mv(bd, i), + NEARESTMV => self.nearest_mv[i], + NEARMV => self.near_mv[i], + _ => Mv::ZERO, + }; + } + mv + } + + fn read_mv(&mut self, bd: &mut BoolDecoder, r: usize) -> Mv { + let use_hp = self.h.allow_high_precision_mv && use_mv_hp(self.best_mv[r]); + let joint = bd.read_tree(&MV_JOINT_TREE, &self.fc.mv_joint); + if self.s.counting { + self.strip.counts.mv_joint[joint as usize] += 1; + } + let mut diff = [0i32; 2]; + if joint == 2 || joint == 3 { + diff[0] = self.read_mv_component(bd, 0, use_hp); + } + if joint == 1 || joint == 3 { + diff[1] = self.read_mv_component(bd, 1, use_hp); + } + Mv::new(self.best_mv[r].row as i32 + diff[0], self.best_mv[r].col as i32 + diff[1]) + } + + fn read_mv_component(&mut self, bd: &mut BoolDecoder, comp: usize, use_hp: bool) -> i32 { + let fc = self.fc; + let counting = self.s.counting; + let counts = &mut self.strip.counts; + let sign = bd.read_bool(fc.mv_sign[comp]); + let class = bd.read_tree(&MV_CLASS_TREE, &fc.mv_class[comp]) as usize; + let mag = if class == 0 { + let c0 = bd.read_bool(fc.mv_class0_bit[comp]) as usize; + let fr = bd.read_tree(&MV_FR_TREE, &fc.mv_class0_fr[comp][c0]) as usize; + let hp = if use_hp { bd.read_bool(fc.mv_class0_hp[comp]) as usize } else { 1 }; + if counting { + counts.mv_class0_bit[comp][c0] += 1; + counts.mv_class0_fr[comp][c0][fr] += 1; + counts.mv_class0_hp[comp][hp] += 1; + } + ((c0 << 3) | (fr << 1) | hp) + 1 + } else { + let mut d = 0usize; + for i in 0..class { + let bit = bd.read_bool(fc.mv_bits[comp][i]) as usize; + if counting { + counts.mv_bits[comp][i][bit] += 1; + } + d |= bit << i; + } + let base = 2usize << (class + 2); + let fr = bd.read_tree(&MV_FR_TREE, &fc.mv_fr[comp]) as usize; + let hp = if use_hp { bd.read_bool(fc.mv_hp[comp]) as usize } else { 1 }; + if counting { + counts.mv_fr[comp][fr] += 1; + counts.mv_hp[comp][hp] += 1; + } + base + ((d << 3) | (fr << 1) | hp) + 1 + }; + if counting { + counts.mv_sign[comp][sign as usize] += 1; + counts.mv_class[comp][class] += 1; + } + if sign { -(mag as i32) } else { mag as i32 } + } + + // ------------------------------------------------------------ motion vector prediction (6.5) + + #[inline] + fn is_inside(&self, r: isize, c: isize) -> bool { + r >= 0 && (r as usize) < self.mi_rows && c >= self.strip.mi_col_start as isize && (c as usize) < self.strip.mi_col_end + } + + #[inline] + fn add_mv_ref_list(&mut self, mv: Mv) { + if self.ref_mv_count >= 2 { + return; + } + if self.ref_mv_count > 0 && mv == self.ref_list_mv[0] { + return; + } + self.ref_list_mv[self.ref_mv_count] = mv; + self.ref_mv_count += 1; + } + + fn if_same_ref_frame_add(&mut self, m: &MiInfo, ref_frame: i8) { + for j in 0..2 { + if m.ref_frame[j] == ref_frame { + self.add_mv_ref_list(m.mv[j][3]); + return; + } + } + } + + fn if_diff_ref_frame_add(&mut self, m: &MiInfo, ref_frame: i8) { + let bias = &self.h.ref_frame_sign_bias; + let mvs = [m.mv[0][3], m.mv[1][3]]; + let same = mvs[0] == mvs[1]; + let scale = + |f: i8, mv: Mv| -> Mv { if bias[f as usize] != bias[ref_frame as usize] { Mv::new(-(mv.row as i32), -(mv.col as i32)) } else { mv } }; + if m.ref_frame[0] > INTRA_FRAME && m.ref_frame[0] != ref_frame { + let mv = scale(m.ref_frame[0], mvs[0]); + self.add_mv_ref_list(mv); + } + if m.ref_frame[1] > INTRA_FRAME && m.ref_frame[1] != ref_frame && !same { + let mv = scale(m.ref_frame[1], mvs[1]); + self.add_mv_ref_list(mv); + } + } + + fn clamp_mv_row(&self, b: &Blk, v: i32, border: i32) -> i32 { + let bh = NUM_8X8_HIGH[b.size as usize] as i32; + let to_top = -((b.r as i32 * 8) * 8); + let to_bottom = ((self.mi_rows as i32 - bh - b.r as i32) * 8) * 8; + v.clamp(to_top - border, (to_bottom + border).max(to_top - border)) + } + + fn clamp_mv_col(&self, b: &Blk, v: i32, border: i32) -> i32 { + let bw = NUM_8X8_WIDE[b.size as usize] as i32; + let to_left = -((b.c as i32 * 8) * 8); + let to_right = ((self.mi_cols as i32 - bw - b.c as i32) * 8) * 8; + v.clamp(to_left - border, (to_right + border).max(to_left - border)) + } + + /// find_mv_refs (6.5.1) for `ref_frame`; `block` is -1 or the sub-8x8 block index. + fn find_mv_refs(&mut self, b: &Blk, ref_frame: i8, block: i32) { + self.ref_mv_count = 0; + self.ref_list_mv = [Mv::ZERO; 2]; + let mut different_ref_found = false; + let mut context_counter = 0usize; + let search = &MV_REF_BLOCKS[b.size as usize * 16..b.size as usize * 16 + 16]; + let (r, c) = (b.r as isize, b.c as isize); + for i in 0..2 { + let (cr, cc) = (r + search[i * 2] as isize, c + search[i * 2 + 1] as isize); + if self.is_inside(cr, cc) { + let m = *self.mi_at(cr as usize, cc as usize); + different_ref_found = true; + context_counter += MODE_2_COUNTER[m.y_mode as usize] as usize; + for j in 0..2 { + if m.ref_frame[j] == ref_frame { + let idx = if block >= 0 { IDX_N_COLUMN_TO_SUBBLOCK[block as usize][(search[i * 2 + 1] == 0) as usize] as usize } else { 3 }; + self.add_mv_ref_list(m.mv[j][idx]); + break; + } + } + } + } + for i in 2..8 { + let (cr, cc) = (r + search[i * 2] as isize, c + search[i * 2 + 1] as isize); + if self.is_inside(cr, cc) { + different_ref_found = true; + let m = *self.mi_at(cr as usize, cc as usize); + self.if_same_ref_frame_add(&m, ref_frame); + } + } + let prev = self.s.prev_mi.map(|g| *g.at(b.r, b.c)); + if let Some(p) = &prev { + self.if_same_ref_frame_add(p, ref_frame); + } + if different_ref_found { + for i in 0..8 { + let (cr, cc) = (r + search[i * 2] as isize, c + search[i * 2 + 1] as isize); + if self.is_inside(cr, cc) { + let m = *self.mi_at(cr as usize, cc as usize); + self.if_diff_ref_frame_add(&m, ref_frame); + } + } + } + if let Some(p) = &prev { + self.if_diff_ref_frame_add(p, ref_frame); + } + self.mode_context[ref_frame as usize] = COUNTER_TO_CONTEXT[context_counter.min(18)]; + for i in 0..2 { + let mv = self.ref_list_mv[i]; + self.ref_list_mv[i] = Mv::new(self.clamp_mv_row(b, mv.row as i32, MV_BORDER), self.clamp_mv_col(b, mv.col as i32, MV_BORDER)); + } + } + + /// find_best_ref_mvs (6.5.12). + fn find_best_ref_mvs(&mut self, b: &Blk, ref_list: usize) { + for i in 0..2 { + let mv = self.ref_list_mv[i]; + let (mut dr, mut dc) = (mv.row as i32, mv.col as i32); + if !self.h.allow_high_precision_mv || !use_mv_hp(mv) { + if dr & 1 != 0 { + dr += if dr > 0 { -1 } else { 1 }; + } + if dc & 1 != 0 { + dc += if dc > 0 { -1 } else { 1 }; + } + } + let border = (BORDERINPIXELS - INTERP_EXTEND) << 3; + self.ref_list_mv[i] = Mv::new(self.clamp_mv_row(b, dr, border), self.clamp_mv_col(b, dc, border)); + } + self.nearest_mv[ref_list] = self.ref_list_mv[0]; + self.near_mv[ref_list] = self.ref_list_mv[1]; + self.best_mv[ref_list] = self.ref_list_mv[0]; + } + + /// append_sub8x8_mvs (6.5.14). + fn append_sub8x8_mvs(&mut self, b: &Blk, block: i32, ref_list: usize) { + self.find_mv_refs(b, b.ref_frame[ref_list], block); + let mut sub = [Mv::ZERO; 2]; + let mut dst; + let bm = &b.block_mvs[ref_list]; + if block == 0 { + sub = self.ref_list_mv; + dst = 2; + } else if block <= 2 { + sub[0] = bm[0]; + dst = 1; + } else { + sub[0] = bm[2]; + dst = 1; + for idx in [1usize, 0] { + if dst < 2 && bm[idx] != sub[0] { + sub[dst] = bm[idx]; + dst += 1; + } + } + } + for n in 0..2 { + if dst < 2 && self.ref_list_mv[n] != sub[0] { + sub[dst] = self.ref_list_mv[n]; + dst += 1; + } + } + if dst < 2 { + sub[dst] = Mv::ZERO; + } + self.nearest_mv[ref_list] = sub[0]; + self.near_mv[ref_list] = sub[1]; + } +} + +#[inline] +pub fn use_mv_hp(mv: Mv) -> bool { + ((mv.row as i32).abs() >> 3) < COMPANDED_MVREF_THRESH && ((mv.col as i32).abs() >> 3) < COMPANDED_MVREF_THRESH +} diff --git a/crates/video/src/vp9/decode/tile/recon.rs b/crates/video/src/vp9/decode/tile/recon.rs new file mode 100644 index 000000000..b8b726ae6 --- /dev/null +++ b/crates/video/src/vp9/decode/tile/recon.rs @@ -0,0 +1,389 @@ +//! Residual syntax (6.4.21 - 6.4.26) with the prediction and reconstruction processes it +//! triggers (8.5, 8.6). + +use super::{Blk, TileDecoder}; +use crate::vp9::decode::boolcoder::BoolDecoder; +use crate::vp9::decode::error::Error; +use crate::vp9::decode::frame::Mv; +use crate::vp9::decode::inter::{self, McBlock, RefPlane}; +use crate::vp9::decode::intra::{self, IntraEdge}; +use crate::vp9::decode::tables::{ + BLOCK_8X8, CAT_PROBS, COEFBAND_4X4, COEFBAND_8X8PLUS, DCT_DCT, ENERGY_CLASS, EXTRA_BITS, INTERP_EXTEND, INTRA_FRAME, MAX_TXSIZE, MODE2TXFM, + NUM_4X4_HIGH, NUM_4X4_WIDE, NUM_8X8_HIGH, NUM_8X8_WIDE, SS_SIZE_LOOKUP, TX_4X4, TX_32X32, neighbors, pareto_full, scan, +}; +use crate::vp9::decode::transform; + +/// Sample rectangle of a block region within its plane. +#[derive(Clone, Copy)] +struct Region { + x: usize, + y: usize, + w: usize, + h: usize, +} + +/// round_mv_comp_q2 / q4 (8.5.2.1). +/// Location and transform parameters of one transform block within a plane. +#[derive(Clone, Copy)] +struct TxBlock { + plane: usize, + start_x: usize, + start_y: usize, + tx_sz: u8, + tx_type: u8, +} + +/// Walks the pareto part of the token tree, returning a token in `2..=10`. +fn read_large_token(bd: &mut BoolDecoder, pp: &[u8; 8]) -> usize { + if !bd.read_bool(pp[0]) { + if !bd.read_bool(pp[1]) { + 2 + } else if !bd.read_bool(pp[2]) { + 3 + } else { + 4 + } + } else if !bd.read_bool(pp[3]) { + if !bd.read_bool(pp[4]) { 5 } else { 6 } + } else if !bd.read_bool(pp[5]) { + if !bd.read_bool(pp[6]) { 7 } else { 8 } + } else if !bd.read_bool(pp[7]) { + 9 + } else { + 10 + } +} + +/// read_coef (6.4.26): the unsigned coefficient magnitude of a token of at least 2. +fn read_coef(bd: &mut BoolDecoder, token: usize, bit_depth: u8) -> i64 { + let (cat, num_extra, base) = EXTRA_BITS[token]; + let mut v = base as i64; + if token == 10 { + for e in 0..(bit_depth as u32 - 8) { + if bd.read_bool(255) { + v += 1 << (5 + bit_depth as u32 - e); + } + } + } + let cp = CAT_PROBS[cat as usize]; + for e in 0..num_extra as usize { + if bd.read_bool(cp[e]) { + v += 1 << (num_extra as usize - 1 - e); + } + } + v +} + +fn round_q2(v: i32) -> i32 { + (if v < 0 { v - 1 } else { v + 1 }) / 2 +} +fn round_q4(v: i32) -> i32 { + (if v < 0 { v - 2 } else { v + 2 }) / 4 +} + +impl<'a> TileDecoder<'a> { + pub(super) fn residual(&mut self, bd: &mut BoolDecoder, b: &mut Blk) { + let bsize = b.size.max(BLOCK_8X8) as usize; + let (ssx, ssy) = (self.ss_x, self.ss_y); + let uv_tx = if b.size < BLOCK_8X8 { 0 } else { b.tx_size.min(MAX_TXSIZE[SS_SIZE_LOOKUP[b.size as usize][ssx][ssy] as usize]) }; + for plane in 0..3 { + let tx_sz = if plane > 0 { uv_tx } else { b.tx_size }; + let step = 1usize << tx_sz; + let (sx, sy) = if plane > 0 { (ssx, ssy) } else { (0, 0) }; + let plane_sz = if plane > 0 { SS_SIZE_LOOKUP[bsize][ssx][ssy] as usize } else { bsize }; + let n4w = NUM_4X4_WIDE[plane_sz] as usize; + let n4h = NUM_4X4_HIGH[plane_sz] as usize; + let base_x = (b.c * 8) >> sx; + let base_y = (b.r * 8) >> sy; + if b.is_inter { + if b.size < BLOCK_8X8 { + for y in 0..n4h { + for x in 0..n4w { + self.predict_inter(b, plane, Region { x: base_x + 4 * x, y: base_y + 4 * y, w: 4, h: 4 }, y * n4w + x); + } + } + } else { + self.predict_inter(b, plane, Region { x: base_x, y: base_y, w: n4w * 4, h: n4h * 4 }, 0); + } + } + let max_x = (self.mi_cols * 8) >> sx; + let max_y = (self.mi_rows * 8) >> sy; + let mut block_idx = 0; + for y in (0..n4h).step_by(step) { + for x in (0..n4w).step_by(step) { + let start_x = base_x + 4 * x; + let start_y = base_y + 4 * y; + let mut nonzero = false; + if start_x < max_x && start_y < max_y { + let tx_type = self.tx_type(b, plane, tx_sz, block_idx); + if !b.is_inter { + let mode = if plane > 0 { + b.uv_mode + } else if b.size >= BLOCK_8X8 { + b.y_mode + } else { + b.sub_modes[block_idx] + }; + let edge = IntraEdge { + have_left: b.avail_l || x > 0, + have_above: b.avail_u || y > 0, + not_on_right: x + step < n4w, + tx_size: tx_sz, + max_x: max_x - 1, + max_y: max_y - 1, + bit_depth: self.bit_depth, + }; + let (stride, x_off) = (self.strip.strides[plane], self.strip.x_off[plane]); + intra::predict(&mut self.strip.planes[plane], stride, x_off, start_x, start_y, mode, &edge); + } + if !b.skip { + let (eob, rows) = self.tokens(bd, b, &TxBlock { plane, start_x, start_y, tx_sz, tx_type }); + nonzero = eob > 0; + b.eob_total += nonzero as u32; + if eob > 0 { + let (stride, x_off) = (self.strip.strides[plane], self.strip.x_off[plane]); + let o = start_y * stride + start_x - x_off; + transform::inverse_transform_add( + &self.coefs, + &transform::InverseTx { + tx_size: tx_sz, + tx_type, + lossless: self.h.lossless, + eob, + rows, + bit_depth: self.bit_depth, + }, + &mut self.strip.planes[plane][o..], + stride, + ); + let sc = scan(tx_sz, tx_type); + for &p in &sc[..eob] { + self.coefs[p as usize] = 0; + } + } + } + } + let ax = start_x >> 2; + let ly = start_y >> 2; + let an = &mut self.above_nonzero[plane]; + let e = (ax + step).min(an.len()); + an[ax.min(e)..e].fill(nonzero as u8); + let ln = &mut self.left_nonzero[plane]; + let e = (ly + step).min(ln.len()); + ln[ly.min(e)..e].fill(nonzero as u8); + block_idx += 1; + } + } + } + } + + /// Transform type (6.4.25). + fn tx_type(&self, b: &Blk, plane: usize, tx_sz: u8, block_idx: usize) -> u8 { + if plane > 0 || tx_sz == TX_32X32 { + DCT_DCT + } else if tx_sz == TX_4X4 { + if self.h.lossless || b.is_inter { + DCT_DCT + } else { + MODE2TXFM[if b.size < BLOCK_8X8 { b.sub_modes[block_idx] } else { b.y_mode } as usize] + } + } else { + MODE2TXFM[b.y_mode as usize] + } + } + + /// Above/left non-zero context of a transform block (the initial token context). + fn initial_token_ctx(&self, plane: usize, start_x: usize, start_y: usize, tx_sz: u8) -> usize { + let (sx, sy) = if plane > 0 { (self.ss_x, self.ss_y) } else { (0, 0) }; + let max_x4 = (2 * self.mi_cols) >> sx; + let max_y4 = (2 * self.mi_rows) >> sy; + let n = 1usize << tx_sz; + let (x4, y4) = (start_x >> 2, start_y >> 2); + let mut above = 0u8; + let mut left = 0u8; + for i in 0..n { + if x4 + i < max_x4 { + above |= self.above_nonzero[plane][x4 + i]; + } + if y4 + i < max_y4 { + left |= self.left_nonzero[plane][y4 + i]; + } + } + (above + left) as usize + } + + /// tokens (6.4.24): reads and dequantizes the coefficients into `self.coefs`. Returns the end + /// of block position and the number of leading coefficient rows that may be non-zero. + fn tokens(&mut self, bd: &mut BoolDecoder, b: &Blk, t: &TxBlock) -> (usize, usize) { + if self.s.counting { self.tokens_impl::(bd, b, t) } else { self.tokens_impl::(bd, b, t) } + } + + fn tokens_impl(&mut self, bd: &mut BoolDecoder, b: &Blk, t: &TxBlock) -> (usize, usize) { + let TxBlock { plane, start_x, start_y, tx_sz, tx_type } = *t; + let seg_eob = 16usize << (tx_sz << 1); + let sc = scan(tx_sz, tx_type); + let nb = neighbors(tx_sz, tx_type); + let bands: &[u8] = if tx_sz == 0 { &COEFBAND_4X4 } else { &COEFBAND_8X8PLUS }; + let (sc, nb, bands) = (&sc[..seg_eob], &nb[..seg_eob], &bands[..seg_eob]); + let ptype = (plane > 0) as usize; + let rtype = b.is_inter as usize; + let probs = &self.fc.coef[tx_sz as usize][ptype][rtype]; + let ctx0 = self.initial_token_ctx(plane, start_x, start_y, tx_sz); + let q = self.s.seg_q[b.seg_id as usize][ptype]; + let dq_shift = (tx_sz == TX_32X32) as u32; + let pareto = pareto_full(); + let log2n = 2 + tx_sz as usize; + let bit_depth = self.bit_depth; + let mut max_row = 0usize; + let mut check_eob = true; + let mut c = 0usize; + let tc = &mut self.token_cache; + let coefs = &mut self.coefs; + let counts = &mut self.strip.counts; + let (more_counts, token_counts) = (&mut counts.more_coefs[tx_sz as usize][ptype][rtype], &mut counts.token[tx_sz as usize][ptype][rtype]); + while c < seg_eob { + let pos = sc[c] as usize; + let band = bands[c] as usize; + let ctx = if c == 0 { + ctx0 + } else { + let (a, bb) = nb[c]; + ((1 + tc[a as usize] + tc[bb as usize]) >> 1) as usize + }; + let p = &probs[band][ctx]; + if check_eob { + let more = bd.read_bool(p[0]); + if COUNTING { + more_counts[band][ctx][more as usize] += 1; + } + if !more { + break; + } + } + if !bd.read_bool(p[1]) { + tc[pos] = 0; + if COUNTING { + token_counts[band][ctx][0] += 1; + } + check_eob = false; + c += 1; + continue; + } + check_eob = true; + let token: usize; + let coef: i64; + if !bd.read_bool(p[2]) { + if COUNTING { + token_counts[band][ctx][1] += 1; + } + token = 1; + coef = 1; + } else { + if COUNTING { + token_counts[band][ctx][2] += 1; + } + let pp = &pareto[p[2].max(1) as usize - 1]; + token = read_large_token(bd, pp); + coef = read_coef(bd, token, bit_depth); + } + tc[pos] = ENERGY_CLASS[token]; + let sign = bd.read_bool_half(); + let qv = if pos == 0 { q[0] } else { q[1] } as i64; + let d = ((coef * qv) >> dq_shift) as i32; + coefs[pos] = if sign { d.wrapping_neg() } else { d }; + max_row = max_row.max(pos >> log2n); + c += 1; + } + (c, max_row + 1) + } + + /// Inter prediction process (8.5.2) for one block region. + fn predict_inter(&mut self, b: &Blk, plane: usize, region: Region, block_idx: usize) { + let Region { x, y, w, h } = region; + let is_compound = b.ref_frame[1] > INTRA_FRAME; + let (sx, sy) = if plane > 0 { (self.ss_x, self.ss_y) } else { (0, 0) }; + for rl in 0..1 + is_compound as usize { + // Motion vector selection (8.5.2.1). + let bm = &b.block_mvs[rl]; + let mv = if plane == 0 || b.size >= BLOCK_8X8 || (sx == 0 && sy == 0) { + bm[block_idx] + } else if sx == 0 && sy == 1 { + Mv::new( + round_q2(bm[block_idx].row as i32 + bm[block_idx + 2].row as i32), + round_q2(bm[block_idx].col as i32 + bm[block_idx + 2].col as i32), + ) + } else if sx == 1 && sy == 0 { + Mv::new( + round_q2(bm[block_idx].row as i32 + bm[block_idx + 1].row as i32), + round_q2(bm[block_idx].col as i32 + bm[block_idx + 1].col as i32), + ) + } else { + let sr: i32 = bm.iter().map(|m| m.row as i32).sum(); + let sc: i32 = bm.iter().map(|m| m.col as i32).sum(); + Mv::new(round_q4(sr), round_q4(sc)) + }; + // Motion vector clamping (8.5.2.2). + let bh = NUM_8X8_HIGH[b.size as usize] as i32; + let bw = NUM_8X8_WIDE[b.size as usize] as i32; + let (r, c) = (b.r as i32, b.c as i32); + let to_top = (-(r * 8 * 16)) >> sy; + let to_bottom = (((self.mi_rows as i32 - bh - r) * 8) * 16) >> sy; + let to_left = (-(c * 8 * 16)) >> sx; + let to_right = (((self.mi_cols as i32 - bw - c) * 8) * 16) >> sx; + let spel_left = (INTERP_EXTEND + ((bw * 8) >> sx)) << 4; + let spel_right = spel_left - 16; + let spel_top = (INTERP_EXTEND + ((bh * 8) >> sy)) << 4; + let spel_bottom = spel_top - 16; + let cmv_row = ((2 * mv.row as i32) >> sy).clamp(to_top - spel_top, (to_bottom + spel_bottom).max(to_top - spel_top)); + let cmv_col = ((2 * mv.col as i32) >> sx).clamp(to_left - spel_left, (to_right + spel_right).max(to_left - spel_left)); + let ri = (b.ref_frame[rl] - 1).clamp(0, 2) as usize; + let Some(rf) = &self.s.refs[ri] else { + if self.error.is_none() { + self.error = Some(Error::MissingReference(format!("block uses unusable reference {}", b.ref_frame[rl]))); + } + let out = if rl == 0 { &mut self.pred } else { &mut self.pred2 }; + out[..w * h].fill(1 << (self.bit_depth - 1)); + continue; + }; + // Motion vector scaling (8.5.2.3). + let (x_scale, y_scale) = (rf.x_scale as i64, rf.y_scale as i64); + let (start_x, start_y, step_x, step_y) = if rf.x_step == 16 && rf.y_step == 16 && x_scale == 1 << 14 && y_scale == 1 << 14 { + (((x as i32) << 4) + cmv_col, ((y as i32) << 4) + cmv_row, 16, 16) + } else { + self.strip.scaled_blocks += 1; + let base_x = (x as i64 * x_scale) >> 14; + let base_y = (y as i64 * y_scale) >> 14; + let luma_x = (x << sx) as i64; + let luma_y = (y << sy) as i64; + let frac_x = ((16 * luma_x * x_scale) >> 14) & 15; + let frac_y = ((16 * luma_y * y_scale) >> 14) & 15; + let dx = ((cmv_col as i64 * x_scale) >> 14) + frac_x; + let dy = ((cmv_row as i64 * y_scale) >> 14) + frac_y; + (((base_x << 4) + dx) as i32, ((base_y << 4) + dy) as i32, rf.x_step, rf.y_step) + }; + let f = rf.frame; + let rp = RefPlane { + frame: f, + plane, + last_x: ((f.info.width as i32 + sx as i32) >> sx) - 1, + last_y: ((f.info.height as i32 + sy as i32) >> sy) - 1, + }; + let out = if rl == 0 { &mut self.pred } else { &mut self.pred2 }; + let blk = McBlock { x: start_x, y: start_y, step_x, step_y, w, h, filter: b.interp_filter, bit_depth: self.bit_depth }; + inter::predict(&rp, &blk, out, &mut self.mc_tmp, &mut self.mc_win); + } + let (stride, x_off) = (self.strip.strides[plane], self.strip.x_off[plane]); + let dst = &mut self.strip.planes[plane]; + for i in 0..h { + let o = (y + i) * stride + x - x_off; + let row = &mut dst[o..o + w]; + if is_compound { + for ((d, &a), &bb) in row.iter_mut().zip(&self.pred[i * w..i * w + w]).zip(&self.pred2[i * w..i * w + w]) { + *d = ((a as u32 + bb as u32 + 1) >> 1) as u16; + } + } else { + row.copy_from_slice(&self.pred[i * w..i * w + w]); + } + } + } +} diff --git a/crates/video/src/vp9/decode/transform.rs b/crates/video/src/vp9/decode/transform.rs new file mode 100644 index 000000000..0f3150cb2 --- /dev/null +++ b/crates/video/src/vp9/decode/transform.rs @@ -0,0 +1,547 @@ +//! 2D inverse transforms and reconstruction (8.6.2, 8.7.2). The 1D DCT / ADST butterflies are +//! generated from the specification's step lists (`transform_gen.rs`); ADST4 (8.7.1.6) and the +//! Walsh-Hadamard transform (8.7.1.10) are written out here. + +use crate::vp9::decode::tables::{ADST_ADST, ADST_DCT, DCT_ADST, DCT_DCT}; +use crate::vp9::decode::transform_gen::{narrow, wide}; + +const SINPI_1_9: i64 = 5283; +const SINPI_2_9: i64 = 9929; +const SINPI_3_9: i64 = 13377; +const SINPI_4_9: i64 = 15212; + +macro_rules! adst4 { + ($name:ident, $t:ty) => { + #[inline(always)] + fn $name(x: [$t; 4]) -> [$t; 4] { + let (s1_9, s2_9, s3_9, s4_9) = (SINPI_1_9 as $t, SINPI_2_9 as $t, SINPI_3_9 as $t, SINPI_4_9 as $t); + let s0 = s1_9.wrapping_mul(x[0]); + let s1 = s2_9.wrapping_mul(x[0]); + let s2 = s3_9.wrapping_mul(x[1]); + let s3 = s4_9.wrapping_mul(x[2]); + let s4 = s1_9.wrapping_mul(x[2]); + let s5 = s2_9.wrapping_mul(x[3]); + let s6 = s4_9.wrapping_mul(x[3]); + let v = x[0].wrapping_sub(x[2]).wrapping_add(x[3]); + let s7 = s3_9.wrapping_mul(v); + let x0 = s0.wrapping_add(s3).wrapping_add(s5); + let x1 = s1.wrapping_sub(s4).wrapping_sub(s6); + let x2 = s7; + let x3 = s2; + let r = |v: $t| v.wrapping_add(1 << 13) >> 14; + [r(x0.wrapping_add(x3)), r(x1.wrapping_add(x3)), r(x2), r(x0.wrapping_add(x1).wrapping_sub(x3))] + } + }; +} +adst4!(iadst4_narrow, i32); +adst4!(iadst4_wide, i64); + +/// Inverse Walsh-Hadamard transform (8.7.1.10). +fn iwht4(x: [i32; 4], shift: u32) -> [i32; 4] { + let mut a = x[0] >> shift; + let mut c = x[1] >> shift; + let mut d = x[2] >> shift; + let mut b = x[3] >> shift; + a = a.wrapping_add(c); + d = d.wrapping_sub(b); + let e = a.wrapping_sub(d) >> 1; + b = e.wrapping_sub(b); + c = e.wrapping_sub(c); + a = a.wrapping_sub(b); + d = d.wrapping_add(c); + [a, b, c, d] +} + +/// Apply a 1D transform of length `n` (4, 8, 16, 32) in place. +#[inline(always)] +fn tx1d_narrow(buf: &mut [i32], n: usize, adst: bool) { + match (n, adst) { + (4, false) => { + if let Some(a) = buf.first_chunk_mut::<4>() { + *a = narrow::idct4(*a); + } + } + (4, true) => { + if let Some(a) = buf.first_chunk_mut::<4>() { + *a = iadst4_narrow(*a); + } + } + (8, false) => { + if let Some(a) = buf.first_chunk_mut::<8>() { + *a = narrow::idct8(*a); + } + } + (8, true) => { + if let Some(a) = buf.first_chunk_mut::<8>() { + *a = narrow::iadst8(*a); + } + } + (16, false) => { + if let Some(a) = buf.first_chunk_mut::<16>() { + *a = narrow::idct16(*a); + } + } + (16, true) => { + if let Some(a) = buf.first_chunk_mut::<16>() { + *a = narrow::iadst16(*a); + } + } + _ => { + if let Some(a) = buf.first_chunk_mut::<32>() { + *a = narrow::idct32(*a); + } + } + } +} + +#[inline(always)] +fn tx1d_wide(buf: &mut [i64], n: usize, adst: bool) { + match (n, adst) { + (4, false) => { + if let Some(a) = buf.first_chunk_mut::<4>() { + *a = wide::idct4(*a); + } + } + (4, true) => { + if let Some(a) = buf.first_chunk_mut::<4>() { + *a = iadst4_wide(*a); + } + } + (8, false) => { + if let Some(a) = buf.first_chunk_mut::<8>() { + *a = wide::idct8(*a); + } + } + (8, true) => { + if let Some(a) = buf.first_chunk_mut::<8>() { + *a = wide::iadst8(*a); + } + } + (16, false) => { + if let Some(a) = buf.first_chunk_mut::<16>() { + *a = wide::idct16(*a); + } + } + (16, true) => { + if let Some(a) = buf.first_chunk_mut::<16>() { + *a = wide::iadst16(*a); + } + } + _ => { + if let Some(a) = buf.first_chunk_mut::<32>() { + *a = wide::idct32(*a); + } + } + } +} + +/// Clamp coefficients so that garbage streams cannot overflow the 32-bit arithmetic (conformant +/// streams keep far smaller values, 8 + BitDepth bits). +#[inline] +fn sat(v: i32, lim: i32) -> i32 { + v.clamp(-lim, lim) +} + +/// Block description for [`inverse_transform_add`]. +#[derive(Clone, Copy, Debug)] +pub struct InverseTx { + pub tx_size: u8, + pub tx_type: u8, + pub lossless: bool, + /// End-of-block position (number of coded coefficients). + pub eob: usize, + /// Number of leading rows that can hold non-zero values. + pub rows: usize, + pub bit_depth: u8, +} + +/// Inverse transform `coefs` (row-major n x n dequantized coefficients, `rows` = number of leading +/// rows that can hold non-zero values) and add the residual to `dst` (8.6.2 step 4). +pub fn inverse_transform_add(coefs: &[i32], tx: &InverseTx, dst: &mut [u16], stride: usize) { + let InverseTx { tx_size, tx_type, lossless, eob, rows, bit_depth } = *tx; + let n = 4usize << tx_size; + let max = (1i32 << bit_depth) - 1; + if lossless { + let mut t = [0i32; 16]; + for i in 0..4 { + let c = coefs.get(i * 4..).and_then(|c| c.first_chunk::<4>()).copied().unwrap_or_default(); + let r = iwht4(c, 2); + t[i * 4..i * 4 + 4].copy_from_slice(&r); + } + for j in 0..4 { + let r = iwht4([t[j], t[4 + j], t[8 + j], t[12 + j]], 0); + for i in 0..4 { + let p = &mut dst[i * stride + j]; + *p = (*p as i32).saturating_add(r[i]).clamp(0, max) as u16; + } + } + return; + } + let shift = (tx_size as u32 + 4).min(6); + let round = 1i32 << (shift - 1); + if eob == 1 && tx_type == DCT_DCT { + // Only the DC coefficient is present: every output of both passes is equal (8.7.2 with a + // single non-zero input). + let dc = coefs[0] as i64; + let a = (dc * 11585 + (1 << 13)) >> 14; + let b = (a * 11585 + (1 << 13)) >> 14; + let v = ((b + round as i64) >> shift).clamp(-(1 << 20), 1 << 20) as i32; + for i in 0..n { + for p in dst[i * stride..i * stride + n].iter_mut() { + *p = (*p as i32 + v).clamp(0, max) as u16; + } + } + return; + } + let row_adst = matches!(tx_type, DCT_ADST | ADST_ADST); + let col_adst = matches!(tx_type, ADST_DCT | ADST_ADST); + let p = TxParams { row_adst, col_adst, rows, shift, round, max }; + match (tx_size, bit_depth == 8) { + (0, true) => itx_narrow::<4>(coefs, &p, dst, stride), + (1, true) => itx_narrow::<8>(coefs, &p, dst, stride), + (2, true) => itx_narrow::<16>(coefs, &p, dst, stride), + (_, true) => itx_narrow::<32>(coefs, &p, dst, stride), + (0, false) => itx_wide::<4>(coefs, &p, dst, stride), + (1, false) => itx_wide::<8>(coefs, &p, dst, stride), + (2, false) => itx_wide::<16>(coefs, &p, dst, stride), + (_, false) => itx_wide::<32>(coefs, &p, dst, stride), + } +} + +struct TxParams { + row_adst: bool, + col_adst: bool, + rows: usize, + shift: u32, + round: i32, + max: i32, +} + +/// 2D inverse transform of size N with 32-bit intermediates (8-bit streams). +fn itx_narrow(coefs: &[i32], p: &TxParams, dst: &mut [u16], stride: usize) { + let lim = 1 << 24; + let mut t = [[0i32; N]; N]; + for (i, row) in t.iter_mut().enumerate().take(p.rows.min(N)) { + for (d, s) in row.iter_mut().zip(&coefs[i * N..i * N + N]) { + *d = sat(*s, lim); + } + tx1d_narrow(row, N, p.row_adst); + for v in row.iter_mut() { + *v = sat(*v, lim); + } + } + // Column pass across 4 / 8 columns at once (the same butterflies on lane vectors), then + // the rounding, residual add and clipping per row of lanes. + if N == 4 { + let x: [Cols<4>; N] = std::array::from_fn(|i| Cols(t[i].first_chunk::<4>().copied().unwrap_or_default())); + let y = col_tx4(x, p.col_adst); + add_cols(&y, 0, p, dst, stride); + } else { + for c0 in (0..N).step_by(8) { + let x: [Cols<8>; N] = std::array::from_fn(|i| Cols(t[i].get(c0..).and_then(|r| r.first_chunk::<8>()).copied().unwrap_or_default())); + let y = col_tx8(x, p.col_adst); + add_cols(&y, c0, p, dst, stride); + } + } +} + +/// Round the column pass output `y` (columns c0..c0 + W of every row) and add it to `dst`. +#[inline(always)] +fn add_cols(y: &[Cols; N], c0: usize, p: &TxParams, dst: &mut [u16], stride: usize) { + for (i, v) in y.iter().enumerate() { + let Some(d) = dst.get_mut(i * stride + c0..).and_then(|d| d.first_chunk_mut::()) else { + return; + }; + for j in 0..W { + let r = (v.0[j].wrapping_add(p.round) >> p.shift).clamp(-(1 << 20), 1 << 20); + d[j] = (d[j] as i32 + r).max(0).min(p.max) as u16; + } + } +} + +/// `W` columns of 32-bit values processed together: the generated butterflies +/// (`transform_gen::cols4` / `cols8`) run on these lane vectors with the scalar arithmetic of +/// `narrow` applied per lane, so the column pass vectorises. +#[derive(Clone, Copy)] +pub struct Cols(pub [i32; W]); + +impl From for Cols { + #[inline(always)] + fn from(v: i32) -> Self { + Cols([v; W]) + } +} + +impl Cols { + #[inline(always)] + pub fn wrapping_add>(self, o: O) -> Self { + let o = o.into(); + Cols(std::array::from_fn(|i| self.0[i].wrapping_add(o.0[i]))) + } + #[inline(always)] + pub fn wrapping_sub(self, o: Self) -> Self { + Cols(std::array::from_fn(|i| self.0[i].wrapping_sub(o.0[i]))) + } + #[inline(always)] + pub fn wrapping_mul(self, c: i32) -> Self { + Cols(self.0.map(|v| v.wrapping_mul(c))) + } + #[inline(always)] + pub fn wrapping_neg(self) -> Self { + Cols(self.0.map(|v| v.wrapping_neg())) + } +} + +impl std::ops::Shr for Cols { + type Output = Self; + #[inline(always)] + fn shr(self, s: i32) -> Self { + Cols(self.0.map(|v| v >> s)) + } +} + +/// `x` (N rows of 4 columns) through the 1D column transform. +#[inline(always)] +fn col_tx4(x: [Cols<4>; N], adst: bool) -> [Cols<4>; N] { + use crate::vp9::decode::transform_gen::cols4; + let r = if adst { iadst4_cols(fixed(&x)) } else { cols4::idct4(fixed(&x)) }; + std::array::from_fn(|i| r[i]) +} + +/// `x` (N rows of 8 columns) through the 1D column transform. +#[inline(always)] +fn col_tx8(x: [Cols<8>; N], adst: bool) -> [Cols<8>; N] { + use crate::vp9::decode::transform_gen::cols8; + let mut out = x; + match (N, adst) { + (8, false) => out.copy_from_slice(&cols8::idct8(fixed(&x))), + (8, true) => out.copy_from_slice(&cols8::iadst8(fixed(&x))), + (16, false) => out.copy_from_slice(&cols8::idct16(fixed(&x))), + (16, true) => out.copy_from_slice(&cols8::iadst16(fixed(&x))), + _ => out.copy_from_slice(&cols8::idct32(fixed(&x))), + } + out +} + +/// A slice of known length as an array. +#[inline(always)] +fn fixed, const M: usize>(x: &[T]) -> [T; M] { + x.first_chunk::().copied().unwrap_or([T::from(0); M]) +} + +/// ADST4 (8.7.1.6) on 4 columns. +#[inline(always)] +fn iadst4_cols(x: [Cols<4>; 4]) -> [Cols<4>; 4] { + let s0 = x[0].wrapping_mul(SINPI_1_9 as i32); + let s1 = x[0].wrapping_mul(SINPI_2_9 as i32); + let s2 = x[1].wrapping_mul(SINPI_3_9 as i32); + let s3 = x[2].wrapping_mul(SINPI_4_9 as i32); + let s4 = x[2].wrapping_mul(SINPI_1_9 as i32); + let s5 = x[3].wrapping_mul(SINPI_2_9 as i32); + let s6 = x[3].wrapping_mul(SINPI_4_9 as i32); + let v = x[0].wrapping_sub(x[2]).wrapping_add(x[3]); + let s7 = v.wrapping_mul(SINPI_3_9 as i32); + let x0 = s0.wrapping_add(s3).wrapping_add(s5); + let x1 = s1.wrapping_sub(s4).wrapping_sub(s6); + let x2 = s7; + let x3 = s2; + let r = |v: Cols<4>| v.wrapping_add(1 << 13) >> 14; + [r(x0.wrapping_add(x3)), r(x1.wrapping_add(x3)), r(x2), r(x0.wrapping_add(x1).wrapping_sub(x3))] +} + +/// 2D inverse transform of size N with 64-bit intermediates (high bit depth). +fn itx_wide(coefs: &[i32], p: &TxParams, dst: &mut [u16], stride: usize) { + let lim = 1i64 << 40; + let mut t = [[0i64; N]; N]; + for (i, row) in t.iter_mut().enumerate().take(p.rows.min(N)) { + for (d, s) in row.iter_mut().zip(&coefs[i * N..i * N + N]) { + *d = *s as i64; + } + tx1d_wide(row, N, p.row_adst); + for v in row.iter_mut() { + *v = (*v).clamp(-lim, lim); + } + } + let mut col = [0i64; N]; + for j in 0..N { + for i in 0..N { + col[i] = t[i][j]; + } + tx1d_wide(&mut col, N, p.col_adst); + for i in 0..N { + let d = &mut dst[i * stride + j]; + let r = ((col[i].clamp(-lim, lim) + p.round as i64) >> p.shift).clamp(-(1 << 30), 1 << 30) as i32; + *d = (*d as i32 + r).clamp(0, p.max) as u16; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Reference inverse DCT (orthonormal, scaled like VP9: DC gain 1/sqrt(2) per pass times + /// sqrt(2) ... ) — compare basis functions up to rounding. + fn float_idct(input: &[f64]) -> Vec { + let n = input.len(); + (0..n) + .map(|x| { + let mut s = input[0] / 2f64.sqrt(); + for (k, &v) in input.iter().enumerate().skip(1) { + s += v * ((2 * x + 1) as f64 * k as f64 * std::f64::consts::PI / (2 * n) as f64).cos(); + } + s + }) + .collect() + } + + #[test] + fn idct_matches_float_reference() { + for n in [4usize, 8, 16, 32] { + for k in 0..n { + let mut buf = vec![0i32; n]; + buf[k] = 1000; + tx1d_narrow(&mut buf, n, false); + let mut inp = vec![0f64; n]; + inp[k] = 1000.0; + let r = float_idct(&inp); + for (a, b) in buf.iter().zip(&r) { + assert!((*a as f64 - b).abs() <= 3.0, "n {n} k {k}: {buf:?} vs {r:?}"); + } + let mut w: Vec = vec![0; n]; + w[k] = 1000; + tx1d_wide(&mut w, n, false); + assert_eq!(w.iter().map(|&v| v as i32).collect::>(), buf); + } + } + } + + #[test] + fn adst_is_sine_basis() { + // VP9's ADST (for n = 8, 16) is a DST-IV variant: out[x] ~ sum in[k] sin(pi (2x+1)(2k+1) / 4n). + for n in [8usize, 16] { + for k in 0..n { + let mut buf = vec![0i32; n]; + buf[k] = 1000; + tx1d_narrow(&mut buf, n, true); + let r: Vec = (0..n) + .map(|x| { + 1000.0 * ((2 * x + 1) as f64 * (2 * k + 1) as f64 * std::f64::consts::PI / (4 * n) as f64).sin() * 2f64.sqrt() / 2f64.sqrt() + }) + .collect(); + for (a, b) in buf.iter().zip(&r) { + assert!((*a as f64 - b).abs() <= 4.0, "n {n} k {k}: {buf:?} vs {r:?}"); + } + } + } + // ADST4 is the sine transform sin(pi (x+1)(2k+1) / 9) scaled. + for k in 0..4 { + let mut buf = vec![0i32; 4]; + buf[k] = 1000; + tx1d_narrow(&mut buf, 4, true); + let r: Vec = + (0..4).map(|x| 1000.0 * (std::f64::consts::PI * (x + 1) as f64 * (2 * k + 1) as f64 / 9.0).sin() * 2.0 * 2f64.sqrt() / 3.0).collect(); + for (a, b) in buf.iter().zip(&r) { + assert!((*a as f64 - b).abs() <= 3.0, "k {k}: {buf:?} vs {r:?}"); + } + } + } + + #[test] + fn dc_only_shortcut_matches_full_transform() { + for tx in 0..4u8 { + let n = 4usize << tx; + for dc in [-5000, -37, -1, 1, 2, 57, 1234, 9999] { + let mut coefs = vec![0i32; n * n]; + coefs[0] = dc; + let mut a = vec![128u16; n * n]; + let mut b = vec![128u16; n * n]; + inverse_transform_add( + &coefs, + &InverseTx { tx_size: tx, tx_type: DCT_DCT, lossless: false, eob: 1, rows: 1, bit_depth: 8 }, + &mut a, + n, + ); + inverse_transform_add( + &coefs, + &InverseTx { tx_size: tx, tx_type: DCT_DCT, lossless: false, eob: 2, rows: n, bit_depth: 8 }, + &mut b, + n, + ); + assert_eq!(a, b, "tx {tx} dc {dc}"); + } + } + } + + /// The per-column formulation the vectorised column pass replaced. + fn itx_narrow_ref(coefs: &[i32], p: &TxParams, dst: &mut [u16], stride: usize) { + let lim = 1 << 24; + let mut t = [[0i32; N]; N]; + for (i, row) in t.iter_mut().enumerate().take(p.rows.min(N)) { + for (d, s) in row.iter_mut().zip(&coefs[i * N..i * N + N]) { + *d = sat(*s, lim); + } + tx1d_narrow(row, N, p.row_adst); + for v in row.iter_mut() { + *v = sat(*v, lim); + } + } + let mut col = [0i32; N]; + for j in 0..N { + for i in 0..N { + col[i] = t[i][j]; + } + tx1d_narrow(&mut col, N, p.col_adst); + for i in 0..N { + let d = &mut dst[i * stride + j]; + *d = (*d as i32 + (col[i].wrapping_add(p.round) >> p.shift).clamp(-(1 << 20), 1 << 20)).clamp(0, p.max) as u16; + } + } + } + + fn check_cols(seed: &mut u32) { + let mut rnd = || { + *seed ^= *seed << 13; + *seed ^= *seed >> 17; + *seed ^= *seed << 5; + *seed + }; + let tx_size = N.trailing_zeros() - 2; + let shift = (tx_size + 4).min(6); + for case in 0..200 { + let (row_adst, col_adst) = if N == 32 { (false, false) } else { (case & 1 == 1, case & 2 == 2) }; + let rows = 1 + rnd() as usize % N; + let big = if case % 7 == 0 { 1 << 22 } else { 2000 }; + let coefs: Vec = + (0..N * N).map(|i| if i / N < rows && rnd() % 3 == 0 { (rnd() % (2 * big)) as i32 - big as i32 } else { 0 }).collect(); + let p = TxParams { row_adst, col_adst, rows, shift, round: 1 << (shift - 1), max: 255 }; + let stride = N + 5; + let base: Vec = (0..N * stride).map(|_| (rnd() % 256) as u16).collect(); + let (mut a, mut b) = (base.clone(), base); + itx_narrow::(&coefs, &p, &mut a, stride); + itx_narrow_ref::(&coefs, &p, &mut b, stride); + assert_eq!(a, b, "N {N} case {case}"); + } + } + + #[test] + fn column_pass_matches_per_column_transform() { + let mut seed = 0x9e37_79b9u32; + check_cols::<4>(&mut seed); + check_cols::<8>(&mut seed); + check_cols::<16>(&mut seed); + check_cols::<32>(&mut seed); + } + + #[test] + fn wht_round_trip_identity() { + // A single DC coefficient of 4 * v spreads v / 4 ... check invertibility on a simple case: + // input [4, 0, ..] (after the >> 2 pre-scaling a unit impulse). + let mut coefs = [0i32; 16]; + coefs[0] = 4 * 4; + let mut dst = [100u16; 16]; + inverse_transform_add(&coefs, &InverseTx { tx_size: 0, tx_type: DCT_DCT, lossless: true, eob: 1, rows: 4, bit_depth: 8 }, &mut dst, 4); + let sum: i32 = dst.iter().map(|&v| v as i32 - 100).sum(); + assert_eq!(sum, 16); + } +} diff --git a/crates/video/src/vp9/decode/transform_gen.rs b/crates/video/src/vp9/decode/transform_gen.rs new file mode 100644 index 000000000..5a1598e62 --- /dev/null +++ b/crates/video/src/vp9/decode/transform_gen.rs @@ -0,0 +1,489 @@ +// Generated by tools/gen_transforms.py from the butterfly descriptions of the VP9 specification +// v0.6 section 8.7.1. Do not edit by hand. + +macro_rules! transforms_1d { + ($t:ty) => { + #[inline(always)] + fn r14(x: $t) -> $t { + x.wrapping_add(1 << 13) >> 14 + } + + #[inline(always)] + pub fn idct4(x: [$t; 4]) -> [$t; 4] { + let v0 = r14(x[0].wrapping_mul(11585).wrapping_add(x[2].wrapping_mul(-11585))); + let v1 = r14(x[0].wrapping_mul(11585).wrapping_add(x[2].wrapping_mul(11585))); + let v2 = r14(x[1].wrapping_mul(6270).wrapping_add(x[3].wrapping_mul(-15137))); + let v3 = r14(x[1].wrapping_mul(15137).wrapping_add(x[3].wrapping_mul(6270))); + let v4 = v1.wrapping_add(v3); + let v5 = v1.wrapping_sub(v3); + let v6 = v0.wrapping_add(v2); + let v7 = v0.wrapping_sub(v2); + [v4, v6, v7, v5] + } + + #[inline(always)] + pub fn idct8(x: [$t; 8]) -> [$t; 8] { + let v0 = r14(x[0].wrapping_mul(11585).wrapping_add(x[4].wrapping_mul(-11585))); + let v1 = r14(x[0].wrapping_mul(11585).wrapping_add(x[4].wrapping_mul(11585))); + let v2 = r14(x[2].wrapping_mul(6270).wrapping_add(x[6].wrapping_mul(-15137))); + let v3 = r14(x[2].wrapping_mul(15137).wrapping_add(x[6].wrapping_mul(6270))); + let v4 = v1.wrapping_add(v3); + let v5 = v1.wrapping_sub(v3); + let v6 = v0.wrapping_add(v2); + let v7 = v0.wrapping_sub(v2); + let v8 = r14(x[1].wrapping_mul(3196).wrapping_add(x[7].wrapping_mul(-16069))); + let v9 = r14(x[1].wrapping_mul(16069).wrapping_add(x[7].wrapping_mul(3196))); + let v10 = r14(x[5].wrapping_mul(13623).wrapping_add(x[3].wrapping_mul(-9102))); + let v11 = r14(x[5].wrapping_mul(9102).wrapping_add(x[3].wrapping_mul(13623))); + let v12 = v8.wrapping_add(v10); + let v13 = v8.wrapping_sub(v10); + let v14 = v9.wrapping_add(v11); + let v15 = v9.wrapping_sub(v11); + let v16 = r14(v15.wrapping_mul(11585).wrapping_add(v13.wrapping_mul(-11585))); + let v17 = r14(v15.wrapping_mul(11585).wrapping_add(v13.wrapping_mul(11585))); + let v18 = v4.wrapping_add(v14); + let v19 = v4.wrapping_sub(v14); + let v20 = v6.wrapping_add(v17); + let v21 = v6.wrapping_sub(v17); + let v22 = v7.wrapping_add(v16); + let v23 = v7.wrapping_sub(v16); + let v24 = v5.wrapping_add(v12); + let v25 = v5.wrapping_sub(v12); + [v18, v20, v22, v24, v25, v23, v21, v19] + } + + #[inline(always)] + pub fn idct16(x: [$t; 16]) -> [$t; 16] { + let v0 = r14(x[0].wrapping_mul(11585).wrapping_add(x[8].wrapping_mul(-11585))); + let v1 = r14(x[0].wrapping_mul(11585).wrapping_add(x[8].wrapping_mul(11585))); + let v2 = r14(x[4].wrapping_mul(6270).wrapping_add(x[12].wrapping_mul(-15137))); + let v3 = r14(x[4].wrapping_mul(15137).wrapping_add(x[12].wrapping_mul(6270))); + let v4 = v1.wrapping_add(v3); + let v5 = v1.wrapping_sub(v3); + let v6 = v0.wrapping_add(v2); + let v7 = v0.wrapping_sub(v2); + let v8 = r14(x[2].wrapping_mul(3196).wrapping_add(x[14].wrapping_mul(-16069))); + let v9 = r14(x[2].wrapping_mul(16069).wrapping_add(x[14].wrapping_mul(3196))); + let v10 = r14(x[10].wrapping_mul(13623).wrapping_add(x[6].wrapping_mul(-9102))); + let v11 = r14(x[10].wrapping_mul(9102).wrapping_add(x[6].wrapping_mul(13623))); + let v12 = v8.wrapping_add(v10); + let v13 = v8.wrapping_sub(v10); + let v14 = v9.wrapping_add(v11); + let v15 = v9.wrapping_sub(v11); + let v16 = r14(v15.wrapping_mul(11585).wrapping_add(v13.wrapping_mul(-11585))); + let v17 = r14(v15.wrapping_mul(11585).wrapping_add(v13.wrapping_mul(11585))); + let v18 = v4.wrapping_add(v14); + let v19 = v4.wrapping_sub(v14); + let v20 = v6.wrapping_add(v17); + let v21 = v6.wrapping_sub(v17); + let v22 = v7.wrapping_add(v16); + let v23 = v7.wrapping_sub(v16); + let v24 = v5.wrapping_add(v12); + let v25 = v5.wrapping_sub(v12); + let v26 = r14(x[1].wrapping_mul(1606).wrapping_add(x[15].wrapping_mul(-16305))); + let v27 = r14(x[1].wrapping_mul(16305).wrapping_add(x[15].wrapping_mul(1606))); + let v28 = r14(x[9].wrapping_mul(12665).wrapping_add(x[7].wrapping_mul(-10394))); + let v29 = r14(x[9].wrapping_mul(10394).wrapping_add(x[7].wrapping_mul(12665))); + let v30 = r14(x[5].wrapping_mul(7723).wrapping_add(x[11].wrapping_mul(-14449))); + let v31 = r14(x[5].wrapping_mul(14449).wrapping_add(x[11].wrapping_mul(7723))); + let v32 = r14(x[13].wrapping_mul(15679).wrapping_add(x[3].wrapping_mul(-4756))); + let v33 = r14(x[13].wrapping_mul(4756).wrapping_add(x[3].wrapping_mul(15679))); + let v34 = v26.wrapping_add(v28); + let v35 = v26.wrapping_sub(v28); + let v36 = v32.wrapping_add(v30); + let v37 = v32.wrapping_sub(v30); + let v38 = v33.wrapping_add(v31); + let v39 = v33.wrapping_sub(v31); + let v40 = v27.wrapping_add(v29); + let v41 = v27.wrapping_sub(v29); + let v42 = r14(v41.wrapping_mul(6270).wrapping_add(v35.wrapping_mul(-15137))); + let v43 = r14(v41.wrapping_mul(15137).wrapping_add(v35.wrapping_mul(6270))); + let v44 = r14(v37.wrapping_mul(-15137).wrapping_add(v39.wrapping_mul(6270))); + let v45 = r14(v37.wrapping_mul(-6270).wrapping_add(v39.wrapping_mul(-15137))); + let v46 = v34.wrapping_add(v36); + let v47 = v34.wrapping_sub(v36); + let v48 = v40.wrapping_add(v38); + let v49 = v40.wrapping_sub(v38); + let v50 = v42.wrapping_add(v45); + let v51 = v42.wrapping_sub(v45); + let v52 = v43.wrapping_add(v44); + let v53 = v43.wrapping_sub(v44); + let v54 = r14(v53.wrapping_mul(11585).wrapping_add(v51.wrapping_mul(-11585))); + let v55 = r14(v53.wrapping_mul(11585).wrapping_add(v51.wrapping_mul(11585))); + let v56 = r14(v49.wrapping_mul(11585).wrapping_add(v47.wrapping_mul(-11585))); + let v57 = r14(v49.wrapping_mul(11585).wrapping_add(v47.wrapping_mul(11585))); + let v58 = v18.wrapping_add(v48); + let v59 = v18.wrapping_sub(v48); + let v60 = v20.wrapping_add(v52); + let v61 = v20.wrapping_sub(v52); + let v62 = v22.wrapping_add(v55); + let v63 = v22.wrapping_sub(v55); + let v64 = v24.wrapping_add(v57); + let v65 = v24.wrapping_sub(v57); + let v66 = v25.wrapping_add(v56); + let v67 = v25.wrapping_sub(v56); + let v68 = v23.wrapping_add(v54); + let v69 = v23.wrapping_sub(v54); + let v70 = v21.wrapping_add(v50); + let v71 = v21.wrapping_sub(v50); + let v72 = v19.wrapping_add(v46); + let v73 = v19.wrapping_sub(v46); + [v58, v60, v62, v64, v66, v68, v70, v72, v73, v71, v69, v67, v65, v63, v61, v59] + } + + #[inline(always)] + pub fn idct32(x: [$t; 32]) -> [$t; 32] { + let v0 = r14(x[0].wrapping_mul(11585).wrapping_add(x[16].wrapping_mul(-11585))); + let v1 = r14(x[0].wrapping_mul(11585).wrapping_add(x[16].wrapping_mul(11585))); + let v2 = r14(x[8].wrapping_mul(6270).wrapping_add(x[24].wrapping_mul(-15137))); + let v3 = r14(x[8].wrapping_mul(15137).wrapping_add(x[24].wrapping_mul(6270))); + let v4 = v1.wrapping_add(v3); + let v5 = v1.wrapping_sub(v3); + let v6 = v0.wrapping_add(v2); + let v7 = v0.wrapping_sub(v2); + let v8 = r14(x[4].wrapping_mul(3196).wrapping_add(x[28].wrapping_mul(-16069))); + let v9 = r14(x[4].wrapping_mul(16069).wrapping_add(x[28].wrapping_mul(3196))); + let v10 = r14(x[20].wrapping_mul(13623).wrapping_add(x[12].wrapping_mul(-9102))); + let v11 = r14(x[20].wrapping_mul(9102).wrapping_add(x[12].wrapping_mul(13623))); + let v12 = v8.wrapping_add(v10); + let v13 = v8.wrapping_sub(v10); + let v14 = v9.wrapping_add(v11); + let v15 = v9.wrapping_sub(v11); + let v16 = r14(v15.wrapping_mul(11585).wrapping_add(v13.wrapping_mul(-11585))); + let v17 = r14(v15.wrapping_mul(11585).wrapping_add(v13.wrapping_mul(11585))); + let v18 = v4.wrapping_add(v14); + let v19 = v4.wrapping_sub(v14); + let v20 = v6.wrapping_add(v17); + let v21 = v6.wrapping_sub(v17); + let v22 = v7.wrapping_add(v16); + let v23 = v7.wrapping_sub(v16); + let v24 = v5.wrapping_add(v12); + let v25 = v5.wrapping_sub(v12); + let v26 = r14(x[2].wrapping_mul(1606).wrapping_add(x[30].wrapping_mul(-16305))); + let v27 = r14(x[2].wrapping_mul(16305).wrapping_add(x[30].wrapping_mul(1606))); + let v28 = r14(x[18].wrapping_mul(12665).wrapping_add(x[14].wrapping_mul(-10394))); + let v29 = r14(x[18].wrapping_mul(10394).wrapping_add(x[14].wrapping_mul(12665))); + let v30 = r14(x[10].wrapping_mul(7723).wrapping_add(x[22].wrapping_mul(-14449))); + let v31 = r14(x[10].wrapping_mul(14449).wrapping_add(x[22].wrapping_mul(7723))); + let v32 = r14(x[26].wrapping_mul(15679).wrapping_add(x[6].wrapping_mul(-4756))); + let v33 = r14(x[26].wrapping_mul(4756).wrapping_add(x[6].wrapping_mul(15679))); + let v34 = v26.wrapping_add(v28); + let v35 = v26.wrapping_sub(v28); + let v36 = v32.wrapping_add(v30); + let v37 = v32.wrapping_sub(v30); + let v38 = v33.wrapping_add(v31); + let v39 = v33.wrapping_sub(v31); + let v40 = v27.wrapping_add(v29); + let v41 = v27.wrapping_sub(v29); + let v42 = r14(v41.wrapping_mul(6270).wrapping_add(v35.wrapping_mul(-15137))); + let v43 = r14(v41.wrapping_mul(15137).wrapping_add(v35.wrapping_mul(6270))); + let v44 = r14(v37.wrapping_mul(-15137).wrapping_add(v39.wrapping_mul(6270))); + let v45 = r14(v37.wrapping_mul(-6270).wrapping_add(v39.wrapping_mul(-15137))); + let v46 = v34.wrapping_add(v36); + let v47 = v34.wrapping_sub(v36); + let v48 = v40.wrapping_add(v38); + let v49 = v40.wrapping_sub(v38); + let v50 = v42.wrapping_add(v45); + let v51 = v42.wrapping_sub(v45); + let v52 = v43.wrapping_add(v44); + let v53 = v43.wrapping_sub(v44); + let v54 = r14(v53.wrapping_mul(11585).wrapping_add(v51.wrapping_mul(-11585))); + let v55 = r14(v53.wrapping_mul(11585).wrapping_add(v51.wrapping_mul(11585))); + let v56 = r14(v49.wrapping_mul(11585).wrapping_add(v47.wrapping_mul(-11585))); + let v57 = r14(v49.wrapping_mul(11585).wrapping_add(v47.wrapping_mul(11585))); + let v58 = v18.wrapping_add(v48); + let v59 = v18.wrapping_sub(v48); + let v60 = v20.wrapping_add(v52); + let v61 = v20.wrapping_sub(v52); + let v62 = v22.wrapping_add(v55); + let v63 = v22.wrapping_sub(v55); + let v64 = v24.wrapping_add(v57); + let v65 = v24.wrapping_sub(v57); + let v66 = v25.wrapping_add(v56); + let v67 = v25.wrapping_sub(v56); + let v68 = v23.wrapping_add(v54); + let v69 = v23.wrapping_sub(v54); + let v70 = v21.wrapping_add(v50); + let v71 = v21.wrapping_sub(v50); + let v72 = v19.wrapping_add(v46); + let v73 = v19.wrapping_sub(v46); + let v74 = r14(x[1].wrapping_mul(804).wrapping_add(x[31].wrapping_mul(-16364))); + let v75 = r14(x[1].wrapping_mul(16364).wrapping_add(x[31].wrapping_mul(804))); + let v76 = r14(x[17].wrapping_mul(12140).wrapping_add(x[15].wrapping_mul(-11003))); + let v77 = r14(x[17].wrapping_mul(11003).wrapping_add(x[15].wrapping_mul(12140))); + let v78 = r14(x[9].wrapping_mul(7005).wrapping_add(x[23].wrapping_mul(-14811))); + let v79 = r14(x[9].wrapping_mul(14811).wrapping_add(x[23].wrapping_mul(7005))); + let v80 = r14(x[25].wrapping_mul(15426).wrapping_add(x[7].wrapping_mul(-5520))); + let v81 = r14(x[25].wrapping_mul(5520).wrapping_add(x[7].wrapping_mul(15426))); + let v82 = r14(x[5].wrapping_mul(3981).wrapping_add(x[27].wrapping_mul(-15893))); + let v83 = r14(x[5].wrapping_mul(15893).wrapping_add(x[27].wrapping_mul(3981))); + let v84 = r14(x[21].wrapping_mul(14053).wrapping_add(x[11].wrapping_mul(-8423))); + let v85 = r14(x[21].wrapping_mul(8423).wrapping_add(x[11].wrapping_mul(14053))); + let v86 = r14(x[13].wrapping_mul(9760).wrapping_add(x[19].wrapping_mul(-13160))); + let v87 = r14(x[13].wrapping_mul(13160).wrapping_add(x[19].wrapping_mul(9760))); + let v88 = r14(x[29].wrapping_mul(16207).wrapping_add(x[3].wrapping_mul(-2404))); + let v89 = r14(x[29].wrapping_mul(2404).wrapping_add(x[3].wrapping_mul(16207))); + let v90 = v74.wrapping_add(v76); + let v91 = v74.wrapping_sub(v76); + let v92 = v80.wrapping_add(v78); + let v93 = v80.wrapping_sub(v78); + let v94 = v82.wrapping_add(v84); + let v95 = v82.wrapping_sub(v84); + let v96 = v88.wrapping_add(v86); + let v97 = v88.wrapping_sub(v86); + let v98 = v89.wrapping_add(v87); + let v99 = v89.wrapping_sub(v87); + let v100 = v83.wrapping_add(v85); + let v101 = v83.wrapping_sub(v85); + let v102 = v81.wrapping_add(v79); + let v103 = v81.wrapping_sub(v79); + let v104 = v75.wrapping_add(v77); + let v105 = v75.wrapping_sub(v77); + let v106 = r14(v105.wrapping_mul(3196).wrapping_add(v91.wrapping_mul(-16069))); + let v107 = r14(v105.wrapping_mul(16069).wrapping_add(v91.wrapping_mul(3196))); + let v108 = r14(v97.wrapping_mul(-9102).wrapping_add(v99.wrapping_mul(13623))); + let v109 = r14(v97.wrapping_mul(-13623).wrapping_add(v99.wrapping_mul(-9102))); + let v110 = r14(v101.wrapping_mul(13623).wrapping_add(v95.wrapping_mul(-9102))); + let v111 = r14(v101.wrapping_mul(9102).wrapping_add(v95.wrapping_mul(13623))); + let v112 = r14(v93.wrapping_mul(-16069).wrapping_add(v103.wrapping_mul(3196))); + let v113 = r14(v93.wrapping_mul(-3196).wrapping_add(v103.wrapping_mul(-16069))); + let v114 = v90.wrapping_add(v92); + let v115 = v90.wrapping_sub(v92); + let v116 = v96.wrapping_add(v94); + let v117 = v96.wrapping_sub(v94); + let v118 = v98.wrapping_add(v100); + let v119 = v98.wrapping_sub(v100); + let v120 = v104.wrapping_add(v102); + let v121 = v104.wrapping_sub(v102); + let v122 = v106.wrapping_add(v113); + let v123 = v106.wrapping_sub(v113); + let v124 = v109.wrapping_add(v110); + let v125 = v109.wrapping_sub(v110); + let v126 = v108.wrapping_add(v111); + let v127 = v108.wrapping_sub(v111); + let v128 = v107.wrapping_add(v112); + let v129 = v107.wrapping_sub(v112); + let v130 = r14(v129.wrapping_mul(6270).wrapping_add(v123.wrapping_mul(-15137))); + let v131 = r14(v129.wrapping_mul(15137).wrapping_add(v123.wrapping_mul(6270))); + let v132 = r14(v125.wrapping_mul(-15137).wrapping_add(v127.wrapping_mul(6270))); + let v133 = r14(v125.wrapping_mul(-6270).wrapping_add(v127.wrapping_mul(-15137))); + let v134 = r14(v121.wrapping_mul(6270).wrapping_add(v115.wrapping_mul(-15137))); + let v135 = r14(v121.wrapping_mul(15137).wrapping_add(v115.wrapping_mul(6270))); + let v136 = r14(v117.wrapping_mul(-15137).wrapping_add(v119.wrapping_mul(6270))); + let v137 = r14(v117.wrapping_mul(-6270).wrapping_add(v119.wrapping_mul(-15137))); + let v138 = v114.wrapping_add(v116); + let v139 = v114.wrapping_sub(v116); + let v140 = v120.wrapping_add(v118); + let v141 = v120.wrapping_sub(v118); + let v142 = v122.wrapping_add(v124); + let v143 = v122.wrapping_sub(v124); + let v144 = v128.wrapping_add(v126); + let v145 = v128.wrapping_sub(v126); + let v146 = v130.wrapping_add(v133); + let v147 = v130.wrapping_sub(v133); + let v148 = v131.wrapping_add(v132); + let v149 = v131.wrapping_sub(v132); + let v150 = v134.wrapping_add(v137); + let v151 = v134.wrapping_sub(v137); + let v152 = v135.wrapping_add(v136); + let v153 = v135.wrapping_sub(v136); + let v154 = r14(v153.wrapping_mul(11585).wrapping_add(v151.wrapping_mul(-11585))); + let v155 = r14(v153.wrapping_mul(11585).wrapping_add(v151.wrapping_mul(11585))); + let v156 = r14(v149.wrapping_mul(11585).wrapping_add(v147.wrapping_mul(-11585))); + let v157 = r14(v149.wrapping_mul(11585).wrapping_add(v147.wrapping_mul(11585))); + let v158 = r14(v145.wrapping_mul(11585).wrapping_add(v143.wrapping_mul(-11585))); + let v159 = r14(v145.wrapping_mul(11585).wrapping_add(v143.wrapping_mul(11585))); + let v160 = r14(v141.wrapping_mul(11585).wrapping_add(v139.wrapping_mul(-11585))); + let v161 = r14(v141.wrapping_mul(11585).wrapping_add(v139.wrapping_mul(11585))); + let v162 = v58.wrapping_add(v140); + let v163 = v58.wrapping_sub(v140); + let v164 = v60.wrapping_add(v144); + let v165 = v60.wrapping_sub(v144); + let v166 = v62.wrapping_add(v148); + let v167 = v62.wrapping_sub(v148); + let v168 = v64.wrapping_add(v152); + let v169 = v64.wrapping_sub(v152); + let v170 = v66.wrapping_add(v155); + let v171 = v66.wrapping_sub(v155); + let v172 = v68.wrapping_add(v157); + let v173 = v68.wrapping_sub(v157); + let v174 = v70.wrapping_add(v159); + let v175 = v70.wrapping_sub(v159); + let v176 = v72.wrapping_add(v161); + let v177 = v72.wrapping_sub(v161); + let v178 = v73.wrapping_add(v160); + let v179 = v73.wrapping_sub(v160); + let v180 = v71.wrapping_add(v158); + let v181 = v71.wrapping_sub(v158); + let v182 = v69.wrapping_add(v156); + let v183 = v69.wrapping_sub(v156); + let v184 = v67.wrapping_add(v154); + let v185 = v67.wrapping_sub(v154); + let v186 = v65.wrapping_add(v150); + let v187 = v65.wrapping_sub(v150); + let v188 = v63.wrapping_add(v146); + let v189 = v63.wrapping_sub(v146); + let v190 = v61.wrapping_add(v142); + let v191 = v61.wrapping_sub(v142); + let v192 = v59.wrapping_add(v138); + let v193 = v59.wrapping_sub(v138); + [v162, v164, v166, v168, v170, v172, v174, v176, v178, v180, v182, v184, v186, v188, v190, v192, v193, v191, v189, v187, v185, v183, v181, v179, v177, v175, v173, v171, v169, v167, v165, v163] + } + + #[inline(always)] + pub fn iadst8(x: [$t; 8]) -> [$t; 8] { + let v0 = x[7].wrapping_mul(1606).wrapping_add(x[0].wrapping_mul(-16305)); + let v1 = x[7].wrapping_mul(16305).wrapping_add(x[0].wrapping_mul(1606)); + let v2 = x[5].wrapping_mul(7723).wrapping_add(x[2].wrapping_mul(-14449)); + let v3 = x[5].wrapping_mul(14449).wrapping_add(x[2].wrapping_mul(7723)); + let v4 = x[3].wrapping_mul(12665).wrapping_add(x[4].wrapping_mul(-10394)); + let v5 = x[3].wrapping_mul(10394).wrapping_add(x[4].wrapping_mul(12665)); + let v6 = x[1].wrapping_mul(15679).wrapping_add(x[6].wrapping_mul(-4756)); + let v7 = x[1].wrapping_mul(4756).wrapping_add(x[6].wrapping_mul(15679)); + let v8 = r14(v1.wrapping_add(v5)); + let v9 = r14(v1.wrapping_sub(v5)); + let v10 = r14(v0.wrapping_add(v4)); + let v11 = r14(v0.wrapping_sub(v4)); + let v12 = r14(v3.wrapping_add(v7)); + let v13 = r14(v3.wrapping_sub(v7)); + let v14 = r14(v2.wrapping_add(v6)); + let v15 = r14(v2.wrapping_sub(v6)); + let v16 = v9.wrapping_mul(6270).wrapping_add(v11.wrapping_mul(-15137)); + let v17 = v9.wrapping_mul(15137).wrapping_add(v11.wrapping_mul(6270)); + let v18 = v15.wrapping_mul(15137).wrapping_add(v13.wrapping_mul(-6270)); + let v19 = v15.wrapping_mul(6270).wrapping_add(v13.wrapping_mul(15137)); + let v20 = r14(v17.wrapping_add(v18)); + let v21 = r14(v17.wrapping_sub(v18)); + let v22 = r14(v16.wrapping_add(v19)); + let v23 = r14(v16.wrapping_sub(v19)); + let v24 = v8.wrapping_add(v12); + let v25 = v8.wrapping_sub(v12); + let v26 = v10.wrapping_add(v14); + let v27 = v10.wrapping_sub(v14); + let v28 = r14(v25.wrapping_mul(11585).wrapping_add(v27.wrapping_mul(-11585))); + let v29 = r14(v25.wrapping_mul(11585).wrapping_add(v27.wrapping_mul(11585))); + let v30 = r14(v21.wrapping_mul(11585).wrapping_add(v23.wrapping_mul(-11585))); + let v31 = r14(v21.wrapping_mul(11585).wrapping_add(v23.wrapping_mul(11585))); + let v32 = v20.wrapping_neg(); + let v33 = v29.wrapping_neg(); + let v34 = v30.wrapping_neg(); + let v35 = v26.wrapping_neg(); + [v24, v32, v31, v33, v28, v34, v22, v35] + } + + #[inline(always)] + pub fn iadst16(x: [$t; 16]) -> [$t; 16] { + let v0 = x[15].wrapping_mul(804).wrapping_add(x[0].wrapping_mul(-16364)); + let v1 = x[15].wrapping_mul(16364).wrapping_add(x[0].wrapping_mul(804)); + let v2 = x[13].wrapping_mul(3981).wrapping_add(x[2].wrapping_mul(-15893)); + let v3 = x[13].wrapping_mul(15893).wrapping_add(x[2].wrapping_mul(3981)); + let v4 = x[11].wrapping_mul(7005).wrapping_add(x[4].wrapping_mul(-14811)); + let v5 = x[11].wrapping_mul(14811).wrapping_add(x[4].wrapping_mul(7005)); + let v6 = x[9].wrapping_mul(9760).wrapping_add(x[6].wrapping_mul(-13160)); + let v7 = x[9].wrapping_mul(13160).wrapping_add(x[6].wrapping_mul(9760)); + let v8 = x[7].wrapping_mul(12140).wrapping_add(x[8].wrapping_mul(-11003)); + let v9 = x[7].wrapping_mul(11003).wrapping_add(x[8].wrapping_mul(12140)); + let v10 = x[5].wrapping_mul(14053).wrapping_add(x[10].wrapping_mul(-8423)); + let v11 = x[5].wrapping_mul(8423).wrapping_add(x[10].wrapping_mul(14053)); + let v12 = x[3].wrapping_mul(15426).wrapping_add(x[12].wrapping_mul(-5520)); + let v13 = x[3].wrapping_mul(5520).wrapping_add(x[12].wrapping_mul(15426)); + let v14 = x[1].wrapping_mul(16207).wrapping_add(x[14].wrapping_mul(-2404)); + let v15 = x[1].wrapping_mul(2404).wrapping_add(x[14].wrapping_mul(16207)); + let v16 = r14(v1.wrapping_add(v9)); + let v17 = r14(v1.wrapping_sub(v9)); + let v18 = r14(v0.wrapping_add(v8)); + let v19 = r14(v0.wrapping_sub(v8)); + let v20 = r14(v3.wrapping_add(v11)); + let v21 = r14(v3.wrapping_sub(v11)); + let v22 = r14(v2.wrapping_add(v10)); + let v23 = r14(v2.wrapping_sub(v10)); + let v24 = r14(v5.wrapping_add(v13)); + let v25 = r14(v5.wrapping_sub(v13)); + let v26 = r14(v4.wrapping_add(v12)); + let v27 = r14(v4.wrapping_sub(v12)); + let v28 = r14(v7.wrapping_add(v15)); + let v29 = r14(v7.wrapping_sub(v15)); + let v30 = r14(v6.wrapping_add(v14)); + let v31 = r14(v6.wrapping_sub(v14)); + let v32 = v17.wrapping_mul(3196).wrapping_add(v19.wrapping_mul(-16069)); + let v33 = v17.wrapping_mul(16069).wrapping_add(v19.wrapping_mul(3196)); + let v34 = v21.wrapping_mul(13623).wrapping_add(v23.wrapping_mul(-9102)); + let v35 = v21.wrapping_mul(9102).wrapping_add(v23.wrapping_mul(13623)); + let v36 = v25.wrapping_mul(16069).wrapping_add(v27.wrapping_mul(3196)); + let v37 = v25.wrapping_mul(-3196).wrapping_add(v27.wrapping_mul(16069)); + let v38 = v29.wrapping_mul(9102).wrapping_add(v31.wrapping_mul(13623)); + let v39 = v29.wrapping_mul(-13623).wrapping_add(v31.wrapping_mul(9102)); + let v40 = r14(v33.wrapping_add(v37)); + let v41 = r14(v33.wrapping_sub(v37)); + let v42 = r14(v32.wrapping_add(v36)); + let v43 = r14(v32.wrapping_sub(v36)); + let v44 = r14(v35.wrapping_add(v39)); + let v45 = r14(v35.wrapping_sub(v39)); + let v46 = r14(v34.wrapping_add(v38)); + let v47 = r14(v34.wrapping_sub(v38)); + let v48 = v16.wrapping_add(v24); + let v49 = v16.wrapping_sub(v24); + let v50 = v18.wrapping_add(v26); + let v51 = v18.wrapping_sub(v26); + let v52 = v20.wrapping_add(v28); + let v53 = v20.wrapping_sub(v28); + let v54 = v22.wrapping_add(v30); + let v55 = v22.wrapping_sub(v30); + let v56 = v49.wrapping_mul(6270).wrapping_add(v51.wrapping_mul(-15137)); + let v57 = v49.wrapping_mul(15137).wrapping_add(v51.wrapping_mul(6270)); + let v58 = v55.wrapping_mul(15137).wrapping_add(v53.wrapping_mul(-6270)); + let v59 = v55.wrapping_mul(6270).wrapping_add(v53.wrapping_mul(15137)); + let v60 = v41.wrapping_mul(6270).wrapping_add(v43.wrapping_mul(-15137)); + let v61 = v41.wrapping_mul(15137).wrapping_add(v43.wrapping_mul(6270)); + let v62 = v47.wrapping_mul(15137).wrapping_add(v45.wrapping_mul(-6270)); + let v63 = v47.wrapping_mul(6270).wrapping_add(v45.wrapping_mul(15137)); + let v64 = r14(v57.wrapping_add(v58)); + let v65 = r14(v57.wrapping_sub(v58)); + let v66 = r14(v61.wrapping_add(v62)); + let v67 = r14(v61.wrapping_sub(v62)); + let v68 = r14(v56.wrapping_add(v59)); + let v69 = r14(v56.wrapping_sub(v59)); + let v70 = r14(v60.wrapping_add(v63)); + let v71 = r14(v60.wrapping_sub(v63)); + let v72 = v48.wrapping_add(v52); + let v73 = v48.wrapping_sub(v52); + let v74 = v40.wrapping_add(v44); + let v75 = v40.wrapping_sub(v44); + let v76 = v50.wrapping_add(v54); + let v77 = v50.wrapping_sub(v54); + let v78 = v42.wrapping_add(v46); + let v79 = v42.wrapping_sub(v46); + let v80 = r14(v73.wrapping_mul(-11585).wrapping_add(v77.wrapping_mul(-11585))); + let v81 = r14(v73.wrapping_mul(11585).wrapping_add(v77.wrapping_mul(-11585))); + let v82 = r14(v65.wrapping_mul(11585).wrapping_add(v69.wrapping_mul(11585))); + let v83 = r14(v65.wrapping_mul(-11585).wrapping_add(v69.wrapping_mul(11585))); + let v84 = r14(v75.wrapping_mul(11585).wrapping_add(v79.wrapping_mul(11585))); + let v85 = r14(v75.wrapping_mul(-11585).wrapping_add(v79.wrapping_mul(11585))); + let v86 = r14(v67.wrapping_mul(-11585).wrapping_add(v71.wrapping_mul(-11585))); + let v87 = r14(v67.wrapping_mul(11585).wrapping_add(v71.wrapping_mul(-11585))); + let v88 = v74.wrapping_neg(); + let v89 = v70.wrapping_neg(); + let v90 = v64.wrapping_neg(); + let v91 = v76.wrapping_neg(); + [v72, v88, v66, v90, v82, v86, v84, v80, v81, v85, v87, v83, v68, v89, v78, v91] + } + + }; +} + +pub(crate) mod narrow { + transforms_1d!(i32); +} + +pub(crate) mod wide { + transforms_1d!(i64); +} + +/// The same butterflies on 4 / 8 columns at once (column pass of the 2D transforms). +pub(crate) mod cols4 { + transforms_1d!(crate::vp9::decode::transform::Cols<4>); +} + +pub(crate) mod cols8 { + transforms_1d!(crate::vp9::decode::transform::Cols<8>); +} diff --git a/crates/video/src/vp9/README.md b/crates/video/src/vp9/encode/README.md similarity index 100% rename from crates/video/src/vp9/README.md rename to crates/video/src/vp9/encode/README.md diff --git a/crates/video/src/vp9/bool.rs b/crates/video/src/vp9/encode/bool.rs similarity index 100% rename from crates/video/src/vp9/bool.rs rename to crates/video/src/vp9/encode/bool.rs diff --git a/crates/video/src/vp9/lf.rs b/crates/video/src/vp9/encode/lf.rs similarity index 99% rename from crates/video/src/vp9/lf.rs rename to crates/video/src/vp9/encode/lf.rs index 0e78bd8c2..6b2b4206c 100644 --- a/crates/video/src/vp9/lf.rs +++ b/crates/video/src/vp9/encode/lf.rs @@ -4,7 +4,7 @@ //! Transform edges inside a skipped inter block are left alone; every 4×4 edge uses the 4-tap //! filter except edges on a 32-sample boundary, which get the 8-tap (flat) filter. -use crate::vp9::{BLOCK_16X16, Mi, Plane}; +use crate::vp9::encode::{BLOCK_16X16, Mi, Plane}; #[derive(Clone, Copy)] struct Limits { diff --git a/crates/video/src/vp9/mc.rs b/crates/video/src/vp9/encode/mc.rs similarity index 99% rename from crates/video/src/vp9/mc.rs rename to crates/video/src/vp9/encode/mc.rs index 9d95cd9ba..25ba6d43e 100644 --- a/crates/video/src/vp9/mc.rs +++ b/crates/video/src/vp9/encode/mc.rs @@ -2,7 +2,7 @@ //! the unscaled block inter prediction process (8.5.2.4) with the regular 8-tap filters and //! reference samples clamped to the reference frame's visible area. -use crate::vp9::tables::SUBPEL_REGULAR; +use crate::vp9::encode::tables::SUBPEL_REGULAR; /// A motion vector in 1/8 luma samples. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/crates/video/src/vp9/encode/mod.rs b/crates/video/src/vp9/encode/mod.rs new file mode 100644 index 000000000..0bfdf3dd5 --- /dev/null +++ b/crates/video/src/vp9/encode/mod.rs @@ -0,0 +1,510 @@ +//! A VP9 encoder (profile 0, 8-bit 4:2:0), written from the *VP9 Bitstream & Decoding Process +//! Specification* v0.6 by mirroring its decoding process in reverse (see README.md). +//! +//! * **Key frames**: 64×64 superblocks split to 8×8 intra blocks (DC / V / H / TM chosen per +//! block), 4×4 DCT/ADST transforms (the Walsh–Hadamard transform at quality 100: lossless). +//! * **Inter frames** (error resilient, single reference: the previous frame as LAST): 16×16 or +//! 8×8 blocks with motion vectors found by a candidate + diamond search refined to quarter +//! samples with the regular 8-tap filters; NEARESTMV / NEARMV / ZEROMV / NEWMV chosen by cost, +//! motion vector prediction exactly as the decoder derives it; skipped blocks; 4×4 DCT residual. +//! * **In-loop deblocking** (8.8) on every lossy frame; the level is picked per frame by trial. +//! * **Probabilities**: per-frame forward updates (compressed header) of the coefficient, skip, +//! inter mode, reference, partition and motion vector probabilities, chosen from the frame's +//! own symbol counts (no backward adaptation: `refresh_frame_context` = 0). +//! * **Rate control**: constant quality (quality → base_q_idx), or a target bitrate with a +//! per-frame q_idx model. +//! +//! The encoder reconstructs exactly what a decoder reconstructs (prediction, integer inverse +//! transforms, loop filter), so prediction never drifts; [`Vp9Encoder::reconstruction`] returns +//! it. + +#![allow(clippy::needless_range_loop)] + +use craft_media::bitstream::BitWriter; +use craft_media::isobmff::VpcConfig; +use craft_media::{CodecId, CodecParams}; + +mod bool; +mod lf; +mod mc; +mod probs; +mod tables; +mod tile; +mod transform; + +pub use mc::Mv; +use probs::Probs; +use tables::{AC_QLOOKUP, DC_QLOOKUP}; +use transform::Quant; + +/// Largest width or height the 16-bit frame size fields can hold. +const MAX_DIMENSION: u32 = 1 << 16; + +pub(crate) const BLOCK_8X8: u8 = 3; +pub(crate) const BLOCK_16X16: u8 = 6; + +/// Encoder settings. +#[derive(Clone, Debug, PartialEq)] +pub struct EncoderConfig { + /// Frame size in samples; the stream stores size - 1 in 16 bits, so values above 65536 are + /// clamped to 65536 by [`Vp9Encoder::new`]. + pub width: u32, + pub height: u32, + /// 1..=100; 100 is lossless. With a target bitrate: the quality of the first frame. + pub quality: u8, + /// Full-range (0–255) rather than limited (16–235) YUV; only signalled. + pub full_range: bool, + /// A key frame every this many frames (1 = every frame is a key frame; 0 = only the first). + pub keyframe_interval: u32, + /// Target bitrate in kbit/s (`None`: constant quality). + pub target_kbps: Option, + /// Frames per second (rate control). + pub frame_rate: f64, + /// The in-loop deblocking filter. + pub loop_filter: bool, +} + +impl Default for EncoderConfig { + fn default() -> Self { + EncoderConfig { + width: 0, + height: 0, + quality: 80, + full_range: false, + keyframe_interval: 60, + target_kbps: None, + frame_rate: 30.0, + loop_filter: true, + } + } +} + +/// One coded frame. +#[derive(Clone, Debug)] +pub struct EncodedFrame { + pub data: Vec, + pub key: bool, + pub q_idx: u8, + pub filter_level: u8, +} + +/// A plane being coded: source and reconstruction at superblock-aligned size. +pub(crate) struct Plane { + /// Stride (superblock-aligned width). + pub w: usize, + /// Last valid column / row for intra prediction edges (the 8-aligned mode-info grid). + pub max_x: usize, + pub max_y: usize, + /// Visible size. + pub vis_w: usize, + pub vis_h: usize, + pub src: Vec, + pub rec: Vec, +} + +impl Plane { + fn new(data: &[u8], vw: usize, vh: usize, w: usize, h: usize, max_x: usize, max_y: usize) -> Plane { + let mut src = vec![0u8; w * h]; + for y in 0..h { + let sy = y.min(vh.saturating_sub(1)); + for x in 0..w { + let sx = x.min(vw.saturating_sub(1)); + src[y * w + x] = data.get(sy * vw + sx).copied().unwrap_or(128); + } + } + Plane { w, max_x, max_y, vis_w: vw, vis_h: vh, src, rec: vec![0; w * h] } + } +} + +/// Mode info of one 8×8 position. +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct Mi { + pub inter: bool, + /// Intra mode (0..=9) or inter mode (NEARESTMV..=NEWMV). + pub mode: u8, + pub mv: Mv, + /// Skip as the loop filter sees it (inter blocks without coefficients). + pub skip: bool, + pub size: u8, +} + +/// Rate control state (target bitrate mode). +#[derive(Clone, Debug)] +struct RateControl { + /// bits ≈ complexity / step: per frame type, from the last frame of that type. + key_complexity: Option, + inter_complexity: Option, + /// Bits spent minus bits budgeted so far. + debt: f64, + last_inter_q: Option, +} + +pub struct Vp9Encoder { + cfg: EncoderConfig, + base_qidx: u8, + lossless: bool, + frames: u64, + since_key: u32, + /// The reconstruction of the last frame (the LAST reference): planes at their strides. + reference: Option<[Plane; 3]>, + mvs: Vec, + rc: RateControl, +} + +fn quality_to_qidx(q: u8) -> u8 { + // 99 ≈ 3, 90 ≈ 30, 50 ≈ 150, 1 ≈ 255. + ((100 - q.clamp(1, 99) as i32) * 255 / 85).clamp(1, 255) as u8 +} + +impl Vp9Encoder { + pub fn new(mut cfg: EncoderConfig) -> Self { + // The frame header stores width - 1 and height - 1 in 16 bits. + cfg.width = cfg.width.min(MAX_DIMENSION); + cfg.height = cfg.height.min(MAX_DIMENSION); + let q = cfg.quality.clamp(1, 100); + let lossless = q >= 100 && cfg.target_kbps.is_none(); + let base_qidx = if lossless { 0 } else { quality_to_qidx(q) }; + Vp9Encoder { + cfg, + base_qidx, + lossless, + frames: 0, + since_key: 0, + reference: None, + mvs: vec![], + rc: RateControl { key_complexity: None, inter_complexity: None, debt: 0.0, last_inter_q: None }, + } + } + + pub fn config(&self) -> &EncoderConfig { + &self.cfg + } + + /// VPCodecConfigurationRecord (`vpcC` box payload, version 1) for MP4 / WebM muxing. + pub fn vpcc(&self) -> Vec { + VpcConfig { + profile: 0, + // 0 = undefined: the encoder enforces no level limits, so none may be claimed. + level: 0, + bit_depth: 8, + chroma_subsampling: 1, + full_range: self.cfg.full_range, + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + codec_init: Vec::new(), + } + .to_bytes() + } + + /// Track description for muxers (`CodecId::Vp9`, `vp09` tag, coded picture size, and `vpcC` extradata). + pub fn codec_params(&self) -> CodecParams { + CodecParams::new(CodecId::Vp9).with_tag(*b"vp09").with_size(self.cfg.width, self.cfg.height).with_extradata(self.vpcc()) + } + + /// Encode one frame from planar 8-bit 4:2:0 (`y`: width×height, `u`/`v`: + /// ((w+1)/2)×((h+1)/2), tightly packed). + pub fn encode_yuv420(&mut self, y: &[u8], u: &[u8], v: &[u8]) -> Vec { + self.encode_frame(y, u, v, false).data + } + + /// The decoder's reconstruction of the last coded frame (visible area, tightly packed Y, U, + /// V). + pub fn reconstruction(&self) -> Option<[Vec; 3]> { + let r = self.reference.as_ref()?; + Some(std::array::from_fn(|i| { + let p = &r[i]; + (0..p.vis_h).flat_map(|y| p.rec[y * p.w..y * p.w + p.vis_w].iter().copied()).collect() + })) + } + + fn is_key_due(&self, force: bool) -> bool { + force + || self.reference.is_none() + || self.cfg.keyframe_interval == 1 + || (self.cfg.keyframe_interval > 1 && self.since_key >= self.cfg.keyframe_interval) + } + + /// The q index of the next frame. + fn pick_qidx(&mut self, key: bool) -> u8 { + if self.lossless { + return 0; + } + let Some(kbps) = self.cfg.target_kbps else { return self.base_qidx }; + let per_frame = kbps as f64 * 1000.0 / self.cfg.frame_rate.max(1.0); + let interval = if self.cfg.keyframe_interval == 0 { 300.0 } else { self.cfg.keyframe_interval as f64 }; + // Key frames get a larger share of the group's budget. + let boost = (interval / 6.0).clamp(1.0, 5.0); + let group = per_frame * interval; + let key_share = group * boost / (boost + interval - 1.0); + let inter_share = (group - key_share) / (interval - 1.0).max(1.0); + let mut target = if key { key_share } else { inter_share }; + // Pay back (or spend) the running difference over about a second. + let horizon = self.cfg.frame_rate.max(1.0); + target = (target - self.rc.debt / horizon).max(target * 0.25); + let complexity = if key { self.rc.key_complexity } else { self.rc.inter_complexity.or(self.rc.key_complexity.map(|c| c / (boost * 2.0))) }; + let Some(cx) = complexity else { return self.base_qidx }; + let step = (cx / target).max(4.0); + let mut q = (0..=255u8).min_by_key(|&q| ((AC_QLOOKUP[q as usize] as f64 - step).abs() * 16.0) as i64).unwrap_or(self.base_qidx); + if !key && let Some(last) = self.rc.last_inter_q { + q = q.clamp(last.saturating_sub(24), last.saturating_add(24)); + } + q.max(1) + } + + fn rc_update(&mut self, key: bool, qidx: u8, bytes: usize) { + let Some(kbps) = self.cfg.target_kbps else { return }; + let bits = bytes as f64 * 8.0; + let cx = bits * AC_QLOOKUP[qidx as usize] as f64; + if key { + self.rc.key_complexity = Some(cx); + } else { + self.rc.inter_complexity = Some(match self.rc.inter_complexity { + Some(c) => c * 0.5 + cx * 0.5, + None => cx, + }); + self.rc.last_inter_q = Some(qidx); + } + let per_frame = kbps as f64 * 1000.0 / self.cfg.frame_rate.max(1.0); + self.rc.debt += bits - per_frame; + } + + /// Encode one frame; `force_key` starts a new key frame. + pub fn encode_frame(&mut self, y: &[u8], u: &[u8], v: &[u8], force_key: bool) -> EncodedFrame { + let (w, h) = (self.cfg.width.max(1) as usize, self.cfg.height.max(1) as usize); + let key = self.is_key_due(force_key); + let qidx = self.pick_qidx(key); + let lossless = qidx == 0; + let mi_cols = w.div_ceil(8); + let mi_rows = h.div_ceil(8); + let sb_cols = mi_cols.div_ceil(8); + let sb_rows = mi_rows.div_ceil(8); + let (pw, ph) = (sb_cols * 64, sb_rows * 64); + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let mut planes = [ + Plane::new(y, w, h, pw, ph, mi_cols * 8 - 1, mi_rows * 8 - 1), + Plane::new(u, cw, ch, pw / 2, ph / 2, mi_cols * 4 - 1, mi_rows * 4 - 1), + Plane::new(v, cw, ch, pw / 2, ph / 2, mi_cols * 4 - 1, mi_rows * 4 - 1), + ]; + // Tile columns: as few as the width allows (tile_info, 6.2.13). + let mut min_log2 = 0; + while (64usize << min_log2) < sb_cols { + min_log2 += 1; + } + let mut max_log2 = 1; + while (sb_cols >> max_log2) >= 4 { + max_log2 += 1; + } + max_log2 -= 1; + let tile_cols = 1usize << min_log2; + let quant = Quant { dcq: DC_QLOOKUP[qidx as usize], acq: AC_QLOOKUP[qidx as usize], lossless, round_dc: 0.5, round_ac: 0.38 }; + let step = AC_QLOOKUP[qidx as usize] as f64 / 8.0; + let lambda_sse = 0.35 * step * step; + let refs = if key { + None + } else { + self.reference + .as_ref() + .map(|r| std::array::from_fn::<_, 3, _>(|i| mc::RefPlane { data: &r[i].rec, stride: r[i].w, w: r[i].vis_w, h: r[i].vis_h })) + }; + let prev = (!key && self.mvs.len() == mi_cols * mi_rows).then_some(&self.mvs[..]); + let phases = refs.as_ref().map(|r| mc::PhasePlanes::new(&r[0], 48)); + let fp = tile::FrameParams { key, quant, mi_cols, mi_rows, refs, phases, prev_mvs: prev, lambda_sad: lambda_sse.sqrt(), lambda_sse }; + let mut mi = vec![Mi::default(); mi_cols * mi_rows]; + let mut writers = vec![]; + for t in 0..tile_cols { + let start = (((t * sb_cols) >> min_log2) * 8).min(mi_cols); + let end = ((((t + 1) * sb_cols) >> min_log2) * 8).min(mi_cols); + let mut tile = tile::Tile::new(&fp, &mut planes, &mut mi, start, end); + tile.code(); + writers.push(tile.w); + } + // Forward probability updates from this frame's counts. + let defaults = Probs::defaults(); + let mut counts = vec![[0u32; 2]; probs::NPROBS]; + for wr in &writers { + wr.count(&mut counts); + } + let wanted = probs::plan_updates(&defaults, &counts, !key); + let (compressed, used) = probs::compressed_header(&defaults, &wanted, lossless, !key); + let tiles: Vec> = writers.iter().map(|wr| wr.emit(&used)).collect(); + drop(fp); + // Loop filter: the level with the least error against the source. + let level = if lossless || !self.cfg.loop_filter { 0 } else { choose_level(&mut planes, &mi, mi_rows, mi_cols, qidx, key) }; + lf::filter_frame(&mut planes, &mi, mi_rows, mi_cols, level); + // Uncompressed header. + let mut bw = BitWriter::new(); + bw.write_bits(2, 2); // frame_marker + bw.write_bits(0, 1); // profile low bit + bw.write_bits(0, 1); // profile high bit + bw.write_bits(0, 1); // show_existing_frame + bw.write_bits(!key as u32, 1); // frame_type + bw.write_bits(1, 1); // show_frame + if key { + bw.write_bits(0, 1); // error_resilient_mode + bw.write_bits(0x49, 8); + bw.write_bits(0x83, 8); + bw.write_bits(0x42, 8); + bw.write_bits(2, 3); // color_space CS_BT_709 + bw.write_bits(self.cfg.full_range as u32, 1); + bw.write_bits(w as u32 - 1, 16); + bw.write_bits(h as u32 - 1, 16); + bw.write_bits(0, 1); // render_and_frame_size_different + bw.write_bits(0, 1); // refresh_frame_context + bw.write_bits(1, 1); // frame_parallel_decoding_mode + } else { + bw.write_bits(1, 1); // error_resilient_mode (no reliance on earlier contexts or vectors) + bw.write_bits(0x01, 8); // refresh_frame_flags: slot 0 becomes the new LAST + for _ in 0..3 { + bw.write_bits(0, 3); // ref_frame_idx: slot 0 + bw.write_bits(0, 1); // sign bias + } + bw.write_bits(1, 1); // found_ref: the size of LAST + bw.write_bits(0, 1); // render_and_frame_size_different + bw.write_bits(0, 1); // allow_high_precision_mv + bw.write_bits(0, 1); // is_filter_switchable + bw.write_bits(1, 2); // raw_interpolation_filter → EIGHTTAP (regular) + } + bw.write_bits(0, 2); // frame_context_idx + bw.write_bits(level as u32, 6); // loop_filter_level + bw.write_bits(0, 3); // sharpness + bw.write_bits(0, 1); // mode_ref_delta_enabled + bw.write_bits(qidx as u32, 8); + bw.write_bits(0, 1); // delta_q_y_dc + bw.write_bits(0, 1); // delta_q_uv_dc + bw.write_bits(0, 1); // delta_q_uv_ac + bw.write_bits(0, 1); // segmentation_enabled + if min_log2 < max_log2 { + bw.write_bits(0, 1); // no extra tile columns + } + bw.write_bits(0, 1); // tile_rows_log2 = 0 + bw.write_bits(compressed.len() as u32, 16); + let mut out = bw.finish(); + out.extend_from_slice(&compressed); + let n = tiles.len(); + for (i, t) in tiles.into_iter().enumerate() { + if i + 1 < n { + out.extend_from_slice(&(t.len() as u32).to_be_bytes()); + } + out.extend_from_slice(&t); + } + // A frame mustn't end in a byte that reads as a superframe marker (0b110xxxxx, Annex B): + // decoders would take its last bytes for a superframe index and reject the frame. The + // last tile's bool-coded data may end in zero padding (9.2.3), so a zero byte fixes it. + if out.last().is_some_and(|b| b & 0xe0 == 0xc0) { + out.push(0); + } + // State for the next frame. + self.mvs = mi.iter().map(|m| if m.inter { m.mv } else { Mv::ZERO }).collect(); + self.reference = Some(planes); + self.frames += 1; + self.since_key = if key { 1 } else { self.since_key + 1 }; + self.rc_update(key, qidx, out.len()); + EncodedFrame { data: out, key, q_idx: qidx, filter_level: level } + } +} + +/// Try a few loop filter levels on the luma reconstruction; the one closest to the source +/// (squared error over the visible area) wins (0 when filtering doesn't help). +fn choose_level(planes: &mut [Plane; 3], mi: &[Mi], mi_rows: usize, mi_cols: usize, qidx: u8, key: bool) -> u8 { + let guess = ((qidx as f64 * if key { 0.25 } else { 0.2 }).round() as u8).clamp(1, 63); + let sse = |p: &Plane| -> u64 { + let mut s = 0u64; + for y in 0..p.vis_h { + for x in 0..p.vis_w { + let d = p.src[y * p.w + x] as i64 - p.rec[y * p.w + x] as i64; + s += (d * d) as u64; + } + } + s + }; + let mut best = (sse(&planes[0]), 0u8); + let saved = planes[0].rec.clone(); + for level in [guess / 2, guess, (guess as u16 * 3 / 2).min(63) as u8] { + if level == 0 { + continue; + } + lf::filter_frame(&mut planes[..1], mi, mi_rows, mi_cols, level); + let s = sse(&planes[0]); + planes[0].rec.copy_from_slice(&saved); + if s < best.0 { + best = (s, level); + } + } + best.1 +} + +// ---------------------------------------------------------------- colour conversion + +/// BT.709 RGB(A) 8-bit → planar YUV 4:2:0 (limited or full range). +pub fn rgba_to_yuv420(rgba: &[u8], w: u32, h: u32, full_range: bool) -> (Vec, Vec, Vec) { + let (w, h) = (w as usize, h as usize); + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let mut y = vec![0u8; w * h]; + let mut u = vec![0u8; cw * ch]; + let mut v = vec![0u8; cw * ch]; + let (ys, yo, cs) = if full_range { (255.0, 0.0, 255.0) } else { (219.0, 16.0, 224.0) }; + let px = |x: usize, yy: usize| -> [f32; 3] { + let i = (yy.min(h.saturating_sub(1)) * w + x.min(w.saturating_sub(1))) * 4; + let c = rgba.get(i..i + 3).unwrap_or(&[0, 0, 0]); + [c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0] + }; + let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; + for yy in 0..h { + for x in 0..w { + y[yy * w + x] = (luma(px(x, yy)) * ys + yo).round().clamp(0.0, 255.0) as u8; + } + } + for cy in 0..ch { + for cx in 0..cw { + let mut acc = [0.0f32; 3]; + for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] { + let c = px(cx * 2 + dx, cy * 2 + dy); + for k in 0..3 { + acc[k] += c[k] / 4.0; + } + } + let l = luma(acc); + let cb = (acc[2] - l) / 1.8556; + let cr = (acc[0] - l) / 1.5748; + u[cy * cw + cx] = (cb * cs + 128.0).round().clamp(0.0, 255.0) as u8; + v[cy * cw + cx] = (cr * cs + 128.0).round().clamp(0.0, 255.0) as u8; + } + } + (y, u, v) +} + +/// The alpha channel as a luma plane (full range) with neutral chroma — the second VP9 stream of +/// WebM alpha. +pub fn alpha_to_yuv420(rgba: &[u8], w: u32, h: u32) -> (Vec, Vec, Vec) { + let (w, h) = (w as usize, h as usize); + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let y: Vec = rgba.as_chunks::<4>().0.iter().take(w * h).map(|p| p[3]).collect(); + (y, vec![128; cw * ch], vec![128; cw * ch]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sizes_beyond_the_16_bit_header_fields_are_clamped() { + let enc = Vp9Encoder::new(EncoderConfig { width: 70_000, height: u32::MAX, ..Default::default() }); + assert_eq!((enc.cfg.width, enc.cfg.height), (65_536, 65_536)); + let enc = Vp9Encoder::new(EncoderConfig { width: 65_536, height: 1, ..Default::default() }); + assert_eq!((enc.cfg.width, enc.cfg.height), (65_536, 1)); + } + + /// A 1080p stream is above the 983,040 luma samples of level 3.1, so it must not claim 3.1 in + /// its `vpcC`; level 0 means "undefined" in the MP4 binding. + #[test] + fn vpcc_claims_no_level_the_encoder_does_not_enforce() { + for (w, h) in [(1920, 1080), (64, 64), (3840, 2160)] { + let enc = Vp9Encoder::new(EncoderConfig { width: w, height: h, ..Default::default() }); + let rec = VpcConfig::parse(&enc.vpcc()).unwrap(); + assert_eq!(rec.level, 0, "{w}x{h}"); + let params = enc.codec_params(); + assert_eq!(VpcConfig::parse(¶ms.extradata).unwrap().level, 0); + } + } +} diff --git a/crates/video/src/vp9/probs.rs b/crates/video/src/vp9/encode/probs.rs similarity index 99% rename from crates/video/src/vp9/probs.rs rename to crates/video/src/vp9/encode/probs.rs index cca880881..fea854209 100644 --- a/crates/video/src/vp9/probs.rs +++ b/crates/video/src/vp9/encode/probs.rs @@ -8,8 +8,8 @@ //! 1, picks updates that pay for themselves, writes them in the compressed header and finally //! entropy codes the recorded symbols with the updated probabilities. -use crate::vp9::bool::BoolEncoder; -use crate::vp9::tables::{ +use crate::vp9::encode::bool::BoolEncoder; +use crate::vp9::encode::tables::{ DEFAULT_COEF_PROBS, DEFAULT_INTER_MODE_PROBS, DEFAULT_IS_INTER_PROB, DEFAULT_MV_BITS_PROB, DEFAULT_MV_CLASS_PROBS, DEFAULT_MV_CLASS0_BIT_PROB, DEFAULT_MV_CLASS0_FR_PROBS, DEFAULT_MV_FR_PROBS, DEFAULT_MV_JOINT_PROBS, DEFAULT_MV_SIGN_PROB, DEFAULT_PARTITION_PROBS, DEFAULT_SINGLE_REF_PROB, DEFAULT_SKIP_PROB, PARETO_TABLE, inv_map_table, diff --git a/crates/video/src/vp9/tables.rs b/crates/video/src/vp9/encode/tables.rs similarity index 100% rename from crates/video/src/vp9/tables.rs rename to crates/video/src/vp9/encode/tables.rs diff --git a/crates/video/src/vp9/tile.rs b/crates/video/src/vp9/encode/tile.rs similarity index 99% rename from crates/video/src/vp9/tile.rs rename to crates/video/src/vp9/encode/tile.rs index ddf215780..d5ecec753 100644 --- a/crates/video/src/vp9/tile.rs +++ b/crates/video/src/vp9/encode/tile.rs @@ -1,19 +1,19 @@ //! Tile coding: partitions, intra blocks (key frames) and inter blocks (motion search, mode -//! decision, motion vector prediction, residual), recorded as symbols (see [`crate::vp9::probs`]). +//! decision, motion vector prediction, residual), recorded as symbols (see [`crate::vp9::encode::probs`]). //! //! Mode info, motion vector candidate lists and contexts follow the decoding process of the //! VP9 specification (6.4, 6.4.x `find_mv_refs` / `find_best_ref_mvs`) for the subset of the //! syntax this encoder emits: 8×8 and 16×16 blocks, single LAST references, no //! `UsePrevFrameMvs` (inter frames are error resilient), no high-precision vectors. -use crate::vp9::mc::{Mv, PhasePlanes, RefPlane, clamp_for_plane, predict}; -use crate::vp9::probs::{self, P, Writer, cost, tree_cost}; -use crate::vp9::tables::{ +use crate::vp9::encode::mc::{Mv, PhasePlanes, RefPlane, clamp_for_plane, predict}; +use crate::vp9::encode::probs::{self, P, Writer, cost, tree_cost}; +use crate::vp9::encode::tables::{ COEFBAND_4X4, COL_SCAN_4X4, COUNTER_TO_CONTEXT, DEFAULT_SCAN_4X4, ENERGY_CLASS, KF_PARTITION_PROBS, KF_UV_MODE_PROBS, KF_Y_MODE_PROBS, MODE_2_COUNTER, MV_REF_8X8, MV_REF_16X16, ROW_SCAN_4X4, }; -use crate::vp9::transform::{ADST_ADST, ADST_DCT, DCT_ADST, DCT_DCT, Quant}; -use crate::vp9::{BLOCK_8X8, BLOCK_16X16, Mi, Plane}; +use crate::vp9::encode::transform::{ADST_ADST, ADST_DCT, DCT_ADST, DCT_DCT, Quant}; +use crate::vp9::encode::{BLOCK_8X8, BLOCK_16X16, Mi, Plane}; pub const DC_PRED: u8 = 0; pub const V_PRED: u8 = 1; diff --git a/crates/video/src/vp9/transform.rs b/crates/video/src/vp9/encode/transform.rs similarity index 100% rename from crates/video/src/vp9/transform.rs rename to crates/video/src/vp9/encode/transform.rs diff --git a/crates/video/src/vp9/mod.rs b/crates/video/src/vp9/mod.rs index 0bfdf3dd5..471c04425 100644 --- a/crates/video/src/vp9/mod.rs +++ b/crates/video/src/vp9/mod.rs @@ -1,510 +1,16 @@ -//! A VP9 encoder (profile 0, 8-bit 4:2:0), written from the *VP9 Bitstream & Decoding Process -//! Specification* v0.6 by mirroring its decoding process in reverse (see README.md). +//! VP9, written from the *VP9 Bitstream & Decoding Process Specification* v0.6. //! -//! * **Key frames**: 64×64 superblocks split to 8×8 intra blocks (DC / V / H / TM chosen per -//! block), 4×4 DCT/ADST transforms (the Walsh–Hadamard transform at quality 100: lossless). -//! * **Inter frames** (error resilient, single reference: the previous frame as LAST): 16×16 or -//! 8×8 blocks with motion vectors found by a candidate + diamond search refined to quarter -//! samples with the regular 8-tap filters; NEARESTMV / NEARMV / ZEROMV / NEWMV chosen by cost, -//! motion vector prediction exactly as the decoder derives it; skipped blocks; 4×4 DCT residual. -//! * **In-loop deblocking** (8.8) on every lossy frame; the level is picked per frame by trial. -//! * **Probabilities**: per-frame forward updates (compressed header) of the coefficient, skip, -//! inter mode, reference, partition and motion vector probabilities, chosen from the frame's -//! own symbol counts (no backward adaptation: `refresh_frame_context` = 0). -//! * **Rate control**: constant quality (quality → base_q_idx), or a target bitrate with a -//! per-frame q_idx model. +//! | Feature | Module | What | +//! |---|---|---| +//! | `vp9-decode` | [`decode`] | Decoder: profiles 0-3, all coding tools | +//! | `vp9-encode` | this module | Encoder: profile 0, 8-bit 4:2:0 ([`Vp9Encoder`]) | //! -//! The encoder reconstructs exactly what a decoder reconstructs (prediction, integer inverse -//! transforms, loop filter), so prediction never drifts; [`Vp9Encoder::reconstruction`] returns -//! it. +//! Feature `vp9` turns on both. -#![allow(clippy::needless_range_loop)] +#[cfg(feature = "vp9-decode")] +pub mod decode; -use craft_media::bitstream::BitWriter; -use craft_media::isobmff::VpcConfig; -use craft_media::{CodecId, CodecParams}; - -mod bool; -mod lf; -mod mc; -mod probs; -mod tables; -mod tile; -mod transform; - -pub use mc::Mv; -use probs::Probs; -use tables::{AC_QLOOKUP, DC_QLOOKUP}; -use transform::Quant; - -/// Largest width or height the 16-bit frame size fields can hold. -const MAX_DIMENSION: u32 = 1 << 16; - -pub(crate) const BLOCK_8X8: u8 = 3; -pub(crate) const BLOCK_16X16: u8 = 6; - -/// Encoder settings. -#[derive(Clone, Debug, PartialEq)] -pub struct EncoderConfig { - /// Frame size in samples; the stream stores size - 1 in 16 bits, so values above 65536 are - /// clamped to 65536 by [`Vp9Encoder::new`]. - pub width: u32, - pub height: u32, - /// 1..=100; 100 is lossless. With a target bitrate: the quality of the first frame. - pub quality: u8, - /// Full-range (0–255) rather than limited (16–235) YUV; only signalled. - pub full_range: bool, - /// A key frame every this many frames (1 = every frame is a key frame; 0 = only the first). - pub keyframe_interval: u32, - /// Target bitrate in kbit/s (`None`: constant quality). - pub target_kbps: Option, - /// Frames per second (rate control). - pub frame_rate: f64, - /// The in-loop deblocking filter. - pub loop_filter: bool, -} - -impl Default for EncoderConfig { - fn default() -> Self { - EncoderConfig { - width: 0, - height: 0, - quality: 80, - full_range: false, - keyframe_interval: 60, - target_kbps: None, - frame_rate: 30.0, - loop_filter: true, - } - } -} - -/// One coded frame. -#[derive(Clone, Debug)] -pub struct EncodedFrame { - pub data: Vec, - pub key: bool, - pub q_idx: u8, - pub filter_level: u8, -} - -/// A plane being coded: source and reconstruction at superblock-aligned size. -pub(crate) struct Plane { - /// Stride (superblock-aligned width). - pub w: usize, - /// Last valid column / row for intra prediction edges (the 8-aligned mode-info grid). - pub max_x: usize, - pub max_y: usize, - /// Visible size. - pub vis_w: usize, - pub vis_h: usize, - pub src: Vec, - pub rec: Vec, -} - -impl Plane { - fn new(data: &[u8], vw: usize, vh: usize, w: usize, h: usize, max_x: usize, max_y: usize) -> Plane { - let mut src = vec![0u8; w * h]; - for y in 0..h { - let sy = y.min(vh.saturating_sub(1)); - for x in 0..w { - let sx = x.min(vw.saturating_sub(1)); - src[y * w + x] = data.get(sy * vw + sx).copied().unwrap_or(128); - } - } - Plane { w, max_x, max_y, vis_w: vw, vis_h: vh, src, rec: vec![0; w * h] } - } -} - -/// Mode info of one 8×8 position. -#[derive(Clone, Copy, Debug, Default)] -pub(crate) struct Mi { - pub inter: bool, - /// Intra mode (0..=9) or inter mode (NEARESTMV..=NEWMV). - pub mode: u8, - pub mv: Mv, - /// Skip as the loop filter sees it (inter blocks without coefficients). - pub skip: bool, - pub size: u8, -} - -/// Rate control state (target bitrate mode). -#[derive(Clone, Debug)] -struct RateControl { - /// bits ≈ complexity / step: per frame type, from the last frame of that type. - key_complexity: Option, - inter_complexity: Option, - /// Bits spent minus bits budgeted so far. - debt: f64, - last_inter_q: Option, -} - -pub struct Vp9Encoder { - cfg: EncoderConfig, - base_qidx: u8, - lossless: bool, - frames: u64, - since_key: u32, - /// The reconstruction of the last frame (the LAST reference): planes at their strides. - reference: Option<[Plane; 3]>, - mvs: Vec, - rc: RateControl, -} - -fn quality_to_qidx(q: u8) -> u8 { - // 99 ≈ 3, 90 ≈ 30, 50 ≈ 150, 1 ≈ 255. - ((100 - q.clamp(1, 99) as i32) * 255 / 85).clamp(1, 255) as u8 -} - -impl Vp9Encoder { - pub fn new(mut cfg: EncoderConfig) -> Self { - // The frame header stores width - 1 and height - 1 in 16 bits. - cfg.width = cfg.width.min(MAX_DIMENSION); - cfg.height = cfg.height.min(MAX_DIMENSION); - let q = cfg.quality.clamp(1, 100); - let lossless = q >= 100 && cfg.target_kbps.is_none(); - let base_qidx = if lossless { 0 } else { quality_to_qidx(q) }; - Vp9Encoder { - cfg, - base_qidx, - lossless, - frames: 0, - since_key: 0, - reference: None, - mvs: vec![], - rc: RateControl { key_complexity: None, inter_complexity: None, debt: 0.0, last_inter_q: None }, - } - } - - pub fn config(&self) -> &EncoderConfig { - &self.cfg - } - - /// VPCodecConfigurationRecord (`vpcC` box payload, version 1) for MP4 / WebM muxing. - pub fn vpcc(&self) -> Vec { - VpcConfig { - profile: 0, - // 0 = undefined: the encoder enforces no level limits, so none may be claimed. - level: 0, - bit_depth: 8, - chroma_subsampling: 1, - full_range: self.cfg.full_range, - colour_primaries: 1, - transfer_characteristics: 1, - matrix_coefficients: 1, - codec_init: Vec::new(), - } - .to_bytes() - } - - /// Track description for muxers (`CodecId::Vp9`, `vp09` tag, coded picture size, and `vpcC` extradata). - pub fn codec_params(&self) -> CodecParams { - CodecParams::new(CodecId::Vp9).with_tag(*b"vp09").with_size(self.cfg.width, self.cfg.height).with_extradata(self.vpcc()) - } - - /// Encode one frame from planar 8-bit 4:2:0 (`y`: width×height, `u`/`v`: - /// ((w+1)/2)×((h+1)/2), tightly packed). - pub fn encode_yuv420(&mut self, y: &[u8], u: &[u8], v: &[u8]) -> Vec { - self.encode_frame(y, u, v, false).data - } - - /// The decoder's reconstruction of the last coded frame (visible area, tightly packed Y, U, - /// V). - pub fn reconstruction(&self) -> Option<[Vec; 3]> { - let r = self.reference.as_ref()?; - Some(std::array::from_fn(|i| { - let p = &r[i]; - (0..p.vis_h).flat_map(|y| p.rec[y * p.w..y * p.w + p.vis_w].iter().copied()).collect() - })) - } - - fn is_key_due(&self, force: bool) -> bool { - force - || self.reference.is_none() - || self.cfg.keyframe_interval == 1 - || (self.cfg.keyframe_interval > 1 && self.since_key >= self.cfg.keyframe_interval) - } - - /// The q index of the next frame. - fn pick_qidx(&mut self, key: bool) -> u8 { - if self.lossless { - return 0; - } - let Some(kbps) = self.cfg.target_kbps else { return self.base_qidx }; - let per_frame = kbps as f64 * 1000.0 / self.cfg.frame_rate.max(1.0); - let interval = if self.cfg.keyframe_interval == 0 { 300.0 } else { self.cfg.keyframe_interval as f64 }; - // Key frames get a larger share of the group's budget. - let boost = (interval / 6.0).clamp(1.0, 5.0); - let group = per_frame * interval; - let key_share = group * boost / (boost + interval - 1.0); - let inter_share = (group - key_share) / (interval - 1.0).max(1.0); - let mut target = if key { key_share } else { inter_share }; - // Pay back (or spend) the running difference over about a second. - let horizon = self.cfg.frame_rate.max(1.0); - target = (target - self.rc.debt / horizon).max(target * 0.25); - let complexity = if key { self.rc.key_complexity } else { self.rc.inter_complexity.or(self.rc.key_complexity.map(|c| c / (boost * 2.0))) }; - let Some(cx) = complexity else { return self.base_qidx }; - let step = (cx / target).max(4.0); - let mut q = (0..=255u8).min_by_key(|&q| ((AC_QLOOKUP[q as usize] as f64 - step).abs() * 16.0) as i64).unwrap_or(self.base_qidx); - if !key && let Some(last) = self.rc.last_inter_q { - q = q.clamp(last.saturating_sub(24), last.saturating_add(24)); - } - q.max(1) - } - - fn rc_update(&mut self, key: bool, qidx: u8, bytes: usize) { - let Some(kbps) = self.cfg.target_kbps else { return }; - let bits = bytes as f64 * 8.0; - let cx = bits * AC_QLOOKUP[qidx as usize] as f64; - if key { - self.rc.key_complexity = Some(cx); - } else { - self.rc.inter_complexity = Some(match self.rc.inter_complexity { - Some(c) => c * 0.5 + cx * 0.5, - None => cx, - }); - self.rc.last_inter_q = Some(qidx); - } - let per_frame = kbps as f64 * 1000.0 / self.cfg.frame_rate.max(1.0); - self.rc.debt += bits - per_frame; - } - - /// Encode one frame; `force_key` starts a new key frame. - pub fn encode_frame(&mut self, y: &[u8], u: &[u8], v: &[u8], force_key: bool) -> EncodedFrame { - let (w, h) = (self.cfg.width.max(1) as usize, self.cfg.height.max(1) as usize); - let key = self.is_key_due(force_key); - let qidx = self.pick_qidx(key); - let lossless = qidx == 0; - let mi_cols = w.div_ceil(8); - let mi_rows = h.div_ceil(8); - let sb_cols = mi_cols.div_ceil(8); - let sb_rows = mi_rows.div_ceil(8); - let (pw, ph) = (sb_cols * 64, sb_rows * 64); - let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); - let mut planes = [ - Plane::new(y, w, h, pw, ph, mi_cols * 8 - 1, mi_rows * 8 - 1), - Plane::new(u, cw, ch, pw / 2, ph / 2, mi_cols * 4 - 1, mi_rows * 4 - 1), - Plane::new(v, cw, ch, pw / 2, ph / 2, mi_cols * 4 - 1, mi_rows * 4 - 1), - ]; - // Tile columns: as few as the width allows (tile_info, 6.2.13). - let mut min_log2 = 0; - while (64usize << min_log2) < sb_cols { - min_log2 += 1; - } - let mut max_log2 = 1; - while (sb_cols >> max_log2) >= 4 { - max_log2 += 1; - } - max_log2 -= 1; - let tile_cols = 1usize << min_log2; - let quant = Quant { dcq: DC_QLOOKUP[qidx as usize], acq: AC_QLOOKUP[qidx as usize], lossless, round_dc: 0.5, round_ac: 0.38 }; - let step = AC_QLOOKUP[qidx as usize] as f64 / 8.0; - let lambda_sse = 0.35 * step * step; - let refs = if key { - None - } else { - self.reference - .as_ref() - .map(|r| std::array::from_fn::<_, 3, _>(|i| mc::RefPlane { data: &r[i].rec, stride: r[i].w, w: r[i].vis_w, h: r[i].vis_h })) - }; - let prev = (!key && self.mvs.len() == mi_cols * mi_rows).then_some(&self.mvs[..]); - let phases = refs.as_ref().map(|r| mc::PhasePlanes::new(&r[0], 48)); - let fp = tile::FrameParams { key, quant, mi_cols, mi_rows, refs, phases, prev_mvs: prev, lambda_sad: lambda_sse.sqrt(), lambda_sse }; - let mut mi = vec![Mi::default(); mi_cols * mi_rows]; - let mut writers = vec![]; - for t in 0..tile_cols { - let start = (((t * sb_cols) >> min_log2) * 8).min(mi_cols); - let end = ((((t + 1) * sb_cols) >> min_log2) * 8).min(mi_cols); - let mut tile = tile::Tile::new(&fp, &mut planes, &mut mi, start, end); - tile.code(); - writers.push(tile.w); - } - // Forward probability updates from this frame's counts. - let defaults = Probs::defaults(); - let mut counts = vec![[0u32; 2]; probs::NPROBS]; - for wr in &writers { - wr.count(&mut counts); - } - let wanted = probs::plan_updates(&defaults, &counts, !key); - let (compressed, used) = probs::compressed_header(&defaults, &wanted, lossless, !key); - let tiles: Vec> = writers.iter().map(|wr| wr.emit(&used)).collect(); - drop(fp); - // Loop filter: the level with the least error against the source. - let level = if lossless || !self.cfg.loop_filter { 0 } else { choose_level(&mut planes, &mi, mi_rows, mi_cols, qidx, key) }; - lf::filter_frame(&mut planes, &mi, mi_rows, mi_cols, level); - // Uncompressed header. - let mut bw = BitWriter::new(); - bw.write_bits(2, 2); // frame_marker - bw.write_bits(0, 1); // profile low bit - bw.write_bits(0, 1); // profile high bit - bw.write_bits(0, 1); // show_existing_frame - bw.write_bits(!key as u32, 1); // frame_type - bw.write_bits(1, 1); // show_frame - if key { - bw.write_bits(0, 1); // error_resilient_mode - bw.write_bits(0x49, 8); - bw.write_bits(0x83, 8); - bw.write_bits(0x42, 8); - bw.write_bits(2, 3); // color_space CS_BT_709 - bw.write_bits(self.cfg.full_range as u32, 1); - bw.write_bits(w as u32 - 1, 16); - bw.write_bits(h as u32 - 1, 16); - bw.write_bits(0, 1); // render_and_frame_size_different - bw.write_bits(0, 1); // refresh_frame_context - bw.write_bits(1, 1); // frame_parallel_decoding_mode - } else { - bw.write_bits(1, 1); // error_resilient_mode (no reliance on earlier contexts or vectors) - bw.write_bits(0x01, 8); // refresh_frame_flags: slot 0 becomes the new LAST - for _ in 0..3 { - bw.write_bits(0, 3); // ref_frame_idx: slot 0 - bw.write_bits(0, 1); // sign bias - } - bw.write_bits(1, 1); // found_ref: the size of LAST - bw.write_bits(0, 1); // render_and_frame_size_different - bw.write_bits(0, 1); // allow_high_precision_mv - bw.write_bits(0, 1); // is_filter_switchable - bw.write_bits(1, 2); // raw_interpolation_filter → EIGHTTAP (regular) - } - bw.write_bits(0, 2); // frame_context_idx - bw.write_bits(level as u32, 6); // loop_filter_level - bw.write_bits(0, 3); // sharpness - bw.write_bits(0, 1); // mode_ref_delta_enabled - bw.write_bits(qidx as u32, 8); - bw.write_bits(0, 1); // delta_q_y_dc - bw.write_bits(0, 1); // delta_q_uv_dc - bw.write_bits(0, 1); // delta_q_uv_ac - bw.write_bits(0, 1); // segmentation_enabled - if min_log2 < max_log2 { - bw.write_bits(0, 1); // no extra tile columns - } - bw.write_bits(0, 1); // tile_rows_log2 = 0 - bw.write_bits(compressed.len() as u32, 16); - let mut out = bw.finish(); - out.extend_from_slice(&compressed); - let n = tiles.len(); - for (i, t) in tiles.into_iter().enumerate() { - if i + 1 < n { - out.extend_from_slice(&(t.len() as u32).to_be_bytes()); - } - out.extend_from_slice(&t); - } - // A frame mustn't end in a byte that reads as a superframe marker (0b110xxxxx, Annex B): - // decoders would take its last bytes for a superframe index and reject the frame. The - // last tile's bool-coded data may end in zero padding (9.2.3), so a zero byte fixes it. - if out.last().is_some_and(|b| b & 0xe0 == 0xc0) { - out.push(0); - } - // State for the next frame. - self.mvs = mi.iter().map(|m| if m.inter { m.mv } else { Mv::ZERO }).collect(); - self.reference = Some(planes); - self.frames += 1; - self.since_key = if key { 1 } else { self.since_key + 1 }; - self.rc_update(key, qidx, out.len()); - EncodedFrame { data: out, key, q_idx: qidx, filter_level: level } - } -} - -/// Try a few loop filter levels on the luma reconstruction; the one closest to the source -/// (squared error over the visible area) wins (0 when filtering doesn't help). -fn choose_level(planes: &mut [Plane; 3], mi: &[Mi], mi_rows: usize, mi_cols: usize, qidx: u8, key: bool) -> u8 { - let guess = ((qidx as f64 * if key { 0.25 } else { 0.2 }).round() as u8).clamp(1, 63); - let sse = |p: &Plane| -> u64 { - let mut s = 0u64; - for y in 0..p.vis_h { - for x in 0..p.vis_w { - let d = p.src[y * p.w + x] as i64 - p.rec[y * p.w + x] as i64; - s += (d * d) as u64; - } - } - s - }; - let mut best = (sse(&planes[0]), 0u8); - let saved = planes[0].rec.clone(); - for level in [guess / 2, guess, (guess as u16 * 3 / 2).min(63) as u8] { - if level == 0 { - continue; - } - lf::filter_frame(&mut planes[..1], mi, mi_rows, mi_cols, level); - let s = sse(&planes[0]); - planes[0].rec.copy_from_slice(&saved); - if s < best.0 { - best = (s, level); - } - } - best.1 -} - -// ---------------------------------------------------------------- colour conversion - -/// BT.709 RGB(A) 8-bit → planar YUV 4:2:0 (limited or full range). -pub fn rgba_to_yuv420(rgba: &[u8], w: u32, h: u32, full_range: bool) -> (Vec, Vec, Vec) { - let (w, h) = (w as usize, h as usize); - let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); - let mut y = vec![0u8; w * h]; - let mut u = vec![0u8; cw * ch]; - let mut v = vec![0u8; cw * ch]; - let (ys, yo, cs) = if full_range { (255.0, 0.0, 255.0) } else { (219.0, 16.0, 224.0) }; - let px = |x: usize, yy: usize| -> [f32; 3] { - let i = (yy.min(h.saturating_sub(1)) * w + x.min(w.saturating_sub(1))) * 4; - let c = rgba.get(i..i + 3).unwrap_or(&[0, 0, 0]); - [c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0] - }; - let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; - for yy in 0..h { - for x in 0..w { - y[yy * w + x] = (luma(px(x, yy)) * ys + yo).round().clamp(0.0, 255.0) as u8; - } - } - for cy in 0..ch { - for cx in 0..cw { - let mut acc = [0.0f32; 3]; - for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] { - let c = px(cx * 2 + dx, cy * 2 + dy); - for k in 0..3 { - acc[k] += c[k] / 4.0; - } - } - let l = luma(acc); - let cb = (acc[2] - l) / 1.8556; - let cr = (acc[0] - l) / 1.5748; - u[cy * cw + cx] = (cb * cs + 128.0).round().clamp(0.0, 255.0) as u8; - v[cy * cw + cx] = (cr * cs + 128.0).round().clamp(0.0, 255.0) as u8; - } - } - (y, u, v) -} - -/// The alpha channel as a luma plane (full range) with neutral chroma — the second VP9 stream of -/// WebM alpha. -pub fn alpha_to_yuv420(rgba: &[u8], w: u32, h: u32) -> (Vec, Vec, Vec) { - let (w, h) = (w as usize, h as usize); - let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); - let y: Vec = rgba.as_chunks::<4>().0.iter().take(w * h).map(|p| p[3]).collect(); - (y, vec![128; cw * ch], vec![128; cw * ch]) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sizes_beyond_the_16_bit_header_fields_are_clamped() { - let enc = Vp9Encoder::new(EncoderConfig { width: 70_000, height: u32::MAX, ..Default::default() }); - assert_eq!((enc.cfg.width, enc.cfg.height), (65_536, 65_536)); - let enc = Vp9Encoder::new(EncoderConfig { width: 65_536, height: 1, ..Default::default() }); - assert_eq!((enc.cfg.width, enc.cfg.height), (65_536, 1)); - } - - /// A 1080p stream is above the 983,040 luma samples of level 3.1, so it must not claim 3.1 in - /// its `vpcC`; level 0 means "undefined" in the MP4 binding. - #[test] - fn vpcc_claims_no_level_the_encoder_does_not_enforce() { - for (w, h) in [(1920, 1080), (64, 64), (3840, 2160)] { - let enc = Vp9Encoder::new(EncoderConfig { width: w, height: h, ..Default::default() }); - let rec = VpcConfig::parse(&enc.vpcc()).unwrap(); - assert_eq!(rec.level, 0, "{w}x{h}"); - let params = enc.codec_params(); - assert_eq!(VpcConfig::parse(¶ms.extradata).unwrap().level, 0); - } - } -} +#[cfg(feature = "vp9-encode")] +mod encode; +#[cfg(feature = "vp9-encode")] +pub use encode::*; diff --git a/crates/video/tests/vp9_decode/common/mod.rs b/crates/video/tests/vp9_decode/common/mod.rs new file mode 100644 index 000000000..07eaeffef --- /dev/null +++ b/crates/video/tests/vp9_decode/common/mod.rs @@ -0,0 +1,421 @@ +//! Test fixture generation (ffmpeg + libvpx-vp9 as external encoder and oracle), a tiny IVF +//! reader, and comparison helpers. +#![allow(dead_code)] + +use craft_video::vp9::decode::{Decoder, Picture}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// One encoder configuration of the fixture matrix. +pub struct Fixture { + pub name: &'static str, + /// lavfi source (without size / rate). + pub source: &'static str, + pub width: u32, + pub height: u32, + pub frames: u32, + /// Extra video filter appended after the source. + pub filter: &'static str, + /// Pixel format of the encode (and of the reference decode). + pub pix_fmt: &'static str, + /// libvpx-vp9 arguments. + pub args: &'static [&'static str], +} + +const NOISE: &str = "noise=alls=10:allf=t+u"; + +macro_rules! fx { + ($name:expr, $src:expr, $w:expr, $h:expr, $n:expr, $filter:expr, $pf:expr, [$($a:expr),* $(,)?]) => { + Fixture { name: $name, source: $src, width: $w, height: $h, frames: $n, filter: $filter, pix_fmt: $pf, args: &[$($a),*] } + }; +} + +const P420: &str = "yuv420p"; +const P420_10: &str = "yuv420p10le"; + +pub const FIXTURES: &[Fixture] = &[ + fx!("intra_only_keyframes", "testsrc2", 352, 288, 3, NOISE, P420, ["-g", "1", "-b:v", "1M"]), + fx!("default_good", "testsrc2", 352, 288, 20, NOISE, P420, ["-b:v", "800k", "-frame-parallel", "0"]), + fx!("profile2_10bit", "testsrc2", 352, 288, 12, NOISE, P420_10, ["-b:v", "800k", "-profile:v", "2", "-frame-parallel", "0"]), + fx!("profile2_12bit", "mandelbrot", 176, 144, 8, NOISE, "yuv420p12le", ["-b:v", "500k", "-profile:v", "2", "-frame-parallel", "0"]), + fx!("profile1_444", "testsrc2", 176, 144, 8, NOISE, "yuv444p", ["-b:v", "500k", "-profile:v", "1", "-frame-parallel", "0"]), + fx!("profile1_422", "testsrc2", 176, 144, 8, NOISE, "yuv422p", ["-b:v", "500k", "-profile:v", "1", "-frame-parallel", "0"]), + fx!("profile1_440", "testsrc2", 176, 144, 8, NOISE, "yuv440p", ["-b:v", "500k", "-profile:v", "1", "-frame-parallel", "0"]), + fx!("profile3_444_10bit", "testsrc2", 176, 144, 6, NOISE, "yuv444p10le", ["-b:v", "500k", "-profile:v", "3", "-frame-parallel", "0"]), + fx!("profile3_422_12bit", "mandelbrot", 176, 144, 6, NOISE, "yuv422p12le", ["-b:v", "500k", "-profile:v", "3", "-frame-parallel", "0"]), + fx!("lossless", "testsrc2", 176, 144, 5, NOISE, P420, ["-lossless", "1", "-frame-parallel", "0"]), + fx!("lossless_10bit", "mandelbrot", 176, 144, 4, NOISE, P420_10, ["-lossless", "1", "-profile:v", "2", "-frame-parallel", "0"]), + fx!("tiles_cols4", "testsrc2", 1280, 720, 6, NOISE, P420, ["-b:v", "2M", "-tile-columns", "2", "-speed", "4", "-frame-parallel", "0"]), + fx!( + "tiles_rows_cols", + "testsrc2", + 1280, + 720, + 6, + NOISE, + P420, + ["-b:v", "2M", "-tile-columns", "1", "-tile-rows", "2", "-speed", "4", "-frame-parallel", "0"] + ), + fx!("tiles_rows4", "testsrc2", 640, 480, 5, NOISE, P420, ["-b:v", "1M", "-tile-columns", "0", "-tile-rows", "2", "-frame-parallel", "0"]), + fx!( + "altref_hidden", + "testsrc2", + 352, + 288, + 30, + NOISE, + P420, + ["2pass", "-b:v", "600k", "-auto-alt-ref", "1", "-lag-in-frames", "25", "-frame-parallel", "0"] + ), + fx!( + "altref_10bit", + "mandelbrot", + 352, + 288, + 24, + NOISE, + P420_10, + ["2pass", "-b:v", "600k", "-profile:v", "2", "-auto-alt-ref", "1", "-lag-in-frames", "16", "-frame-parallel", "0"] + ), + fx!( + "altref_tiles", + "testsrc2", + 1280, + 720, + 12, + NOISE, + P420, + ["2pass", "-b:v", "2M", "-auto-alt-ref", "1", "-lag-in-frames", "10", "-tile-columns", "2", "-speed", "2", "-frame-parallel", "0"] + ), + fx!("frame_parallel", "testsrc2", 352, 288, 16, NOISE, P420, ["-b:v", "600k", "-frame-parallel", "1"]), + fx!("error_resilient", "testsrc2", 352, 288, 16, NOISE, P420, ["-b:v", "600k", "-error-resilient", "1"]), + fx!("odd_size", "testsrc2", 347, 251, 10, NOISE, P420, ["-b:v", "600k", "-frame-parallel", "0"]), + fx!("tiny_odd", "testsrc2", 33, 17, 8, NOISE, P420, ["-b:v", "200k", "-frame-parallel", "0"]), + // Two superblock columns and twelve rows: the loop filter wavefront waits for whole rows. + fx!("narrow_tall", "testsrc2", 120, 720, 6, NOISE, P420, ["-b:v", "400k", "-frame-parallel", "0"]), + fx!("row_mt", "testsrc2", 640, 360, 10, NOISE, P420, ["-b:v", "1M", "-row-mt", "1", "-tile-columns", "1", "-frame-parallel", "0"]), + fx!("speed0", "mandelbrot", 176, 144, 8, NOISE, P420, ["2pass", "-b:v", "300k", "-speed", "0", "-frame-parallel", "0"]), + fx!("speed1", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "1", "-frame-parallel", "0"]), + fx!("speed2", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "2", "-frame-parallel", "0"]), + fx!("speed3", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "3", "-frame-parallel", "0"]), + fx!("speed4", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "4", "-frame-parallel", "0"]), + fx!("speed5", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "5", "-deadline", "realtime", "-frame-parallel", "0"]), + fx!("speed6", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "6", "-deadline", "realtime", "-frame-parallel", "0"]), + fx!("speed7", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "7", "-deadline", "realtime", "-frame-parallel", "0"]), + fx!("speed8", "testsrc2", 352, 288, 10, NOISE, P420, ["-b:v", "600k", "-speed", "8", "-deadline", "realtime", "-frame-parallel", "0"]), + fx!("cq_low", "testsrc2", 352, 288, 8, NOISE, P420, ["-crf", "0", "-b:v", "0", "-frame-parallel", "0"]), + fx!("cq_q4", "testsrc2", 352, 288, 8, NOISE, P420, ["-crf", "4", "-b:v", "0", "-frame-parallel", "0"]), + fx!("cq_high", "testsrc2", 352, 288, 12, NOISE, P420, ["-crf", "63", "-b:v", "0", "-frame-parallel", "0"]), + fx!("cq_10bit_low", "testsrc2", 176, 144, 6, NOISE, P420_10, ["-crf", "2", "-b:v", "0", "-profile:v", "2", "-frame-parallel", "0"]), + fx!("cq_12bit_high", "testsrc2", 176, 144, 6, NOISE, "yuv420p12le", ["-crf", "60", "-b:v", "0", "-profile:v", "2", "-frame-parallel", "0"]), + fx!("aq_segmentation", "testsrc2", 352, 288, 12, NOISE, P420, ["-b:v", "600k", "-aq-mode", "3", "-frame-parallel", "0"]), + fx!("aq_variance", "mandelbrot", 352, 288, 12, NOISE, P420, ["-b:v", "600k", "-aq-mode", "1", "-frame-parallel", "0"]), + fx!("aq_complexity", "testsrc2", 352, 288, 12, NOISE, P420, ["2pass", "-b:v", "600k", "-aq-mode", "2", "-frame-parallel", "0"]), + fx!("sharpness", "testsrc2", 352, 288, 8, NOISE, P420, ["-b:v", "400k", "-sharpness", "5", "-frame-parallel", "0"]), + fx!("fade_intra_heavy", "testsrc2", 352, 288, 20, "fade=in:0:15", P420, ["-b:v", "400k", "-g", "8", "-frame-parallel", "0"]), + fx!("hd_1080p", "testsrc2", 1920, 1080, 3, NOISE, P420, ["-b:v", "4M", "-speed", "5", "-deadline", "realtime"]), + // Two temporal layers: every other frame refreshes no reference slot (draft mode test). + fx!( + "temporal_layers", + "testsrc2", + 352, + 288, + 20, + NOISE, + P420, + [ + "-b:v", + "800k", + "-speed", + "8", + "-deadline", + "realtime", + "-ts-parameters", + "ts_number_layers=2:ts_target_bitrate=400,800:ts_rate_decimator=2,1:ts_periodicity=2:ts_layer_id=0,1:ts_layering_mode=2" + ] + ), + fx!( + "bench_1080p", + "testsrc2", + 1920, + 1080, + 60, + "noise=alls=3:allf=t", + P420, + ["-b:v", "4M", "-speed", "4", "-tile-columns", "2", "-row-mt", "1", "-frame-parallel", "0"] + ), + fx!( + "bench_4k", + "testsrc2", + 3840, + 2160, + 20, + "noise=alls=3:allf=t", + P420, + ["-b:v", "12M", "-speed", "6", "-tile-columns", "3", "-row-mt", "1", "-frame-parallel", "0"] + ), +]; + +pub fn fixture(name: &str) -> &'static Fixture { + FIXTURES.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("unknown fixture {name}")) +} + +pub fn ffmpeg() -> Option { + craft_media_testkit::ffmpeg_or_skip("VP9 conformance tests") +} + +pub fn fixtures_dir() -> PathBuf { + craft_media_testkit::fixtures_dir("vp9") +} + +/// Unique suffix for temporary files: tests run on several threads of one process and may +/// generate the same fixture concurrently. +pub fn unique() -> String { + static N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + format!("{}-{n}", std::process::id()) +} + +pub fn run(cmd: &mut Command) -> bool { + match cmd.output() { + Ok(o) if o.status.success() => true, + Ok(o) => { + eprintln!("command failed: {:?}\n{}", cmd, String::from_utf8_lossy(&o.stderr)); + false + } + Err(e) => { + eprintln!("failed to run {:?}: {e}", cmd); + false + } + } +} + +/// Bytes per sample and chroma subsampling of a pixel format. +pub fn format_info(pix_fmt: &str) -> (usize, u32, u32) { + let bps = if pix_fmt.contains("p10") || pix_fmt.contains("p12") { 2 } else { 1 }; + let (sx, sy) = if pix_fmt.starts_with("yuv420") { + (1, 1) + } else if pix_fmt.starts_with("yuv422") { + (1, 0) + } else if pix_fmt.starts_with("yuv440") { + (0, 1) + } else { + (0, 0) + }; + (bps, sx, sy) +} + +/// Reference decode of `ivf` with ffmpeg (native vp9 decoder, every frame, no fps conversion). +pub fn reference_decode(ivf: &Path, yuv: &Path, pix_fmt: &str) -> bool { + let Some(ff) = ffmpeg() else { return false }; + let tmp = yuv.with_extension(format!("tmp.{}.yuv", unique())); + let mut c = Command::new(&ff); + c.args(["-hide_banner", "-loglevel", "error", "-y", "-c:v", "vp9", "-i"]).arg(ivf); + c.args(["-fps_mode", "passthrough", "-noautoscale", "-f", "rawvideo", "-pix_fmt", pix_fmt]).arg(&tmp); + if !run(&mut c) { + return false; + } + std::fs::rename(&tmp, yuv).is_ok() || yuv.exists() +} + +/// Generate (if needed) the fixture stream and its ffmpeg reference decode. +pub fn ensure(f: &Fixture) -> Option<(PathBuf, PathBuf)> { + // A fixture that cannot be made is a skip, and a failure when oracles are required. + ensure_inner(f).or_else(|| craft_media_testkit::skip(&format!("VP9 fixture {} could not be generated", f.name))) +} + +fn ensure_inner(f: &Fixture) -> Option<(PathBuf, PathBuf)> { + let Some(ff) = ffmpeg() else { + eprintln!("SKIP {}: ffmpeg not found", f.name); + return None; + }; + let dir = fixtures_dir(); + let ivf = dir.join(format!("{}.ivf", f.name)); + let yuv = dir.join(format!("{}.yuv", f.name)); + if !ivf.exists() { + let tmp = dir.join(format!("{}.tmp.{}.ivf", f.name, unique())); + let mut vf = format!("{}=size={}x{}:rate=25,format={}", f.source, f.width, f.height, f.pix_fmt); + if !f.filter.is_empty() { + vf.push(','); + vf.push_str(f.filter); + } + let two_pass = f.args.first() == Some(&"2pass"); + let args: Vec<&str> = f.args.iter().copied().filter(|a| *a != "2pass").collect(); + let log = dir.join(format!("{}.{}.passlog", f.name, unique())); + let enc = |pass: Option, out: &Path| { + let mut c = Command::new(&ff); + c.args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", &vf]); + c.args(["-frames:v", &f.frames.to_string(), "-c:v", "libvpx-vp9", "-pix_fmt", f.pix_fmt]); + c.args(&args); + if let Some(p) = pass { + c.args(["-pass", &p.to_string(), "-passlogfile"]).arg(&log); + } + if pass == Some(1) { + c.args(["-f", "null", "-"]); + } else { + c.args(["-f", "ivf"]).arg(out); + } + c + }; + if two_pass && !run(&mut enc(Some(1), &tmp)) { + eprintln!("SKIP {}: first pass failed", f.name); + return None; + } + let mut c = enc(if two_pass { Some(2) } else { None }, &tmp); + if !run(&mut c) { + let _ = std::fs::remove_file(&tmp); + eprintln!("SKIP {}: libvpx-vp9 encode failed", f.name); + return None; + } + let _ = std::fs::remove_file(format!("{}-0.log", log.display())); + if std::fs::rename(&tmp, &ivf).is_err() && !ivf.exists() { + panic!("could not store fixture {}", ivf.display()); + } + } + if !yuv.exists() { + assert!(reference_decode(&ivf, &yuv, f.pix_fmt), "reference decode failed for {}", f.name); + } + Some((ivf, yuv)) +} + +/// Frames of an IVF file (32-byte file header, then 12-byte frame headers). +pub fn ivf_frames(data: &[u8]) -> Vec<&[u8]> { + let mut out = Vec::new(); + if data.len() < 32 || &data[..4] != b"DKIF" { + return out; + } + let hl = u16::from_le_bytes([data[6], data[7]]) as usize; + let mut p = hl.max(32); + while p + 12 <= data.len() { + let sz = u32::from_le_bytes(data[p..p + 4].try_into().unwrap()) as usize; + p += 12; + if p + sz > data.len() { + break; + } + out.push(&data[p..p + sz]); + p += sz; + } + out +} + +/// Write an IVF file. +pub fn write_ivf(path: &Path, w: u16, h: u16, frames: &[Vec]) { + let mut d = Vec::new(); + d.extend(b"DKIF"); + d.extend(0u16.to_le_bytes()); + d.extend(32u16.to_le_bytes()); + d.extend(b"VP90"); + d.extend(w.to_le_bytes()); + d.extend(h.to_le_bytes()); + d.extend(25u32.to_le_bytes()); + d.extend(1u32.to_le_bytes()); + d.extend((frames.len() as u32).to_le_bytes()); + d.extend(0u32.to_le_bytes()); + for (i, f) in frames.iter().enumerate() { + d.extend((f.len() as u32).to_le_bytes()); + d.extend((i as u64).to_le_bytes()); + d.extend(f); + } + std::fs::write(path, d).unwrap(); +} + +pub type DecodeFailure = (usize, craft_video::vp9::decode::Error, Vec); + +/// Decode an IVF file with `threads` workers (0 = default), pts = frame index. +pub fn decode_file_threads(path: &Path, threads: usize) -> Result, DecodeFailure> { + let data = std::fs::read(path).unwrap(); + let mut dec = if threads == 0 { Decoder::new() } else { Decoder::with_threads(threads) }; + let mut out = Vec::new(); + for (i, f) in ivf_frames(&data).into_iter().enumerate() { + match dec.decode(f, i as i64) { + Ok(p) => out.extend(p), + Err(e) => return Err((i, e, out)), + } + } + out.extend(dec.flush()); + Ok(out) +} + +/// Frame size in bytes of a raw picture. +pub fn raw_size(w: usize, h: usize, pix_fmt: &str) -> usize { + let (bps, sx, sy) = format_info(pix_fmt); + let (cw, ch) = ((w + sx as usize) >> sx, (h + sy as usize) >> sy); + (w * h + 2 * cw * ch) * bps +} + +/// Compare decoded pictures with a raw reference with frames of possibly varying sizes (sizes +/// are taken from the decoded pictures). Reports the first mismatch. +pub fn compare(pics: &[Picture], reference: &[u8], pix_fmt: &str) -> Result<(), String> { + let (bps, _, _) = format_info(pix_fmt); + let mut off = 0usize; + for (i, p) in pics.iter().enumerate() { + let (w, h) = (p.width as usize, p.height as usize); + let (cw, ch) = (p.chroma_width as usize, p.chroma_height as usize); + let fsize = raw_size(w, h, pix_fmt); + if off + fsize > reference.len() { + return Err(format!("decoder produced {} frames, reference has fewer (at frame {i})", pics.len())); + } + let f = &reference[off..off + fsize]; + off += fsize; + let sample = |buf: &[u8], k: usize| if bps == 2 { u16::from_le_bytes([buf[2 * k], buf[2 * k + 1]]) } else { buf[k] as u16 }; + let (ys, cs) = (w * h * bps, cw * ch * bps); + let planes = [("Y", &p.y, &f[..ys], w, h), ("U", &p.u, &f[ys..ys + cs], cw, ch), ("V", &p.v, &f[ys + cs..], cw, ch)]; + for (name, got, exp, pw, ph) in planes { + let mut first = None; + let mut count = 0usize; + let mut maxd = 0i32; + for y in 0..ph { + for x in 0..pw { + let (a, b) = (got.get(y * pw + x), sample(exp, y * pw + x)); + if a != b { + count += 1; + maxd = maxd.max((a as i32 - b as i32).abs()); + if first.is_none() { + first = Some((x, y, a, b)); + } + } + } + } + if let Some((x, y, a, b)) = first { + return Err(format!( + "frame {i} (pts {}, {w}x{h}) plane {name}: first mismatch at ({x},{y}) [SB64 of luma ~({},{})]: got {a} want {b}; {count} samples differ, max diff {maxd}", + p.pts, + x / 64, + y / 64 + )); + } + } + } + if off != reference.len() { + return Err(format!("decoder produced {} frames, reference has more data", pics.len())); + } + Ok(()) +} + +/// Per-frame MD5-like digest is not needed: comparison is sample exact. Full check of one +/// fixture: generate, decode (single- and multi-threaded), compare. Ok(false) when skipped. +pub fn check_fixture(name: &str) -> Result { + let f = fixture(name); + let Some((ivf, yuv)) = ensure(f) else { return Ok(false) }; + let reference = std::fs::read(&yuv).unwrap(); + check_files(name, &ivf, &reference, f.pix_fmt)?; + Ok(true) +} + +pub fn check_files(name: &str, ivf: &Path, reference: &[u8], pix_fmt: &str) -> Result<(), String> { + // Single-threaded, frame threads with a small pool, and every core. + for threads in [1, 3, 0] { + let pics = match decode_file_threads(ivf, threads) { + Ok(p) => p, + Err((fr, e, partial)) => { + let cmp = compare(&partial, reference, pix_fmt).err().unwrap_or_default(); + return Err(format!("{name} (threads {threads}): decode error at chunk {fr}: {e} (after {} pictures) {cmp}", partial.len())); + } + }; + compare(&pics, reference, pix_fmt).map_err(|e| format!("{name} (threads {threads}): {e}"))?; + if pics.iter().any(|p| p.draft) { + return Err(format!("{name} (threads {threads}): picture flagged draft without draft mode")); + } + } + Ok(()) +} diff --git a/crates/video/tests/vp9_decode/conformance.rs b/crates/video/tests/vp9_decode/conformance.rs new file mode 100644 index 000000000..d4d8164aa --- /dev/null +++ b/crates/video/tests/vp9_decode/conformance.rs @@ -0,0 +1,193 @@ +//! Bit-exactness against ffmpeg's VP9 decoder on libvpx-vp9 fixtures (skipped when ffmpeg or +//! libvpx is absent). + +use crate::common; + +macro_rules! fixture_tests { + ($($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + match common::check_fixture(stringify!($name)) { + Ok(true) => {} + Ok(false) => eprintln!("skipped {}", stringify!($name)), + Err(e) => panic!("{e}"), + } + } + )* + }; +} + +fixture_tests!( + intra_only_keyframes, + default_good, + profile2_10bit, + profile2_12bit, + profile1_444, + profile1_422, + profile1_440, + profile3_444_10bit, + profile3_422_12bit, + lossless, + lossless_10bit, + tiles_cols4, + tiles_rows_cols, + tiles_rows4, + altref_hidden, + altref_10bit, + altref_tiles, + frame_parallel, + error_resilient, + odd_size, + tiny_odd, + narrow_tall, + row_mt, + speed0, + speed1, + speed2, + speed3, + speed4, + speed5, + speed6, + speed7, + speed8, + cq_low, + cq_q4, + cq_high, + cq_10bit_low, + cq_12bit_high, + aq_segmentation, + aq_variance, + aq_complexity, + sharpness, + fade_intra_heavy, + hd_1080p, + temporal_layers, +); + +/// Draft mode: frames no other frame references skip the loop filter and are flagged; every +/// other picture stays bit-exact, single-threaded and with frame threads. +#[test] +fn draft_mode_changes_only_flagged_non_reference_frames() { + let f = common::fixture("temporal_layers"); + let Some((ivf, yuv)) = common::ensure(f) else { return }; + let reference = std::fs::read(&yuv).unwrap(); + let data = std::fs::read(&ivf).unwrap(); + let fsize = common::raw_size(f.width as usize, f.height as usize, f.pix_fmt); + for threads in [1, 3] { + let mut dec = craft_video::vp9::decode::Decoder::with_threads(threads); + dec.set_draft(true); + let mut pics = Vec::new(); + for (i, c) in common::ivf_frames(&data).into_iter().enumerate() { + pics.extend(dec.decode(c, i as i64).unwrap()); + } + pics.extend(dec.flush()); + assert_eq!(pics.len(), f.frames as usize, "threads {threads}"); + let (mut flagged, mut changed) = (0, 0); + for (i, p) in pics.iter().enumerate() { + let mut bytes = Vec::new(); + for pl in [&p.y, &p.u, &p.v] { + bytes.extend(pl.to_le_bytes()); + } + let same = bytes == reference[i * fsize..(i + 1) * fsize]; + if p.draft { + flagged += 1; + changed += !same as usize; + } else { + assert!(same, "threads {threads}: unflagged frame {i} differs"); + } + } + assert!(flagged >= f.frames as usize / 3, "threads {threads}: only {flagged} draft frames"); + assert!(changed > 0, "threads {threads}: draft frames identical to the filtered ones"); + } +} + +/// The loop filter runs as a wavefront over superblock rows on the decoder's pool: repeated +/// decodes with several thread counts all match ffmpeg (a race shows up as a mismatch). +#[test] +fn loop_filter_wavefront_repeatable() { + let f = common::fixture("hd_1080p"); + let Some((ivf, yuv)) = common::ensure(f) else { return }; + let reference = std::fs::read(yuv).unwrap(); + for threads in [2, 5, 16, 16, 16] { + let pics = common::decode_file_threads(&ivf, threads).map_err(|e| e.1).unwrap(); + common::compare(&pics, &reference, f.pix_fmt).unwrap_or_else(|e| panic!("threads {threads}: {e}")); + } +} + +/// Two streams of different sizes concatenated into one IVF (key frame at the size change). +#[test] +fn resize_concatenated() { + let a = common::fixture("default_good"); + let b = common::fixture("odd_size"); + let (Some((ia, ya)), Some((ib, yb))) = (common::ensure(a), common::ensure(b)) else { return }; + let da = std::fs::read(ia).unwrap(); + let db = std::fs::read(ib).unwrap(); + let mut frames: Vec> = common::ivf_frames(&da).into_iter().map(|f| f.to_vec()).collect(); + frames.extend(common::ivf_frames(&db).into_iter().map(|f| f.to_vec())); + let path = common::fixtures_dir().join("resize_concat.ivf"); + common::write_ivf(&path, 352, 288, &frames); + let mut reference = std::fs::read(ya).unwrap(); + reference.extend(std::fs::read(yb).unwrap()); + common::check_files("resize_concat", &path, &reference, "yuv420p").unwrap(); +} + +/// Print which coding tools each fixture exercises (run with `-- --ignored --nocapture`). +#[test] +#[ignore] +fn coverage_report() { + for f in common::FIXTURES { + let Some((ivf, _)) = common::ensure(f) else { continue }; + let data = std::fs::read(ivf).unwrap(); + let mut dec = craft_video::vp9::decode::Decoder::new(); + for fr in common::ivf_frames(&data) { + let _ = dec.decode(fr, 0); + } + println!("{:<22} {:?}", f.name, dec.stats()); + } +} + +/// The comparison must report a single flipped sample (guards against a vacuous harness). +#[test] +fn compare_detects_mismatch() { + let f = common::fixture("intra_only_keyframes"); + let Some((ivf, yuv)) = common::ensure(f) else { return }; + let pics = common::decode_file_threads(&ivf, 1).map_err(|e| e.1).unwrap(); + let mut reference = std::fs::read(yuv).unwrap(); + common::compare(&pics, &reference, f.pix_fmt).unwrap(); + let pos = reference.len() - 7; + reference[pos] ^= 1; + let err = common::compare(&pics, &reference, f.pix_fmt).unwrap_err(); + assert!(err.contains("plane V"), "{err}"); +} + +/// Key frame detection (random access points) and decoder reset: decoding again from the +/// second key frame after `reset` gives the same pictures as the uninterrupted decode. +#[test] +fn keyframes_and_reset() { + let f = common::fixture("fade_intra_heavy"); + let Some((ivf, _)) = common::ensure(f) else { return }; + let data = std::fs::read(ivf).unwrap(); + let chunks = common::ivf_frames(&data); + let keys: Vec = (0..chunks.len()).filter(|&i| craft_video::vp9::decode::is_keyframe(chunks[i])).collect(); + assert_eq!(keys.first(), Some(&0)); + assert!(keys.len() >= 2, "key frames {keys:?}"); + let info = craft_video::vp9::decode::keyframe_info(chunks[0]).unwrap(); + assert_eq!((info.width, info.height, info.bit_depth), (f.width, f.height, 8)); + assert!(info.subsampling_x && info.subsampling_y); + let mut dec = craft_video::vp9::decode::Decoder::with_threads(1); + let mut all = Vec::new(); + for (i, c) in chunks.iter().enumerate() { + all.extend(dec.decode(c, i as i64).unwrap()); + } + // Garbage in between, then a reset and a restart at the second key frame. + let _ = dec.decode(&[0x82, 0x49, 0x83, 0x42, 0x00], 0); + dec.reset(); + let k = keys[1]; + for (i, c) in chunks.iter().enumerate().skip(k) { + for p in dec.decode(c, i as i64).unwrap() { + let want = all.iter().find(|q| q.pts == p.pts).unwrap(); + assert!(p.y == want.y && p.u == want.u && p.v == want.v, "picture {} differs after reset", p.pts); + } + } +} diff --git a/crates/video/tests/vp9_decode/fuzz_hostile.rs b/crates/video/tests/vp9_decode/fuzz_hostile.rs new file mode 100644 index 000000000..255f5e9b7 --- /dev/null +++ b/crates/video/tests/vp9_decode/fuzz_hostile.rs @@ -0,0 +1,221 @@ +//! Hostile-input robustness: truncation, header-region sweeps and random mutation of small +//! ffmpeg-generated VP9 streams (frame by frame) must never panic or run away. Skips without ffmpeg. + +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use craft_video::vp9::decode::Decoder; + +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + fn below(&mut self, n: usize) -> usize { + (self.next() % n.max(1) as u64) as usize + } +} + +/// (name, encoder args, pix_fmt) +const SEEDS: &[(&str, &str, &str)] = &[ + ("vp9_fz_default", "-c:v libvpx-vp9 -g 4 -b:v 300k -lag-in-frames 0", "yuv420p"), + // libvpx needs 256 px per tile column, so two columns need 512 px + ("vp9_fz_2tiles_10bit", "-c:v libvpx-vp9 -g 6 -b:v 300k -lag-in-frames 0 -tile-columns 1 -profile:v 2 -s 512x48", "yuv420p10le"), + ("vp9_fz_444", "-c:v libvpx-vp9 -g 6 -b:v 300k -lag-in-frames 0 -profile:v 1", "yuv444p"), + ("vp9_fz_altref", "-c:v libvpx-vp9 -g 6 -b:v 300k -auto-alt-ref 1 -lag-in-frames 5", "yuv420p"), +]; + +fn ivf_frames(data: &[u8]) -> Vec> { + let mut out = Vec::new(); + let Some(h) = data.get(6..8) else { return out }; + let mut pos = usize::from(u16::from_le_bytes([h[0], h[1]])); + while let Some(hd) = data.get(pos..pos + 12) { + let size = u32::from_le_bytes([hd[0], hd[1], hd[2], hd[3]]) as usize; + pos += 12; + let end = pos.saturating_add(size).min(data.len()); + out.push(data[pos..end].to_vec()); + pos = end; + } + out +} + +fn make(ff: &std::path::Path, name: &str, args: &str, pix: &str) -> Option>> { + let out = craft_media_testkit::fixtures_dir("vp9").join(format!("{name}.ivf")); + let p = craft_media_testkit::fixtures::generate(&out, |tmp| { + let a: Vec<&str> = args.split_whitespace().collect(); + matches!( + Command::new(ff) + .args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc2=size=64x48:rate=25", "-frames:v", "6", "-pix_fmt", pix]) + .args(a) + .args(["-f", "ivf"]) + .arg(tmp) + .stdin(Stdio::null()) + .status(), + Ok(s) if s.success() + ) + })?; + Some(ivf_frames(&std::fs::read(p).ok()?)) +} + +static PANICS: AtomicUsize = AtomicUsize::new(0); + +/// Decoder worker threads may catch panics and turn them into errors; count them so they still fail the test. +fn count_panics() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + std::panic::set_hook(Box::new(|info| { + PANICS.fetch_add(1, Ordering::SeqCst); + eprintln!("PANIC: {info}"); + })); + }); +} + +fn run_all(frames: &[Vec], label: &str) { + craft_media_testkit::set_progress(label); + count_panics(); + let before = PANICS.load(Ordering::SeqCst); + let res = catch_unwind(AssertUnwindSafe(|| { + for threads in [1, 3] { + let mut dec = Decoder::with_threads(threads); + for (i, f) in frames.iter().enumerate() { + let _ = dec.decode(f, i as i64); + } + let _ = dec.flush(); + } + })); + assert!(res.is_ok() && PANICS.load(Ordering::SeqCst) == before, "panic: {label}"); +} + +const VALUES: [u8; 5] = [0, 0xff, 0x80, 0x01, 0x7f]; + +/// How much of the sweep runs. `FILMCRAFT_FUZZ_ITERS=N` runs all of it (about 200 truncation +/// points spread over each of the first three frames, every header byte, overwrite value and +/// bit flip) plus N random rounds per seed: 300 is the full depth, more hunts longer. Unset, CI +/// runs a third of the truncation points and of the full depth's random rounds and, for each +/// header byte of each frame, one value and one bit, shifted by the frame index. A rotation +/// offset shifts which share runs (`FILMCRAFT_FUZZ_SEED`, else the UTC day number), so every +/// case of the full sweep runs within a few days, and a random round's mutations match the same +/// round of a full run. +struct Depth { + full: bool, + truncations: usize, + rounds: usize, + rotation: usize, +} + +impl Depth { + fn from_env() -> Self { + match std::env::var("FILMCRAFT_FUZZ_ITERS").ok().and_then(|s| s.parse().ok()) { + Some(rounds) => Depth { full: true, truncations: 200, rounds, rotation: 0 }, + None => { + let rotation = rotation(); + eprintln!("fuzz subset rotation {rotation} (FILMCRAFT_FUZZ_SEED={rotation} replays it; FILMCRAFT_FUZZ_ITERS=300 runs everything)"); + Depth { full: false, truncations: 200, rounds: 300, rotation } + } + } + } + /// Truncation lengths for a `len`-byte input: every point of the full sweep, or every third one. + fn truncations(&self, len: usize) -> std::iter::StepBy> { + let stride = (len / self.truncations).max(1); + let (start, step) = if self.full { (0, stride) } else { ((self.rotation % 3) * stride, 3 * stride) }; + (start..len).step_by(step) + } + fn value(&self, fi: usize, p: usize, i: usize) -> bool { + self.full || i == (p + fi + self.rotation % VALUES.len()) % VALUES.len() + } + fn bit(&self, fi: usize, p: usize, b: usize) -> bool { + self.full || b == (p + fi + self.rotation % 8) % 8 + } + fn round(&self, i: usize) -> bool { + self.full || i % 3 == self.rotation % 3 + } +} + +/// The default subset's rotation offset: `FILMCRAFT_FUZZ_SEED`, else days since the Unix epoch (UTC). +fn rotation() -> usize { + std::env::var("FILMCRAFT_FUZZ_SEED").ok().and_then(|s| s.parse().ok()).unwrap_or_else(|| { + let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); + usize::try_from(secs / 86_400).unwrap_or(0) + }) +} + +/// Seconds the whole sweep may take before it counts as a hang (override: FILMCRAFT_HOSTILE_TIMEOUT_SECS). +const DEADLINE_SECS: u64 = 600; + +/// CI runs a rotating share of every part of the sweep; `FILMCRAFT_FUZZ_ITERS=300` runs all of it (see [`Depth`]). +#[test] +fn hostile_streams_never_panic() { + craft_media_testkit::with_deadline(DEADLINE_SECS, sweep); +} + +fn sweep() { + let Some(ff) = craft_media_testkit::ffmpeg_or_skip("VP9 fuzz") else { return }; + let depth = Depth::from_env(); + for &(name, args, pix) in SEEDS { + if !craft_media_testkit::ffmpeg_has_encoder(&ff, "libvpx-vp9") { + craft_media_testkit::skip_test("VP9 fuzz", &format!("this ffmpeg has no libvpx-vp9 encoder (seed {name})")); + continue; + } + let Some(seed) = make(&ff, name, args, pix) else { + panic!("ffmpeg has libvpx-vp9 but could not encode the {name} seed"); + }; + if seed.is_empty() { + craft_media_testkit::skip_test("VP9 fuzz", &format!("the {name} seed has no frames")); + continue; + } + // truncate each of the first frames at every length + for fi in 0..seed.len().min(3) { + let len = seed[fi].len(); + for n in depth.truncations(len) { + let mut d = seed.clone(); + d[fi].truncate(n); + run_all(&d, &format!("{name} frame {fi} truncate {n}")); + } + } + // header region of every frame: every byte to a few values, every bit + for fi in 0..seed.len() { + for p in 0..seed[fi].len().min(96) { + for (i, v) in VALUES.into_iter().enumerate() { + if !depth.value(fi, p, i) { + continue; + } + let mut d = seed.clone(); + d[fi][p] = v; + run_all(&d, &format!("{name} frame {fi} byte {p}={v:#x}")); + } + for b in 0..8 { + if !depth.bit(fi, p, b) { + continue; + } + let mut d = seed.clone(); + d[fi][p] ^= 1 << b; + run_all(&d, &format!("{name} frame {fi} flip {p}.{b}")); + } + } + } + let mut rng = Rng(0x9e3779b97f4a7c15 ^ name.len() as u64); + // Skipped rounds still draw their mutations, so round i matches round i of a full run. + for i in 0..depth.rounds { + let mut d = seed.clone(); + for _ in 0..1 + rng.below(6) { + let fi = rng.below(d.len()); + if d[fi].is_empty() { + continue; + } + let p = rng.below(d[fi].len()); + if rng.below(2) == 0 { + d[fi][p] ^= 1 << rng.below(8); + } else { + d[fi][p] = rng.next() as u8; + } + } + if depth.round(i) { + run_all(&d, &format!("{name} random {i}")); + } + } + } +} diff --git a/crates/video/tests/vp9_decode/main.rs b/crates/video/tests/vp9_decode/main.rs new file mode 100644 index 000000000..66697e009 --- /dev/null +++ b/crates/video/tests/vp9_decode/main.rs @@ -0,0 +1,6 @@ +mod common; +mod conformance; +mod fuzz_hostile; +mod perf; +mod robustness; +mod synthetic; diff --git a/crates/video/tests/vp9_decode/perf.rs b/crates/video/tests/vp9_decode/perf.rs new file mode 100644 index 000000000..ac891f8da --- /dev/null +++ b/crates/video/tests/vp9_decode/perf.rs @@ -0,0 +1,48 @@ +//! Decode speed (ignored by default; run with +//! `cargo test --release -p craft-video --features vp9-decode,threads --test vp9_decode perf:: -- --ignored --nocapture`). + +use crate::common; + +use craft_video::vp9::decode::Decoder; +use std::time::Instant; + +fn bench(frames: &[&[u8]], threads: usize, iters: usize) -> (usize, f64) { + let mut best = f64::MAX; + let mut n = 0; + for _ in 0..iters { + let mut dec = Decoder::with_threads(threads); + let t0 = Instant::now(); + n = 0; + for f in frames { + n += dec.decode(f, 0).expect("decode").len(); + } + n += dec.flush().len(); + best = best.min(t0.elapsed().as_secs_f64()); + } + (n, best) +} + +fn run(name: &str, label: &str) { + let f = common::fixture(name); + let Some((ivf, _)) = common::ensure(f) else { return }; + common::check_fixture(name).unwrap(); + let data = std::fs::read(ivf).unwrap(); + let frames = common::ivf_frames(&data); + let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1); + for threads in [1, cores.min(16)] { + let (n, secs) = bench(&frames, threads, 3); + println!("{label} ({} KB): {threads} thread(s): {n} frames in {secs:.3}s = {:.1} fps", data.len() / 1024, n as f64 / secs); + } +} + +#[test] +#[ignore] +fn perf_1080p() { + run("bench_1080p", "1080p 8-bit, 4 tile columns"); +} + +#[test] +#[ignore] +fn perf_4k() { + run("bench_4k", "2160p 8-bit, 8 tile columns"); +} diff --git a/crates/video/tests/vp9_decode/robustness.rs b/crates/video/tests/vp9_decode/robustness.rs new file mode 100644 index 000000000..706ea3982 --- /dev/null +++ b/crates/video/tests/vp9_decode/robustness.rs @@ -0,0 +1,131 @@ +//! Damaged, truncated and random input must never panic or hang (errors are fine). + +use crate::common; + +use craft_video::vp9::decode::Decoder; +use std::time::{Duration, Instant}; + +fn xorshift(seed: &mut u64) -> u64 { + *seed ^= *seed << 13; + *seed ^= *seed >> 7; + *seed ^= *seed << 17; + *seed +} + +fn decode_all(frames: &[Vec], threads: usize) { + let mut dec = Decoder::with_threads(threads); + for (i, f) in frames.iter().enumerate() { + let _ = dec.decode(f, i as i64); + } + let _ = dec.flush(); +} + +#[test] +fn corrupted_streams_do_not_panic() { + for name in ["default_good", "altref_hidden", "profile2_10bit", "tiles_cols4", "aq_segmentation", "lossless", "profile1_444", "tiles_rows4"] { + let f = common::fixture(name); + let Some((ivf, _)) = common::ensure(f) else { return }; + let data = std::fs::read(ivf).unwrap(); + let frames: Vec> = common::ivf_frames(&data).into_iter().map(|f| f.to_vec()).collect(); + let mut seed = 0x1234_5678_9abc_def0u64 ^ name.len() as u64; + for round in 0..10 { + let t0 = Instant::now(); + let mut d = frames.clone(); + for _ in 0..(1 + round * 4) { + let fi = (xorshift(&mut seed) as usize) % d.len(); + let fr = &mut d[fi]; + if fr.is_empty() { + continue; + } + let pos = (xorshift(&mut seed) as usize) % fr.len(); + match round % 3 { + 0 => fr[pos] ^= 1 << (xorshift(&mut seed) % 8), + 1 => fr[pos] = xorshift(&mut seed) as u8, + _ => fr.truncate(pos), + } + } + for threads in [1, 4] { + decode_all(&d, threads); + } + assert!(t0.elapsed() < Duration::from_secs(60), "{name} round {round} too slow"); + } + } +} + +#[test] +fn truncated_frames_do_not_panic() { + let f = common::fixture("default_good"); + let Some((ivf, _)) = common::ensure(f) else { return }; + let data = std::fs::read(ivf).unwrap(); + let frames: Vec> = common::ivf_frames(&data).into_iter().map(|f| f.to_vec()).collect(); + for cut in [1usize, 2, 3, 5, 8, 13, 21, 40, 100, 400] { + let d: Vec> = frames.iter().map(|f| f[..cut.min(f.len())].to_vec()).collect(); + decode_all(&d, 1); + decode_all(&d, 3); + } +} + +#[test] +fn garbage_input_is_rejected_gracefully() { + let mut seed = 42u64; + for len in [0usize, 1, 2, 5, 30, 100, 5000] { + for k in 0..40 { + let mut g: Vec = (0..len).map(|_| xorshift(&mut seed) as u8).collect(); + if len > 10 && k % 2 == 0 { + // A plausible key frame start: frame marker, profile 0, key frame, sync code. + g[0] = 0x82; + g[1] = 0x49; + g[2] = 0x83; + g[3] = 0x42; + } + let mut dec = Decoder::with_threads(1 + k % 3); + let _ = dec.decode(&g, 0); + let _ = dec.decode(&g, 1); + } + } + // Inter frame / show_existing without references. + let mut dec = Decoder::new(); + assert!(dec.decode(&[0x88], 0).is_err()); + assert!(dec.decode(&[0x86, 0, 0, 0, 0], 0).is_err()); +} + +/// Huge declared frame sizes must not allocate absurd amounts or panic. +#[test] +fn oversized_frame_is_rejected() { + // Key frame header: marker 2, profile 0, show_existing 0, key, show, no error res, + // sync, color (4 bits), width-1 = 0xffff, height-1 = 0xffff. + let bits = "10".to_string() + "00" + "0" + "0" + "1" + "0" + "010010011000001101000010" + "0000" + &"1".repeat(32); + let mut v = vec![0u8; bits.len().div_ceil(8) + 8]; + for (i, c) in bits.chars().enumerate() { + if c == '1' { + v[i / 8] |= 0x80 >> (i % 8); + } + } + let mut dec = Decoder::new(); + assert!(dec.decode(&v, 0).is_err()); +} + +/// A key frame whose picture store would not fit the decoder's memory budget is refused before +/// anything is allocated for it. +#[test] +fn frame_over_the_memory_budget_is_rejected() { + // The header of `oversized_frame_is_rejected` with width-1 = height-1 = 63, then the rest of + // the uncompressed header as zeros (28 bits) and header_size_in_bytes = 1. + let size = "0000000000111111"; + let bits = + "10".to_string() + "00" + "0" + "0" + "1" + "0" + "010010011000001101000010" + "0000" + size + size + &"0".repeat(28) + "0000000000000001"; + let mut v = vec![0u8; bits.len().div_ceil(8) + 8]; + for (i, c) in bits.chars().enumerate() { + if c == '1' { + v[i / 8] |= 0x80 >> (i % 8); + } + } + let over_budget = |e: craft_video::vp9::decode::Error| e.to_string().contains("memory budget"); + // One 64x64 4:2:0 picture is 12288 bytes at two bytes a sample; one thread holds 12 of them. + let mut dec = Decoder::with_threads(1); + dec.set_memory_budget(12 * 12288 - 1); + assert!(dec.decode(&v, 0).is_err_and(over_budget)); + let mut dec = Decoder::with_threads(1); + dec.set_memory_budget(12 * 12288); + assert!(!dec.decode(&v, 0).is_err_and(over_budget)); +} diff --git a/crates/video/tests/vp9_decode/synthetic.rs b/crates/video/tests/vp9_decode/synthetic.rs new file mode 100644 index 000000000..2e76398d9 --- /dev/null +++ b/crates/video/tests/vp9_decode/synthetic.rs @@ -0,0 +1,362 @@ +//! Streams libvpx (as driven by ffmpeg) does not produce, built by rewriting the uncompressed +//! headers of libvpx-vp9 output, all checked against ffmpeg's decoder: +//! - reference scaling: inter frames of a stream encoded at another size are spliced after a key +//! frame, with their frame size coded explicitly, so they predict from differently sized +//! references (down- and up-scaling); +//! - intra-only frames (a key frame rewritten as a hidden intra-only frame) shown with +//! show_existing_frame; +//! - show_existing_frame of hidden alt-ref frames and of arbitrary slots. + +use crate::common; + +use std::path::Path; + +struct Bits<'a> { + d: &'a [u8], + pos: usize, +} + +impl Bits<'_> { + fn f(&mut self, n: usize) -> u32 { + let mut v = 0; + for _ in 0..n { + v = (v << 1) | ((self.d[self.pos / 8] >> (7 - self.pos % 8)) & 1) as u32; + self.pos += 1; + } + v + } +} + +#[derive(Default)] +struct W { + bits: Vec, +} + +impl W { + fn put(&mut self, v: u32, n: usize) { + for i in (0..n).rev() { + self.bits.push((v >> i) & 1 == 1); + } + } + fn copy(&mut self, d: &[u8], from: usize, to: usize) { + let mut b = Bits { d, pos: from }; + for _ in from..to { + self.bits.push(b.f(1) == 1); + } + } + fn bytes(&self) -> Vec { + let mut out = vec![0u8; self.bits.len().div_ceil(8)]; + for (i, &b) in self.bits.iter().enumerate() { + if b { + out[i / 8] |= 0x80 >> (i % 8); + } + } + out + } +} + +/// Parse the header fields from refresh_frame_context to header_size_in_bytes (profile 0 +/// layouts); returns the bit position after header_size_in_bytes. +fn parse_tail(b: &mut Bits, error_res: bool, width: u32) -> usize { + if !error_res { + b.f(2); + } + b.f(2); // frame_context_idx + b.f(6); + b.f(3); + if b.f(1) == 1 && b.f(1) == 1 { + for _ in 0..6 { + if b.f(1) == 1 { + b.f(7); + } + } + } + b.f(8); + for _ in 0..3 { + if b.f(1) == 1 { + b.f(5); + } + } + if b.f(1) == 1 { + if b.f(1) == 1 { + for _ in 0..7 { + if b.f(1) == 1 { + b.f(8); + } + } + if b.f(1) == 1 { + for _ in 0..3 { + if b.f(1) == 1 { + b.f(8); + } + } + } + } + if b.f(1) == 1 { + b.f(1); + for _ in 0..8 { + for (bits, signed) in [(8, true), (6, true), (2, false), (0, false)] { + if b.f(1) == 1 { + b.f(bits); + if signed { + b.f(1); + } + } + } + } + } + } + let sb_cols = (width.div_ceil(8)).div_ceil(8); + let mut min_log2 = 0; + while (64 << min_log2) < sb_cols { + min_log2 += 1; + } + let mut max_log2 = 1; + while (sb_cols >> max_log2) >= 4 { + max_log2 += 1; + } + max_log2 -= 1; + let mut l = min_log2; + while l < max_log2 { + if b.f(1) == 1 { + l += 1; + } else { + break; + } + } + if b.f(1) == 1 { + b.f(1); + } + b.f(16); + b.pos +} + +/// Rewrite a profile 0 inter frame so that its frame size (`w` x `h`) is coded explicitly. +fn explicit_size(frame: &[u8], w: u32, h: u32) -> Vec { + let mut b = Bits { d: frame, pos: 0 }; + assert_eq!(b.f(2), 2); + assert_eq!(b.f(2), 0, "profile 0 only"); + assert_eq!(b.f(1), 0, "show_existing"); + assert_eq!(b.f(1), 1, "inter frame expected"); + let show = b.f(1) == 1; + let err = b.f(1) == 1; + if !show { + assert_eq!(b.f(1), 0, "intra-only"); + } + if !err { + b.f(2); + } + b.f(8 + 12); + let found_at = b.pos; + let mut found = false; + for _ in 0..3 { + if b.f(1) == 1 { + found = true; + break; + } + } + if !found { + b.f(32); + } + let after_size = b.pos; + // render_size, allow_high_precision_mv, interpolation filter + if b.f(1) == 1 { + b.f(32); + } + b.f(1); + if b.f(1) == 0 { + b.f(2); + } + let end = parse_tail(&mut b, err, w); + let mut o = W::default(); + o.copy(frame, 0, found_at); + o.put(0, 3); + o.put(w - 1, 16); + o.put(h - 1, 16); + o.copy(frame, after_size, end); + let mut out = o.bytes(); + out.extend_from_slice(&frame[end.div_ceil(8)..]); + out +} + +/// Rewrite a profile 0 key frame as a hidden intra-only frame refreshing `refresh` slots. +fn key_to_intra_only(frame: &[u8], refresh: u8, reset: u32) -> Vec { + let mut b = Bits { d: frame, pos: 0 }; + assert_eq!(b.f(2), 2); + assert_eq!(b.f(2), 0); + assert_eq!(b.f(1), 0); + assert_eq!(b.f(1), 0, "key frame expected"); + b.f(1); // show_frame + let err = b.f(1) == 1; + assert_eq!(b.f(24), 0x498342); + b.f(4); // color_space, color_range + let size_at = b.pos; + let w = b.f(16) + 1; + b.f(16); + if b.f(1) == 1 { + b.f(32); + } + let after_render = b.pos; + let end = parse_tail(&mut b, err, w); + let mut o = W::default(); + o.put(2, 2); + o.put(0, 2); + o.put(0, 1); + o.put(1, 1); // non-key + o.put(0, 1); // hidden + o.put(err as u32, 1); + o.put(1, 1); // intra_only + if !err { + o.put(reset, 2); + } + o.put(0x498342, 24); + o.put(refresh as u32, 8); + o.copy(frame, size_at, after_render); + o.copy(frame, after_render, end); + let mut out = o.bytes(); + out.extend_from_slice(&frame[end.div_ceil(8)..]); + out +} + +fn reference(ivf: &Path, pix_fmt: &str) -> Vec { + let yuv = ivf.with_extension("yuv"); + let _ = std::fs::remove_file(&yuv); + assert!(common::reference_decode(ivf, &yuv, pix_fmt), "ffmpeg failed on {}", ivf.display()); + let r = std::fs::read(&yuv).unwrap(); + let _ = std::fs::remove_file(&yuv); + r +} + +fn encode(name: &str, w: u32, h: u32, frames: u32, extra: &[&str]) -> Option>> { + let ff = common::ffmpeg()?; + let path = common::fixtures_dir().join(format!("{name}.ivf")); + if !path.exists() { + let mut c = std::process::Command::new(ff); + c.args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i"]); + c.arg(format!("testsrc2=size={w}x{h}:rate=25,noise=alls=8:allf=t+u")); + c.args(["-frames:v", &frames.to_string(), "-c:v", "libvpx-vp9", "-b:v", "500k", "-frame-parallel", "0"]); + c.args(extra); + // written aside and renamed: a test in another process may be reading this fixture + let tmp = path.with_extension(format!("tmp.{}.ivf", common::unique())); + c.args(["-f", "ivf"]).arg(&tmp); + if !common::run(&mut c) { + return None; + } + if std::fs::rename(&tmp, &path).is_err() && !path.exists() { + return None; + } + } + let d = std::fs::read(&path).unwrap(); + Some(common::ivf_frames(&d).into_iter().map(|f| f.to_vec()).collect()) +} + +fn check(name: &str, frames: &[Vec]) { + let path = common::fixtures_dir().join(format!("{name}.ivf")); + common::write_ivf(&path, 0, 0, frames); + let r = reference(&path, "yuv420p"); + common::check_files(name, &path, &r, "yuv420p").unwrap(); + let _ = std::fs::remove_file(path); +} + +/// Splice the inter frames of a `w2` x `h2` stream after frames of a 352x288 stream. +fn scaled_splice(name: &str, w2: u32, h2: u32, interp: &[&str]) -> Option>> { + let a = encode("syn_base_352", 352, 288, 6, &[])?; + let mut args = vec!["-error-resilient", "1"]; + args.extend_from_slice(interp); + let b = encode(&format!("syn_er_{w2}x{h2}{}", interp.join("")), w2, h2, 10, &args)?; + let mut frames: Vec> = a[..4].to_vec(); + for f in &b[1..] { + frames.push(explicit_size(f, w2, h2)); + } + let _ = name; + Some(frames) +} + +#[test] +fn scaled_reference_downscale() { + let Some(f) = scaled_splice("down", 256, 208, &[]) else { return }; + let mut dec = craft_video::vp9::decode::Decoder::with_threads(1); + for c in &f { + let _ = dec.decode(c, 0); + } + assert!(dec.stats().scaled_ref_blocks > 0, "{:?}", dec.stats()); + check("syn_scaled_down", &f); +} + +#[test] +fn splice_same_size_control() { + let Some(f) = scaled_splice("same", 352, 288, &[]) else { return }; + check("syn_same", &f); +} + +#[test] +fn scaled_reference_upscale() { + let Some(f) = scaled_splice("up", 480, 400, &[]) else { return }; + check("syn_scaled_up", &f); +} + +#[test] +fn scaled_reference_odd_sizes() { + let Some(f) = scaled_splice("odd", 301, 199, &[]) else { return }; + check("syn_scaled_odd", &f); +} + +fn build_superframe(frames: &[Vec]) -> Vec { + if frames.len() == 1 { + return frames[0].clone(); + } + let mut out = Vec::new(); + for f in frames { + out.extend_from_slice(f); + } + let marker = 0xc0 | (3 << 3) | (frames.len() as u8 - 1); + out.push(marker); + for f in frames { + out.extend((f.len() as u32).to_le_bytes()); + } + out.push(marker); + out +} + +#[test] +fn intra_only_frames() { + // Error resilient source: the frame after the intra-only frame does not use the previous + // frame's motion vectors either way, so its symbols parse as the encoder intended. + let Some(src) = encode("syn_er_g8", 352, 288, 20, &["-g", "8", "-error-resilient", "1"]) else { return }; + let keys: Vec = src.iter().enumerate().filter(|(_, f)| f[0] & 0x04 == 0 && f[0] & 0x08 == 0).map(|(i, _)| i).collect(); + assert!(keys.len() >= 2, "need a second key frame: {keys:?}"); + for (reset, refresh, show_slot) in [(3u32, 0xffu8, 0u8), (2, 0x05, 2), (0, 0x10, 4)] { + let mut frames = Vec::new(); + for (i, f) in src.iter().enumerate() { + if i == keys[1] { + frames.push(key_to_intra_only(f, refresh, reset)); + frames.push(vec![0x88 | show_slot]); + } else { + frames.push(f.clone()); + } + } + let mut dec = craft_video::vp9::decode::Decoder::with_threads(1); + for c in &frames { + dec.decode(c, 0).unwrap(); + } + let s = dec.stats(); + assert!(s.intra_only == 1 && s.show_existing == 1, "{s:?}"); + check(&format!("syn_intra_only_{reset}"), &frames); + } +} + +#[test] +fn show_existing_frames() { + let Some(src) = encode("syn_er_352", 352, 288, 6, &["-error-resilient", "1"]) else { return }; + let mut frames = Vec::new(); + for (i, f) in src.iter().enumerate() { + frames.push(f.clone()); + if i >= 1 { + frames.push(vec![0x88 | (i as u8 % 8)]); + } + } + // A superframe holding a frame and a show_existing_frame. + let sf = build_superframe(&[src[2].clone(), vec![0x88]]); + frames.push(sf); + check("syn_show_existing", &frames); +} diff --git a/crates/video/tools/vp9_extract_tables.py b/crates/video/tools/vp9_extract_tables.py new file mode 100644 index 000000000..81329447b --- /dev/null +++ b/crates/video/tools/vp9_extract_tables.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Extract the VP9 constant tables from the text of the public VP9 Bitstream & Decoding Process +Specification (v0.6, WebM project / Google). + +Usage: + curl -L -o vp9.pdf 'https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf' + pdftotext -layout vp9.pdf vp9.txt + python3 vp9_extract_tables.py vp9.txt > ../src/vp9/decode/spec_tables.rs + +Only the specification text is read. Every table is emitted flat (row-major, as printed) with its +element count checked against the declared dimensions. +""" +import re +import sys +from pathlib import Path + + +def _spec_path(arg): + """Resolve a CLI path argument and require it to be an existing regular file.""" + path = Path(arg).resolve() + if not path.is_file(): + sys.exit(f"not a readable file: {arg}") + return path + + +text = open(_spec_path(sys.argv[1]), encoding="utf-8").read().replace("−", "-").replace("–", "-") +lines = [l for l in text.split("\n") if "Copyright" not in l and "VP9 Bitstream & Decoding Process Specification" not in l] +text = "\n".join(lines) + + +def table(name, count): + """Integers of the table `name[...] = { ... }` (the last definition in the text).""" + starts = [m.start() for m in re.finditer(r"\b%s\s*\[" % re.escape(name), text)] + for s in reversed(starts): + eq = text.find("=", s) + brace = text.find("{", s) + if eq < 0 or brace < 0 or eq > brace or text[s:eq].count("\n") > 2: + continue + depth = 0 + i = brace + while True: + ch = text[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + break + i += 1 + body = text[brace:i] + body = re.sub(r"//[^\n]*", "", body) + body = re.sub(r"/\*.*?\*/", "", body, flags=re.S) + vals = [int(v) for v in re.findall(r"-?\d+", body)] + assert len(vals) == count, (name, len(vals), count) + return vals + raise AssertionError("table not found: " + name) + + +def emit(rust_name, ty, vals, per_line=16): + print("pub const %s: [%s; %d] = [" % (rust_name, ty, len(vals))) + for k in range(0, len(vals), per_line): + print(" " + ", ".join(str(v) for v in vals[k:k + per_line]) + ",") + print("];") + print() + + +print("// Generated by tools/extract_tables.py from the VP9 Bitstream & Decoding Process Specification") +print("// v0.6 (section numbers refer to that document). Do not edit by hand.") +print() + +emit("INV_MAP_TABLE", "u8", table("inv_map_table", 255)) +emit("SUBPEL_FILTERS", "i16", table("subpel_filters", 4 * 16 * 8), 8) +emit("DC_QLOOKUP", "i32", table("dc_qlookup", 3 * 256)) +emit("AC_QLOOKUP", "i32", table("ac_qlookup", 3 * 256)) +emit("COS64_LOOKUP", "i32", table("cos64_lookup", 33)) +for n, c in [("4x4", 16), ("8x8", 64), ("16x16", 256)]: + for kind in ["default", "col", "row"]: + emit("%s_SCAN_%s" % (kind.upper(), n.upper()), "u16", table("%s_scan_%s" % (kind, n), c)) +emit("DEFAULT_SCAN_32X32", "u16", table("default_scan_32x32", 1024)) +emit("COEFBAND_4X4", "u8", table("coefband_4x4", 16)) +emit("COEFBAND_8X8PLUS", "u8", table("coefband_8x8plus", 1024)) +emit("ENERGY_CLASS", "u8", table("energy_class", 12)) +emit("PARETO_TABLE", "u8", table("pareto_table", 128 * 8), 8) +emit("MV_REF_BLOCKS", "i8", table("mv_ref_blocks", 13 * 8 * 2)) +emit("MODE_2_COUNTER", "u8", table("mode_2_counter", 14)) +emit("KF_PARTITION_PROBS", "u8", table("kf_partition_probs", 16 * 3), 3) +emit("KF_Y_MODE_PROBS", "u8", table("kf_y_mode_probs", 10 * 10 * 9), 9) +emit("KF_UV_MODE_PROBS", "u8", table("kf_uv_mode_probs", 10 * 9), 9) +emit("DEFAULT_PARTITION_PROBS", "u8", table("default_partition_probs", 16 * 3), 3) +emit("DEFAULT_Y_MODE_PROBS", "u8", table("default_y_mode_probs", 4 * 9), 9) +emit("DEFAULT_UV_MODE_PROBS", "u8", table("default_uv_mode_probs", 10 * 9), 9) +emit("DEFAULT_SKIP_PROB", "u8", table("default_skip_prob", 3)) +emit("DEFAULT_IS_INTER_PROB", "u8", table("default_is_inter_prob", 4)) +emit("DEFAULT_COMP_MODE_PROB", "u8", table("default_comp_mode_prob", 5)) +emit("DEFAULT_COMP_REF_PROB", "u8", table("default_comp_ref_prob", 5)) +emit("DEFAULT_SINGLE_REF_PROB", "u8", table("default_single_ref_prob", 10), 2) +emit("DEFAULT_MV_SIGN_PROB", "u8", table("default_mv_sign_prob", 2)) +emit("DEFAULT_MV_BITS_PROB", "u8", table("default_mv_bits_prob", 20), 10) +emit("DEFAULT_MV_CLASS0_BIT_PROB", "u8", table("default_mv_class0_bit_prob", 2)) +emit("DEFAULT_TX_PROBS", "u8", table("default_tx_probs", 4 * 2 * 3), 3) +emit("DEFAULT_INTER_MODE_PROBS", "u8", table("default_inter_mode_probs", 7 * 3), 3) +emit("DEFAULT_INTERP_FILTER_PROBS", "u8", table("default_interp_filter_probs", 8), 2) +emit("DEFAULT_MV_JOINT_PROBS", "u8", table("default_mv_joint_probs", 3)) +emit("DEFAULT_MV_CLASS_PROBS", "u8", table("default_mv_class_probs", 20), 10) +emit("DEFAULT_MV_CLASS0_FR_PROBS", "u8", table("default_mv_class0_fr_probs", 12), 3) +emit("DEFAULT_MV_CLASS0_HP_PROB", "u8", table("default_mv_class0_hp_prob", 2)) +emit("DEFAULT_MV_FR_PROBS", "u8", table("default_mv_fr_probs", 6), 3) +emit("DEFAULT_MV_HP_PROB", "u8", table("default_mv_hp_prob", 2)) +emit("DEFAULT_COEF_PROBS", "u8", table("default_coef_probs", 4 * 2 * 2 * 6 * 6 * 3), 3) diff --git a/crates/video/tools/vp9_gen_transforms.py b/crates/video/tools/vp9_gen_transforms.py new file mode 100644 index 000000000..bc69ef172 --- /dev/null +++ b/crates/video/tools/vp9_gen_transforms.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""Generate straight-line 1D inverse transforms from the butterfly descriptions of the VP9 +specification (v0.6 section 8.7.1): inverse DCT (8.7.1.2 / 8.7.1.3, n = 2..5) and inverse ADST +(8.7.1.4 - 8.7.1.8, n = 3, 4). ADST4 and WHT are small enough to be written by hand in +src/vp9/decode/transform.rs. + +The generator symbolically executes the ordered steps of the specification on an array T (and the +higher precision array S of the ADST) and emits one `let` per written element, with the cos64 / +sin64 constants folded in. + +Usage: python3 vp9_gen_transforms.py > ../src/vp9/decode/transform_gen.rs +""" + +COS64 = [16384, 16364, 16305, 16207, 16069, 15893, 15679, 15426, 15137, 14811, 14449, 14053, 13623, + 13160, 12665, 12140, 11585, 11003, 10394, 9760, 9102, 8423, 7723, 7005, 6270, 5520, 4756, + 3981, 3196, 2404, 1606, 804, 0] + + +def cos64(angle): + a = angle & 127 + if a <= 32: + return COS64[a] + if a <= 64: + return -COS64[64 - a] + if a <= 96: + return -COS64[a - 64] + return COS64[128 - a] + + +def sin64(angle): + return cos64(angle - 32) + + +def brev(bits, x): + t = 0 + for i in range(bits): + t |= ((x >> i) & 1) << (bits - 1 - i) + return t + + +class Gen: + def __init__(self, n): + self.n0 = 1 << n + self.T = ["x[%d]" % i for i in range(self.n0)] + self.S = [None] * self.n0 + self.lines = [] + self.k = 0 + + def new(self, expr): + name = "v%d" % self.k + self.k += 1 + self.lines.append(" let %s = %s;" % (name, expr)) + return name + + @staticmethod + def lin(terms): + # Wrapping arithmetic: conformant streams never overflow (8.7.1.1), damaged ones must not + # panic in debug builds. + s = None + for var, c in terms: + if c == 0: + continue + t = "%s.wrapping_mul(%d)" % (var, c) + s = t if s is None else "%s.wrapping_add(%s)" % (s, t) + return s if s is not None else "0" + + def B(self, a, b, angle, flip): + ta, tb = self.T[a], self.T[b] + c, s = cos64(angle), sin64(angle) + x = self.new("r14(%s)" % self.lin([(ta, c), (tb, -s)])) + y = self.new("r14(%s)" % self.lin([(ta, s), (tb, c)])) + self.T[a], self.T[b] = (y, x) if flip else (x, y) + + def H(self, a, b, flip): + if flip: + a, b = b, a + x, y = self.T[a], self.T[b] + self.T[a] = self.new("%s.wrapping_add(%s)" % (x, y)) + self.T[b] = self.new("%s.wrapping_sub(%s)" % (x, y)) + + def SB(self, a, b, angle, flip): + ta, tb = self.T[a], self.T[b] + c, s = cos64(angle), sin64(angle) + x = self.new(self.lin([(ta, c), (tb, -s)])) + y = self.new(self.lin([(ta, s), (tb, c)])) + self.S[a], self.S[b] = (y, x) if flip else (x, y) + + def SH(self, a, b): + sa, sb = self.S[a], self.S[b] + self.T[a] = self.new("r14(%s.wrapping_add(%s))" % (sa, sb)) + self.T[b] = self.new("r14(%s.wrapping_sub(%s))" % (sa, sb)) + + +def idct_steps(g, n): + n0, n1, n2, n3 = 1 << n, 1 << (n - 1), 1 << (n - 2), (1 << (n - 3)) if n >= 3 else 0 + if n == 2: + g.B(0, 1, 16, 1) + else: + idct_steps(g, n - 1) + for i in range(n2): + g.B(n1 + i, n0 - 1 - i, 32 - brev(5, n1 + i), 0) + if n >= 3: + for i in range(n3): + for j in range(2): + g.H(n1 + 4 * i + 2 * j, n1 + 1 + 4 * i + 2 * j, j) + if n == 5: + for i in range(2): + for j in range(2): + g.B(n0 - n + 3 - n2 * j - 4 * i, n1 + n - 4 + n2 * j + 4 * i, 28 - 16 * i + 56 * j, 1) + for i in range(2): + for j in range(4): + g.H(n1 + n3 * j + i, n1 + n2 - 5 + n3 * j - i, j & 1) + if n >= 4: + for i in range(2 if n == 5 else 1): + for j in range(2): + g.B(n0 - n + 2 - i - n2 * j, n1 + n - 3 + i + n2 * j, 24 + 48 * j, 1) + for i in range(2 * n - 6): + for j in range(2): + g.H(n1 + n2 * j + i, n1 + n2 - 1 + n2 * j - i, j & 1) + if n >= 3: + for i in range(n3): + g.B(n0 - n3 - 1 - i, n1 + n3 + i, 16, 1) + for i in range(n1): + g.H(i, n0 - 1 - i, 0) + + +def idct(n): + g = Gen(n) + n0 = 1 << n + g.T = ["x[%d]" % brev(n, i) for i in range(n0)] # 8.7.1.2 permutation + idct_steps(g, n) + return g + + +def adst_in_perm(g, n): + n0, n1 = 1 << n, 1 << (n - 1) + c = list(g.T) + for i in range(n1): + g.T[2 * i] = c[n0 - 1 - 2 * i] + g.T[2 * i + 1] = c[2 * i] + + +def adst_out_perm(g, n): + c = list(g.T) + if n == 4: + for a in range(2): + for b in range(2): + for cc in range(2): + for d in range(2): + g.T[8 * a + 4 * b + 2 * cc + d] = c[8 * (d ^ cc) + 4 * (cc ^ b) + 2 * (b ^ a) + a] + else: + for a in range(2): + for b in range(2): + for cc in range(2): + g.T[4 * a + 2 * b + cc] = c[4 * (cc ^ b) + 2 * (b ^ a) + a] + + +def neg(g, i): + g.T[i] = g.new("%s.wrapping_neg()" % g.T[i]) + + +def adst8(): + g = Gen(3) + adst_in_perm(g, 3) + for i in range(4): + g.SB(2 * i, 1 + 2 * i, 30 - 8 * i, 1) + for i in range(4): + g.SH(i, 4 + i) + for i in range(2): + g.SB(4 + 3 * i, 5 + i, 24 - 16 * i, 1) + for i in range(2): + g.SH(4 + i, 6 + i) + for i in range(2): + g.H(i, 2 + i, 0) + for i in range(2): + g.B(2 + 4 * i, 3 + 4 * i, 16, 1) + adst_out_perm(g, 3) + for i in range(4): + neg(g, 1 + 2 * i) + return g + + +def adst16(): + g = Gen(4) + adst_in_perm(g, 4) + for i in range(8): + g.SB(2 * i, 1 + 2 * i, 31 - 4 * i, 1) + for i in range(8): + g.SH(i, 8 + i) + for i in range(4): + g.SB(8 + 2 * i, 9 + 2 * i, 28 - 16 * i, 1) + for i in range(4): + g.SH(8 + i, 12 + i) + for i in range(4): + g.H(i, 4 + i, 0) + for i in range(2): + for j in range(2): + g.SB(4 + 8 * i + 3 * j, 5 + 8 * i + j, 24 - 16 * j, 1) + for i in range(2): + for j in range(2): + g.SH(4 + 8 * j + i, 6 + 8 * j + i) + for i in range(2): + for j in range(2): + g.H(8 * j + i, 2 + 8 * j + i, 0) + for i in range(2): + for j in range(2): + g.B(2 + 4 * j + 8 * i, 3 + 4 * j + 8 * i, 48 + 64 * (i ^ j), 0) + adst_out_perm(g, 4) + for i in range(2): + for j in range(2): + neg(g, 1 + 12 * j + 2 * i) + return g + + +def emit(name, g): + n0 = g.n0 + print(" #[inline(always)]") + print(" pub fn %s(x: [$t; %d]) -> [$t; %d] {" % (name, n0, n0)) + for l in g.lines: + print(" " + l) + print(" [%s]" % ", ".join(g.T)) + print(" }") + print() + + +print("// Generated by tools/gen_transforms.py from the butterfly descriptions of the VP9 specification") +print("// v0.6 section 8.7.1. Do not edit by hand.") +print() +print("macro_rules! transforms_1d {") +print(" ($t:ty) => {") +print(" #[inline(always)]") +print(" fn r14(x: $t) -> $t {") +print(" x.wrapping_add(1 << 13) >> 14") +print(" }") +print() +for n in range(2, 6): + emit("idct%d" % (1 << n), idct(n)) +emit("iadst8", adst8()) +emit("iadst16", adst16()) +print(" };") +print("}") +print() +print("pub(crate) mod narrow {") +print(" transforms_1d!(i32);") +print("}") +print() +print("pub(crate) mod wide {") +print(" transforms_1d!(i64);") +print("}") +print() +print("/// The same butterflies on 4 / 8 columns at once (column pass of the 2D transforms).") +print("pub(crate) mod cols4 {") +print(" transforms_1d!(crate::transform::Cols<4>);") +print("}") +print() +print("pub(crate) mod cols8 {") +print(" transforms_1d!(crate::transform::Cols<8>);") +print("}")