Merge main into M10 (text engine + graphics)

# Conflicts:
#	ROADMAP.md
This commit is contained in:
Brandon Thomas 2026-10-01 01:57:41 -04:00
commit 97967d865a
37 changed files with 8962 additions and 14 deletions

10
Cargo.lock generated
View File

@ -1399,6 +1399,7 @@ dependencies = [
"filmcraft-opus",
"filmcraft-prores",
"filmcraft-time",
"filmcraft-vp9",
"image",
"log",
"symphonia",
@ -1709,6 +1710,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "filmcraft-vp9"
version = "0.1.0"
dependencies = [
"filmcraft-bitstream",
"rayon",
"thiserror 2.0.21",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.14"

View File

@ -20,6 +20,7 @@ filmcraft-isobmff = { path = "crates/isobmff" }
filmcraft-matroska = { path = "crates/matroska" }
filmcraft-h264 = { path = "crates/h264" }
filmcraft-hevc = { path = "crates/hevc" }
filmcraft-vp9 = { path = "crates/vp9" }
filmcraft-prores = { path = "crates/prores" }
filmcraft-aac = { path = "crates/aac" }
filmcraft-opus = { path = "crates/opus" }
@ -92,6 +93,9 @@ opt-level = 3
[profile.dev.package.filmcraft-opus]
opt-level = 3
[profile.dev.package.filmcraft-vp9]
opt-level = 3
[profile.release]
lto = "thin"
codegen-units = 1

View File

@ -2,7 +2,7 @@
Progress toward feature parity with Adobe Premiere Pro, with estimates. Updated as milestones land.
**Last updated:** 2026-09-30 (midday) · **Overall parity:** ~32% · **Code:** ~71k lines of Rust, 400+ tests
**Last updated:** 2026-10-01 · **Overall parity:** ~40% · **Code:** ~106k lines of Rust in 30 crates, 724 tests
## Estimate to parity
@ -37,18 +37,21 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining
| M6 | Export | ✅ | Own H.264 encoder (High/Main/Baseline, B-frames, VBR/CBR/2-pass) → MP4 + own AAC; ProRes, MJPEG, PNG, GIF, WAV; background jobs | Preset library, queue UI, smart render | 6–8 |
| M7 | Audio | 🟡 | Mixer basics, peak meters, BS.1770 loudness meters (M/S/I LUFS, true peak; matches ffmpeg), DSP crate, 12 clip audio effects running on it (EQ, filters, dynamics, limiter, delay, reverb, DeNoise, DeHum, pitch) with keyframes | Effects UI polish (EQ curve), remaining effects (multiband, convolution reverb), track effects/mixers with automation, Essential Sound, audio transitions, 5.1 | 6–10 |
| M8 | Colour | 🟡 | Lumetri: basic, creative + looks, RGB & hue curves, wheels, HSL secondary, vignette; basic scopes | LUT import UI, colour match, colour management + HDR (PQ/HLG, log, tone mapping) | 8–12 |
| M9 | More codecs | 🟡 | ProRes decode+encode, AAC decode+encode, HEVC Main/Main 10 decoder (bit-exact on 41 fixtures, ~225 fps 1080p), Matroska/WebM import (H.264/HEVC/ProRes/MJPEG + AAC/Opus/FLAC/MP3/Vorbis/PCM), Opus decoder (SILK/CELT/hybrid, 5.1/7.1 multistream; all RFC 8251 vectors range-exact; WebM/MKV/MP4) | VP9 (in progress), Ogg Opus files, AV1 (rav1d), DNxHR, MXF, hardware decode | 12–20 |
| M9 | More codecs | 🟡 | ProRes decode+encode, AAC decode+encode, HEVC Main/Main 10 decoder (bit-exact on 41 fixtures, ~225 fps 1080p), VP9 decoder (profiles 0–3, 8/10/12-bit, bit-exact on 50+ fixtures; WebM/MKV `V_VP9` and MP4 `vp09` import with key-frame-checked seeking), Matroska/WebM import (H.264/HEVC/VP9/ProRes/MJPEG + AAC/Opus/FLAC/MP3/Vorbis/PCM), Opus decoder (SILK/CELT/hybrid, 5.1/7.1 multistream; all RFC 8251 vectors range-exact; WebM/MKV/MP4) | VP9 frame threading, Ogg Opus files, AV1 (rav1d), DNxHR, MXF, hardware decode | 12–20 |
| M10 | Graphics & captions | 🟡 | Caption tracks (Subtitle/CEA-608/708/Teletext formats, track style), SRT/WebVTT/SCC import+export (frame-exact, property-tested), caption editing (add/split/merge/trim/move, sync-locked insert/extract), Text panel Captions tab, burn-in in Program monitor and export; text engine (`crates/text`: bundled + system fonts, harfrust shaping, bidi, line breaking, paragraph layout, glyph cache; 3-line 1080p title ≈ 0.15 ms warm); graphic clips with text + shape layers (fill, 2 strokes, background, shadow, keyframable transform), Type tool with on-monitor editing, shape/pen tools, Properties/Essential Graphics editor, align/distribute | Responsive design pins, rolls/crawls, per-character styles, motion graphics templates, MCC/STL/TTML, 608/708 embedding, speech-to-text | 12–18 |
| M11 | Interchange & project management | 🟡 | `.fcproj` schema versions + migrations, atomic saves, Save a Copy/Revert, auto-save ring + crash-recovery journal (Preferences ▸ Auto Save, recovery prompt), FCP7 XML, FCPXML, EDL, OTIO | Relink/offline, project manager, proxies | 8 |
| M12–M16 | Web (WASM), platform, long tail | 🟡 | L0–L4 crates compile to wasm32 | Web app shell (file access, WebCodecs, audio), ~850 remaining commands and dialogs, performance hardening | 40–65 |
## Running now
- VP9 decoder (`crates/vp9`), from the public VP9 bitstream spec
- Interchange (`crates/interchange`): CMX 3600 EDL, FCP7 XML, FCPXML, OTIO import and export
- Playback: fix a UI hang after preview renders, then a load-robust playback benchmark and profiling (target: 3 × 1080p streams with 0 dropped frames)
- Text engine (shaping, layout), Type tool, graphics clips and Essential Graphics (M10.1–2)
- Audio Track Mixer: track effects, sends, submixes, automation modes (M7.2)
## Log
- **2026-10-01:** recovered from a machine crash with no lost work. Merged: VP9 decoder (profiles 0–3, bit-exact, WebM/MKV/MP4), Opus decoder (RFC 8251 range-exact; WebM/MKV/MP4), captions (SRT/VTT/SCC, burn-in), render bar + render previews, project schema versioning + atomic saves + auto-save + crash-recovery journal, test infrastructure (golden images, loudness oracle vs ffmpeg, headless scripted UI tests; fixed a GPU stale-texture bug), public contributor docs and licence files.
- **2026-09-30 (evening):** Opus decoder (RFC 6716/8251, range-exact on every conformance vector, ~80–110× realtime 48 kHz stereo) wired into WebM/MKV and MP4 import.
- **2026-09-30 (afternoon):** Matroska/WebM import; LUFS meters; clip audio effects on the DSP crate.
- **2026-09-30 (midday):** HEVC decoder, Matroska/WebM demuxer and audio DSP merged; HEVC import wired; asset rules (AGENTS.md, ATTRIBUTION.md, `cargo xtask assets`); README with hero screenshot and the Craft family.

View File

@ -19,6 +19,7 @@ filmcraft-matroska = { workspace = true }
filmcraft-bitstream = { workspace = true }
filmcraft-h264 = { workspace = true }
filmcraft-hevc = { workspace = true }
filmcraft-vp9 = { workspace = true }
filmcraft-prores = { workspace = true }
filmcraft-aac = { workspace = true }
filmcraft-opus = { workspace = true }

View File

@ -80,10 +80,19 @@ impl GopCache {
st.decoder = Some(s.make_decoder()?);
st.next = usize::MAX;
}
let key = s.sync_before(i);
let mut key = s.sync_before(i);
// Continue the running decoder when the wanted sample is ahead within this GOP run.
let continuing = st.next != usize::MAX && st.next > key && st.next <= i + 16 && st.next <= n;
if !continuing {
// The container's sync flags may be wrong for the codec (an MP4 without `stss` marks
// every sample): step back to a sample the decoder can start from.
while key > 0 {
let data = s.read(key)?;
if st.decoder.as_ref().expect("decoder").is_random_access(&data) != Some(false) {
break;
}
key = s.sync_before(key - 1);
}
if let Some(d) = st.decoder.as_mut() {
d.reset();
}

View File

@ -1,6 +1,6 @@
//! The container + codec hub.
//!
//! - [`VideoDecoder`]: the trait every video codec implements (our own H.264/ProRes/…, MJPEG, and
//! - [`VideoDecoder`]: the trait every video codec implements (our own H.264/HEVC/VP9/ProRes/…, MJPEG, and
//! OS hardware decoders registered by the platform layer). Factories are tried in registration
//! order, so a hardware decoder can take precedence over the pure-Rust one.
//! - [`Mp4Source`]: a [`MediaSource`](filmcraft_media::MediaSource) over MP4/MOV using
@ -46,7 +46,7 @@ impl From<CodecError> for filmcraft_media::MediaError {
fn factories() -> &'static RwLock<Vec<VideoDecoderFactory>> {
static F: std::sync::OnceLock<RwLock<Vec<VideoDecoderFactory>>> = std::sync::OnceLock::new();
F.get_or_init(|| RwLock::new(vec![video::h264_factory, video::hevc_factory, video::prores_factory, video::mjpeg_factory]))
F.get_or_init(|| RwLock::new(vec![video::h264_factory, video::hevc_factory, video::vp9_factory, video::prores_factory, video::mjpeg_factory]))
}
/// Register a video decoder factory (tried before previously registered ones).

View File

@ -1,7 +1,7 @@
//! Matroska/WebM media source (`filmcraft-matroska` demux, GOP-aware video via [`crate::gop`]).
//!
//! Video codecs are mapped onto ISO-BMFF sample entries so the same decoder factories serve both
//! containers (H.264, HEVC, ProRes, MJPEG; VP9/AV1 once their decoders land). Audio: AAC and Opus
//! containers (H.264, HEVC, VP9, ProRes, MJPEG; AV1 once its decoder lands). Audio: AAC and Opus
//! via our decoders, PCM directly, MP3/FLAC/Vorbis via the bootstrap decoders.
//!
//! Opus (`A_OPUS`): `CodecPrivate` is the `OpusHead`; output is always 48 kHz. The demuxer
@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex};
use filmcraft_color::{ColorInfo, Matrix, Primaries, Range, Transfer};
use filmcraft_frame::{AudioBuffer, VideoFrame};
use filmcraft_isobmff::{AvcConfig, FourCc, HevcConfig, PcmConfig, SampleEntry};
use filmcraft_isobmff::{AvcConfig, CodecConfig, FourCc, HevcConfig, PcmConfig, SampleEntry, VpcConfig};
use filmcraft_matroska::{Codec, MkvFile, TrackKind};
use filmcraft_media::{AudioStreamInfo, FrameRequest, MediaError, MediaInfo, MediaKind, MediaSource, SharedSource, VideoStreamInfo};
use filmcraft_time::{FrameRate, Tick};
@ -105,8 +105,42 @@ fn from_tick(t: &filmcraft_matroska::Track, time: Tick) -> i64 {
time.to_rational_floor(n, d)
}
fn sample_entry(c: &Codec, w: u16, h: u16) -> Option<SampleEntry> {
/// VP9 configuration from the Matroska `CodecPrivate` feature list (ID / length / value triples:
/// 1 profile, 2 level, 3 bit depth, 4 chroma subsampling) and the track's `Colour`.
fn vp9_config(private: &[u8], v: Option<&filmcraft_matroska::VideoInfo>) -> VpcConfig {
let mut c = VpcConfig { bit_depth: 8, colour_primaries: 2, transfer_characteristics: 2, matrix_coefficients: 2, ..Default::default() };
let mut p = 0;
while p + 2 <= private.len() {
let (id, len) = (private[p], private[p + 1] as usize);
let Some(val) = private.get(p + 2..p + 2 + len) else { break };
let x = val.first().copied().unwrap_or(0);
match id {
1 => c.profile = x,
2 => c.level = x,
3 => c.bit_depth = x,
4 => c.chroma_subsampling = x,
_ => {}
}
p += 2 + len;
}
if let Some(col) = v.and_then(|v| v.colour.as_ref()) {
if let Some(t) = col.transfer_characteristics {
c.transfer_characteristics = t as u8;
}
if let Some(pr) = col.primaries {
c.colour_primaries = pr as u8;
}
if let Some(m) = col.matrix_coefficients {
c.matrix_coefficients = m as u8;
}
c.full_range = col.full_range();
}
c
}
fn sample_entry(c: &Codec, v: Option<&filmcraft_matroska::VideoInfo>, w: u16, h: u16) -> Option<SampleEntry> {
Some(match c {
Codec::Vp9 { private } => SampleEntry::video(FourCc(*b"vp09"), CodecConfig::Vp9(vp9_config(private, v)), w, h),
Codec::Avc { avcc } => SampleEntry::avc(AvcConfig::parse(avcc).ok()?, w, h),
Codec::Hevc { hvcc } => SampleEntry::hevc(HevcConfig::parse(hvcc).ok()?, w, h),
Codec::ProRes { fourcc } => SampleEntry::prores(FourCc(fourcc.unwrap_or(*b"apcn")), w, h),
@ -190,7 +224,7 @@ impl MkvSource {
if explicit {
explicit_color = Some(color);
}
ventry = sample_entry(&t.codec, w as u16, h as u16);
ventry = sample_entry(&t.codec, t.video.as_ref(), w as u16, h as u16);
let secs = file.duration_ns().unwrap_or(0) as f64 / 1e9;
let bitrate = (secs > 0.0).then(|| (t.samples.iter().map(|s| s.size as u64).sum::<u64>() as f64 * 8.0 / secs) as u64);
VideoStreamInfo {

View File

@ -48,9 +48,17 @@ pub fn sniff(b: &[u8]) -> bool {
fn color_from(entry: &filmcraft_isobmff::SampleEntry, w: u32, h: u32) -> ColorInfo {
let mut c = ColorInfo { matrix: filmcraft_frame::default_matrix(w, h), transfer: Transfer::Bt709, primaries: Primaries::Bt709, range: Range::Limited };
if let Some(v) = &entry.video
&& let Some(col) = &v.color
{
// VP9 carries its colour description in vpcC (used when there is no colr box).
let vpc = match &entry.codec {
CodecConfig::Vp9(v) => Some(filmcraft_isobmff::ColorInfo::Nclx {
primaries: v.colour_primaries as u16,
transfer: v.transfer_characteristics as u16,
matrix: v.matrix_coefficients as u16,
full_range: v.full_range,
}),
_ => None,
};
if let Some(col) = entry.video.as_ref().and_then(|v| v.color.as_ref()).or(vpc.as_ref()) {
let (p, t, m, full) = match col {
filmcraft_isobmff::ColorInfo::Nclx { primaries, transfer, matrix, full_range } => (*primaries, *transfer, *matrix, *full_range),
filmcraft_isobmff::ColorInfo::Nclc { primaries, transfer, matrix } => (*primaries, *transfer, *matrix, false),
@ -369,6 +377,7 @@ fn codec_label(c: &CodecConfig) -> String {
}
),
CodecConfig::Hevc(_) => "HEVC".into(),
CodecConfig::Vp9(c) => format!("VP9 (Profile {})", c.profile),
CodecConfig::ProRes { fourcc } => match &fourcc.0 {
b"apco" => "Apple ProRes 422 Proxy".into(),
b"apcs" => "Apple ProRes 422 LT".into(),
@ -399,6 +408,14 @@ fn pixfmt_label(c: &CodecConfig) -> String {
244 => "YUV 4:4:4".into(),
_ => "YUV 4:2:0 8-bit".into(),
},
CodecConfig::Vp9(c) => {
let sub = match c.chroma_subsampling {
2 => "4:2:2",
3 => "4:4:4",
_ => "4:2:0",
};
format!("YUV {sub} {}-bit", c.bit_depth.max(8))
}
CodecConfig::ProRes { fourcc } if fourcc.0[2] == b'4' => "YUVA 4:4:4 12-bit".into(),
CodecConfig::ProRes { .. } => "YUV 4:2:2 10-bit".into(),
CodecConfig::Jpeg { .. } => "YUV 4:2:x 8-bit".into(),

View File

@ -179,6 +179,169 @@ fn matroska_prores_pcm() {
check_mkv("blue_prores_pcm.mkv", &["-c:v", "prores_ks", "-profile:v", "2"], &["-c:a", "pcm_s16le"]);
}
/// Path of a fixture under `target/fixtures/codecs/`, generated with ffmpeg when missing
/// (`None` when ffmpeg or the encoder is unavailable).
fn fixture_path(name: &str, args: &[&str]) -> Option<PathBuf> {
fixture(name, args)?;
Some(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/codecs").join(name))
}
/// ffmpeg's decode of `src` as raw planar video (the oracle).
fn reference_yuv(src: &std::path::Path, pix_fmt: &str) -> Option<Vec<u8>> {
let name = format!("{}.{pix_fmt}.yuv", src.file_name()?.to_str()?);
let path = src.to_str()?;
let b = fixture(&name, &["-i", path, "-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", pix_fmt])?;
Some(b.to_vec())
}
/// Planes of a decoded YUV frame as little-endian bytes (ffmpeg rawvideo layout).
fn yuv_bytes(f: &filmcraft_frame::VideoFrame) -> Vec<u8> {
match &f.data {
filmcraft_frame::PixelData::Yuv8 { planes, .. } => planes.iter().flat_map(|p| p.iter().copied()).collect(),
filmcraft_frame::PixelData::Yuv16 { planes, .. } => planes.iter().flat_map(|p| p.iter().flat_map(|s| s.to_le_bytes())).collect(),
_ => panic!("not a YUV frame: {}", f.format_label()),
}
}
/// Random access into a VP9 file: frames requested out of order (seeks back and forth across key
/// frames, then sequential playback) are sample-exact against ffmpeg's decode.
fn check_vp9_seeks(name: &str, container: &str, pix_fmt: &str, extra: &[&str], frames: usize) {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/codecs");
let log = dir.join(format!("{name}.passlog")).to_string_lossy().into_owned();
let mut args = vec!["-f", "lavfi", "-i", "testsrc2=s=352x288:r=25,noise=alls=8:allf=t", "-frames:v"];
let n = frames.to_string();
args.push(&n);
args.extend_from_slice(&["-c:v", "libvpx-vp9", "-pix_fmt", pix_fmt, "-g", "25", "-b:v", "600k"]);
args.extend_from_slice(extra);
if extra.contains(&"-auto-alt-ref") {
// libvpx only uses alternate reference frames in two-pass mode: run the first pass.
let Some(ff) = ffmpeg() else { return };
if !dir.join(name).exists() {
let _ = std::fs::create_dir_all(&dir);
let ok = Command::new(ff).args(["-y", "-v", "error"]).args(&args).args(["-pass", "1", "-passlogfile"]).arg(&log).args(["-f", "null", "-"]).status();
if !ok.is_ok_and(|s| s.success()) {
eprintln!("first pass for {name} failed; skipping");
return;
}
}
args.extend_from_slice(&["-pass", "2", "-passlogfile"]);
args.push(&log);
}
let Some(path) = fixture_path(name, &args) else { return };
let Some(reference) = reference_yuv(&path, pix_fmt) else { return };
let bytes: Arc<[u8]> = std::fs::read(&path).unwrap().into();
let src = crate::open_bytes(name, bytes).unwrap();
let info = src.info().clone();
assert_eq!(info.container, container);
let v = info.video.as_ref().unwrap();
assert!(v.codec.contains("VP9"), "{}", v.codec);
assert_eq!((v.width, v.height), (352, 288));
let frame_len = reference.len() / frames;
let rate = info.frame_rate();
let order: Vec<usize> = [frames - 3, 3, 40, 26, 24, frames - 1, 0, 12].into_iter().chain(30..45).filter(|&k| k < frames).collect();
for k in order {
let f = src.video_frame(FrameRequest::full(rate.tick_of(k as i64))).unwrap();
assert_eq!((f.width, f.height), (352, 288));
let got = yuv_bytes(&f);
assert!(got == reference[k * frame_len..(k + 1) * frame_len], "{name}: frame {k} differs from ffmpeg");
}
}
#[test]
fn webm_vp9_seeks_bit_exact() {
check_vp9_seeks("noise_vp9.webm", "WebM", "yuv420p", &["-deadline", "realtime", "-speed", "8"], 60);
}
#[test]
fn webm_vp9_altref_superframes_seek_bit_exact() {
// Good-quality encode with alternate reference frames: hidden frames packed into superframes.
check_vp9_seeks("noise_vp9_altref.webm", "WebM", "yuv420p", &["-speed", "4", "-auto-alt-ref", "1", "-lag-in-frames", "16"], 60);
}
#[test]
fn mp4_vp9_10bit_seeks_bit_exact() {
check_vp9_seeks("noise_vp9_10bit.mp4", "MPEG-4", "yuv420p10le", &["-profile:v", "2", "-deadline", "realtime", "-speed", "8"], 50);
}
#[test]
fn mkv_vp9_444_12bit_seeks_bit_exact() {
check_vp9_seeks("noise_vp9_444_12bit.mkv", "Matroska", "yuv444p12le", &["-profile:v", "3", "-deadline", "realtime", "-speed", "8"], 30);
}
#[test]
fn mkv_vp9_422_seeks_bit_exact() {
check_vp9_seeks("noise_vp9_422.mkv", "Matroska", "yuv422p", &["-profile:v", "1", "-deadline", "realtime", "-speed", "8"], 30);
}
/// Every sample flagged as sync (as in an MP4 without `stss`): the GOP cache must still start
/// decoding at a real VP9 key frame.
#[test]
fn vp9_random_access_ignores_bogus_sync_flags() {
use crate::gop::{GopCache, VideoSamples};
let Some(path) = fixture_path(
"noise_vp9_g20.ivf",
&["-f", "lavfi", "-i", "testsrc2=s=176x144:r=25", "-frames:v", "50", "-c:v", "libvpx-vp9", "-g", "20", "-deadline", "realtime", "-speed", "8"],
) else {
return;
};
let Some(reference) = reference_yuv(&path, "yuv420p") else { return };
let data = std::fs::read(&path).unwrap();
let mut chunks = Vec::new();
let mut p = 32;
while p + 12 <= data.len() {
let sz = u32::from_le_bytes(data[p..p + 4].try_into().unwrap()) as usize;
chunks.push(data[p + 12..p + 12 + sz].to_vec());
p += 12 + sz;
}
struct AllSync(Vec<Vec<u8>>);
impl VideoSamples for AllSync {
fn count(&self) -> usize {
self.0.len()
}
fn pts(&self, i: usize) -> i64 {
i as i64
}
fn sync_before(&self, i: usize) -> usize {
i
}
fn sample_at(&self, t: i64) -> Option<usize> {
(t >= 0 && (t as usize) < self.0.len()).then_some(t as usize)
}
fn read(&self, i: usize) -> crate::Result<Vec<u8>> {
Ok(self.0[i].clone())
}
fn make_decoder(&self) -> crate::Result<Box<dyn crate::VideoDecoder>> {
Ok(Box::new(crate::video::Vp9Decoder::new(None)))
}
}
let s = AllSync(chunks);
let cache = GopCache::new(None);
let frame_len = 176 * 144 * 3 / 2;
for k in [37usize, 5, 49, 21, 20, 19] {
let f = cache.frame(&s, k as i64).unwrap();
assert!(yuv_bytes(&f) == reference[k * frame_len..(k + 1) * frame_len], "frame {k} differs from ffmpeg");
}
}
/// VP9 RGB (profile 1, colour space sRGB; planes G, B, R) decodes to RGBA in the right order.
#[test]
fn mkv_vp9_rgb() {
let Some(b) = fixture(
"orange_vp9_gbrp.mkv",
&["-f", "lavfi", "-i", "color=c=0xe08020:s=128x96:r=25:d=0.4", "-c:v", "libvpx-vp9", "-pix_fmt", "gbrp", "-deadline", "realtime", "-lossless", "1"],
) else {
return;
};
let src = crate::open_bytes("orange_vp9_gbrp.mkv", b).unwrap();
let f = src.video_frame(FrameRequest::full(Tick(TICKS_PER_SECOND / 5))).unwrap();
let px = f.to_rgba8();
let c = &px[(48 * 128 + 64) * 4..][..3];
// (the lavfi colour source is converted to RGB by ffmpeg, which rounds by a level)
for (got, want) in c.iter().zip([0xe0u8, 0x80, 0x20]) {
assert!((*got as i32 - want as i32).abs() <= 2, "{c:?}");
}
}
/// ffmpeg + libopus decode of a container fixture (pre-skip / codec delay / edit list applied) as
/// interleaved f32 at 48 kHz: the reference for our Opus path.
fn opus_reference(name: &str) -> Option<Vec<f32>> {

View File

@ -20,6 +20,12 @@ pub trait VideoDecoder: Send {
/// Forget all state (called after seeking to a sync sample).
fn reset(&mut self);
fn name(&self) -> &str;
/// Whether decoding can start at `sample` (`None`: unknown, trust the container's sync flags).
/// Containers may flag samples as sync that the codec cannot start from (an MP4 without
/// `stss` marks every sample), so codecs that can tell say so.
fn is_random_access(&self, _sample: &[u8]) -> Option<bool> {
None
}
}
/// A factory returns `None` when it does not handle the entry.
@ -212,6 +218,141 @@ pub fn hevc_factory(e: &SampleEntry) -> Option<Result<Box<dyn VideoDecoder>>> {
}
}
/// Our pure-Rust VP9 decoder (profiles 0-3, 8/10/12-bit; tile columns and loop filter decode in
/// parallel).
pub struct Vp9Decoder {
dec: filmcraft_vp9::Decoder,
/// Container colour (vpcC / Matroska `Colour`): transfer and primaries are not in the VP9
/// bitstream.
transfer: Option<filmcraft_color::Transfer>,
primaries: Option<filmcraft_color::Primaries>,
}
/// Colour primaries from an ISO/IEC 23091-2 code.
pub(crate) fn primaries_from_code(c: u8) -> Option<filmcraft_color::Primaries> {
use filmcraft_color::Primaries;
match c {
1 => Some(Primaries::Bt709),
5 => Some(Primaries::Bt601_625),
6 => Some(Primaries::Bt601_525),
9 => Some(Primaries::Bt2020),
12 => Some(Primaries::P3D65),
_ => None,
}
}
impl Vp9Decoder {
pub fn new(cfg: Option<&filmcraft_isobmff::VpcConfig>) -> Self {
Self {
dec: filmcraft_vp9::Decoder::new(),
transfer: cfg.and_then(|c| filmcraft_color::Transfer::from_code(c.transfer_characteristics)),
primaries: cfg.and_then(|c| primaries_from_code(c.colour_primaries)),
}
}
fn convert(&self, p: filmcraft_vp9::Picture) -> DecodedFrame {
use filmcraft_color::{Matrix, Primaries, Range};
use filmcraft_frame::{Chroma, PixelData};
use filmcraft_vp9::Plane;
use std::sync::Arc;
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 mut color = filmcraft_color::ColorInfo { matrix: filmcraft_frame::default_matrix(p.width, p.height), ..filmcraft_color::ColorInfo::REC709 };
// color_space (7.2.2): 1 BT.601, 2 BT.709, 3 SMPTE-170, 4 SMPTE-240, 5 BT.2020, 7 sRGB.
match p.color.color_space {
1 | 3 => color.matrix = Matrix::Bt601,
2 | 4 => color.matrix = Matrix::Bt709,
5 => {
color.matrix = Matrix::Bt2020Ncl;
color.primaries = Primaries::Bt2020;
}
_ => {}
}
if let Some(t) = self.transfer {
color.transfer = t;
}
if let Some(pr) = self.primaries {
color.primaries = pr;
}
if p.color.full_range {
color.range = Range::Full;
}
let pts = p.pts;
if p.color.color_space == 7 {
// RGB (profiles 1 / 3, 4:4:4): the planes carry G, B, R.
let shift = p.bit_depth - 8;
let mut rgba = Vec::with_capacity(w * h * 4);
for i in 0..w * h {
rgba.extend_from_slice(&[(p.v.get(i) >> shift) as u8, (p.y.get(i) >> shift) as u8, (p.u.get(i) >> shift) as u8, 255]);
}
let mut frame = VideoFrame::rgba8(p.width, p.height, rgba);
frame.color = filmcraft_color::ColorInfo { range: Range::Full, transfer: filmcraft_color::Transfer::Srgb, ..color };
return DecodedFrame { pts, frame };
}
// 4:4:0 has no frame format of its own: chroma rows are repeated to 4:4:4.
let (chroma, rows_440) = match (p.subsampling_x, p.subsampling_y) {
(true, true) => (Chroma::C420, false),
(true, false) => (Chroma::C422, false),
(false, false) => (Chroma::C444, false),
(false, true) => (Chroma::C444, true),
};
fn expand<T: Copy>(v: &[T], cw: usize, ch: usize, h: usize, rows_440: bool) -> Vec<T> {
if !rows_440 {
return v[..cw * ch].to_vec();
}
let mut out = Vec::with_capacity(cw * h);
for y in 0..h {
out.extend_from_slice(&v[(y >> 1) * cw..(y >> 1) * cw + cw]);
}
out
}
let data = match (&p.y, &p.u, &p.v) {
(Plane::U8(y), Plane::U8(u), Plane::U8(v)) => PixelData::Yuv8 {
planes: [Arc::new(y[..w * h].to_vec()), Arc::new(expand(u, cw, ch, h, rows_440)), Arc::new(expand(v, cw, ch, h, rows_440))],
chroma,
alpha: None,
},
(Plane::U16(y), Plane::U16(u), Plane::U16(v)) => PixelData::Yuv16 {
planes: [Arc::new(y[..w * h].to_vec()), Arc::new(expand(u, cw, ch, h, rows_440)), Arc::new(expand(v, cw, ch, h, rows_440))],
chroma,
bits: p.bit_depth,
alpha: None,
},
_ => unreachable!("VP9 planes share one sample type"),
};
// render_size (the intended display size) is not applied: the container's display
// dimensions / pixel aspect describe the presentation.
let par = (1, 1);
DecodedFrame { pts, frame: VideoFrame { width: p.width, height: p.height, data, color, par, pts: filmcraft_time::Tick::ZERO } }
}
}
impl VideoDecoder for Vp9Decoder {
fn decode(&mut self, sample: &[u8], pts: i64) -> Result<Vec<DecodedFrame>> {
let pics = self.dec.decode(sample, pts).map_err(|e| CodecError::Decode(e.to_string()))?;
Ok(pics.into_iter().map(|p| self.convert(p)).collect())
}
fn flush(&mut self) -> Vec<DecodedFrame> {
self.dec.flush().into_iter().map(|p| self.convert(p)).collect()
}
fn reset(&mut self) {
self.dec.reset();
}
fn name(&self) -> &str {
"FilmCraft VP9"
}
fn is_random_access(&self, sample: &[u8]) -> Option<bool> {
Some(filmcraft_vp9::is_keyframe(sample))
}
}
pub fn vp9_factory(e: &SampleEntry) -> Option<Result<Box<dyn VideoDecoder>>> {
match &e.codec {
CodecConfig::Vp9(c) => Some(Ok(Box::new(Vp9Decoder::new(Some(c))) as Box<dyn VideoDecoder>)),
_ => None,
}
}
/// Our ProRes decoder (every frame is intra; slices decode in parallel).
pub struct ProResDecoder;

21
crates/vp9/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "filmcraft-vp9"
description = "Clean-room pure-Rust VP9 decoder (VP9 Bitstream & Decoding Process Specification)"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[features]
default = ["threads"]
threads = ["dep:rayon"]
[dependencies]
filmcraft-bitstream = { workspace = true }
thiserror = { workspace = true }
rayon = { workspace = true, optional = true }
[dev-dependencies]

176
crates/vp9/README.md Normal file
View File

@ -0,0 +1,176 @@
# filmcraft-vp9
Clean-room, pure-Rust (no `unsafe`) VP9 decoder, implemented from the public *VP9 Bitstream &
Decoding Process Specification* v0.6 (WebM project). It is an L0 crate: it depends only on
`filmcraft-bitstream`, `thiserror`, and optionally `rayon` (default 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/extract_tables.py`](tools/extract_tables.py) into `src/spec_tables.rs`. The 1D inverse
DCT / ADST butterflies are generated by [`tools/gen_transforms.py`](tools/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 |
| Loop filter in parallel (superblock wavefront) | done |
| Frame-level threading (overlapping consecutive frames) | not yet |
## API
```rust
use filmcraft_vp9::{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 _ = dec.flush(); // VP9 has no reordering: always empty, kept for API symmetry
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 = filmcraft_vp9::is_keyframe(chunk);
let info: Option<filmcraft_vp9::KeyframeInfo> = filmcraft_vp9::keyframe_info(chunk);
```
`filmcraft-codecs` wires the decoder into Matroska / WebM (`V_VP9`, profile / bit depth from
`CodecPrivate`, colour from `Colour`) and MP4 (`vp09` + `vpcC`) import; its GOP cache asks
`is_keyframe` before starting a decode at a container sync sample, 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.
### 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); strips are then
assembled, the loop filter runs as a superblock wavefront (superblock (r, c) waits for (r-1, c+1);
each superblock filters a private copy of its area plus 8 samples above / left and writes it
back), counts are merged for backward adaptation and the frame is stored. Samples are `u16`
internally for every bit depth.
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`, `decoder`.
## Tests
`cargo test --release -p filmcraft-vp9`. 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, single-threaded and with the default thread pool; the first mismatch reports frame, plane,
position and superblock.
- 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, scaled MC path vs. unscaled,
superframe index, `inv_remap_prob`, probability merging.
- Conformance (`tests/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/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/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.
### 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 |
## Performance
`cargo test --release -p filmcraft-vp9 --test perf -- --ignored --nocapture` (bit-exactness is
verified first), or `VP9_THREADS=n VP9_LOOPS=k cargo run --release -p filmcraft-vp9 --example
vp9dec -- in.ivf` (decodes the file k times and prints the statistics).
Apple M4 Pro (14 cores), 1080p 8-bit `bench_1080p` fixture (libvpx speed 4, 4 MB/s, 4 tile
columns), 120 frames per run, before / after binaries alternated. The machine was shared with
other builds (load average 80-110 during the multi-thread runs, up to 200 earlier), so
wall-clock numbers are the best of 3 runs and still well below what an idle machine gives;
single-thread CPU time is the stable figure.
| | before (5e9d7d9) | now |
|---|---|---|
| 1 thread, CPU time | 52-55 fps | 98-102 fps |
| 14 threads, wall clock (best of 3, load ~90) | 82-89 fps | 143-145 fps |
What made the difference:
- Loop filter: each group of 8 samples along an edge is filtered as 8 lanes at once (16-bit lanes
for 8 / 10-bit, 32-bit for 12-bit); the mask / flat / flat2 decisions are lane-wise and every
filter result is selected per lane, so the arithmetic vectorizes. Per-group decisions use masks
instead of divisions and skip groups with no edge before looking up levels. The parallel loop
filter (superblock wavefront) locks per superblock-row band only while copying, instead of
holding one frame-wide lock.
- Motion compensation: the 8-tap filters run tap-outer over fixed-width rows (4..64) and no longer
zero a 10 KB edge window per block; the 2D inverse transform buffers are sized per transform.
- Memory: strip / frame planes and mode-info grids are recycled through a pool (no per-frame
multi-megabyte allocations and page faults); assembling tile strips into the frame and the
cropping / 8-bit narrowing of output pictures run in parallel chunks of rows.
The loop filter is still the largest single cost (~30% single-threaded), followed by inter
prediction and coefficient decoding.
## Known gaps
- No frame-level threading: parallelism is tile columns + loop filter wavefront + row-parallel
copies, so streams with a single tile column (narrower than 512 pixels or encoded without tile
columns) decode mostly on one thread, and 1080p (at most 4 tile columns) uses 4 threads for
coefficient decoding and prediction.
- 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`.

View File

@ -0,0 +1,43 @@
//! Decode an IVF file: `cargo run --release -p filmcraft-vp9 --example 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 { filmcraft_vp9::Decoder::with_threads(threads) } else { filmcraft_vp9::Decoder::new() };
// 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 {
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());
}

173
crates/vp9/src/boolcoder.rs Normal file
View File

@ -0,0 +1,173 @@
//! 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 self.pos + 8 <= self.data.len() {
let bytes: [u8; 8] = self.data[self.pos..self.pos + 8].try_into().expect("8 bytes");
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;
let bit = if self.value >= bigsplit {
self.range -= split;
self.value -= bigsplit;
true
} else {
self.range = split;
false
};
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}");
}
}
}
}

658
crates/vp9/src/decoder.rs Normal file
View File

@ -0,0 +1,658 @@
//! Top-level decoder: superframes, frame header handling, tile scheduling (tile columns in
//! parallel), frame assembly, loop filter (superblock wavefront in parallel), probability
//! adaptation and reference frame management.
use crate::error::{Error, Result, ensure};
use crate::frame::{Frame, MiGrid, MiInfo, Plane as FPlane, Pools, plane_geometry};
use crate::header::{FrameHeader, HeaderState, KEY_FRAME, RefInfo, parse_compressed, parse_uncompressed, split_superframe};
use crate::loopfilter::{LfFrame, PlaneView, filter_superblock};
use crate::probs::{Counts, adapt_coef_probs, adapt_noncoef_probs};
use crate::tables::*;
use crate::tile::{FrameShared, RefUse, Strip, TileDecoder};
use crate::{ColorInfo, Picture, Plane};
use std::sync::Arc;
/// Counters describing what the decoder has seen (useful to check test coverage).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DecodeStats {
pub frames: u64,
pub shown: u64,
pub hidden: u64,
pub key_frames: u64,
pub intra_only: u64,
pub inter_frames: u64,
pub show_existing: u64,
pub superframes: u64,
pub error_resilient: u64,
pub no_backward_adaptation: u64,
pub size_changes: u64,
pub scaled_ref_blocks: u64,
pub compound_blocks: u64,
pub intra_blocks: u64,
pub inter_blocks: u64,
pub lossless_frames: u64,
pub max_tile_cols: u32,
pub max_tile_rows: u32,
pub segmentation_frames: u64,
pub tx_select_frames: u64,
pub switchable_interp_frames: u64,
pub high_precision_mv_frames: u64,
pub compound_frames: u64,
pub bit_depths: [u64; 3],
pub profiles: [u64; 4],
}
/// VP9 decoder.
pub struct Decoder {
st: HeaderState,
slots: [Option<Arc<Frame>>; 8],
prev_mi: Option<Arc<MiGrid>>,
prev_seg_ids: Vec<u8>,
last_size: Option<(u32, u32)>,
last_show_frame: bool,
stats: DecodeStats,
threads: usize,
bufs: Pools,
#[cfg(feature = "threads")]
pool: Option<rayon::ThreadPool>,
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
fn default_threads() -> usize {
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
{
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).min(16)
}
#[cfg(not(all(feature = "threads", not(target_arch = "wasm32"))))]
{
1
}
}
/// get_tile_offset (6.4.1).
fn tile_offset(i: usize, mis: usize, log2: u32) -> usize {
let sbs = (mis + 7) >> 3;
(((i * sbs) >> log2) << 3).min(mis)
}
impl Decoder {
/// A decoder using all available cores (with the `threads` feature).
pub fn new() -> Self {
Self::with_threads(default_threads())
}
/// A decoder using up to `threads` worker threads (1 = decode on the calling thread).
pub fn with_threads(threads: usize) -> Self {
#[cfg(feature = "threads")]
let pool = if threads > 1 && cfg!(not(target_arch = "wasm32")) {
rayon::ThreadPoolBuilder::new().num_threads(threads).thread_name(|i| format!("vp9-{i}")).build().ok()
} else {
None
};
Decoder {
st: HeaderState::default(),
slots: Default::default(),
prev_mi: None,
prev_seg_ids: Vec::new(),
last_size: None,
last_show_frame: false,
stats: DecodeStats::default(),
threads: threads.max(1),
bufs: Pools::default(),
#[cfg(feature = "threads")]
pool,
}
}
/// Statistics accumulated so far.
pub fn stats(&self) -> DecodeStats {
self.stats.clone()
}
/// Decode one chunk (a frame or a superframe, as stored in one IVF / WebM / MP4 sample).
/// Returns the frames to show, in order; each carries `pts`.
pub fn decode(&mut self, data: &[u8], pts: i64) -> Result<Vec<Picture>> {
let frames = split_superframe(data);
if frames.len() > 1 {
self.stats.superframes += 1;
}
let mut out = Vec::new();
for f in frames {
if f.is_empty() {
continue;
}
if let Some(frame) = self.decode_frame(f)? {
out.push(make_picture(&frame, pts, self.par()));
}
}
Ok(out)
}
/// End of stream. VP9 has no reordering delay, so this always returns no pictures.
pub fn flush(&mut self) -> Vec<Picture> {
Vec::new()
}
/// Forget all decoding state (references, probability contexts, segmentation), e.g. before
/// decoding from another key frame after a seek. The thread pool and statistics are kept.
pub fn reset(&mut self) {
self.st = HeaderState::default();
self.slots = Default::default();
self.prev_mi = None;
self.prev_seg_ids = Vec::new();
self.last_size = None;
self.last_show_frame = false;
}
fn decode_frame(&mut self, data: &[u8]) -> Result<Option<Arc<Frame>>> {
let refs: [Option<RefInfo>; 8] = std::array::from_fn(|i| self.slots[i].as_ref().map(|f| RefInfo { width: f.width, height: f.height }));
let mut h = parse_uncompressed(data, &mut self.st, &refs)?;
if h.show_existing_frame {
self.stats.show_existing += 1;
let f = self.slots[h.frame_to_show_map_idx as usize]
.clone()
.ok_or_else(|| Error::MissingReference(format!("show_existing_frame of empty slot {}", h.frame_to_show_map_idx)))?;
self.stats.shown += 1;
return Ok(Some(f));
}
ensure!(h.width <= 16384 && h.height <= 16384, "frame size {}x{} too large", h.width, h.height);
let (mi_rows, mi_cols) = (h.mi_rows as usize, h.mi_cols as usize);
// compute_image_size semantics (7.2.6).
let size = (h.width, h.height);
let size_changed = self.last_size != Some(size);
if size_changed && self.last_size.is_some() {
self.stats.size_changes += 1;
}
if self.st.reset_segment_map || size_changed || self.prev_seg_ids.len() != mi_rows * mi_cols {
self.prev_seg_ids = vec![0; mi_rows * mi_cols];
self.st.reset_segment_map = false;
}
let use_prev = !size_changed
&& self.last_show_frame
&& !h.error_resilient_mode
&& !h.frame_is_intra
&& self.prev_mi.as_ref().is_some_and(|m| m.rows == mi_rows && m.cols == mi_cols);
self.last_size = Some(size);
self.last_show_frame = h.show_frame;
let off = h.uncompressed_size;
let hs = h.header_size_in_bytes as usize;
ensure!(off + hs <= data.len(), "compressed header exceeds frame data");
let mut fc = self.st.contexts[h.frame_context_idx as usize].clone();
parse_compressed(&data[off..off + hs], &mut h, &mut fc)?;
self.count_frame(&h);
// References (8.5.2.3).
let mut refs: [Option<RefUse>; 3] = [None, None, None];
let ref_frames: Vec<Option<Arc<Frame>>> =
(0..3).map(|i| if h.frame_is_intra { None } else { self.slots[h.ref_frame_idx[i] as usize].clone() }).collect();
if !h.frame_is_intra {
for i in 0..3 {
let Some(rf) = &ref_frames[i] else {
return Err(Error::MissingReference(format!("reference slot {} is empty", h.ref_frame_idx[i])));
};
ensure!(
rf.bit_depth == h.color.bit_depth && rf.ss_x == h.color.subsampling_x && rf.ss_y == h.color.subsampling_y,
"reference frame format differs from the current frame"
);
let (rw, rh) = (rf.width as i64, rf.height as i64);
let (w, hh) = (h.width as i64, h.height as i64);
if 2 * w >= rw && 2 * hh >= rh && w <= 16 * rw && hh <= 16 * rh {
let x_scale = ((rw << 14) / w) as i32;
let y_scale = ((rh << 14) / hh) as i32;
refs[i] = Some(RefUse { frame: rf, x_scale, y_scale, x_step: (16 * x_scale) >> 14, y_step: (16 * y_scale) >> 14 });
}
}
}
// Quantizers per segment (8.6.1).
let bd_idx = ((h.color.bit_depth - 8) >> 1) as usize;
let seg = self.st.seg.clone();
let mut seg_q = [[[0i32; 2]; 2]; 8];
for (s, q) in seg_q.iter_mut().enumerate() {
let qindex = if seg.feature_active(s as u8, SEG_LVL_ALT_Q) {
let d = seg.feature_data[s][SEG_LVL_ALT_Q] as i32;
(if seg.abs_or_delta_update { d } else { h.base_q_idx as i32 + d }).clamp(0, 255)
} else {
h.base_q_idx as i32
};
let dc = |b: i32| DC_QLOOKUP[bd_idx * 256 + b.clamp(0, 255) as usize];
let ac = |b: i32| AC_QLOOKUP[bd_idx * 256 + b.clamp(0, 255) as usize];
q[0] = [dc(qindex + h.delta_q_y_dc as i32), ac(qindex)];
q[1] = [dc(qindex + h.delta_q_uv_dc as i32), ac(qindex + h.delta_q_uv_ac as i32)];
}
// Tile layout and data (6.4).
let tile_cols = 1usize << h.tile_cols_log2;
let tile_rows = 1usize << h.tile_rows_log2;
let mut pos = off + hs;
let mut tile_data: Vec<Vec<&[u8]>> = vec![Vec::with_capacity(tile_rows); tile_cols];
for tr in 0..tile_rows {
for (tc, td) in tile_data.iter_mut().enumerate() {
let last = tr == tile_rows - 1 && tc == tile_cols - 1;
let sz = if last {
data.len() - pos
} else {
ensure!(pos + 4 <= data.len(), "tile size beyond frame data");
let s = u32::from_be_bytes(data[pos..pos + 4].try_into().expect("4 bytes")) as usize;
pos += 4;
ensure!(s <= data.len() - pos, "tile size {s} beyond frame data");
s
};
td.push(&data[pos..pos + sz]);
pos += sz;
}
}
let counting = !h.error_resilient_mode && !h.frame_parallel_decoding_mode;
let prev_mi = if use_prev { self.prev_mi.clone() } else { None };
let shared =
FrameShared { h: &h, fc: &fc, seg: &seg, prev_seg_ids: &self.prev_seg_ids, prev_mi: prev_mi.as_deref(), refs, seg_q, counting, pools: &self.bufs };
let col_bounds: Vec<(usize, usize)> =
(0..tile_cols).map(|i| (tile_offset(i, mi_cols, h.tile_cols_log2), tile_offset(i + 1, mi_cols, h.tile_cols_log2))).collect();
let row_bounds: Vec<(usize, usize)> =
(0..tile_rows).map(|i| (tile_offset(i, mi_rows, h.tile_rows_log2), tile_offset(i + 1, mi_rows, h.tile_rows_log2))).collect();
let decode_col = |ci: usize| -> (Strip, Option<Error>) {
let (cs, ce) = col_bounds[ci];
let mut td = TileDecoder::new(&shared, cs, ce);
let mut err = None;
if ce > cs {
for (ri, &(rs, re)) in row_bounds.iter().enumerate() {
if !td.decode_tile(tile_data[ci][ri], rs, re) {
err = Some(Error::Invalid("tile data exhausted".into()));
break;
}
}
}
let e = td.error.take().or(err);
(td.strip, e)
};
let results: Vec<(Strip, Option<Error>)> = self.run_parallel(tile_cols, &decode_col);
let mut counts = Counts::default();
let mut strips = Vec::with_capacity(results.len());
for (s, e) in results {
if let Some(e) = e {
return Err(e);
}
if counting {
counts.add(&s.counts);
}
self.stats.compound_blocks += s.compound_blocks;
self.stats.scaled_ref_blocks += s.scaled_blocks;
self.stats.intra_blocks += s.intra_blocks;
self.stats.inter_blocks += s.inter_blocks;
strips.push(s);
}
// Release the references before the slots are refreshed, so that evicted frames can be
// recycled.
drop(ref_frames);
drop(prev_mi);
// Assemble the frame and mode info.
let geo = plane_geometry(h.width, h.height, h.color.subsampling_x, h.color.subsampling_y);
let (planes, mi, seg_ids) = assemble(strips, &geo, mi_rows, mi_cols, &self.bufs, self.par());
let mut frame = Frame {
planes,
width: h.width,
height: h.height,
ss_x: h.color.subsampling_x,
ss_y: h.color.subsampling_y,
bit_depth: h.color.bit_depth,
color_space: h.color.color_space,
color_range: h.color.color_range,
render_width: h.render_width,
render_height: h.render_height,
key: h.frame_type == KEY_FRAME,
intra_only: h.intra_only,
};
// Loop filter (8.8).
if self.st.lf.level > 0 {
let lf = LfFrame::new(&self.st.lf, &seg, mi_rows, mi_cols, h.color.subsampling_x, h.color.subsampling_y, h.color.bit_depth);
self.loop_filter(&mut frame, &mi, &lf);
}
// refresh_probs (6.1.2).
if counting {
let saved = &self.st.contexts[h.frame_context_idx as usize];
let mut a = saved.clone();
a.tx = fc.tx;
a.skip = fc.skip;
adapt_coef_probs(&mut a, &counts, h.frame_is_intra, self.st.last_frame_type == KEY_FRAME);
if !h.frame_is_intra {
a.tx = saved.tx;
a.skip = saved.skip;
adapt_noncoef_probs(&mut a, &counts, h.interp_filter == SWITCHABLE, h.tx_mode == TX_MODE_SELECT, h.allow_high_precision_mv);
}
fc = a;
}
if h.refresh_frame_context {
self.st.contexts[h.frame_context_idx as usize] = fc;
}
if seg.enabled && seg.update_map {
self.prev_seg_ids = seg_ids;
}
let frame = Arc::new(frame);
let mut evicted = Vec::new();
for i in 0..8 {
if (h.refresh_frame_flags >> i) & 1 == 1 {
evicted.extend(self.slots[i].replace(frame.clone()));
}
}
// Recycle the planes of frames no longer referenced anywhere.
for f in evicted {
if let Ok(f) = Arc::try_unwrap(f) {
for p in f.planes {
self.bufs.samples.put(p.data);
}
}
}
if let Some(old) = self.prev_mi.replace(Arc::new(mi))
&& let Ok(old) = Arc::try_unwrap(old)
{
self.bufs.mi.put(old.mi);
}
if h.show_frame {
self.stats.shown += 1;
Ok(Some(frame))
} else {
self.stats.hidden += 1;
Ok(None)
}
}
fn count_frame(&mut self, h: &FrameHeader) {
let s = &mut self.stats;
s.frames += 1;
if h.frame_type == KEY_FRAME {
s.key_frames += 1;
} else if h.intra_only {
s.intra_only += 1;
} else {
s.inter_frames += 1;
}
s.error_resilient += h.error_resilient_mode as u64;
s.no_backward_adaptation += (h.error_resilient_mode || h.frame_parallel_decoding_mode) as u64;
s.lossless_frames += h.lossless as u64;
s.max_tile_cols = s.max_tile_cols.max(1 << h.tile_cols_log2);
s.max_tile_rows = s.max_tile_rows.max(1 << h.tile_rows_log2);
s.segmentation_frames += self.st.seg.enabled as u64;
s.tx_select_frames += (h.tx_mode == TX_MODE_SELECT) as u64;
s.switchable_interp_frames += (!h.frame_is_intra && h.interp_filter == SWITCHABLE) as u64;
s.high_precision_mv_frames += (!h.frame_is_intra && h.allow_high_precision_mv) as u64;
s.compound_frames += (!h.frame_is_intra && h.reference_mode != 0) as u64;
s.bit_depths[((h.color.bit_depth - 8) >> 1) as usize] += 1;
s.profiles[h.profile as usize] += 1;
}
fn par(&self) -> Par<'_> {
#[cfg(feature = "threads")]
{
Par(self.pool.as_ref())
}
#[cfg(not(feature = "threads"))]
{
Par(std::marker::PhantomData)
}
}
/// Run `f(0..n)`, in parallel when a pool is available.
fn run_parallel<T: Send>(&self, n: usize, f: &(dyn Fn(usize) -> T + Sync)) -> Vec<T> {
#[cfg(feature = "threads")]
if let Some(pool) = &self.pool
&& n > 1
{
use rayon::prelude::*;
return pool.install(|| (0..n).into_par_iter().map(f).collect());
}
(0..n).map(f).collect()
}
fn loop_filter(&self, frame: &mut Frame, mi: &MiGrid, lf: &LfFrame) {
let sb_rows = lf.mi_rows.div_ceil(8);
let sb_cols = lf.mi_cols.div_ceil(8);
#[cfg(feature = "threads")]
if let Some(pool) = &self.pool
&& sb_rows > 1
{
lf_parallel(pool, self.threads, frame, mi, lf, sb_rows, sb_cols);
return;
}
let _ = self.threads;
let [p0, p1, p2] = &mut frame.planes;
let mut views = [
PlaneView { stride: p0.stride, data: &mut p0.data, ox: 0, oy: 0 },
PlaneView { stride: p1.stride, data: &mut p1.data, ox: 0, oy: 0 },
PlaneView { stride: p2.stride, data: &mut p2.data, ox: 0, oy: 0 },
];
for r in 0..sb_rows {
for c in 0..sb_cols {
filter_superblock(&mut views, mi, lf, r * 8, c * 8);
}
}
}
}
/// Loop filter with a superblock wavefront: superblock (r, c) runs once (r - 1, c + 1) is done.
/// The planes are split into superblock-row bands, each behind its own lock. Each superblock
/// copies the samples it may touch (its area plus 8 samples above / left, i.e. the bottom rows
/// of band r - 1 and its own band) out, filters them locally and writes them back; the locks are
/// only held while copying, and only neighbouring rows ever contend.
#[cfg(feature = "threads")]
fn lf_parallel(pool: &rayon::ThreadPool, threads: usize, frame: &mut Frame, mi: &MiGrid, lf: &LfFrame, sb_rows: usize, sb_cols: usize) {
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Condvar, Mutex};
let strides = [frame.planes[0].stride, frame.planes[1].stride, frame.planes[2].stride];
let sub = [(0usize, 0usize), (lf.ss_x, lf.ss_y), (lf.ss_x, lf.ss_y)];
let band_h = [64usize, 64 >> lf.ss_y, 64 >> lf.ss_y];
// bands[r][p]: rows [r * band_h[p], (r + 1) * band_h[p]) of plane p.
let bands: Vec<Mutex<[&mut [u16]; 3]>> = {
let [a, b, c] = &mut frame.planes;
let mut ia = a.data.chunks_mut(band_h[0] * strides[0]);
let mut ib = b.data.chunks_mut(band_h[1] * strides[1]);
let mut ic = c.data.chunks_mut(band_h[2] * strides[2]);
(0..sb_rows).map(|_| Mutex::new([ia.next().unwrap_or_default(), ib.next().unwrap_or_default(), ic.next().unwrap_or_default()])).collect()
};
let progress = Mutex::new(vec![0usize; sb_rows]);
let cv = Condvar::new();
let next = AtomicUsize::new(0);
// Copy rows [ry, ry + rh) x [rx, rx + rw) of plane p between the bands and `buf` (row-major,
// stride rw); `to_buf` selects the direction.
let xfer = |r: usize, p: usize, rx: usize, ry: usize, rw: usize, rh: usize, buf: &mut [u16], to_buf: bool| {
let bh = band_h[p];
let mut yy = 0;
while yy < rh {
let y = ry + yy;
let br = y / bh;
let n = ((br + 1) * bh - y).min(rh - yy);
debug_assert!(br + 1 >= r && br <= r);
let mut g = bands[br].lock().expect("lf band");
let band = &mut g[p];
for k in 0..n {
let s = (y + k - br * bh) * strides[p] + rx;
let b = (yy + k) * rw;
if to_buf {
buf[b..b + rw].copy_from_slice(&band[s..s + rw]);
} else {
band[s..s + rw].copy_from_slice(&buf[b..b + rw]);
}
}
yy += n;
}
};
pool.install(|| {
(0..threads.min(sb_rows)).into_par_iter().for_each(|_| {
let mut bufs: [Vec<u16>; 3] = [vec![0; 72 * 72], vec![0; 72 * 72], vec![0; 72 * 72]];
loop {
let r = next.fetch_add(1, Ordering::Relaxed);
if r >= sb_rows {
break;
}
for c in 0..sb_cols {
if r > 0 {
let need = (c + 2).min(sb_cols);
let mut p = progress.lock().expect("lf progress");
while p[r - 1] < need {
p = cv.wait(p).expect("lf progress");
}
}
// Region of each plane: [x0 - 8, x0 + size) x [y0 - 8, y0 + size).
let mut regions = [(0usize, 0usize, 0usize, 0usize); 3];
for p in 0..3 {
let (sx, sy) = sub[p];
let (x0, y0) = ((c * 64) >> sx, (r * 64) >> sy);
let (rx, ry) = (x0.saturating_sub(8), y0.saturating_sub(8));
let rw = (x0 + (64 >> sx)).min(strides[p]) - rx;
let rh = (y0 + band_h[p]) - ry;
regions[p] = (rx, ry, rw, rh);
xfer(r, p, rx, ry, rw, rh, &mut bufs[p], true);
}
{
let [b0, b1, b2] = &mut bufs;
let mut views = [
PlaneView { data: b0, stride: regions[0].2, ox: regions[0].0, oy: regions[0].1 },
PlaneView { data: b1, stride: regions[1].2, ox: regions[1].0, oy: regions[1].1 },
PlaneView { data: b2, stride: regions[2].2, ox: regions[2].0, oy: regions[2].1 },
];
filter_superblock(&mut views, mi, lf, r * 8, c * 8);
}
for p in 0..3 {
let (rx, ry, rw, rh) = regions[p];
xfer(r, p, rx, ry, rw, rh, &mut bufs[p], false);
}
let mut p = progress.lock().expect("lf progress");
p[r] = c + 1;
cv.notify_all();
}
}
});
});
}
/// Optional thread pool for data-parallel copies.
#[derive(Clone, Copy)]
struct Par<'a>(#[cfg(feature = "threads")] Option<&'a rayon::ThreadPool>, #[cfg(not(feature = "threads"))] std::marker::PhantomData<&'a ()>);
impl Par<'_> {
/// `f(i, chunk)` for the consecutive `chunk_len`-element chunks of `data`.
fn chunks<T: Send>(self, data: &mut [T], chunk_len: usize, f: impl Fn(usize, &mut [T]) + Sync) {
let chunk_len = chunk_len.max(1);
#[cfg(feature = "threads")]
if let Some(pool) = self.0
&& data.len() > chunk_len
{
use rayon::prelude::*;
pool.install(|| data.par_chunks_mut(chunk_len).enumerate().for_each(|(i, c)| f(i, c)));
return;
}
for (i, c) in data.chunks_mut(chunk_len).enumerate() {
f(i, c);
}
}
}
/// Rows per chunk of the parallel frame copies.
const COPY_ROWS: usize = 64;
/// Build full-frame planes, mode info and segment ids from the tile column strips.
fn assemble(
mut strips: Vec<Strip>,
geo: &[(usize, usize, usize, usize); 3],
mi_rows: usize,
mi_cols: usize,
pools: &Pools,
par: Par,
) -> ([FPlane; 3], MiGrid, Vec<u8>) {
if strips.len() == 1 {
let s = strips.pop().expect("one strip");
let [a, b, c] = s.planes;
let mk = |d: Vec<u16>, g: (usize, usize, usize, usize)| FPlane { data: d, stride: g.0, width: g.2, height: g.3 };
return ([mk(a, geo[0]), mk(b, geo[1]), mk(c, geo[2])], MiGrid { cols: mi_cols, rows: mi_rows, mi: s.mi }, s.seg_ids);
}
let mk = |g: (usize, usize, usize, usize)| FPlane { data: pools.samples.take(g.0 * g.1, 0), stride: g.0, width: g.2, height: g.3 };
let mut planes = [mk(geo[0]), mk(geo[1]), mk(geo[2])];
let mut mi = pools.mi.take(mi_rows * mi_cols, MiInfo::default());
let mut seg = vec![0u8; mi_rows * mi_cols];
for (p, plane) in planes.iter_mut().enumerate() {
let fs = plane.stride;
// Each chunk of destination rows gathers its part of every strip.
par.chunks(&mut plane.data, COPY_ROWS * fs, |ci, dst| {
let y0 = ci * COPY_ROWS;
for s in &strips {
let ss = s.strides[p];
let x0 = s.x_off[p];
let w = ss.min(fs - x0);
for (k, row) in dst.chunks_mut(fs).enumerate() {
let y = y0 + k;
if let Some(src) = s.planes[p].get(y * ss..y * ss + w) {
row[x0..x0 + w].copy_from_slice(src);
}
}
}
});
}
for s in &strips {
let mw = s.mi_col_end - s.mi_col_start;
for r in 0..mi_rows {
mi[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.mi[r * mw..r * mw + mw]);
seg[r * mi_cols + s.mi_col_start..r * mi_cols + s.mi_col_end].copy_from_slice(&s.seg_ids[r * mw..r * mw + mw]);
}
}
for s in strips {
for p in s.planes {
pools.samples.put(p);
}
pools.mi.put(s.mi);
}
(planes, MiGrid { cols: mi_cols, rows: mi_rows, mi }, seg)
}
fn make_picture(f: &Frame, pts: i64, par: Par) -> Picture {
// Crop (and narrow 8-bit samples), in parallel chunks of rows.
let conv = |p: &FPlane| -> Plane {
let w = p.width;
if w == 0 || p.height == 0 {
return if f.bit_depth == 8 { Plane::U8(Vec::new()) } else { Plane::U16(Vec::new()) };
}
if f.bit_depth == 8 {
let mut out = vec![0u8; w * p.height];
par.chunks(&mut out, COPY_ROWS * w, |ci, dst| {
for (k, row) in dst.chunks_mut(w).enumerate() {
for (d, &s) in row.iter_mut().zip(&p.row(ci * COPY_ROWS + k)[..w]) {
*d = s as u8;
}
}
});
Plane::U8(out)
} else {
let mut out = vec![0u16; w * p.height];
par.chunks(&mut out, COPY_ROWS * w, |ci, dst| {
for (k, row) in dst.chunks_mut(w).enumerate() {
row.copy_from_slice(&p.row(ci * COPY_ROWS + k)[..w]);
}
});
Plane::U16(out)
}
};
let [y, u, v] = &f.planes;
Picture {
width: f.width,
height: f.height,
chroma_width: u.width as u32,
chroma_height: u.height as u32,
bit_depth: f.bit_depth as u32,
subsampling_x: f.ss_x,
subsampling_y: f.ss_y,
y: conv(y),
u: conv(u),
v: conv(v),
y_stride: y.width,
uv_stride: u.width,
pts,
key: f.key,
intra_only: f.intra_only,
color: ColorInfo { color_space: f.color_space, full_range: f.color_range },
render_width: f.render_width,
render_height: f.render_height,
}
}

31
crates/vp9/src/error.rs Normal file
View File

@ -0,0 +1,31 @@
use filmcraft_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::error::Error::Invalid(format!($($arg)+)));
}
};
}
pub(crate) use ensure;

158
crates/vp9/src/frame.rs Normal file
View File

@ -0,0 +1,158 @@
//! Decoded frame storage and the per-8x8 mode info grid.
use crate::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]
}
}
/// One plane of samples. The allocation covers whole 64x64 superblocks; `width` x `height` is the
/// visible (cropped) area.
#[derive(Clone, Debug, Default)]
pub struct Plane {
pub data: Vec<u16>,
pub stride: usize,
pub width: usize,
pub height: usize,
}
impl Plane {
#[inline]
pub fn row(&self, y: usize) -> &[u16] {
&self.data[y * self.stride..(y + 1) * self.stride]
}
}
/// A decoded frame (also used as reference).
#[derive(Clone, Debug)]
pub struct Frame {
pub planes: [Plane; 3],
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,
}
/// 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>>>,
}
impl<T> Default for Pool<T> {
fn default() -> Self {
Pool { bufs: Default::default() }
}
}
impl<T: Clone> Pool<T> {
const MAX: usize = 32;
/// A buffer of `len` copies of `fill` (recycled memory when available).
pub fn take(&self, len: usize, fill: T) -> Vec<T> {
let found = {
let mut b = self.bufs.lock().expect("buffer pool");
b.iter().position(|v| v.capacity() >= len).map(|i| b.swap_remove(i))
};
match found {
Some(mut v) => {
v.resize(len, fill);
v
}
None => vec![fill; len],
}
}
pub fn put(&self, mut v: Vec<T>) {
if v.capacity() == 0 {
return;
}
v.clear();
let mut b = self.bufs.lock().expect("buffer pool");
if b.len() < Self::MAX {
b.push(v);
}
}
}
/// The decoder's buffer pools.
#[derive(Default)]
pub struct Pools {
pub samples: Pool<u16>,
pub mi: Pool<MiInfo>,
}

