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.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-11 13:47:53 -05:00
1 parent 9dfc35965c
commit 6ac2a6a34a
42 files changed
+10761 -521

No files matched your search

+4
View File
@@ -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
+12 -2
View File
@@ -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"]
+1 -1
View File
@@ -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` |
+45
View File
@@ -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<String> = 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());
}
+82
View File
@@ -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<u64> {
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<u64> {
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));
}
}
+6 -2
View File
@@ -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")]
+220
View File
@@ -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<u8>) for 8-bit, Plane::U16(Vec<u16>) 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::KeyframeInfo> = 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`.
+190
View File
@@ -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<u8> = (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}");
}
}
}
}
File diff suppressed because it is too large. Load diff
+31
View File
@@ -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<T> = std::result::Result<T, Error>;
/// `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;
+311
View File
@@ -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<MiInfo>,
}
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<u16>; 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<Band>]>,
pools: std::sync::Arc<Pools>,
}
impl Frame {
pub fn new(info: FrameInfo, pools: std::sync::Arc<Pools>) -> 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<T> {
bufs: std::sync::Mutex<Vec<Vec<T>>>,
max: usize,
}
impl<T> Default for Pool<T> {
fn default() -> Self {
Pool::with_max(32)
}
}
impl<T> Pool<T> {
/// A pool keeping at most `max` buffers.
pub fn with_max(max: usize) -> Self {
Pool { bufs: Default::default(), max }
}
}
impl<T: Clone> Pool<T> {
/// 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<T> {
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<T>) {
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<u16>,
/// Frame bands (many small buffers per frame).
pub bands: Pool<u16>,
pub mi: Pool<MiInfo>,
}
impl Default for Pools {
fn default() -> Self {
Pools { samples: Pool::default(), bands: Pool::with_max(1024), mi: Pool::default() }
}
}
+758
View File
@@ -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<i32> {
let v = r.read_bits(bits)? as i32;
Ok(if r.read_flag()? { -v } else { v })
}
fn read_delta_q(r: &mut BitReader) -> Result<i8> {
Ok(if r.read_flag()? { read_s(r, 4)? as i8 } else { 0 })
}
fn read_prob(r: &mut BitReader) -> Result<u8> {
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<ColorConfig> {
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<RefInfo>; 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<RefInfo>; 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<RefInfo>; 8]) -> Result<FrameHeader> {
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());
}
}
}
+394
View File
@@ -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<const W: usize>(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
let Some(o) = o.first_chunk_mut::<W>() 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::<W>()) 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::<W>()) 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<const W: usize>(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<const W: usize>(rows: &[&[u16]; 8], f: &[i16], max: i32, o: &mut [u16]) {
let Some(o) = o.first_chunk_mut::<W>() 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::<W>() 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::<W>() 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<u16> = (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}");
}
}
}
}
}
+230
View File
@@ -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::<i32>();
let above = n.above[1..=size].iter().sum::<i32>();
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);
}
}
}
+378
View File
@@ -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<const R: usize, const ROWS: bool>(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<const LOG2: u32>(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);
+169
View File
@@ -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<u8> = 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<KeyframeInfo> {
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<u8>),
U16(Vec<u16>),
}
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<u8> {
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,
}
+322
View File
@@ -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<const N: usize>(s: &[u8]) -> [u8; N] {
s.first_chunk::<N>().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]);
}
}
File diff suppressed because it is too large. Load diff
+288
View File
@@ -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<Vec<Vec<(u16, u16)>>> = 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<i8> {
let mut v: Vec<i8> =
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::<Vec<i8>>());
assert_eq!(leaves(&MV_CLASS_TREE), (0..11).collect::<Vec<i8>>());
assert_eq!(leaves(&PARTITION_TREE), (0..4).collect::<Vec<i8>>());
assert_eq!(leaves(&INTER_MODE_TREE), (0..4).collect::<Vec<i8>>());
}
#[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}");
}
}
}
}
}
File diff suppressed because it is too large. Load diff
+389
View File
@@ -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::<true>(bd, b, t) } else { self.tokens_impl::<false>(bd, b, t) }
}
fn tokens_impl<const COUNTING: bool>(&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]);
}
}
}
}
+547
View File
@@ -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<const N: usize>(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<const N: usize, const W: usize>(y: &[Cols<W>; 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::<W>()) 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<const W: usize>(pub [i32; W]);
impl<const W: usize> From<i32> for Cols<W> {
#[inline(always)]
fn from(v: i32) -> Self {
Cols([v; W])
}
}
impl<const W: usize> Cols<W> {
#[inline(always)]
pub fn wrapping_add<O: Into<Self>>(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<const W: usize> std::ops::Shr<i32> for Cols<W> {
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<const N: usize>(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<const N: usize>(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<T: Copy + From<i32>, const M: usize>(x: &[T]) -> [T; M] {
x.first_chunk::<M>().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<const N: usize>(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<f64> {
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<i64> = vec![0; n];
w[k] = 1000;
tx1d_wide(&mut w, n, false);
assert_eq!(w.iter().map(|&v| v as i32).collect::<Vec<_>>(), 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<f64> = (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<f64> =
(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<const N: usize>(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<const N: usize>(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<i32> =
(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<u16> = (0..N * stride).map(|_| (rnd() % 256) as u16).collect();
let (mut a, mut b) = (base.clone(), base);
itx_narrow::<N>(&coefs, &p, &mut a, stride);
itx_narrow_ref::<N>(&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);
}
}
@@ -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>);
}
File renamed without changes.
File renamed without changes.
@@ -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 {
@@ -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)]
+510
View File
@@ -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<u32>,
/// 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<u8>,
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<u8>,
pub rec: Vec<u8>,
}
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<f64>,
inter_complexity: Option<f64>,
/// Bits spent minus bits budgeted so far.
debt: f64,
last_inter_q: Option<u8>,
}
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<Mv>,
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<u8> {
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<u8> {
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<u8>; 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<Vec<u8>> = 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<u8>, Vec<u8>, Vec<u8>) {
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<u8>, Vec<u8>, Vec<u8>) {
let (w, h) = (w as usize, h as usize);
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let y: Vec<u8> = 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(&params.extradata).unwrap().level, 0);
}
}
}
@@ -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,
File renamed without changes.
@@ -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;
File renamed without changes.
+12 -506
View File
@@ -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<u32>,
/// 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<u8>,
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<u8>,
pub rec: Vec<u8>,
}
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<f64>,
inter_complexity: Option<f64>,
/// Bits spent minus bits budgeted so far.
debt: f64,
last_inter_q: Option<u8>,
}
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<Mv>,
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<u8> {
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<u8> {
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<u8>; 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<Vec<u8>> = 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<u8>, Vec<u8>, Vec<u8>) {
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<u8>, Vec<u8>, Vec<u8>) {
let (w, h) = (w as usize, h as usize);
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let y: Vec<u8> = 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(&params.extradata).unwrap().level, 0);
}
}
}
#[cfg(feature = "vp9-encode")]
mod encode;
#[cfg(feature = "vp9-encode")]
pub use encode::*;
+421
View File
@@ -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<PathBuf> {
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<u32>, 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<u8>]) {
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<Picture>);
/// Decode an IVF file with `threads` workers (0 = default), pts = frame index.
pub fn decode_file_threads(path: &Path, threads: usize) -> Result<Vec<Picture>, 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<bool, String> {
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(())
}
@@ -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<Vec<u8>> = 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<usize> = (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);
}
}
}
@@ -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<Vec<u8>> {
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<Vec<Vec<u8>>> {
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<u8>], 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<std::ops::Range<usize>> {
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}"));
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
mod common;
mod conformance;
mod fuzz_hostile;
mod perf;
mod robustness;
mod synthetic;
+48
View File
@@ -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");
}
+131
View File
@@ -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<u8>], 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<Vec<u8>> = 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<Vec<u8>> = 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<Vec<u8>> = 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<u8> = (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));
}
+362
View File
@@ -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<bool>,
}
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<Vec<Vec<u8>>> {
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<u8>]) {
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<Vec<Vec<u8>>> {
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<Vec<u8>> = 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<u8>]) -> Vec<u8> {
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<usize> = 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);
}
+109
View File
@@ -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)
+258
View File
@@ -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("}")