From 8cdcdce3260b825b4883bedddd60cf8c99d1ceb6 Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Wed, 11 Nov 2026 19:09:32 -0600 Subject: [PATCH] craft-video: VP8 key-frame encoder (`vp8-encode` feature) (#969) Moves effectcraft's effectcraft-vp8enc here: the lossy picture of a WebP image or animation frame, written from RFC 6386. Rate-distortion choice between the 16x16 and B_PRED intra modes, per-frame token probability updates, loop filter level from the quantizer. `vp8` now turns on `vp8-decode` and `vp8-encode`, as `vp9` does; the decoder's tests and examples require `vp8-decode`. The tests decode the encoder's frames with this crate's decoder: with the loop filter off it reproduces the encoder's reconstruction exactly. --- crates/video/Cargo.toml | 16 +- crates/video/src/lib.rs | 4 +- crates/video/src/vp8/encode/README.md | 34 + crates/video/src/vp8/encode/bool.rs | 233 +++++++ crates/video/src/vp8/encode/frame.rs | 787 +++++++++++++++++++++++ crates/video/src/vp8/encode/mod.rs | 106 +++ crates/video/src/vp8/encode/predict.rs | 278 ++++++++ crates/video/src/vp8/encode/tables.rs | 510 +++++++++++++++ crates/video/src/vp8/encode/transform.rs | 157 +++++ crates/video/src/vp8/encode/yuv.rs | 85 +++ crates/video/src/vp8/mod.rs | 30 +- crates/video/tests/vp8_encode/main.rs | 124 ++++ 12 files changed, 2355 insertions(+), 9 deletions(-) create mode 100644 crates/video/src/vp8/encode/README.md create mode 100644 crates/video/src/vp8/encode/bool.rs create mode 100644 crates/video/src/vp8/encode/frame.rs create mode 100644 crates/video/src/vp8/encode/mod.rs create mode 100644 crates/video/src/vp8/encode/predict.rs create mode 100644 crates/video/src/vp8/encode/tables.rs create mode 100644 crates/video/src/vp8/encode/transform.rs create mode 100644 crates/video/src/vp8/encode/yuv.rs create mode 100644 crates/video/tests/vp8_encode/main.rs diff --git a/crates/video/Cargo.toml b/crates/video/Cargo.toml index 197b850ba..e25f1b85b 100644 --- a/crates/video/Cargo.toml +++ b/crates/video/Cargo.toml @@ -21,8 +21,10 @@ dnx = [] apv = ["apv-decode", "apv-encode"] apv-decode = ["craft-media/bitstream"] apv-encode = ["craft-media/bitstream"] -# VP8 (RFC 6386): decoder. -vp8 = [] +# VP8 (RFC 6386): decoder and encoder; `vp8-decode` or `vp8-encode` for one of them. +vp8 = ["vp8-decode", "vp8-encode"] +vp8-decode = [] +vp8-encode = [] # MPEG-2 video (ITU-T H.262) and MPEG-1 video (ISO/IEC 11172-2): decoder. mpeg2 = [] # Apple ProRes (SMPTE RDD 36): decoder and encoder. @@ -64,15 +66,15 @@ required-features = ["apv"] [[test]] name = "vp8" -required-features = ["vp8"] +required-features = ["vp8-decode"] [[example]] name = "vp8_fuzz" -required-features = ["vp8"] +required-features = ["vp8-decode"] [[example]] name = "vp8_vp8dec" -required-features = ["vp8"] +required-features = ["vp8-decode"] [[test]] name = "av1_decode" @@ -98,6 +100,10 @@ required-features = ["hevc-decode"] name = "hevc_hevcdec" required-features = ["hevc-decode"] +[[test]] +name = "vp8_encode" +required-features = ["vp8"] + [[test]] name = "vp9_decode" required-features = ["vp9-decode"] diff --git a/crates/video/src/lib.rs b/crates/video/src/lib.rs index 80cbcee35..33901e723 100644 --- a/crates/video/src/lib.rs +++ b/crates/video/src/lib.rs @@ -7,7 +7,7 @@ //! |---|---|---| //! | `dnx` | [`dnx`] | Avid DNxHD / DNxHR (SMPTE ST 2019-1): decoder and encoder | //! | `apv` | [`apv`] | APV (RFC 9924): decoder (`apv-decode`) and encoder (`apv-encode`) | -//! | `vp8` | [`vp8`] | VP8 (RFC 6386): decoder | +//! | `vp8` | [`vp8`] | VP8 (RFC 6386): decoder (`vp8-decode`) and key-frame encoder (`vp8-encode`) | //! | `mpeg2` | [`mpeg2`] | MPEG-2 video (ITU-T H.262) and MPEG-1 video: decoder | //! | `prores` | [`prores`] | Apple ProRes (SMPTE RDD 36): decoder and encoder | //! | `h264` | [`h264`] | ITU-T H.264 / AVC: decoder (`h264-decode`) and encoder (`h264-encode`) | @@ -31,7 +31,7 @@ pub mod apv; #[cfg(feature = "dnx")] pub mod dnx; -#[cfg(feature = "vp8")] +#[cfg(any(feature = "vp8-decode", feature = "vp8-encode"))] pub mod vp8; #[cfg(feature = "mpeg2")] diff --git a/crates/video/src/vp8/encode/README.md b/crates/video/src/vp8/encode/README.md new file mode 100644 index 000000000..5538ddb19 --- /dev/null +++ b/crates/video/src/vp8/encode/README.md @@ -0,0 +1,34 @@ +# VP8 encoder (`vp8-encode`) + +A VP8 key-frame encoder: the lossy picture inside a WebP image or animation frame (a `VP8 ` +chunk). WebP's lossy frames are VP8 key frames, so that is all this encodes; there are no inter +frames. + +* **Prediction**: every macroblock is coded with the best of the four 16×16 luma modes (DC, V, H, + TM, with the second-order Walsh–Hadamard "Y2" block of the subblocks' DC coefficients) or + B_PRED (each 4×4 subblock with the best of its ten modes, predicted from the subblocks before + it), and the best of the four 8×8 chroma modes. +* **Choice**: by rate–distortion cost, squared error plus λ times the bits, the bits counted from + the token and mode probabilities in the contexts a decoder uses. +* **Quantization**: one quality setting (0–100) picks the frame's quantizer index; luma DC, Y2 and + chroma factors follow from it as the format derives them. Macroblocks left with nothing to code + are skipped (`mb_skip_coeff`). +* **Probabilities**: the coefficient token probabilities are updated per frame where the frame's + own token counts save more than the update costs. +* **Loop filter**: the level is chosen from the quantizer (normal filter, sharpness 0). Intra + prediction reads the unfiltered reconstruction, so the filter shapes only the decoded picture. +* **Colour**: RGBA → BT.601 studio-range Y′CbCr 4:2:0, chroma averaged per 2×2 weighted by alpha + so colour under full transparency does not tint visible edges. + +The encoder reconstructs exactly what a decoder reconstructs before loop filtering (prediction +with the frame-edge values of §12, the 16-bit inverse transforms of §14) and returns it with the +frame; the tests require a decoder (ffmpeg, when installed) to reproduce it exactly, and the +`image` crate's WebP decoder to read the frame back. + +Written from **RFC 6386, *VP8 Data Format and Decoding Guide* (November 2011)**, sections 1–19 (the +format description and Annex A's bitstream syntax). The constant tables (§11.5, §13.4, §13.5, +§14.1) are generated from the RFC's text. Its Attachment One, the reference decoder source, was +not used; neither was any other encoder or decoder's source. Where the RFC's text leaves a detail +to that source — the coefficient scan order and the Y2 and chroma dequantization factors — the +encoder uses the format's known values, checked by decoding its output with independent decoders. +The forward transforms are derived from §14's inverse transforms (see `src/transform.rs`). diff --git a/crates/video/src/vp8/encode/bool.rs b/crates/video/src/vp8/encode/bool.rs new file mode 100644 index 000000000..440b4723b --- /dev/null +++ b/crates/video/src/vp8/encode/bool.rs @@ -0,0 +1,233 @@ +//! The boolean entropy encoder (RFC 6386 §7) and the cost of coding a bool. + +use std::sync::OnceLock; + +/// Writes bools at 8-bit probabilities, as §7.3 describes: `bottom` and `range` are the coding +/// interval, `bit_count` the shifts left before a byte of `bottom` is ready. +pub(crate) struct BoolWriter { + out: Vec, + range: u32, + bottom: u32, + bit_count: i32, +} + +impl BoolWriter { + /// An empty writer, its range full (§7.3). + pub(crate) fn new() -> BoolWriter { + BoolWriter { out: Vec::new(), range: 255, bottom: 0, bit_count: 24 } + } + + /// A bool whose probability of being false is `prob / 256`. + pub(crate) fn put(&mut self, prob: u8, bit: bool) { + let split = 1 + (((self.range - 1) * u32::from(prob)) >> 8); + if bit { + self.bottom = self.bottom.wrapping_add(split); + self.range -= split; + } else { + self.range = split; + } + while self.range < 128 { + self.range <<= 1; + if self.bottom & (1 << 31) != 0 { + self.carry(); + } + self.bottom <<= 1; + self.bit_count -= 1; + if self.bit_count == 0 { + self.out.push((self.bottom >> 24) as u8); + self.bottom &= (1 << 24) - 1; + self.bit_count = 8; + } + } + } + + /// An unsigned `bits`-wide literal, high bit first, each bit at probability 1/2. + pub(crate) fn literal(&mut self, bits: u32, value: u32) { + for b in (0..bits).rev() { + self.put(128, (value >> b) & 1 == 1); + } + } + + /// The bytes written, the interval's remainder appended (§7.3, flush). + pub(crate) fn finish(mut self) -> Vec { + let mut c = self.bit_count; + let mut v = self.bottom; + if c < 32 && v & (1 << (32 - c)) != 0 { + self.carry(); + } + v <<= c & 7; + c >>= 3; + while c > 0 { + v <<= 8; + c -= 1; + } + for _ in 0..4 { + self.out.push((v >> 24) as u8); + v <<= 8; + } + self.out + } + + /// Adds one to the bytes already written. The coded value is always less than one, so the + /// carry never runs off the front. + fn carry(&mut self) { + for b in self.out.iter_mut().rev() { + if *b == 255 { + *b = 0; + } else { + *b += 1; + return; + } + } + } +} + +/// Bits a bool costs at `prob`: −log2 of its probability. +pub(crate) fn cost(prob: u8, bit: bool) -> f32 { + static TABLE: OnceLock<[f32; 256]> = OnceLock::new(); + let t = TABLE.get_or_init(|| { + let mut t = [0f32; 256]; + for (p, c) in t.iter_mut().enumerate() { + *c = -((p.max(1) as f32) / 256.0).log2(); + } + t + }); + let p = if bit { 256 - usize::from(prob) } else { usize::from(prob) }; + t[p.min(255)] +} + +/// Writes `leaf` of a tree in the RFC's array form (§8.1): even entries are pairs of branches, a +/// positive entry the index of a deeper pair, a non-positive one the leaf `-entry`. `probs` are +/// indexed by half the pair's index. Returns false when the tree has no such leaf. +pub(crate) fn put_tree(w: &mut BoolWriter, tree: &[i8], probs: &[u8], leaf: u8) -> bool { + let mut path = Vec::with_capacity(12); + if !find(tree, 0, leaf, &mut path, 0) { + return false; + } + for (node, bit) in path { + w.put(probs.get(node / 2).copied().unwrap_or(128), bit); + } + true +} + +/// Bits [`put_tree`] would spend. +pub(crate) fn tree_cost(tree: &[i8], probs: &[u8], leaf: u8) -> f32 { + let mut path = Vec::with_capacity(12); + if !find(tree, 0, leaf, &mut path, 0) { + return 0.0; + } + path.iter().map(|&(node, bit)| cost(probs.get(node / 2).copied().unwrap_or(128), bit)).sum() +} + +/// Find the path from `node` to `leaf` in a token tree, pushing each (node, bit) on the way; false if it isn't there. +fn find(tree: &[i8], node: usize, leaf: u8, path: &mut Vec<(usize, bool)>, depth: usize) -> bool { + if depth > 16 { + return false; + } + for bit in [false, true] { + let Some(&entry) = tree.get(node + usize::from(bit)) else { return false }; + path.push((node, bit)); + let hit = if entry > 0 { find(tree, entry as usize, leaf, path, depth + 1) } else { (-i16::from(entry)) as u8 == leaf }; + if hit { + return true; + } + path.pop(); + } + false +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + /// The decoder of §7.3, to read back what the writer wrote. + pub(crate) struct BoolReader<'a> { + data: &'a [u8], + pos: usize, + value: u32, + range: u32, + bit_count: u32, + } + + impl<'a> BoolReader<'a> { + /// A reader over `data`, primed with its first two bytes. + pub(crate) fn new(data: &'a [u8]) -> Self { + let byte = |i: usize| u32::from(data.get(i).copied().unwrap_or(0)); + BoolReader { data, pos: 2, value: (byte(0) << 8) | byte(1), range: 255, bit_count: 0 } + } + /// Read one bool coded with probability `prob` (of a 0). + pub(crate) fn get(&mut self, prob: u8) -> bool { + let split = 1 + (((self.range - 1) * u32::from(prob)) >> 8); + let big = split << 8; + let bit = if self.value >= big { + self.range -= split; + self.value -= big; + true + } else { + self.range = split; + false + }; + while self.range < 128 { + self.value <<= 1; + self.range <<= 1; + self.bit_count += 1; + if self.bit_count == 8 { + self.bit_count = 0; + self.value |= u32::from(self.data.get(self.pos).copied().unwrap_or(0)); + self.pos += 1; + } + } + bit + } + } + + /// Bools written at assorted probabilities read back unchanged. + #[test] + fn bools_read_back() { + let mut w = BoolWriter::new(); + let mut want = Vec::new(); + let mut x = 12345u32; + for i in 0..20000 { + x = x.wrapping_mul(1103515245).wrapping_add(12345); + let prob = ((x >> 16) % 255 + 1) as u8; + let bit = (x >> 8) % 256 >= u32::from(prob) || i % 97 == 0; + w.put(prob, bit); + want.push((prob, bit)); + } + let data = w.finish(); + let mut r = BoolReader::new(&data); + for (i, (prob, bit)) in want.into_iter().enumerate() { + assert_eq!(r.get(prob), bit, "bool {i}"); + } + } + + /// Each leaf of a tree reads back as the leaf that was written. + #[test] + fn trees_write_their_leaves() { + // kf_ymode_tree (§11.2): B_PRED = 0, DC = 100, V = 101, H = 110, TM = 111. + const TREE: [i8; 8] = [-4, 2, 4, 6, 0, -1, -2, -3]; + for leaf in 0..5u8 { + let mut w = BoolWriter::new(); + assert!(put_tree(&mut w, &TREE, &[145, 156, 163, 128], leaf)); + let data = w.finish(); + let mut r = BoolReader::new(&data); + let read = if !r.get(145) { + 4 + } else if !r.get(156) { + u8::from(r.get(163)) + } else { + 2 + u8::from(r.get(128)) + }; + assert_eq!(read, leaf); + } + assert!(!put_tree(&mut BoolWriter::new(), &TREE, &[128; 4], 9)); + } + + /// A bool's cost is its information in bits: −log₂ of its probability. + #[test] + fn costs_are_information() { + assert!((cost(128, false) - 1.0).abs() < 1e-6); + assert!((cost(192, false) - 0.415).abs() < 1e-3); + assert!((cost(192, true) - 2.0).abs() < 1e-6); + } +} diff --git a/crates/video/src/vp8/encode/frame.rs b/crates/video/src/vp8/encode/frame.rs new file mode 100644 index 000000000..ddf86f467 --- /dev/null +++ b/crates/video/src/vp8/encode/frame.rs @@ -0,0 +1,787 @@ +//! Mode decision, quantization, reconstruction and the bitstream of a key frame (RFC 6386 §9–§14 +//! and Annex A). +//! +//! Three passes over the macroblocks: the first chooses each one's modes and quantized +//! coefficients and reconstructs it as a decoder will (later macroblocks are predicted from that +//! reconstruction); the second counts the coefficient tokens, from which the probability updates +//! are chosen; the third writes the first partition (header and modes) and the token partition. + +use super::bool::{BoolWriter, cost, put_tree, tree_cost}; +use super::predict::{B_DC_PRED, B_HE_PRED, B_PRED, B_TM_PRED, B_VE_PRED, DC_PRED, Edges, H_PRED, TM_PRED, V_PRED, predict_block, predict_sub}; +use super::tables::{ + AC_QLOOKUP, CAT_BASE, COEFF_BANDS, COEFF_UPDATE_PROBS, DC_QLOOKUP, DEFAULT_COEFF_PROBS, KF_BMODE_PROB, KF_UV_MODE_PROB, KF_YMODE_PROB, PCAT, +}; +use super::transform::{fdct, fwht, idct, iwht}; +use super::{Vp8Error, Vp8Frame, Vp8Params, q_index}; + +/// One 8-bit plane. +#[derive(Clone, Debug, PartialEq)] +pub struct Plane { + pub width: usize, + pub height: usize, + pub data: Vec, +} + +impl Plane { + /// A zeroed `width`×`height` plane. + pub(crate) fn new(width: usize, height: usize) -> Plane { + Plane { width, height, data: vec![0; width * height] } + } + /// The sample at (x, y); 0 outside the plane. + pub fn get(&self, x: usize, y: usize) -> u8 { + if x < self.width { self.data.get(y * self.width + x).copied().unwrap_or(0) } else { 0 } + } + /// Set the sample at (x, y); writes outside the plane are ignored. + pub(crate) fn set(&mut self, x: usize, y: usize, v: u8) { + if x < self.width + && let Some(p) = self.data.get_mut(y * self.width + x) + { + *p = v; + } + } +} + +/// The scan order of coefficients: position in the scan → raster index in the 4×4 block. +const ZIGZAG: [usize; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; + +/// The mode trees of §11.2 and §11.4 in the RFC's array form. +const KF_YMODE_TREE: [i8; 8] = [-4, 2, 4, 6, 0, -1, -2, -3]; +const UV_MODE_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3]; +const BMODE_TREE: [i8; 18] = [0, 2, -1, 4, -2, 6, 8, 12, -3, 10, -5, -6, -4, 14, -7, 16, -8, -9]; + +/// Coefficient block types, the outer index of the token probabilities (§13.3). +const TYPE_Y_AFTER_Y2: usize = 0; +const TYPE_Y2: usize = 1; +const TYPE_UV: usize = 2; +const TYPE_Y_WITH_DC: usize = 3; + +type Probs = [[[[u8; 11]; 3]; 8]; 4]; + +/// Lagrange multiplier per squared AC step: the weight of a bit against squared error. +const LAMBDA_PER_STEP2: f64 = 0.02; + +/// Dequantization factors, `[dc, ac]` for luma, Y2 and chroma, from one quantizer index. +#[derive(Clone, Copy)] +struct Quant { + y1: [i32; 2], + y2: [i32; 2], + uv: [i32; 2], +} + +impl Quant { + /// The dequantization factors for quantizer index `q` (0–127, §14.1). + fn new(q: u8) -> Quant { + let q = usize::from(q.min(127)); + let (dc, ac) = (DC_QLOOKUP[q], AC_QLOOKUP[q]); + Quant { y1: [dc, ac], y2: [dc * 2, (ac * 155 / 100).max(8)], uv: [dc.min(132), ac] } + } +} + +/// A coefficient quantized to a level a decoder can hold: the token alphabet stops at 2048 and the +/// dequantized value is a 16-bit integer. +fn quantize(c: f64, dq: i32, dc: bool) -> i16 { + let bias = if dc { 0.45 } else { 0.38 }; + let max = (32767 / dq.max(1)).min(2048); + let level = ((c.abs() / f64::from(dq.max(1))) + bias).floor().min(f64::from(max)) as i16; + if c < 0.0 { -level } else { level } +} + +/// A level times its dequantization factor, as a decoder computes it. +fn dequant(level: i16, dq: i32) -> i16 { + (i32::from(level) * dq) as i16 +} + +/// Whether neighbouring blocks had non-zero coefficients, per plane: the first token's context. +#[derive(Clone, Copy, Default)] +struct Nz { + y: [bool; 4], + u: [bool; 2], + v: [bool; 2], + y2: bool, +} + +/// What is decided for a macroblock. Levels are in raster order within each 4×4 block. +#[derive(Clone)] +struct Mb { + ymode: u8, + bmodes: [u8; 16], + uvmode: u8, + skip: bool, + y2: [i16; 16], + y: [[i16; 16]; 16], + u: [[i16; 16]; 4], + v: [[i16; 16]; 4], +} + +/// The subblock mode a whole-block luma mode stands for in later blocks' contexts (§11.3). +fn implied_bmode(ymode: u8) -> u8 { + match ymode { + V_PRED => B_VE_PRED, + H_PRED => B_HE_PRED, + TM_PRED => B_TM_PRED, + _ => B_DC_PRED, + } +} + +/// Receives the bools of coefficient tokens: to cost them, count them or write them. +trait TokenSink { + /// `node` is the token tree node (for counting), `None` for extra bits and signs. + fn bit(&mut self, ty: usize, band: usize, ctx: usize, node: Option, prob: u8, bit: bool); +} + +struct CostSink(f32); +impl TokenSink for CostSink { + /// Adds the bit's cost. + fn bit(&mut self, _: usize, _: usize, _: usize, _: Option, prob: u8, bit: bool) { + self.0 += cost(prob, bit); + } +} + +/// A subblock mode candidate: cost, mode, levels, reconstruction, squared error, bits. +type BestSub = (f64, u8, [i16; 16], [u8; 16], u64, f32); + +/// How often each token tree node saw a 0 and a 1: by block type, band, context and node. +type TokenCounts = Box<[[[[[u32; 2]; 11]; 3]; 8]; 4]>; + +struct CountSink(TokenCounts); +impl TokenSink for CountSink { + /// Counts the bit against its tree node, for choosing probabilities. + fn bit(&mut self, ty: usize, band: usize, ctx: usize, node: Option, _: u8, bit: bool) { + if let Some(n) = node { + self.0[ty][band][ctx][n][usize::from(bit)] += 1; + } + } +} + +impl TokenSink for BoolWriter { + /// Writes the bit. + fn bit(&mut self, _: usize, _: usize, _: usize, _: Option, prob: u8, bit: bool) { + self.put(prob, bit); + } +} + +/// Codes one token (§13.2): its tree path, extra bits and sign. +fn token(s: &mut S, probs: &Probs, ty: usize, band: usize, ctx: usize, v: i16, skip_eob: bool) { + let p = &probs[ty][band][ctx]; + let node = |s: &mut S, n: usize, bit: bool| s.bit(ty, band, ctx, Some(n), p[n], bit); + if !skip_eob { + node(s, 0, true); + } + let a = u32::from(v.unsigned_abs()); + if a == 0 { + node(s, 1, false); + return; + } + node(s, 1, true); + if a == 1 { + node(s, 2, false); + } else { + node(s, 2, true); + if a <= 4 { + node(s, 3, false); + if a == 2 { + node(s, 4, false); + } else { + node(s, 4, true); + node(s, 5, a == 4); + } + } else { + node(s, 3, true); + let cat = match a { + 5..=6 => 0, + 7..=10 => 1, + 11..=18 => 2, + 19..=34 => 3, + 35..=66 => 4, + _ => 5, + }; + if cat <= 1 { + node(s, 6, false); + node(s, 7, cat == 1); + } else { + node(s, 6, true); + if cat <= 3 { + node(s, 8, false); + node(s, 9, cat == 3); + } else { + node(s, 8, true); + node(s, 10, cat == 5); + } + } + let extra = a - CAT_BASE[cat]; + let bits = PCAT[cat]; + for (k, &pp) in bits.iter().enumerate() { + s.bit(ty, band, ctx, None, pp, (extra >> (bits.len() - 1 - k)) & 1 == 1); + } + } + } + s.bit(ty, band, ctx, None, 128, v < 0); +} + +/// Codes a block's coefficients from scan position `first` with neighbour context `ctx`; +/// returns whether any of them is non-zero. +fn code_block(s: &mut S, probs: &Probs, levels: &[i16; 16], ty: usize, first: usize, ctx: usize) -> bool { + let Some(last) = (first..16).rev().find(|&i| levels[ZIGZAG[i]] != 0) else { + let band = COEFF_BANDS[first]; + s.bit(ty, band, ctx, Some(0), probs[ty][band][ctx][0], false); + return false; + }; + let mut c = ctx; + let mut skip_eob = false; + for i in first..=last { + let v = levels[ZIGZAG[i]]; + token(s, probs, ty, COEFF_BANDS[i], c, v, skip_eob); + let a = v.unsigned_abs(); + c = match a { + 0 => 0, + 1 => 1, + _ => 2, + }; + skip_eob = a == 0; + } + if last < 15 { + let band = COEFF_BANDS[last + 1]; + s.bit(ty, band, c, Some(0), probs[ty][band][c][0], false); + } + true +} + +/// The bits a block of levels costs to code, from its first coefficient. +fn block_cost(probs: &Probs, levels: &[i16; 16], ty: usize, first: usize, ctx: usize) -> f32 { + let mut s = CostSink(0.0); + code_block(&mut s, probs, levels, ty, first, ctx); + s.0 +} + +/// Whether any coefficient from `first` on is non-zero. +fn any_coded(levels: &[i16; 16], first: usize) -> bool { + (first..16).any(|i| levels[ZIGZAG[i]] != 0) +} + +/// Runs every macroblock's coefficients through `sink` with the contexts a decoder keeps. +fn tokens(s: &mut S, probs: &Probs, mbs: &[Mb], mbw: usize) { + let mut above = vec![Nz::default(); mbw]; + let mut left = Nz::default(); + for (i, mb) in mbs.iter().enumerate() { + let x = i % mbw; + if x == 0 { + left = Nz::default(); + } + let Some(up) = above.get_mut(x) else { return }; + let has_y2 = mb.ymode != B_PRED; + if mb.skip { + // Nothing coded: every block is empty, and so is Y2 for a macroblock that has one. + up.y = [false; 4]; + up.u = [false; 2]; + up.v = [false; 2]; + left.y = [false; 4]; + left.u = [false; 2]; + left.v = [false; 2]; + if has_y2 { + up.y2 = false; + left.y2 = false; + } + continue; + } + let (ty, first) = if has_y2 { + let nz = code_block(s, probs, &mb.y2, TYPE_Y2, 0, usize::from(left.y2) + usize::from(up.y2)); + left.y2 = nz; + up.y2 = nz; + (TYPE_Y_AFTER_Y2, 1) + } else { + (TYPE_Y_WITH_DC, 0) + }; + for (b, levels) in mb.y.iter().enumerate() { + let (r, c) = (b / 4, b % 4); + let nz = code_block(s, probs, levels, ty, first, usize::from(left.y[r]) + usize::from(up.y[c])); + left.y[r] = nz; + up.y[c] = nz; + } + for (b, levels) in mb.u.iter().enumerate() { + let (r, c) = (b / 2, b % 2); + let nz = code_block(s, probs, levels, TYPE_UV, 0, usize::from(left.u[r]) + usize::from(up.u[c])); + left.u[r] = nz; + up.u[c] = nz; + } + for (b, levels) in mb.v.iter().enumerate() { + let (r, c) = (b / 2, b % 2); + let nz = code_block(s, probs, levels, TYPE_UV, 0, usize::from(left.v[r]) + usize::from(up.v[c])); + left.v[r] = nz; + up.v[c] = nz; + } + } +} + +/// The probabilities to code the frame's tokens with: each default replaced where the frame's +/// own counts save more than the update costs (§13.4). +fn updated_probs(counts: &CountSink) -> Probs { + let mut probs = DEFAULT_COEFF_PROBS; + for t in 0..4 { + for b in 0..8 { + for c in 0..3 { + for n in 0..11 { + let [n0, n1] = counts.0[t][b][c][n]; + let total = n0 + n1; + if total == 0 { + continue; + } + let old = DEFAULT_COEFF_PROBS[t][b][c][n]; + let new = ((u64::from(n0) * 256 + u64::from(total) / 2) / u64::from(total)).clamp(1, 255) as u8; + let bits = |p: u8| n0 as f32 * cost(p, false) + n1 as f32 * cost(p, true); + let upd = COEFF_UPDATE_PROBS[t][b][c][n]; + let keep = bits(old) + cost(upd, false); + let change = bits(new) + cost(upd, true) + 8.0; + if new != old && change < keep { + probs[t][b][c][n] = new; + } + } + } + } + } + probs +} + +struct Encoder<'a> { + src_y: &'a Plane, + src_u: &'a Plane, + src_v: &'a Plane, + rec_y: Plane, + rec_u: Plane, + rec_v: Plane, + mbw: usize, + q: Quant, + lambda: f64, + probs: Probs, + above_nz: Vec, + left_nz: Nz, + above_bmodes: Vec<[u8; 4]>, + left_bmodes: [u8; 4], +} + +/// A candidate luma coding of a macroblock. +struct Luma { + mode: u8, + bmodes: [u8; 16], + y2: [i16; 16], + y: [[i16; 16]; 16], + recon: [u8; 256], + sse: u64, + bits: f32, +} + +/// A candidate chroma coding. +struct Chroma { + mode: u8, + u: [[i16; 16]; 4], + v: [[i16; 16]; 4], + recon_u: [u8; 64], + recon_v: [u8; 64], + sse: u64, + bits: f32, +} + +/// `v` clamped to a sample. +fn clamp255(v: i32) -> u8 { + v.clamp(0, 255) as u8 +} + +impl Encoder<'_> { + /// The rate-distortion cost: the squared error plus λ times the bits. + fn cost_of(&self, sse: u64, bits: f32) -> f64 { + sse as f64 + self.lambda * f64::from(bits) + } + + /// The 16×16 luma edges, and the four pixels above and to the right (§12.3). + fn luma_edges(&self, mbx: usize, mby: usize) -> (Edges<16>, [u8; 4]) { + let (x0, y0) = (mbx * 16, mby * 16); + let rec = &self.rec_y; + let above: [u8; 16] = std::array::from_fn(|i| if mby == 0 { 127 } else { rec.get(x0 + i, y0 - 1) }); + let left: [u8; 16] = std::array::from_fn(|i| if mbx == 0 { 129 } else { rec.get(x0 - 1, y0 + i) }); + let corner = if mby == 0 { + 127 + } else if mbx == 0 { + 129 + } else { + rec.get(x0 - 1, y0 - 1) + }; + let right: [u8; 4] = std::array::from_fn(|i| { + if mby == 0 { + 127 + } else if mbx + 1 == self.mbw { + rec.get(x0 + 15, y0 - 1) + } else { + rec.get(x0 + 16 + i, y0 - 1) + } + }); + (Edges { above, left, corner, have_above: mby > 0, have_left: mbx > 0 }, right) + } + + /// The reconstructed edges an 8×8 chroma block predicts from, with the spec's 127 / 129 outside the frame. + fn chroma_edges(rec: &Plane, mbx: usize, mby: usize) -> Edges<8> { + let (x0, y0) = (mbx * 8, mby * 8); + let above: [u8; 8] = std::array::from_fn(|i| if mby == 0 { 127 } else { rec.get(x0 + i, y0 - 1) }); + let left: [u8; 8] = std::array::from_fn(|i| if mbx == 0 { 129 } else { rec.get(x0 - 1, y0 + i) }); + let corner = if mby == 0 { + 127 + } else if mbx == 0 { + 129 + } else { + rec.get(x0 - 1, y0 - 1) + }; + Edges { above, left, corner, have_above: mby > 0, have_left: mbx > 0 } + } + + /// A 4×4 block of a plane minus a prediction. + fn residual(src: &Plane, x0: usize, y0: usize, pred: &[u8], stride: usize) -> [i32; 16] { + std::array::from_fn(|i| { + let (r, c) = (i / 4, i % 4); + i32::from(src.get(x0 + c, y0 + r)) - i32::from(pred[r * stride + c]) + }) + } + + /// Code the macroblock's luma with one 16×16 prediction `mode`: its residual through Y2 and its cost. + fn whole_luma(&self, mbx: usize, mby: usize, mode: u8, e: &Edges<16>) -> Luma { + let (x0, y0) = (mbx * 16, mby * 16); + let mut pred = [0u8; 256]; + predict_block(mode, e, &mut pred); + let coefs: [[f64; 16]; 16] = std::array::from_fn(|b| { + let (r, c) = (b / 4, b % 4); + fdct(&Self::residual(self.src_y, x0 + c * 4, y0 + r * 4, &pred[r * 64 + c * 4..], 16)) + }); + let dc: [f64; 16] = std::array::from_fn(|b| coefs[b][0]); + let y2c = fwht(&dc); + let y2: [i16; 16] = std::array::from_fn(|i| quantize(y2c[i], self.q.y2[usize::from(i > 0)], i == 0)); + let y: [[i16; 16]; 16] = + std::array::from_fn(|b| std::array::from_fn(|i| if i == 0 { 0 } else { quantize(coefs[b][i], self.q.y1[1], false) })); + let y2dq: [i16; 16] = std::array::from_fn(|i| dequant(y2[i], self.q.y2[usize::from(i > 0)])); + let dcs = iwht(&y2dq); + let mut recon = [0u8; 256]; + let mut sse = 0u64; + let (mut ly, mut ay) = (self.left_nz.y, self.above_nz.get(mbx).copied().unwrap_or_default().y); + let up = self.above_nz.get(mbx).copied().unwrap_or_default(); + let mut bits = tree_cost(&KF_YMODE_TREE, &KF_YMODE_PROB, mode) + + block_cost(&self.probs, &y2, TYPE_Y2, 0, usize::from(self.left_nz.y2) + usize::from(up.y2)); + for b in 0..16 { + let (r, c) = (b / 4, b % 4); + let mut d: [i16; 16] = std::array::from_fn(|i| dequant(y[b][i], self.q.y1[1])); + d[0] = dcs[b]; + let res = idct(&d); + for i in 0..16 { + let (rr, cc) = (r * 4 + i / 4, c * 4 + i % 4); + let v = clamp255(i32::from(pred[rr * 16 + cc]) + i32::from(res[i])); + recon[rr * 16 + cc] = v; + let diff = i64::from(v) - i64::from(self.src_y.get(x0 + cc, y0 + rr)); + sse += (diff * diff) as u64; + } + bits += block_cost(&self.probs, &y[b], TYPE_Y_AFTER_Y2, 1, usize::from(ly[r]) + usize::from(ay[c])); + let nz = any_coded(&y[b], 1); + ly[r] = nz; + ay[c] = nz; + } + Luma { mode, bmodes: [implied_bmode(mode); 16], y2, y, recon, sse, bits } + } + + /// B_PRED: each subblock with its best mode, in order, each predicted from the ones before. + /// Gives up (None) once it costs more than `limit`. + fn sub_luma(&self, mbx: usize, mby: usize, e: &Edges<16>, right: &[u8; 4], limit: f64) -> Option { + let (x0, y0) = (mbx * 16, mby * 16); + let mut wr = [0u8; 256]; + let mut bm = [0u8; 16]; + let mut y = [[0i16; 16]; 16]; + let (mut ly, mut ay) = (self.left_nz.y, self.above_nz.get(mbx).copied().unwrap_or_default().y); + let above_modes = self.above_bmodes.get(mbx).copied().unwrap_or([B_DC_PRED; 4]); + let mut bits = tree_cost(&KF_YMODE_TREE, &KF_YMODE_PROB, B_PRED); + let mut sse = 0u64; + for s in 0..16 { + let (r, c) = (s / 4, s % 4); + let a: [u8; 8] = std::array::from_fn(|i| { + if i < 4 { + if r == 0 { e.above[c * 4 + i] } else { wr[(r * 4 - 1) * 16 + c * 4 + i] } + } else if c == 3 { + right[i - 4] + } else if r == 0 { + e.above[c * 4 + i] + } else { + wr[(r * 4 - 1) * 16 + c * 4 + i] + } + }); + let l: [u8; 4] = std::array::from_fn(|i| if c == 0 { e.left[r * 4 + i] } else { wr[(r * 4 + i) * 16 + c * 4 - 1] }); + let p = match (r, c) { + (0, 0) => e.corner, + (0, _) => e.above[c * 4 - 1], + (_, 0) => e.left[r * 4 - 1], + _ => wr[(r * 4 - 1) * 16 + c * 4 - 1], + }; + let am = if r == 0 { above_modes[c] } else { bm[s - 4] }; + let lm = if c == 0 { self.left_bmodes[r] } else { bm[s - 1] }; + let mode_probs = &KF_BMODE_PROB[usize::from(am)][usize::from(lm)]; + let ctx = usize::from(ly[r]) + usize::from(ay[c]); + let mut best: Option = None; + for m in 0..10u8 { + let pred = predict_sub(m, &a, &l, p); + let coefs = fdct(&Self::residual(self.src_y, x0 + c * 4, y0 + r * 4, &pred, 4)); + let lv: [i16; 16] = std::array::from_fn(|i| quantize(coefs[i], self.q.y1[usize::from(i > 0)], i == 0)); + let d: [i16; 16] = std::array::from_fn(|i| dequant(lv[i], self.q.y1[usize::from(i > 0)])); + let res = idct(&d); + let mut out = [0u8; 16]; + let mut e2 = 0u64; + for i in 0..16 { + out[i] = clamp255(i32::from(pred[i]) + i32::from(res[i])); + let diff = i64::from(out[i]) - i64::from(self.src_y.get(x0 + c * 4 + i % 4, y0 + r * 4 + i / 4)); + e2 += (diff * diff) as u64; + } + let b = tree_cost(&BMODE_TREE, mode_probs, m) + block_cost(&self.probs, &lv, TYPE_Y_WITH_DC, 0, ctx); + let j = self.cost_of(e2, b); + if best.as_ref().is_none_or(|bst| j < bst.0) { + best = Some((j, m, lv, out, e2, b)); + } + } + let (_, m, lv, out, e2, b) = best?; + for i in 0..16 { + wr[(r * 4 + i / 4) * 16 + c * 4 + i % 4] = out[i]; + } + bm[s] = m; + y[s] = lv; + sse += e2; + bits += b; + let nz = any_coded(&lv, 0); + ly[r] = nz; + ay[c] = nz; + if self.cost_of(sse, bits) > limit { + return None; + } + } + Some(Luma { mode: B_PRED, bmodes: bm, y2: [0; 16], y, recon: wr, sse, bits }) + } + + /// Code the macroblock's U and V with prediction `mode`: their levels, reconstruction and cost. + fn chroma(&self, mbx: usize, mby: usize, mode: u8) -> Chroma { + let (x0, y0) = (mbx * 8, mby * 8); + let up = self.above_nz.get(mbx).copied().unwrap_or_default(); + let mut bits = tree_cost(&UV_MODE_TREE, &KF_UV_MODE_PROB, mode); + let mut sse = 0u64; + let mut plane = |src: &Plane, rec: &Plane, lnz: [bool; 2], anz: [bool; 2]| -> ([[i16; 16]; 4], [u8; 64]) { + let e = Self::chroma_edges(rec, mbx, mby); + let mut pred = [0u8; 64]; + predict_block(mode, &e, &mut pred); + let mut levels = [[0i16; 16]; 4]; + let mut recon = [0u8; 64]; + let (mut l, mut a) = (lnz, anz); + for b in 0..4 { + let (r, c) = (b / 2, b % 2); + let coefs = fdct(&Self::residual(src, x0 + c * 4, y0 + r * 4, &pred[r * 32 + c * 4..], 8)); + let lv: [i16; 16] = std::array::from_fn(|i| quantize(coefs[i], self.q.uv[usize::from(i > 0)], i == 0)); + let d: [i16; 16] = std::array::from_fn(|i| dequant(lv[i], self.q.uv[usize::from(i > 0)])); + let res = idct(&d); + for i in 0..16 { + let (rr, cc) = (r * 4 + i / 4, c * 4 + i % 4); + let v = clamp255(i32::from(pred[rr * 8 + cc]) + i32::from(res[i])); + recon[rr * 8 + cc] = v; + let diff = i64::from(v) - i64::from(src.get(x0 + cc, y0 + rr)); + sse += (diff * diff) as u64; + } + bits += block_cost(&self.probs, &lv, TYPE_UV, 0, usize::from(l[r]) + usize::from(a[c])); + let nz = any_coded(&lv, 0); + l[r] = nz; + a[c] = nz; + levels[b] = lv; + } + (levels, recon) + }; + let (u, recon_u) = plane(self.src_u, &self.rec_u, self.left_nz.u, up.u); + let (v, recon_v) = plane(self.src_v, &self.rec_v, self.left_nz.v, up.v); + Chroma { mode, u, v, recon_u, recon_v, sse, bits } + } + + /// Chooses, codes and reconstructs macroblock (mbx, mby). + fn macroblock(&mut self, mbx: usize, mby: usize) -> Mb { + let (e, right) = self.luma_edges(mbx, mby); + let mut luma: Option = None; + for mode in [DC_PRED, V_PRED, H_PRED, TM_PRED] { + let cand = self.whole_luma(mbx, mby, mode, &e); + if luma.as_ref().is_none_or(|l| self.cost_of(cand.sse, cand.bits) < self.cost_of(l.sse, l.bits)) { + luma = Some(cand); + } + } + let limit = luma.as_ref().map_or(f64::INFINITY, |l| self.cost_of(l.sse, l.bits)); + if let Some(sub) = self.sub_luma(mbx, mby, &e, &right, limit) { + luma = Some(sub); + } + let mut chroma: Option = None; + for mode in [DC_PRED, V_PRED, H_PRED, TM_PRED] { + let cand = self.chroma(mbx, mby, mode); + if chroma.as_ref().is_none_or(|c| self.cost_of(cand.sse, cand.bits) < self.cost_of(c.sse, c.bits)) { + chroma = Some(cand); + } + } + let luma = luma.unwrap_or_else(|| self.whole_luma(mbx, mby, DC_PRED, &e)); + let chroma = chroma.unwrap_or_else(|| self.chroma(mbx, mby, DC_PRED)); + + // The reconstruction later macroblocks are predicted from. + for i in 0..256 { + self.rec_y.set(mbx * 16 + i % 16, mby * 16 + i / 16, luma.recon[i]); + } + for i in 0..64 { + self.rec_u.set(mbx * 8 + i % 8, mby * 8 + i / 8, chroma.recon_u[i]); + self.rec_v.set(mbx * 8 + i % 8, mby * 8 + i / 8, chroma.recon_v[i]); + } + + // Contexts, as the token pass will keep them. + let has_y2 = luma.mode != B_PRED; + let first = usize::from(has_y2); + let up = self.above_nz.get(mbx).copied().unwrap_or_default(); + let mut nz = Nz { y2: if has_y2 { any_coded(&luma.y2, 0) } else { up.y2 }, ..Nz::default() }; + let mut left = self.left_nz; + if has_y2 { + left.y2 = nz.y2; + } + for b in 0..16 { + let coded = any_coded(&luma.y[b], first); + left.y[b / 4] = coded; + nz.y[b % 4] = coded; + } + for b in 0..4 { + let cu = any_coded(&chroma.u[b], 0); + let cv = any_coded(&chroma.v[b], 0); + left.u[b / 2] = cu; + nz.u[b % 2] = cu; + left.v[b / 2] = cv; + nz.v[b % 2] = cv; + } + if !has_y2 { + left.y2 = self.left_nz.y2; + } + self.left_nz = left; + if let Some(a) = self.above_nz.get_mut(mbx) { + *a = nz; + } + if let Some(a) = self.above_bmodes.get_mut(mbx) { + *a = [luma.bmodes[12], luma.bmodes[13], luma.bmodes[14], luma.bmodes[15]]; + } + self.left_bmodes = [luma.bmodes[3], luma.bmodes[7], luma.bmodes[11], luma.bmodes[15]]; + + let skip = (!has_y2 || !any_coded(&luma.y2, 0)) + && luma.y.iter().all(|b| !any_coded(b, first)) + && chroma.u.iter().chain(chroma.v.iter()).all(|b| !any_coded(b, 0)); + Mb { ymode: luma.mode, bmodes: luma.bmodes, uvmode: chroma.mode, skip, y2: luma.y2, y: luma.y, u: chroma.u, v: chroma.v } + } +} + +/// Encodes planes already at the coded size (whole macroblocks). +pub(crate) fn encode(y: &Plane, u: &Plane, v: &Plane, width: u32, height: u32, params: &Vp8Params) -> Result { + let (mbw, mbh) = (y.width / 16, y.height / 16); + let q = q_index(params.quality); + let quant = Quant::new(q); + let lf = params.loop_filter.unwrap_or_else(|| ((f64::from(q) * 0.4).round() as u8).min(63)).min(63); + let mut enc = Encoder { + src_y: y, + src_u: u, + src_v: v, + rec_y: Plane::new(y.width, y.height), + rec_u: Plane::new(u.width, u.height), + rec_v: Plane::new(v.width, v.height), + mbw, + q: quant, + lambda: LAMBDA_PER_STEP2 * f64::from(quant.y1[1]).powi(2), + probs: DEFAULT_COEFF_PROBS, + above_nz: vec![Nz::default(); mbw], + left_nz: Nz::default(), + above_bmodes: vec![[B_DC_PRED; 4]; mbw], + left_bmodes: [B_DC_PRED; 4], + }; + let mut mbs = Vec::with_capacity(mbw * mbh); + for mby in 0..mbh { + enc.left_nz = Nz::default(); + enc.left_bmodes = [B_DC_PRED; 4]; + for mbx in 0..mbw { + mbs.push(enc.macroblock(mbx, mby)); + } + } + + // Token counts → probability updates. + let mut counts = CountSink(Box::new([[[[[0; 2]; 11]; 3]; 8]; 4])); + tokens(&mut counts, &DEFAULT_COEFF_PROBS, &mbs, mbw); + let probs = updated_probs(&counts); + + // First partition: the frame header (Annex A §19.2) and the macroblock headers (§19.3). + let skipped = mbs.iter().filter(|m| m.skip).count(); + let use_skip = skipped > 0; + let prob_skip = (((mbs.len() - skipped) * 256 + mbs.len() / 2) / mbs.len().max(1)).clamp(1, 255) as u8; + let mut w = BoolWriter::new(); + w.literal(1, 0); // color_space: BT.601 + w.literal(1, 0); // clamping_type: the decoder clamps + w.literal(1, 0); // segmentation_enabled + w.literal(1, 0); // filter_type: normal + w.literal(6, u32::from(lf)); + w.literal(3, 0); // sharpness_level + w.literal(1, 0); // loop_filter_adj_enable + w.literal(2, 0); // log2_nbr_of_dct_partitions: one + w.literal(7, u32::from(q)); + for _ in 0..5 { + w.literal(1, 0); // no quantizer index deltas + } + w.literal(1, 0); // refresh_entropy_probs + for t in 0..4 { + for b in 0..8 { + for c in 0..3 { + for n in 0..11 { + let new = probs[t][b][c][n]; + let changed = new != DEFAULT_COEFF_PROBS[t][b][c][n]; + w.put(COEFF_UPDATE_PROBS[t][b][c][n], changed); + if changed { + w.literal(8, u32::from(new)); + } + } + } + } + } + w.literal(1, u32::from(use_skip)); + if use_skip { + w.literal(8, u32::from(prob_skip)); + } + let mut above_modes = vec![[B_DC_PRED; 4]; mbw]; + let mut left_modes = [B_DC_PRED; 4]; + for (i, mb) in mbs.iter().enumerate() { + let x = i % mbw; + if x == 0 { + left_modes = [B_DC_PRED; 4]; + } + if use_skip { + w.put(prob_skip, mb.skip); + } + put_tree(&mut w, &KF_YMODE_TREE, &KF_YMODE_PROB, mb.ymode); + let up = above_modes.get(x).copied().unwrap_or([B_DC_PRED; 4]); + if mb.ymode == B_PRED { + for s in 0..16 { + let (r, c) = (s / 4, s % 4); + let am = if r == 0 { up[c] } else { mb.bmodes[s - 4] }; + let lm = if c == 0 { left_modes[r] } else { mb.bmodes[s - 1] }; + put_tree(&mut w, &BMODE_TREE, &KF_BMODE_PROB[usize::from(am)][usize::from(lm)], mb.bmodes[s]); + } + } + put_tree(&mut w, &UV_MODE_TREE, &KF_UV_MODE_PROB, mb.uvmode); + if let Some(a) = above_modes.get_mut(x) { + *a = [mb.bmodes[12], mb.bmodes[13], mb.bmodes[14], mb.bmodes[15]]; + } + left_modes = [mb.bmodes[3], mb.bmodes[7], mb.bmodes[11], mb.bmodes[15]]; + } + let first = w.finish(); + if first.len() >= 1 << 19 { + return Err(Vp8Error::PartitionTooLarge(first.len())); + } + + let mut t = BoolWriter::new(); + tokens(&mut t, &probs, &mbs, mbw); + let second = t.finish(); + + // Frame tag (key frame, version 0, shown) and the key frame start code and size (§9.1). + let tag = (1u32 << 4) | ((first.len() as u32) << 5); + let mut data = Vec::with_capacity(10 + first.len() + second.len()); + data.extend_from_slice(&tag.to_le_bytes()[..3]); + data.extend_from_slice(&[0x9d, 0x01, 0x2a]); + data.extend_from_slice(&((width & 0x3fff) as u16).to_le_bytes()); + data.extend_from_slice(&((height & 0x3fff) as u16).to_le_bytes()); + data.extend_from_slice(&first); + data.extend_from_slice(&second); + Ok(Vp8Frame { data, y: enc.rec_y, u: enc.rec_u, v: enc.rec_v, q_index: q, loop_filter: lf }) +} diff --git a/crates/video/src/vp8/encode/mod.rs b/crates/video/src/vp8/encode/mod.rs new file mode 100644 index 000000000..12487b9dd --- /dev/null +++ b/crates/video/src/vp8/encode/mod.rs @@ -0,0 +1,106 @@ +//! A VP8 key-frame encoder: the lossy picture of a WebP image or animation frame, written from +//! RFC 6386 (*VP8 Data Format and Decoding Guide*, November 2011) by mirroring its decoding +//! process (see README.md). +//! +//! * Every macroblock is predicted with the best of the four 16×16 luma modes (with the Y2 +//! Walsh–Hadamard block of DCs) or B_PRED (each 4×4 subblock with the best of its ten modes), +//! and the best of the four 8×8 chroma modes, chosen by rate–distortion cost: squared error +//! plus λ times the bits, the bits counted from the token and mode probabilities. +//! * Coefficients are quantized from one quality setting (0–100 → the frame's quantizer index); +//! macroblocks left with nothing to code are skipped. +//! * The coefficient probabilities are updated per frame where the frame's own token counts pay +//! for the update (§13.4). +//! * The loop filter level is chosen from the quantizer (it shapes only the decoded picture: intra +//! prediction reads the unfiltered reconstruction, §12). +//! +//! The encoder reconstructs exactly what a decoder reconstructs before loop filtering — the +//! prediction, the 16-bit inverse transforms of §14 — and returns it with the frame. + +mod bool; +mod frame; +mod predict; +mod tables; +mod transform; +mod yuv; + +pub use frame::Plane; + +/// Largest width or height: the key frame header stores 14 bits. +pub const MAX_SIDE: u32 = (1 << 14) - 1; + +/// What to encode with. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Vp8Params { + /// 0 (smallest) to 100 (best). + pub quality: u8, + /// The loop filter level (0–63, 0 = off); `None` chooses one from the quantizer. + pub loop_filter: Option, +} + +impl Default for Vp8Params { + /// Quality 90, the loop filter chosen from the quantizer. + fn default() -> Self { + Vp8Params { quality: 90, loop_filter: None } + } +} + +/// An encoded key frame. +#[derive(Clone, Debug)] +pub struct Vp8Frame { + /// The VP8 frame: frame tag, key frame header, first partition, token partition — what a + /// WebP `VP8 ` chunk holds. + pub data: Vec, + /// What a decoder reconstructs before loop filtering, at the coded (macroblock-padded) size. + pub y: Plane, + pub u: Plane, + pub v: Plane, + /// The quantizer index used (0–127). + pub q_index: u8, + /// The loop filter level signalled. + pub loop_filter: u8, +} + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum Vp8Error { + #[error("VP8 frames are 1 to {MAX_SIDE} pixels a side (this one is {0}×{1})")] + Size(u32, u32), + #[error("{0} bytes of RGBA for a {1}×{2} frame")] + Buffer(usize, u32, u32), + #[error("the first partition is {0} bytes, over the 19-bit limit")] + PartitionTooLarge(usize), +} + +/// Encodes straight (unpremultiplied) 8-bit RGBA as one VP8 key frame. Alpha is not part of a +/// VP8 frame (a WebP file carries it beside the frame); it weights the colour of the chroma +/// samples, so fully transparent pixels do not bleed into the edges of what is visible. +pub fn encode_rgba(rgba: &[u8], width: u32, height: u32, params: &Vp8Params) -> Result { + if width == 0 || height == 0 || width > MAX_SIDE || height > MAX_SIDE { + return Err(Vp8Error::Size(width, height)); + } + let n = (width as usize).checked_mul(height as usize).and_then(|n| n.checked_mul(4)); + if n != Some(rgba.len()) { + return Err(Vp8Error::Buffer(rgba.len(), width, height)); + } + let (y, u, v) = yuv::rgba_to_yuv420(rgba, width, height); + frame::encode(&y, &u, &v, width, height, params) +} + +/// Quality → quantizer index, interpolated between these points. Fitted on rendered motion +/// graphics frames so that a quality gives about the PSNR (within half a decibel) and size the same +/// quality gives in the widely used libwebp encoder; steep at the top, where each step of quality +/// costs the most bytes. +const QUALITY_TO_Q: [(u8, u8); 13] = + [(0, 105), (10, 76), (20, 66), (30, 58), (40, 47), (50, 42), (60, 36), (70, 31), (80, 22), (85, 15), (90, 7), (95, 2), (100, 0)]; + +/// The quantizer index for a quality: 100 is index 0 (the finest steps). +pub fn q_index(quality: u8) -> u8 { + let q = quality.min(100); + for pair in QUALITY_TO_Q.windows(2) { + let [(q0, i0), (q1, i1)] = [pair[0], pair[1]]; + if q <= q1 { + let t = f64::from(q - q0) / f64::from(q1 - q0); + return (f64::from(i0) + t * (f64::from(i1) - f64::from(i0))).round() as u8; + } + } + 0 +} diff --git a/crates/video/src/vp8/encode/predict.rs b/crates/video/src/vp8/encode/predict.rs new file mode 100644 index 000000000..ef1e28127 --- /dev/null +++ b/crates/video/src/vp8/encode/predict.rs @@ -0,0 +1,278 @@ +//! Intra prediction, RFC 6386 §12: the whole-block modes for 16×16 luma and 8×8 chroma, and the +//! ten 4×4 subblock modes. + +/// Whole-block modes, numbered as `intra_mbmode` (§11.2). +pub(crate) const DC_PRED: u8 = 0; +pub(crate) const V_PRED: u8 = 1; +pub(crate) const H_PRED: u8 = 2; +pub(crate) const TM_PRED: u8 = 3; +pub(crate) const B_PRED: u8 = 4; + +/// Subblock modes, numbered as `intra_bmode` (§11.2). +pub(crate) const B_DC_PRED: u8 = 0; +pub(crate) const B_TM_PRED: u8 = 1; +pub(crate) const B_VE_PRED: u8 = 2; +pub(crate) const B_HE_PRED: u8 = 3; +pub(crate) const B_LD_PRED: u8 = 4; +pub(crate) const B_RD_PRED: u8 = 5; +pub(crate) const B_VR_PRED: u8 = 6; +pub(crate) const B_VL_PRED: u8 = 7; +pub(crate) const B_HD_PRED: u8 = 8; +pub(crate) const B_HU_PRED: u8 = 9; + +/// The pixels a block is predicted from, with the frame-edge values of §12 already in place: +/// `above` (and, for 16×16 luma, the four above-right pixels after it), `left`, the corner above +/// and to the left, and whether the real neighbours exist (for DC prediction's averages). +pub(crate) struct Edges { + pub above: [u8; N], + pub left: [u8; N], + pub corner: u8, + pub have_above: bool, + pub have_left: bool, +} + +/// Predicts an N×N whole block (N = 16 luma, 8 chroma) in `mode` (DC, V, H or TM). +pub(crate) fn predict_block(mode: u8, e: &Edges, out: &mut [u8]) { + match mode { + V_PRED => { + for r in 0..N { + out[r * N..r * N + N].copy_from_slice(&e.above); + } + } + H_PRED => { + for r in 0..N { + out[r * N..r * N + N].fill(e.left[r]); + } + } + TM_PRED => { + for r in 0..N { + for c in 0..N { + out[r * N + c] = clamp(i32::from(e.left[r]) + i32::from(e.above[c]) - i32::from(e.corner)); + } + } + } + _ => { + let shift = N.trailing_zeros(); + let sum_a: u32 = e.above.iter().map(|&v| u32::from(v)).sum(); + let sum_l: u32 = e.left.iter().map(|&v| u32::from(v)).sum(); + let dc = match (e.have_above, e.have_left) { + (true, true) => (sum_a + sum_l + (1 << shift)) >> (shift + 1), + (true, false) => (sum_a + (1 << (shift - 1))) >> shift, + (false, true) => (sum_l + (1 << (shift - 1))) >> shift, + (false, false) => 128, + }; + out[..N * N].fill(dc as u8); + } + } +} + +/// `v` clamped to a sample. +fn clamp(v: i32) -> u8 { + v.clamp(0, 255) as u8 +} + +/// The weighted average of three neighbours, `(x + 2y + z + 2) / 4`. +fn avg3(x: u8, y: u8, z: u8) -> u8 { + ((u32::from(x) + 2 * u32::from(y) + u32::from(z) + 2) >> 2) as u8 +} + +/// The average of two neighbours, rounded up. +fn avg2(x: u8, y: u8) -> u8 { + ((u32::from(x) + u32::from(y) + 1) >> 1) as u8 +} + +/// Predicts a 4×4 subblock in `mode` from `a` (the 8 pixels above: 4 over the block, then 4 above +/// and to its right), `l` (the 4 to its left) and `p` (above-left), as §12.3's +/// `subblock_intra_predict`. +pub(crate) fn predict_sub(mode: u8, a: &[u8; 8], l: &[u8; 4], p: u8) -> [u8; 16] { + let mut b = [0u8; 16]; + let mut set = |r: usize, c: usize, v: u8| b[r * 4 + c] = v; + // E: left column bottom to top, the corner, then the row above. + let e = [l[3], l[2], l[1], l[0], p, a[0], a[1], a[2], a[3]]; + // avg3p(A + j) for j = -1..=6 (A[-1] is the corner). + let at = |j: i32| if j < 0 { p } else { a[j as usize] }; + let a3 = |j: i32| avg3(at(j - 1), at(j), at(j + 1)); + let e3 = |j: usize| avg3(e[j - 1], e[j], e[j + 1]); + let e2 = |j: usize| avg2(e[j], e[j + 1]); + match mode { + B_TM_PRED => { + for r in 0..4 { + for c in 0..4 { + set(r, c, clamp(i32::from(l[r]) + i32::from(a[c]) - i32::from(p))); + } + } + } + B_VE_PRED => { + for c in 0..4 { + let v = a3(c as i32); + for r in 0..4 { + set(r, c, v); + } + } + } + B_HE_PRED => { + let rows = [avg3(p, l[0], l[1]), avg3(l[0], l[1], l[2]), avg3(l[1], l[2], l[3]), avg3(l[2], l[3], l[3])]; + for (r, v) in rows.into_iter().enumerate() { + for c in 0..4 { + set(r, c, v); + } + } + } + B_LD_PRED => { + for r in 0..4 { + for c in 0..4 { + let d = r + c; + set(r, c, if d == 6 { avg3(a[6], a[7], a[7]) } else { a3(d as i32 + 1) }); + } + } + } + B_RD_PRED => { + for r in 0..4 { + for c in 0..4 { + // (3,0) is E+1 ... (0,3) is E+7. + set(r, c, e3(4 + c - r)); + } + } + } + B_VR_PRED => { + set(3, 0, e3(2)); + set(2, 0, e3(3)); + let v = e3(4); + set(3, 1, v); + set(1, 0, v); + let v = e2(4); + set(2, 1, v); + set(0, 0, v); + let v = e3(5); + set(3, 2, v); + set(1, 1, v); + let v = e2(5); + set(2, 2, v); + set(0, 1, v); + let v = e3(6); + set(3, 3, v); + set(1, 2, v); + let v = e2(6); + set(2, 3, v); + set(0, 2, v); + set(1, 3, e3(7)); + set(0, 3, e2(7)); + } + B_VL_PRED => { + let a2 = |j: usize| avg2(a[j], a[j + 1]); + set(0, 0, a2(0)); + set(1, 0, a3(1)); + let v = a2(1); + set(2, 0, v); + set(0, 1, v); + let v = a3(2); + set(1, 1, v); + set(3, 0, v); + let v = a2(2); + set(2, 1, v); + set(0, 2, v); + let v = a3(3); + set(3, 1, v); + set(1, 2, v); + let v = a2(3); + set(2, 2, v); + set(0, 3, v); + let v = a3(4); + set(3, 2, v); + set(1, 3, v); + set(2, 3, a3(5)); + set(3, 3, a3(6)); + } + B_HD_PRED => { + set(3, 0, e2(0)); + set(3, 1, e3(1)); + let v = e2(1); + set(2, 0, v); + set(3, 2, v); + let v = e3(2); + set(2, 1, v); + set(3, 3, v); + let v = e2(2); + set(2, 2, v); + set(1, 0, v); + let v = e3(3); + set(2, 3, v); + set(1, 1, v); + let v = e2(3); + set(1, 2, v); + set(0, 0, v); + let v = e3(4); + set(1, 3, v); + set(0, 1, v); + set(0, 2, e3(5)); + set(0, 3, e3(6)); + } + B_HU_PRED => { + set(0, 0, avg2(l[0], l[1])); + set(0, 1, avg3(l[0], l[1], l[2])); + let v = avg2(l[1], l[2]); + set(0, 2, v); + set(1, 0, v); + let v = avg3(l[1], l[2], l[3]); + set(0, 3, v); + set(1, 1, v); + let v = avg2(l[2], l[3]); + set(1, 2, v); + set(2, 0, v); + let v = avg3(l[2], l[3], l[3]); + set(1, 3, v); + set(2, 1, v); + for (r, c) in [(2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)] { + set(r, c, l[3]); + } + } + _ => { + // B_DC_PRED + let sum: u32 = a[..4].iter().chain(l.iter()).map(|&v| u32::from(v)).sum(); + b.fill(((sum + 4) >> 3) as u8); + } + } + b +} + +#[cfg(test)] +mod tests { + use super::*; + + /// V, TM and DC predictions of a whole block, and DC with an edge missing. + #[test] + fn whole_block_modes() { + let e = Edges::<8> { above: [10, 20, 30, 40, 50, 60, 70, 80], left: [5; 8], corner: 10, have_above: true, have_left: true }; + let mut out = [0u8; 64]; + predict_block(V_PRED, &e, &mut out); + assert_eq!(&out[56..64], &e.above); + predict_block(TM_PRED, &e, &mut out); + assert_eq!(out[0], 5, "5 + 10 - 10"); + assert_eq!(out[7], 75); + predict_block(DC_PRED, &e, &mut out); + assert_eq!(out[0], ((360 + 40 + 8) >> 4) as u8); + let top = Edges::<8> { have_above: false, ..e }; + predict_block(DC_PRED, &top, &mut out); + assert_eq!(out[0], 5, "left only"); + } + + /// Each subblock mode follows the direction of its lines. + #[test] + fn subblock_modes_follow_their_lines() { + let a = [10, 20, 30, 40, 50, 60, 70, 80]; + let l = [15, 25, 35, 45]; + let p = 5; + let ld = predict_sub(B_LD_PRED, &a, &l, p); + // Each antidiagonal is one value. + assert_eq!(ld[1], ld[4]); + assert_eq!(ld[3], ld[12]); + assert_eq!(ld[15], avg3(70, 80, 80)); + let rd = predict_sub(B_RD_PRED, &a, &l, p); + assert_eq!(rd[0], rd[5]); + assert_eq!(rd[0], avg3(15, 5, 10)); + let hu = predict_sub(B_HU_PRED, &a, &l, p); + assert_eq!(hu[15], 45); + let dc = predict_sub(B_DC_PRED, &a, &l, p); + assert_eq!(dc[0], ((100 + 120 + 4) >> 3) as u8); + } +} diff --git a/crates/video/src/vp8/encode/tables.rs b/crates/video/src/vp8/encode/tables.rs new file mode 100644 index 000000000..4003faa12 --- /dev/null +++ b/crates/video/src/vp8/encode/tables.rs @@ -0,0 +1,510 @@ +//! Constant tables of the VP8 key-frame syntax, as RFC 6386 (November 2011) gives them. +//! +//! The multi-dimensional tables are generated from the RFC's own text (sections 11.5, 13.4, +//! 13.5 and 14.1) with its comments taken out, not typed. + +/// Probabilities of the key-frame luma mode tree (§11.2). +pub(crate) const KF_YMODE_PROB: [u8; 4] = [145, 156, 163, 128]; + +/// Probabilities of the key-frame chroma mode tree (§11.4). +pub(crate) const KF_UV_MODE_PROB: [u8; 3] = [142, 114, 183]; + +/// Subblock mode probabilities by the modes above and to the left (§11.5). +pub(crate) const KF_BMODE_PROB: [[[u8; 9]; 10]; 10] = [ + [ + [231, 120, 48, 89, 115, 113, 120, 152, 112], + [152, 179, 64, 126, 170, 118, 46, 70, 95], + [175, 69, 143, 80, 85, 82, 72, 155, 103], + [56, 58, 10, 171, 218, 189, 17, 13, 152], + [144, 71, 10, 38, 171, 213, 144, 34, 26], + [114, 26, 17, 163, 44, 195, 21, 10, 173], + [121, 24, 80, 195, 26, 62, 44, 64, 85], + [170, 46, 55, 19, 136, 160, 33, 206, 71], + [63, 20, 8, 114, 114, 208, 12, 9, 226], + [81, 40, 11, 96, 182, 84, 29, 16, 36], + ], + [ + [134, 183, 89, 137, 98, 101, 106, 165, 148], + [72, 187, 100, 130, 157, 111, 32, 75, 80], + [66, 102, 167, 99, 74, 62, 40, 234, 128], + [41, 53, 9, 178, 241, 141, 26, 8, 107], + [104, 79, 12, 27, 217, 255, 87, 17, 7], + [74, 43, 26, 146, 73, 166, 49, 23, 157], + [65, 38, 105, 160, 51, 52, 31, 115, 128], + [87, 68, 71, 44, 114, 51, 15, 186, 23], + [47, 41, 14, 110, 182, 183, 21, 17, 194], + [66, 45, 25, 102, 197, 189, 23, 18, 22], + ], + [ + [88, 88, 147, 150, 42, 46, 45, 196, 205], + [43, 97, 183, 117, 85, 38, 35, 179, 61], + [39, 53, 200, 87, 26, 21, 43, 232, 171], + [56, 34, 51, 104, 114, 102, 29, 93, 77], + [107, 54, 32, 26, 51, 1, 81, 43, 31], + [39, 28, 85, 171, 58, 165, 90, 98, 64], + [34, 22, 116, 206, 23, 34, 43, 166, 73], + [68, 25, 106, 22, 64, 171, 36, 225, 114], + [34, 19, 21, 102, 132, 188, 16, 76, 124], + [62, 18, 78, 95, 85, 57, 50, 48, 51], + ], + [ + [193, 101, 35, 159, 215, 111, 89, 46, 111], + [60, 148, 31, 172, 219, 228, 21, 18, 111], + [112, 113, 77, 85, 179, 255, 38, 120, 114], + [40, 42, 1, 196, 245, 209, 10, 25, 109], + [100, 80, 8, 43, 154, 1, 51, 26, 71], + [88, 43, 29, 140, 166, 213, 37, 43, 154], + [61, 63, 30, 155, 67, 45, 68, 1, 209], + [142, 78, 78, 16, 255, 128, 34, 197, 171], + [41, 40, 5, 102, 211, 183, 4, 1, 221], + [51, 50, 17, 168, 209, 192, 23, 25, 82], + ], + [ + [125, 98, 42, 88, 104, 85, 117, 175, 82], + [95, 84, 53, 89, 128, 100, 113, 101, 45], + [75, 79, 123, 47, 51, 128, 81, 171, 1], + [57, 17, 5, 71, 102, 57, 53, 41, 49], + [115, 21, 2, 10, 102, 255, 166, 23, 6], + [38, 33, 13, 121, 57, 73, 26, 1, 85], + [41, 10, 67, 138, 77, 110, 90, 47, 114], + [101, 29, 16, 10, 85, 128, 101, 196, 26], + [57, 18, 10, 102, 102, 213, 34, 20, 43], + [117, 20, 15, 36, 163, 128, 68, 1, 26], + ], + [ + [138, 31, 36, 171, 27, 166, 38, 44, 229], + [67, 87, 58, 169, 82, 115, 26, 59, 179], + [63, 59, 90, 180, 59, 166, 93, 73, 154], + [40, 40, 21, 116, 143, 209, 34, 39, 175], + [57, 46, 22, 24, 128, 1, 54, 17, 37], + [47, 15, 16, 183, 34, 223, 49, 45, 183], + [46, 17, 33, 183, 6, 98, 15, 32, 183], + [65, 32, 73, 115, 28, 128, 23, 128, 205], + [40, 3, 9, 115, 51, 192, 18, 6, 223], + [87, 37, 9, 115, 59, 77, 64, 21, 47], + ], + [ + [104, 55, 44, 218, 9, 54, 53, 130, 226], + [64, 90, 70, 205, 40, 41, 23, 26, 57], + [54, 57, 112, 184, 5, 41, 38, 166, 213], + [30, 34, 26, 133, 152, 116, 10, 32, 134], + [75, 32, 12, 51, 192, 255, 160, 43, 51], + [39, 19, 53, 221, 26, 114, 32, 73, 255], + [31, 9, 65, 234, 2, 15, 1, 118, 73], + [88, 31, 35, 67, 102, 85, 55, 186, 85], + [56, 21, 23, 111, 59, 205, 45, 37, 192], + [55, 38, 70, 124, 73, 102, 1, 34, 98], + ], + [ + [102, 61, 71, 37, 34, 53, 31, 243, 192], + [69, 60, 71, 38, 73, 119, 28, 222, 37], + [68, 45, 128, 34, 1, 47, 11, 245, 171], + [62, 17, 19, 70, 146, 85, 55, 62, 70], + [75, 15, 9, 9, 64, 255, 184, 119, 16], + [37, 43, 37, 154, 100, 163, 85, 160, 1], + [63, 9, 92, 136, 28, 64, 32, 201, 85], + [86, 6, 28, 5, 64, 255, 25, 248, 1], + [56, 8, 17, 132, 137, 255, 55, 116, 128], + [58, 15, 20, 82, 135, 57, 26, 121, 40], + ], + [ + [164, 50, 31, 137, 154, 133, 25, 35, 218], + [51, 103, 44, 131, 131, 123, 31, 6, 158], + [86, 40, 64, 135, 148, 224, 45, 183, 128], + [22, 26, 17, 131, 240, 154, 14, 1, 209], + [83, 12, 13, 54, 192, 255, 68, 47, 28], + [45, 16, 21, 91, 64, 222, 7, 1, 197], + [56, 21, 39, 155, 60, 138, 23, 102, 213], + [85, 26, 85, 85, 128, 128, 32, 146, 171], + [18, 11, 7, 63, 144, 171, 4, 4, 246], + [35, 27, 10, 146, 174, 171, 12, 26, 128], + ], + [ + [190, 80, 35, 99, 180, 80, 126, 54, 45], + [85, 126, 47, 87, 176, 51, 41, 20, 32], + [101, 75, 128, 139, 118, 146, 116, 128, 85], + [56, 41, 15, 176, 236, 85, 37, 9, 62], + [146, 36, 19, 30, 171, 255, 97, 27, 20], + [71, 30, 17, 119, 118, 255, 17, 18, 138], + [101, 38, 60, 138, 55, 70, 43, 26, 142], + [138, 45, 61, 62, 219, 1, 81, 188, 64], + [32, 41, 20, 117, 151, 142, 20, 21, 163], + [112, 19, 12, 61, 195, 128, 48, 4, 24], + ], +]; + +/// Probabilities of the flags that say a coefficient probability is updated (§13.4). +pub(crate) const COEFF_UPDATE_PROBS: [[[[u8; 11]; 3]; 8]; 4] = [ + [ + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255], + [249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255], + [234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255], + [250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255], + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + ], + [ + [ + [217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255], + [234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255], + ], + [ + [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255], + [250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + ], + [ + [ + [186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255], + [234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255], + [251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255], + ], + [ + [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + ], + [ + [ + [248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255], + [248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], + [246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], + [252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255], + [248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255], + [253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255], + [252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255], + [250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + [ + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + ], + ], +]; + +/// Coefficient token probabilities at the start of every key frame (§13.5). +pub(crate) const DEFAULT_COEFF_PROBS: [[[[u8; 11]; 3]; 8]; 4] = [ + [ + [ + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + ], + [ + [253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128], + [189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128], + [106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128], + ], + [ + [1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128], + [181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128], + [78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128], + ], + [ + [1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128], + [184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128], + [77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128], + ], + [ + [1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128], + [170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128], + [37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128], + ], + [ + [1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128], + [207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128], + [102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128], + ], + [ + [1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128], + [177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128], + [80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128], + ], + [ + [1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], + [246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], + [255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + ], + ], + [ + [ + [198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62], + [131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1], + [68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128], + ], + [ + [1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128], + [184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128], + [81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128], + ], + [ + [1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128], + [99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128], + [23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128], + ], + [ + [1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128], + [109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128], + [44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128], + ], + [ + [1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128], + [94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128], + [22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128], + ], + [ + [1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128], + [124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128], + [35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128], + ], + [ + [1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128], + [121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128], + [45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128], + ], + [ + [1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128], + [203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128], + [137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128], + ], + ], + [ + [ + [253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128], + [175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128], + [73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128], + ], + [ + [1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128], + [239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128], + [155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128], + ], + [ + [1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128], + [201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128], + [69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128], + ], + [ + [1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128], + [223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128], + [141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128], + ], + [ + [1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128], + [190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128], + [149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], + ], + [ + [1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128], + [247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128], + [240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128], + ], + [ + [1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128], + [213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128], + [55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128], + ], + [ + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], + ], + ], + [ + [ + [202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255], + [126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128], + [61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128], + ], + [ + [1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128], + [166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128], + [39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128], + ], + [ + [1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128], + [124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128], + [24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128], + ], + [ + [1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128], + [149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128], + [28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128], + ], + [ + [1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128], + [123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128], + [20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128], + ], + [ + [1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128], + [168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128], + [47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128], + ], + [ + [1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128], + [141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128], + [42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128], + ], + [ + [1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], + [244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], + [238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], + ], + ], +]; + +/// Coefficient position to probability band (§13.3). +pub(crate) const COEFF_BANDS: [usize; 16] = [0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7]; + +/// Extra-bit probabilities of the six value categories, high bit first (§13.2). +pub(crate) const PCAT: [&[u8]; 6] = [ + &[159], + &[165, 145], + &[173, 148, 140], + &[176, 155, 140, 135], + &[180, 157, 141, 134, 130], + &[254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129], +]; + +/// Smallest value of each category (§13.2). +pub(crate) const CAT_BASE: [u32; 6] = [5, 7, 11, 19, 35, 67]; + +/// DC dequantization factor by quantizer index (§14.1). +pub(crate) const DC_QLOOKUP: [i32; 128] = [ + 4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, 18, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, + 69, 70, 71, 72, 73, 74, 75, 76, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 93, 95, 96, 98, 100, 101, 102, 104, 106, 108, 110, + 112, 114, 116, 118, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 143, 145, 148, 151, 154, 157, +]; + +/// AC dequantization factor by quantizer index (§14.1). +pub(crate) const AC_QLOOKUP: [i32; 128] = [ + 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, + 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 161, 164, 167, 170, 173, + 177, 181, 185, 189, 193, 197, 201, 205, 209, 213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259, 264, 269, 274, 279, 284, +]; diff --git a/crates/video/src/vp8/encode/transform.rs b/crates/video/src/vp8/encode/transform.rs new file mode 100644 index 000000000..079f59802 --- /dev/null +++ b/crates/video/src/vp8/encode/transform.rs @@ -0,0 +1,157 @@ +//! The 4×4 transforms: the decoder's inverse DCT and WHT exactly as RFC 6386 §14.3–14.4 gives +//! them, and forward transforms derived from them. +//! +//! The inverse DCT is, apart from its integer rounding, `R = T·X·Tᵀ / 8` with the 1-D basis +//! `T = [[1, C, 1, S], [1, S, −1, −C], [1, −S, −1, C], [1, −C, 1, −S]]` (C = √2·cos π/8, +//! S = √2·sin π/8). T's columns are orthogonal and of norm 2, so `TᵀT = 4I` and the exact inverse +//! is `X = Tᵀ·R·T / 2`. The inverse WHT is likewise `R = H·Y·Hᵀ / 8` with the 4×4 Hadamard +//! matrix H in the order of §14.3, so `Y = Hᵀ·R·H / 2`. + +const COSPI8SQRT2MINUS1: i32 = 20091; +const SINPI8SQRT2: i32 = 35468; + +/// Inverse DCT of dequantized coefficients (raster order) to residue, §14.4. Intermediate and +/// final values are 16-bit, as the RFC stores them. +pub(crate) fn idct(input: &[i16; 16]) -> [i16; 16] { + let mut tmp = [0i16; 16]; + for i in 0..4 { + let x = |r: usize| i32::from(input[r * 4 + i]); + let a1 = x(0) + x(2); + let b1 = x(0) - x(2); + let c1 = ((x(1) * SINPI8SQRT2) >> 16) - (x(3) + ((x(3) * COSPI8SQRT2MINUS1) >> 16)); + let d1 = (x(1) + ((x(1) * COSPI8SQRT2MINUS1) >> 16)) + ((x(3) * SINPI8SQRT2) >> 16); + tmp[i] = (a1 + d1) as i16; + tmp[12 + i] = (a1 - d1) as i16; + tmp[4 + i] = (b1 + c1) as i16; + tmp[8 + i] = (b1 - c1) as i16; + } + let mut out = [0i16; 16]; + for r in 0..4 { + let x = |c: usize| i32::from(tmp[r * 4 + c]); + let a1 = x(0) + x(2); + let b1 = x(0) - x(2); + let c1 = ((x(1) * SINPI8SQRT2) >> 16) - (x(3) + ((x(3) * COSPI8SQRT2MINUS1) >> 16)); + let d1 = (x(1) + ((x(1) * COSPI8SQRT2MINUS1) >> 16)) + ((x(3) * SINPI8SQRT2) >> 16); + out[r * 4] = ((a1 + d1 + 4) >> 3) as i16; + out[r * 4 + 3] = ((a1 - d1 + 4) >> 3) as i16; + out[r * 4 + 1] = ((b1 + c1 + 4) >> 3) as i16; + out[r * 4 + 2] = ((b1 - c1 + 4) >> 3) as i16; + } + out +} + +/// Inverse WHT of the dequantized Y2 block to the 16 luma DC coefficients, §14.3. +pub(crate) fn iwht(input: &[i16; 16]) -> [i16; 16] { + let mut tmp = [0i16; 16]; + for i in 0..4 { + let x = |r: usize| i32::from(input[r * 4 + i]); + let a1 = x(0) + x(3); + let b1 = x(1) + x(2); + let c1 = x(1) - x(2); + let d1 = x(0) - x(3); + tmp[i] = (a1 + b1) as i16; + tmp[4 + i] = (c1 + d1) as i16; + tmp[8 + i] = (a1 - b1) as i16; + tmp[12 + i] = (d1 - c1) as i16; + } + let mut out = [0i16; 16]; + for r in 0..4 { + let x = |c: usize| i32::from(tmp[r * 4 + c]); + let a1 = x(0) + x(3); + let b1 = x(1) + x(2); + let c1 = x(1) - x(2); + let d1 = x(0) - x(3); + out[r * 4] = ((a1 + b1 + 3) >> 3) as i16; + out[r * 4 + 1] = ((c1 + d1 + 3) >> 3) as i16; + out[r * 4 + 2] = ((a1 - b1 + 3) >> 3) as i16; + out[r * 4 + 3] = ((d1 - c1 + 3) >> 3) as i16; + } + out +} + +/// The forward DCT's basis, scaled to match the spec's inverse. +fn basis_dct() -> [[f64; 4]; 4] { + let c = std::f64::consts::SQRT_2 * (std::f64::consts::PI / 8.0).cos(); + let s = std::f64::consts::SQRT_2 * (std::f64::consts::PI / 8.0).sin(); + [[1.0, c, 1.0, s], [1.0, s, -1.0, -c], [1.0, -s, -1.0, c], [1.0, -c, 1.0, -s]] +} + +const HADAMARD: [[f64; 4]; 4] = [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, -1.0, -1.0], [1.0, -1.0, -1.0, 1.0], [1.0, -1.0, 1.0, -1.0]]; + +/// `Mᵀ·R·M / 2` for a 4×4 `R` in raster order. +fn forward(m: &[[f64; 4]; 4], r: &[f64; 16]) -> [f64; 16] { + let mut tmp = [0f64; 16]; // Mᵀ·R + for i in 0..4 { + for j in 0..4 { + tmp[i * 4 + j] = (0..4).map(|k| m[k][i] * r[k * 4 + j]).sum(); + } + } + let mut out = [0f64; 16]; + for i in 0..4 { + for j in 0..4 { + out[i * 4 + j] = (0..4).map(|k| tmp[i * 4 + k] * m[k][j]).sum::() / 2.0; + } + } + out +} + +/// Forward DCT of a 4×4 residue (raster order), in the scale [`idct`] inverts. +pub(crate) fn fdct(residue: &[i32; 16]) -> [f64; 16] { + let r: [f64; 16] = std::array::from_fn(|i| f64::from(residue[i])); + forward(&basis_dct(), &r) +} + +/// Forward WHT of the 16 luma DC coefficients (raster order of their subblocks), in the scale +/// [`iwht`] inverts. +pub(crate) fn fwht(dc: &[f64; 16]) -> [f64; 16] { + forward(&HADAMARD, dc) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The DCT and its inverse round-trip residuals within a level. + #[test] + fn dct_round_trips() { + let mut x = 7u32; + for _ in 0..500 { + let r: [i32; 16] = std::array::from_fn(|_| { + x = x.wrapping_mul(1664525).wrapping_add(1013904223); + (x >> 16) as i32 % 511 - 255 + }); + let c = fdct(&r); + let q: [i16; 16] = std::array::from_fn(|i| c[i].round() as i16); + let back = idct(&q); + for i in 0..16 { + assert!((i32::from(back[i]) - r[i]).abs() <= 1, "{r:?} -> {back:?}"); + } + } + } + + /// The Walsh–Hadamard transform and its inverse round-trip within a level. + #[test] + fn wht_round_trips() { + let mut x = 99u32; + for _ in 0..500 { + let d: [f64; 16] = std::array::from_fn(|_| { + x = x.wrapping_mul(1664525).wrapping_add(1013904223); + f64::from((x >> 16) as i32 % 4001 - 2000) + }); + let y = fwht(&d); + let q: [i16; 16] = std::array::from_fn(|i| y[i].round() as i16); + let back = iwht(&q); + for i in 0..16 { + assert!((f64::from(back[i]) - d[i]).abs() <= 1.0, "{d:?} -> {back:?}"); + } + } + } + + /// A lone DC coefficient inverts to a flat block. + #[test] + fn a_lone_dc_is_flat() { + let mut c = [0i16; 16]; + c[0] = 80; + assert!(idct(&c).iter().all(|&v| v == (80 + 4) >> 3)); + } +} diff --git a/crates/video/src/vp8/encode/yuv.rs b/crates/video/src/vp8/encode/yuv.rs new file mode 100644 index 000000000..b3ebc79e9 --- /dev/null +++ b/crates/video/src/vp8/encode/yuv.rs @@ -0,0 +1,85 @@ +//! RGBA to Y′CbCr 4:2:0 (Recommendation BT.601, studio range), padded to whole macroblocks. +//! +//! RFC 9649 §2.5 asks for BT.601 between RGB and the VP8 planes. Chroma is the average of each +//! 2×2 group weighted by alpha, so colour hidden under full transparency does not tint the edge of +//! what shows; a group with no alpha at all averages evenly. + +use super::frame::Plane; + +/// BT.601 studio-range luma (16–235) from RGB. +fn luma(r: i32, g: i32, b: i32) -> u8 { + (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16).clamp(0, 255) as u8 +} + +/// BT.601 studio-range Cb and Cr (16–240) from RGB. +fn chroma(r: i32, g: i32, b: i32) -> (u8, u8) { + let u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; + let v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; + (u.clamp(0, 255) as u8, v.clamp(0, 255) as u8) +} + +/// The planes at the coded size: whole macroblocks, the picture's last row and column repeated +/// into the padding. The caller has checked `rgba` holds `width × height` pixels. +pub(crate) fn rgba_to_yuv420(rgba: &[u8], width: u32, height: u32) -> (Plane, Plane, Plane) { + let (w, h) = (width as usize, height as usize); + let (mbw, mbh) = (w.div_ceil(16), h.div_ceil(16)); + let mut y = Plane::new(mbw * 16, mbh * 16); + let mut u = Plane::new(mbw * 8, mbh * 8); + let mut v = Plane::new(mbw * 8, mbh * 8); + let px = |x: usize, yy: usize| -> [i32; 4] { + let i = (yy.min(h - 1) * w + x.min(w - 1)) * 4; + match rgba.get(i..i + 4) { + Some(p) => [i32::from(p[0]), i32::from(p[1]), i32::from(p[2]), i32::from(p[3])], + None => [0; 4], + } + }; + for yy in 0..y.height { + for x in 0..y.width { + let [r, g, b, _] = px(x, yy); + y.set(x, yy, luma(r, g, b)); + } + } + for cy in 0..u.height { + for cx in 0..u.width { + let group = [px(2 * cx, 2 * cy), px(2 * cx + 1, 2 * cy), px(2 * cx, 2 * cy + 1), px(2 * cx + 1, 2 * cy + 1)]; + let weight: i32 = group.iter().map(|p| p[3]).sum(); + let (mut r, mut g, mut b) = (0, 0, 0); + for p in &group { + let k = if weight > 0 { p[3] } else { 1 }; + r += p[0] * k; + g += p[1] * k; + b += p[2] * k; + } + let total = if weight > 0 { weight } else { 4 }; + let (cu, cv) = chroma((r + total / 2) / total, (g + total / 2) / total, (b + total / 2) / total); + u.set(cx, cy, cu); + v.set(cx, cy, cv); + } + } + (y, u, v) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Black and white map to 16 and 235, and grey has neutral chroma. + #[test] + fn studio_range() { + assert_eq!(luma(0, 0, 0), 16); + assert_eq!(luma(255, 255, 255), 235); + assert_eq!(chroma(128, 128, 128), (128, 128)); + } + + /// Padding repeats the edge pixel, and chroma ignores fully transparent pixels. + #[test] + fn padding_repeats_the_edge_and_alpha_weights_chroma() { + // 3×1: red opaque, then two fully transparent black pixels. + let rgba = [255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0]; + let (y, u, v) = rgba_to_yuv420(&rgba, 3, 1); + assert_eq!((y.width, y.height, u.width), (16, 16, 8)); + assert_eq!(y.get(15, 15), y.get(2, 0), "the last pixel repeats"); + // The first chroma sample sees only the red pixel's colour. + assert_eq!((u.get(0, 0), v.get(0, 0)), chroma(255, 0, 0)); + } +} diff --git a/crates/video/src/vp8/mod.rs b/crates/video/src/vp8/mod.rs index b8f0b4190..7a0280d66 100644 --- a/crates/video/src/vp8/mod.rs +++ b/crates/video/src/vp8/mod.rs @@ -1,5 +1,11 @@ -//! Clean-room pure-Rust VP8 decoder, implemented from RFC 6386 ("VP8 Data Format and Decoding -//! Guide"), sections 1-19. +//! Clean-room pure-Rust VP8, implemented from RFC 6386 ("VP8 Data Format and Decoding Guide"). +//! +//! | Feature | Module | What | +//! |---|---|---| +//! | `vp8-decode` | this module | Decoder: sections 1-19, key and inter frames ([`Decoder`]) | +//! | `vp8-encode` | this module | Encoder: key frames, as the lossy picture of a WebP image ([`encode_rgba`]) | +//! +//! Feature `vp8` turns on both. The decoder: //! //! Supported: the whole format — key frames and inter frames, all intra modes (16x16, chroma, //! the ten 4x4 subblock modes), inter prediction from the last, golden and altref frames with @@ -18,20 +24,32 @@ #![allow(clippy::needless_range_loop)] +#[cfg(feature = "vp8-decode")] mod boolcoder; +#[cfg(feature = "vp8-decode")] mod decoder; +#[cfg(feature = "vp8-decode")] mod error; +#[cfg(feature = "vp8-decode")] mod header; +#[cfg(feature = "vp8-decode")] mod loopfilter; +#[cfg(feature = "vp8-decode")] mod predict; +#[cfg(feature = "vp8-decode")] #[rustfmt::skip] mod spec_tables; +#[cfg(feature = "vp8-decode")] mod tables; +#[cfg(feature = "vp8-decode")] mod transform; +#[cfg(feature = "vp8-decode")] pub use decoder::{DecodeStats, Decoder}; +#[cfg(feature = "vp8-decode")] pub use error::{Error, Result}; +#[cfg(feature = "vp8-decode")] /// A decoded picture: 8-bit 4:2:0, planes tight (stride = width). #[derive(Clone, Debug)] pub struct Picture { @@ -49,6 +67,7 @@ pub struct Picture { pub color_space: u8, } +#[cfg(feature = "vp8-decode")] /// Stream parameters read from a key frame's uncompressed chunk (see [`keyframe_info`]). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct KeyframeInfo { @@ -60,6 +79,7 @@ pub struct KeyframeInfo { pub version: u8, } +#[cfg(feature = "vp8-decode")] /// If `frame` is a key frame (a random access point), its parameters. pub fn keyframe_info(frame: &[u8]) -> Option { let tag = header::FrameTag::parse(frame).ok()?; @@ -72,7 +92,13 @@ pub fn keyframe_info(frame: &[u8]) -> Option { }) } +#[cfg(feature = "vp8-decode")] /// Whether `frame` is a key frame (decoding can start there). pub fn is_keyframe(frame: &[u8]) -> bool { keyframe_info(frame).is_some() } + +#[cfg(feature = "vp8-encode")] +mod encode; +#[cfg(feature = "vp8-encode")] +pub use encode::*; diff --git a/crates/video/tests/vp8_encode/main.rs b/crates/video/tests/vp8_encode/main.rs new file mode 100644 index 000000000..eabd99891 --- /dev/null +++ b/crates/video/tests/vp8_encode/main.rs @@ -0,0 +1,124 @@ +//! The VP8 encoder's frames read back by this crate's own decoder: the decode must reproduce the +//! encoder's reconstruction exactly (loop filter off) and read as a picture close to the source. + +use craft_video::vp8::{Decoder, Picture, Vp8Frame, Vp8Params, encode_rgba}; + +/// A test picture with something for every mode: flat areas, gradients, hard edges, diagonal +/// stripes, fine noise, colour. +fn picture(w: u32, h: u32, seed: u32) -> Vec { + let mut x = seed.wrapping_mul(2654435761) | 1; + let mut out = Vec::with_capacity((w * h * 4) as usize); + for py in 0..h { + for px in 0..w { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + let noise = (x % 64) as i32 - 32; + let (fx, fy) = (px as f32 / w as f32, py as f32 / h as f32); + let (r, g, b) = if fx < 0.25 { + (200, 40, 40) // flat + } else if fx < 0.5 { + ((fx * 400.0) as i32, (fy * 255.0) as i32, 128) // gradient + } else if fx < 0.75 { + let stripe = ((px + py) / 3) % 2 == 0; // diagonal stripes + if stripe { (240, 240, 240) } else { (20, 30, 90) } + } else { + (128 + noise * 2, 100 + noise, 160 - noise) // noise + }; + let c = |v: i32| v.clamp(0, 255) as u8; + out.extend_from_slice(&[c(r), c(g), c(b), 255]); + } + } + out +} + +/// The decoder's reading of the frame. +fn decode(frame: &Vp8Frame) -> Picture { + Decoder::new().decode(&frame.data, 0).expect("decode").expect("a key frame is a picture") +} + +/// The encoder's reconstruction cropped to the picture, as the decoder's planes: Y, then U, then V. +fn reconstruction(frame: &Vp8Frame, w: u32, h: u32) -> Vec { + let (w, h) = (w as usize, h as usize); + let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); + let mut out = Vec::with_capacity(w * h + 2 * cw * ch); + for y in 0..h { + for x in 0..w { + out.push(frame.y.get(x, y)); + } + } + for p in [&frame.u, &frame.v] { + for y in 0..ch { + for x in 0..cw { + out.push(p.get(x, y)); + } + } + } + out +} + +/// With the loop filter off, the decoder reconstructs exactly what the encoder did. +#[test] +fn the_decoder_reconstructs_exactly_what_the_encoder_did() { + for (w, h, quality) in [(64, 48, 90), (33, 17, 50), (100, 75, 10), (16, 16, 100), (130, 66, 75), (1, 1, 60)] { + let rgba = picture(w, h, w * 31 + h); + let frame = encode_rgba(&rgba, w, h, &Vp8Params { quality, loop_filter: Some(0) }).expect("encode"); + let name = format!("{w}x{h} q{quality}"); + let pic = decode(&frame); + assert_eq!((pic.width, pic.height), (w, h), "{name}: picture size"); + let got: Vec = [&pic.y, &pic.u, &pic.v].into_iter().flatten().copied().collect(); + let want = reconstruction(&frame, w, h); + assert_eq!(got.len(), want.len(), "{name}: plane sizes"); + if let Some(i) = got.iter().zip(&want).position(|(a, b)| a != b) { + panic!("{name}: the decoder differs from the encoder's reconstruction at byte {i} of {} ({} vs {})", got.len(), got[i], want[i]); + } + } +} + +/// The peak signal-to-noise ratio of `b` against `a`, in dB. +fn psnr(a: &[u8], b: &[u8]) -> f64 { + let se: f64 = a.iter().zip(b).map(|(&x, &y)| (f64::from(x) - f64::from(y)).powi(2)).sum(); + let mse = se / a.len() as f64; + if mse == 0.0 { f64::INFINITY } else { 10.0 * (255.0f64 * 255.0 / mse).log10() } +} + +/// A higher quality decodes closer to the quality-100 picture. +#[test] +fn the_decoder_reads_it_back_close_to_the_source() { + // Against the decode at quality 100, so the comparison is of the coding alone and not of + // the colour 4:2:0 cannot hold (this picture's fine coloured stripes and noise). + let (w, h) = (96, 64); + let rgba = picture(w, h, 7); + let planes = |quality| { + let frame = encode_rgba(&rgba, w, h, &Vp8Params { quality, loop_filter: None }).expect("encode"); + let pic = decode(&frame); + assert_eq!((pic.width, pic.height), (w, h)); + [&pic.y, &pic.u, &pic.v].into_iter().flatten().copied().collect::>() + }; + let best = planes(100); + let mut last = 0.0; + for quality in [20u8, 60, 95] { + let q = psnr(&planes(quality), &best); + assert!(q >= last, "quality {quality} is no closer than a lower quality: {q:.1} dB after {last:.1}"); + last = q; + } + assert!(last > 38.0, "quality 95: {last:.1} dB"); +} + +/// A higher quality gives a larger frame. +#[test] +fn higher_quality_spends_more_bytes() { + let (w, h) = (96, 64); + let rgba = picture(w, h, 11); + let size = |quality| encode_rgba(&rgba, w, h, &Vp8Params { quality, loop_filter: None }).expect("encode").data.len(); + let (low, mid, high) = (size(10), size(60), size(98)); + assert!(low < mid && mid < high, "{low} {mid} {high}"); +} + +/// Empty, short and oversized pictures are errors. +#[test] +fn bad_sizes_are_errors() { + assert!(encode_rgba(&[], 0, 0, &Vp8Params::default()).is_err()); + assert!(encode_rgba(&[0; 12], 2, 2, &Vp8Params::default()).is_err()); + assert!(encode_rgba(&[0; 4], 16384, 1, &Vp8Params::default()).is_err()); +}