661
crates/vp9/src/header.rs Normal file
View File

@ -0,0 +1,661 @@
//! 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::boolcoder::BoolDecoder;
use crate::error::{Error, Result, ensure};
use crate::probs::FrameContext;
use crate::tables::*;
use filmcraft_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();
}
/// 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 {
frame_sync_code(&mut r)?;
h.color = if h.profile > 0 {
color_config(&mut 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(&mut r, &mut h)?;
render_size(&mut r, &mut h)?;
} else {
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 (6.2.5)
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(&mut r, &mut h)?;
}
h.size_from_ref = found;
render_size(&mut r, &mut 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] };
}
}
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 {
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)
st.lf.level = r.read_bits(6)? as u8;
st.lf.sharpness = r.read_bits(3)? as u8;
st.lf.delta_enabled = r.read_flag()?;
if st.lf.delta_enabled && r.read_flag()? {
for i in 0..4 {
if r.read_flag()? {
st.lf.ref_deltas[i] = read_s(&mut r, 6)? as i8;
}
}
for i in 0..2 {
if r.read_flag()? {
st.lf.mode_deltas[i] = read_s(&mut r, 6)? as i8;
}
}
}
// 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;
// segmentation_params (6.2.11)
let seg = &mut st.seg;
seg.enabled = r.read_flag()?;
seg.update_map = false;
seg.temporal_update = false;
if seg.enabled {
seg.update_map = r.read_flag()?;
if seg.update_map {
for i in 0..7 {
seg.tree_probs[i] = read_prob(&mut r)?;
}
seg.temporal_update = r.read_flag()?;
for i in 0..3 {
seg.pred_probs[i] = if seg.temporal_update { read_prob(&mut 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;
}
}
}
}
// tile_info (6.2.13)
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)?;
}
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;
}
}
/// 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 {
for i in 0..2 {
diff_update_prob(&mut bd, &mut fc.tx[1][i][0]);
}
for i in 0..2 {
for j in 0..2 {
diff_update_prob(&mut bd, &mut fc.tx[2][i][j]);
}
}
for i in 0..2 {
for j in 0..3 {
diff_update_prob(&mut bd, &mut fc.tx[3][i][j]);
}
}
}
// read_coef_probs (6.3.7)
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(&mut bd, &mut fc.coef[tx][i][j][k][l][m]);
}
}
}
}
}
}
}
for i in 0..3 {
diff_update_prob(&mut bd, &mut fc.skip[i]);
}
if !h.frame_is_intra {
for i in 0..7 {
for j in 0..3 {
diff_update_prob(&mut 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(&mut bd, &mut fc.interp_filter[j][i]);
}
}
}
for i in 0..4 {
diff_update_prob(&mut bd, &mut fc.is_inter[i]);
}
// frame_reference_mode (6.3.12)
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 {
if bd.read_literal(1) == 1 {
h.reference_mode = if bd.read_literal(1) == 1 { 2 } else { 1 };
}
// setup_compound_reference_mode (6.3.18)
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)
if h.reference_mode == 2 {
for i in 0..5 {
diff_update_prob(&mut bd, &mut fc.comp_mode[i]);
}
}
if h.reference_mode != 1 {
for i in 0..5 {
diff_update_prob(&mut bd, &mut fc.single_ref[i][0]);
diff_update_prob(&mut bd, &mut fc.single_ref[i][1]);
}
}
if h.reference_mode != 0 {
for i in 0..5 {
diff_update_prob(&mut bd, &mut fc.comp_ref[i]);
}
}
for i in 0..4 {
for j in 0..9 {
diff_update_prob(&mut bd, &mut fc.y_mode[i][j]);
}
}
for i in 0..16 {
for j in 0..3 {
diff_update_prob(&mut bd, &mut fc.partition[i][j]);
}
}
// mv_probs (6.3.16)
for j in 0..3 {
update_mv_prob(&mut bd, &mut fc.mv_joint[j]);
}
for i in 0..2 {
update_mv_prob(&mut bd, &mut fc.mv_sign[i]);
for j in 0..10 {
update_mv_prob(&mut bd, &mut fc.mv_class[i][j]);
}
update_mv_prob(&mut bd, &mut fc.mv_class0_bit[i]);
for j in 0..10 {
update_mv_prob(&mut 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(&mut bd, &mut fc.mv_class0_fr[i][j][k]);
}
}
for k in 0..3 {
update_mv_prob(&mut bd, &mut fc.mv_fr[i][k]);
}
}
if h.allow_high_precision_mv {
for i in 0..2 {
update_mv_prob(&mut bd, &mut fc.mv_class0_hp[i]);
update_mv_prob(&mut bd, &mut fc.mv_hp[i]);
}
}
}
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;
if pos + sz > end {
frames.push(&data[pos..end]);
return frames;
}
frames.push(&data[pos..pos + sz]);
pos += sz;
}
return frames;
}
}
vec![data]
}
#[cfg(test)]
mod tests {
use super::*;
#[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());
}
}
}

