mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
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:
1 parent
9dfc35965c
commit
6ac2a6a34a
42 files changed
+10761
-521
No files matched your search
@@ -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
@@ -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"]
|
||||
@@ -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` |
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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;
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
@@ -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(¶ms.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
@@ -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(¶ms.extradata).unwrap().level, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "vp9-encode")]
|
||||
mod encode;
|
||||
#[cfg(feature = "vp9-encode")]
|
||||
pub use encode::*;
|
||||
@@ -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}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod common;
|
||||
mod conformance;
|
||||
mod fuzz_hostile;
|
||||
mod perf;
|
||||
mod robustness;
|
||||
mod synthetic;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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("}")
|
||||
Reference in new issue
Block a user