234
crates/vp9/src/inter.rs Normal file
View File

@ -0,0 +1,234 @@
//! 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::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]
}
/// A reference plane: samples addressed `data[y * stride + x]`, valid for x <= last_x, y <= last_y.
pub struct RefPlane<'a> {
pub data: &'a [u16],
pub stride: usize,
pub last_x: i32,
pub last_y: i32,
}
/// 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).
#[allow(clippy::too_many_arguments)]
pub fn predict(
r: &RefPlane,
x: i32,
y: i32,
step_x: i32,
step_y: i32,
w: usize,
h: usize,
filter: u8,
bit_depth: u8,
out: &mut [u16],
tmp: &mut [u16],
win: &mut [u16],
) {
if step_x == 16 && step_y == 16 {
predict_unscaled(r, x, y, w, h, filter, bit_depth, out, tmp, win);
} else {
predict_scaled(r, x, y, step_x, step_y, w, h, filter, bit_depth, 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.data[yy * r.stride..];
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
}
#[allow(clippy::too_many_arguments)]
fn predict_unscaled(r: &RefPlane, x: i32, y: i32, w: usize, h: usize, filter: u8, bit_depth: u8, out: &mut [u16], tmp: &mut [u16], win: &mut [u16]) {
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).
let (src, ss, so): (&[u16], usize, usize) = if inside {
(r.data, r.stride, (y0 - 3) as usize * r.stride + (x0 - 3) as usize)
} else {
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]);
}
}
}
}
/// 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 o: &mut [u16; W] = (&mut o[..W]).try_into().expect("row");
let mut acc = [64i32; W];
for t in 0..8 {
let c = f[t] as i32;
let p: &[u16; W] = s[t..t + W].try_into().expect("row");
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;
}
}
/// 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]) {
match o.len() {
4 => filter_v_n::<4>(s, ss, f, max, o),
8 => filter_v_n::<8>(s, ss, f, max, o),
16 => filter_v_n::<16>(s, ss, f, max, o),
32 => filter_v_n::<32>(s, ss, f, max, o),
64 => filter_v_n::<64>(s, ss, 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 * ss] as i32).sum();
*d = ((acc + 64) >> 7).clamp(0, max) as u16;
}
}
}
}
#[inline(always)]
fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
let o: &mut [u16; W] = (&mut o[..W]).try_into().expect("row");
let mut acc = [64i32; W];
for t in 0..8 {
let c = f[t] as i32;
let p: &[u16; W] = s[t * ss..t * ss + W].try_into().expect("row");
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).
#[allow(clippy::too_many_arguments)]
fn predict_scaled(r: &RefPlane, x: i32, y: i32, step_x: i32, step_y: i32, w: usize, h: usize, filter: u8, bit_depth: u8, out: &mut [u16], tmp: &mut [u16]) {
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.data[yy * r.stride..];
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 r = RefPlane { data: &data, stride: w, 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, x, y, bw, bh, filter, 8, &mut a, &mut tmp, &mut [0u16; 71 * 71]);
predict_scaled(&r, x, y, 16, 16, bw, bh, filter, 8, &mut b, &mut tmp);
assert_eq!(a, b, "filter {filter} pos ({x},{y}) {bw}x{bh}");
}
}
}
}
}

214
crates/vp9/src/intra.rs Normal file
View File

@ -0,0 +1,214 @@
//! Intra prediction process (8.5.1).
use crate::tables::*;
/// 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,
}
/// 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 log2 = e.tx_size as usize + 2;
let size = 1usize << log2;
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 above = [0i32; 65];
let mut left = [0i32; 32];
if !e.have_above {
above[1..=2 * size].fill(base - 1);
} else {
let row = (y - 1) * stride;
for i in 0..size {
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 {
above[1 + i] = buf[row + (x + i).min(e.max_x) - x_off] as i32;
}
} else {
let v = above[size];
above[1 + size..=2 * size].fill(v);
}
}
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 {
left[i] = buf[at(x - 1, (y + i).min(e.max_y))] as i32;
}
} else {
left[..size].fill(base + 1);
}
let a = |i: isize| above[(i + 1) as usize];
let max = (1i32 << e.bit_depth) - 1;
let o = at(x, y);
let mut put = |i: usize, j: usize, v: i32| buf[o + i * stride + j] = v as u16;
let r1 = |v: i32| (v + 1) >> 1;
let r2 = |v: i32| (v + 2) >> 2;
match mode {
V_PRED => {
for i in 0..size {
for j in 0..size {
put(i, j, a(j as isize));
}
}
}
H_PRED => {
for i in 0..size {
for j in 0..size {
put(i, j, left[i]);
}
}
}
D207_PRED => {
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];
}
}
for i in 0..size {
for j in 0..size {
put(i, j, p[i][j]);
}
}
}
D45_PRED => {
for i in 0..size {
for j in 0..size {
let k = (i + j) as isize;
let v = if i + j + 2 < 2 * size { r2(a(k) + 2 * a(k + 1) + a(k + 2)) } else { a(2 * size as isize - 1) };
put(i, j, v);
}
}
}
D63_PRED => {
for i in 0..size {
for j in 0..size {
let k = (i / 2 + j) as isize;
let v = if i & 1 == 1 { r2(a(k) + 2 * a(k + 1) + a(k + 2)) } else { r1(a(k) + a(k + 1)) };
put(i, j, v);
}
}
}
D117_PRED => {
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];
}
}
for i in 0..size {
for j in 0..size {
put(i, j, p[i][j]);
}
}
}
D135_PRED => {
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];
}
}
for i in 0..size {
for j in 0..size {
put(i, j, p[i][j]);
}
}
}
D153_PRED => {
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];
}
}
for i in 0..size {
for j in 0..size {
put(i, j, p[i][j]);
}
}
}
TM_PRED => {
for i in 0..size {
for j in 0..size {
put(i, j, (a(j as isize) + left[i] - a(-1)).clamp(0, max));
}
}
}
_ => {
// DC_PRED
let v = match (e.have_left, e.have_above) {
(true, true) => {
let sum: i32 = left[..size].iter().sum::<i32>() + above[1..=size].iter().sum::<i32>();
(sum + size as i32) >> (log2 + 1)
}
(true, false) => (left[..size].iter().sum::<i32>() + (1 << (log2 - 1))) >> log2,
(false, true) => (above[1..=size].iter().sum::<i32>() + (1 << (log2 - 1))) >> log2,
(false, false) => base,
};
for i in 0..size {
for j in 0..size {
put(i, j, v);
}
}
}
}
}

166
crates/vp9/src/lib.rs Normal file
View File

@ -0,0 +1,166 @@
//! 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 = filmcraft_vp9::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::too_many_arguments)]
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, clippy::all)]
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,
}

View File

@ -0,0 +1,368 @@
//! Loop filter process (8.8).
use crate::frame::{MiGrid, MiInfo};
use crate::header::{LoopFilterParams, Segmentation};
use crate::tables::*;
/// 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::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), with non-short-circuit `&` so that it vectorizes.
let mut mask = [false; LANES];
let mut hev = [false; LANES];
let mut flat = [false; LANES];
let mut flat2 = [false; LANES];
let can_flat = filter_size >= TX_8X8;
let can_flat2 = R == 8 && filter_size >= TX_16X16;
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]
& ((p3[l] - p2[l]).abs() <= limit_bd)
& ((p2[l] - p1[l]).abs() <= limit_bd)
& (d_p1p0 <= limit_bd)
& (d_q1q0 <= limit_bd)
& ((q2[l] - q1[l]).abs() <= limit_bd)
& ((q3[l] - q2[l]).abs() <= limit_bd)
& ((p0[l] - q0[l]).abs() * 2 + (p1[l] - q1[l]).abs() / 2 <= blimit_bd);
hev[l] = (d_p1p0 > thresh_bd) | (d_q1q0 > thresh_bd);
flat[l] = can_flat
& mask[l]
& (d_p1p0 <= one)
& (d_q1q0 <= one)
& ((p2[l] - p0[l]).abs() <= one)
& ((q2[l] - q0[l]).abs() <= one)
& ((p3[l] - p0[l]).abs() <= one)
& ((q3[l] - q0[l]).abs() <= one);
}
if !mask.iter().fold(false, |a, &m| a | m) {
return;
}
if can_flat2 {
for l in 0..LANES {
flat2[l] = flat[l]
& ((v[0][l] - p0[l]).abs() <= one)
& ((v[1][l] - p0[l]).abs() <= one)
& ((v[2][l] - p0[l]).abs() <= one)
& ((v[3][l] - p0[l]).abs() <= one)
& ((v[12][l] - q0[l]).abs() <= one)
& ((v[13][l] - q0[l]).abs() <= one)
& ((v[14][l] - q0[l]).abs() <= one)
& ((v[15][l] - q0[l]).abs() <= one);
}
}
let any_flat = flat.iter().fold(false, |a, &f| a | f);
let any_flat2 = flat2.iter().fold(false, |a, &f| a | f);
// 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.clamp(lo, 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(if hev[l] { c(ps1 - qs1) } else { 0 } + 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] = if narrow { c(qs0 - filter1) + off } else { out[8][l] };
out[7][l] = if narrow { c(ps0 + filter2) + off } else { out[7][l] };
out[9][l] = if outer { c(qs1 - f) + off } else { out[9][l] };
out[6][l] = if outer { c(ps1 + f) + off } else { 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 {
if flat[l] && !flat2[l] {
out[k][l] = w8[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 {
if flat2[l] {
out[k][l] = w16[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);

315
crates/vp9/src/probs.rs Normal file
View File

@ -0,0 +1,315 @@
//! Probability tables (a "frame context"), syntax element counters and the backward adaptation
//! processes of section 8.4.
use crate::tables::*;
/// 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.try_into().expect("table size")
}
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);
}
};
}
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);
for (a, b) in self.token.iter_mut().zip(o.token.iter()) {
for (a, b) in a.iter_mut().zip(b.iter()) {
for (a, b) in a.iter_mut().zip(b.iter()) {
for (a, b) in a.iter_mut().zip(b.iter()) {
add_nested!(a, b, 2);
}
}
}
}
for (a, b) in self.more_coefs.iter_mut().zip(o.more_coefs.iter()) {
for (a, b) in a.iter_mut().zip(b.iter()) {
for (a, b) in a.iter_mut().zip(b.iter()) {
for (a, b) in a.iter_mut().zip(b.iter()) {
add_nested!(a, b, 2);
}
}
}
}
}
}
/// 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

285
crates/vp9/src/tables.rs Normal file
View File

@ -0,0 +1,285 @@
//! 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::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}");
}
}
}
}
}

1082
crates/vp9/src/tile.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,336 @@
//! 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::boolcoder::BoolDecoder;
use crate::error::Error;
use crate::frame::Mv;
use crate::inter::{self, RefPlane};
use crate::intra::{self, IntraEdge};
use crate::tables::*;
use crate::transform;
/// round_mv_comp_q2 / q4 (8.5.2.1).
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, base_x + 4 * x, base_y + 4 * y, 4, 4, y * n4w + x);
}
}
} else {
self.predict_inter(b, plane, base_x, base_y, n4w * 4, 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, 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,
tx_sz,
tx_type,
self.h.lossless,
eob,
rows,
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]
}
}
/// 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.
#[allow(clippy::too_many_arguments)]
fn tokens(&mut self, bd: &mut BoolDecoder, b: &Blk, plane: usize, start_x: usize, start_y: usize, tx_sz: u8, tx_type: u8) -> (usize, usize) {
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 ptype = (plane > 0) as usize;
let rtype = b.is_inter as usize;
let probs = &self.fc.coef[tx_sz as usize][ptype][rtype];
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];
}
}
let ctx0 = (above + left) as usize;
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 counting = self.s.counting;
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;
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 {
counts.more_coefs[tx_sz as usize][ptype][rtype][band][ctx][more as usize] += 1;
}
if !more {
break;
}
}
if !bd.read_bool(p[1]) {
tc[pos] = 0;
if counting {
counts.token[tx_sz as usize][ptype][rtype][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 {
counts.token[tx_sz as usize][ptype][rtype][band][ctx][1] += 1;
}
token = 1;
coef = 1;
} else {
if counting {
counts.token[tx_sz as usize][ptype][rtype][band][ctx][2] += 1;
}
let pp = &pareto[p[2].max(1) as usize - 1];
token = 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)
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);
}
}
coef = v;
}
tc[pos] = ENERGY_CLASS[token];
let sign = bd.read_bool(128);
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.
#[allow(clippy::too_many_arguments)]
fn predict_inter(&mut self, b: &Blk, plane: usize, x: usize, y: usize, w: usize, h: usize, block_idx: usize) {
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 p = &f.planes[plane];
let rp = RefPlane {
data: &p.data,
stride: p.stride,
last_x: ((f.width as i32 + sx as i32) >> sx) - 1,
last_y: ((f.height as i32 + sy as i32) >> sy) - 1,
};
let out = if rl == 0 { &mut self.pred } else { &mut self.pred2 };
inter::predict(&rp, start_x, start_y, step_x, step_y, w, h, b.interp_filter, self.bit_depth, 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]);
}
}
}
}

341
crates/vp9/src/transform.rs Normal file
View File

@ -0,0 +1,341 @@
//! 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::tables::{ADST_ADST, ADST_DCT, DCT_ADST, DCT_DCT};
use crate::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) => {
let r = narrow::idct4(buf[..4].try_into().unwrap());
buf[..4].copy_from_slice(&r);
}
(4, true) => {
let r = iadst4_narrow(buf[..4].try_into().unwrap());
buf[..4].copy_from_slice(&r);
}
(8, false) => {
let r = narrow::idct8(buf[..8].try_into().unwrap());
buf[..8].copy_from_slice(&r);
}
(8, true) => {
let r = narrow::iadst8(buf[..8].try_into().unwrap());
buf[..8].copy_from_slice(&r);
}
(16, false) => {
let r = narrow::idct16(buf[..16].try_into().unwrap());
buf[..16].copy_from_slice(&r);
}
(16, true) => {
let r = narrow::iadst16(buf[..16].try_into().unwrap());
buf[..16].copy_from_slice(&r);
}
_ => {
let r = narrow::idct32(buf[..32].try_into().unwrap());
buf[..32].copy_from_slice(&r);
}
}
}
#[inline(always)]
fn tx1d_wide(buf: &mut [i64], n: usize, adst: bool) {
match (n, adst) {
(4, false) => {
let r = wide::idct4(buf[..4].try_into().unwrap());
buf[..4].copy_from_slice(&r);
}
(4, true) => {
let r = iadst4_wide(buf[..4].try_into().unwrap());
buf[..4].copy_from_slice(&r);
}
(8, false) => {
let r = wide::idct8(buf[..8].try_into().unwrap());
buf[..8].copy_from_slice(&r);
}
(8, true) => {
let r = wide::iadst8(buf[..8].try_into().unwrap());
buf[..8].copy_from_slice(&r);
}
(16, false) => {
let r = wide::idct16(buf[..16].try_into().unwrap());
buf[..16].copy_from_slice(&r);
}
(16, true) => {
let r = wide::iadst16(buf[..16].try_into().unwrap());
buf[..16].copy_from_slice(&r);
}
_ => {
let r = wide::idct32(buf[..32].try_into().unwrap());
buf[..32].copy_from_slice(&r);
}
}
}
/// 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)
}
/// 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).
#[allow(clippy::too_many_arguments)]
pub fn inverse_transform_add(coefs: &[i32], tx_size: u8, tx_type: u8, lossless: bool, eob: usize, rows: usize, bit_depth: u8, dst: &mut [u16], stride: usize) {
let n = 4usize << tx_size;
let max = (1i32 << bit_depth) - 1;
if lossless {
let mut t = [0i32; 16];
for i in 0..4 {
let r = iwht4(coefs[i * 4..i * 4 + 4].try_into().unwrap(), 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);
}
}
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;
}
}
}
/// 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, tx, DCT_DCT, false, 1, 1, 8, &mut a, n);
inverse_transform_add(&coefs, tx, DCT_DCT, false, 2, n, 8, &mut b, n);
assert_eq!(a, b, "tx {tx} dc {dc}");
}
}
}
#[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, 0, DCT_DCT, true, 1, 4, 8, &mut dst, 4);
let sum: i32 = dst.iter().map(|&v| v as i32 - 100).sum();
assert_eq!(sum, 16);
}
}

View File

@ -0,0 +1,480 @@
// 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);
}

View File

@ -0,0 +1,397 @@
//! Test fixture generation (ffmpeg + libvpx-vp9 as external encoder and oracle), a tiny IVF
//! reader, and comparison helpers.
#![allow(dead_code)]
use filmcraft_vp9::{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"]),
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"]),
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> {
for p in ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"] {
if Path::new(p).exists() {
return Some(PathBuf::from(p));
}
}
None
}
pub fn fixtures_dir() -> PathBuf {
let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/vp9");
std::fs::create_dir_all(&d).expect("create fixtures dir");
d
}
/// 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)> {
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, filmcraft_vp9::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> {
for threads in [1, 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}"))?;
}
Ok(())
}

View File

@ -0,0 +1,141 @@
//! Bit-exactness against ffmpeg's VP9 decoder on libvpx-vp9 fixtures (skipped when ffmpeg or
//! libvpx is absent).
mod 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,
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,
);
/// 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 = filmcraft_vp9::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| filmcraft_vp9::is_keyframe(chunks[i])).collect();
assert_eq!(keys.first(), Some(&0));
assert!(keys.len() >= 2, "key frames {keys:?}");
let info = filmcraft_vp9::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 = filmcraft_vp9::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);
}
}
}

47
crates/vp9/tests/perf.rs Normal file
View File

@ -0,0 +1,47 @@
//! Decode speed (ignored by default; run with
//! `cargo test --release -p filmcraft-vp9 --test perf -- --ignored --nocapture`).
mod common;
use filmcraft_vp9::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();
}
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");
}

View File

@ -0,0 +1,106 @@
//! Damaged, truncated and random input must never panic or hang (errors are fine).
mod common;
use filmcraft_vp9::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());
}

View File

@ -0,0 +1,357 @@
//! 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.
mod 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);
c.args(["-f", "ivf"]).arg(&path);
if !common::run(&mut c) {
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 = filmcraft_vp9::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 = filmcraft_vp9::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);
}

View File

@ -0,0 +1,99 @@
#!/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 extract_tables.py vp9.txt > ../src/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
text = open(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)

View File

@ -0,0 +1,249 @@
#!/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/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 gen_transforms.py > ../src/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("}")

View File

@ -27,6 +27,7 @@ const LAYERS: &[(&str, u8)] = &[
("h264", 0),
("h264enc", 0),
("hevc", 0),
("vp9", 0),
("prores", 0),
("mjpeg", 0),
("dnx", 0),