#432: VP8 video decodes (clean-room pure-Rust VP8 decoder)

VP8 WebM showed black video: there was no VP8 decoder. New L0 crate
filmcraft-vp8, implemented from RFC 6386 sections 1-19 (tables extracted
from the RFC text, nothing from the attached reference decoder): every
coding tool, versions 0-3, hidden altref frames, size changes. Bit-exact
with libvpx on 62 libvpx VP8 test vectors, real-world FATE samples and
generated fixtures; never panics on corrupted input.

Wired into codecs for Matroska / WebM (V_VP8, including the alpha layer
of transparent WebM) and MP4 (vp08). VP8 colour is BT.601 (RFC 6386 9.2).
A hidden altref frame is a WebM block of its own with the time of the
frame that follows it: the VP8 time lookup now moves on to that shown
frame instead of repeating the previous one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MrCloudy2andClaude Opus 5.5 committed 2026-10-11 15:13:07 +02:00
1 parent 4e2d0bc2d3
commit 53a0d959fe
32 files changed
+3388 -11

No files matched your search

Generated
+8
View File
@@ -1836,6 +1836,7 @@ dependencies = [
"filmcraft-prores",
"filmcraft-testkit",
"filmcraft-time",
"filmcraft-vp8",
"filmcraft-vp9",
"image",
"log",
@@ -2306,6 +2307,13 @@ dependencies = [
"web-time",
]
[[package]]
name = "filmcraft-vp8"
version = "0.6.0"
dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "filmcraft-vp9"
version = "0.6.0"
+4
View File
@@ -26,6 +26,7 @@ filmcraft-ac3 = { path = "crates/ac3" }
filmcraft-ogg = { path = "crates/ogg" }
filmcraft-h264 = { path = "crates/h264" }
filmcraft-hevc = { path = "crates/hevc" }
filmcraft-vp8 = { path = "crates/vp8" }
filmcraft-vp9 = { path = "crates/vp9" }
filmcraft-prores = { path = "crates/prores" }
filmcraft-dnx = { path = "crates/dnx" }
@@ -125,6 +126,9 @@ opt-level = 3
[profile.dev.package.filmcraft-opus]
opt-level = 3
[profile.dev.package.filmcraft-vp8]
opt-level = 3
[profile.dev.package.filmcraft-vp9]
opt-level = 3
+1
View File
@@ -175,6 +175,7 @@ No FFmpeg inside. The video codecs, AAC, Opus and the containers are our own Rus
| **HEVC / H.265** | ✓ | | Main and Main 10, bit-exact on 41 streams (tiles, WPP, PCM, long-term references), about 225 fps at 1080p |
| **Apple ProRes** | ✓ | ✓ | Decodes 422 Proxy to 4444 XQ; export writes 422 HQ |
| **AAC-LC** | ✓ | ✓ | |
| **VP8** | ✓ | | Every coding tool, bitstream versions 0–3, hidden altref frames, alpha layer of transparent WebM; bit-exact with libvpx on its published VP8 test vectors; in WebM/Matroska and MP4 |
| **VP9** | ✓ | | Profiles 0–3, 8/10/12-bit, 4:2:0 to 4:4:4, tiles and superframes; bit-exact on 50+ streams; in WebM/Matroska and MP4 |
| **Opus** | ✓ | | SILK, CELT, hybrid and multistream surround; range-exact on every RFC 8251 conformance vector; in WebM/Matroska and MP4 |
| **AV1** | ✓ | | Main profile, 8/10-bit 4:2:0 and monochrome, every coding tool incl. film grain, superres and spatial layers; bit-exact with libdav1d on the libaom vectors tested; in MP4 and WebM/Matroska |
+1
View File
@@ -24,6 +24,7 @@ filmcraft-ogg = { workspace = true }
filmcraft-bitstream = { workspace = true }
filmcraft-h264 = { workspace = true }
filmcraft-hevc = { workspace = true }
filmcraft-vp8 = { workspace = true }
filmcraft-vp9 = { workspace = true }
filmcraft-prores = { workspace = true }
filmcraft-dnx = { workspace = true }
+2 -1
View File
@@ -70,9 +70,10 @@ impl From<CodecError> for filmcraft_media::MediaError {
}
/// Our own (pure-Rust) decoders, in the order they are tried.
const BUILTIN_FACTORIES: [VideoDecoderFactory; 9] = [
const BUILTIN_FACTORIES: [VideoDecoderFactory; 10] = [
video::h264_factory,
video::hevc_factory,
video::vp8_factory,
video::vp9_factory,
video::av1_factory,
video::apv_factory,
+35 -4
View File
@@ -58,7 +58,7 @@ pub struct MkvSource {
/// Video codec as an ISO-BMFF sample entry (for the decoder factories).
ventry: Option<SampleEntry>,
video: GopCache,
/// The alpha layer of a VP9 track with `AlphaMode` 1 (WebM transparency, #402).
/// The alpha layer of a VP8 / VP9 track with `AlphaMode` 1 (WebM transparency, #402).
alpha: Option<Arc<crate::webm_alpha::AlphaLayer>>,
/// The playable audio tracks in file order: `info.audio_streams[k]` describes `audios[k]`.
audios: Vec<MkvAudio>,
@@ -232,6 +232,7 @@ fn vp9_config(private: &[u8], v: Option<&filmcraft_matroska::VideoInfo>) -> VpcC
fn sample_entry(c: &Codec, private: &[u8], v: Option<&filmcraft_matroska::VideoInfo>, w: u16, h: u16) -> Option<SampleEntry> {
Some(match c {
Codec::Vp8 => SampleEntry::video(FourCc(*b"vp08"), CodecConfig::Unknown { fourcc: FourCc(*b"vp08"), raw: Vec::new() }, w, h),
Codec::Vp9 { private } => SampleEntry::video(FourCc(*b"vp09"), CodecConfig::Vp9(vp9_config(private, v)), w, h),
Codec::Av1 { av1c } => SampleEntry::video(FourCc(*b"av01"), CodecConfig::Av1(filmcraft_isobmff::Av1Config::parse(av1c).unwrap_or_default()), w, h),
Codec::Apv { apvc } => SampleEntry::apv(filmcraft_isobmff::ApvConfig::parse(apvc).unwrap_or_default(), w, h),
@@ -429,9 +430,11 @@ impl MkvSource {
Some(MkvAudio { track: i, state, starts, preroll })
})
.collect();
// WebM transparency: VP9 alpha in each block's BlockAdditional (ID 1)
// WebM transparency: VP8 / VP9 alpha in each block's BlockAdditional (ID 1)
let alpha = vtrack.and_then(|i| file.tracks.get(i)).filter(|t| {
matches!(t.codec, Codec::Vp9 { .. }) && t.video.as_ref().is_some_and(|v| v.alpha_mode != 0) && t.samples.iter().any(|s| s.addition.is_some())
matches!(t.codec, Codec::Vp8 | Codec::Vp9 { .. })
&& t.video.as_ref().is_some_and(|v| v.alpha_mode != 0)
&& t.samples.iter().any(|s| s.addition.is_some())
});
let alpha = alpha.map(|t| Arc::new(crate::webm_alpha::AlphaLayer::new(bytes.clone(), t.samples.iter().map(|s| (s.pts, s.addition)))));
Ok(Self { info, bytes, file, vtrack, ventry, video: GopCache::new(explicit_color).with_rotation(rotation), alpha, audios })
@@ -441,6 +444,21 @@ impl MkvSource {
self.file.read_sample(&self.bytes, track, i).map_err(|e| CodecError::Container(e.to_string()))
}
/// Whether sample `i` of a VP8 track is a hidden frame (`show_frame` 0, RFC 6386 9.1): only the
/// first byte of the frame is read.
fn vp8_hidden(&self, track: usize, i: usize) -> bool {
let Some(t) = self.file.tracks.get(track) else { return false };
let Some(s) = t.samples.get(i) else { return false };
let first = match t.frame_prefix(u64::from(s.size)).first() {
Some(&b) => Some(b),
None => {
let mut b = [0u8; 1];
(s.size > 0 && self.bytes.0.read_at(s.offset, &mut b).is_ok()).then_some(b[0])
}
};
first.is_some_and(|b| b & 0x10 == 0)
}
fn audio_decoder(&self, c: &Codec, rate: u32) -> crate::Result<PacketDecoder> {
use symphonia::core::codecs::{CODEC_TYPE_FLAC, CODEC_TYPE_MP3, CODEC_TYPE_VORBIS};
match c {
@@ -534,7 +552,20 @@ impl VideoSamples for MkvVideo<'_> {
self.src.file.tracks[self.track].sync_sample_before(i)
}
fn sample_at(&self, t: i64) -> Option<usize> {
self.src.file.tracks[self.track].sample_at_pts(t)
let track = &self.src.file.tracks[self.track];
let i = track.sample_at_pts(t)?;
if !matches!(track.codec, Codec::Vp8) {
return Some(i);
}
// VP8 has no superframes: an encoder stores a hidden (altref) frame as a block of its own
// with the time of the frame that follows it, which vpxenc moves 1 ms later. The hidden
// frame shows nothing, so its time asks for that next shown frame (as decoders that pass
// the two on as one packet show it).
let mut k = i;
while k + 1 < track.samples.len() && k < i + 8 && self.src.vp8_hidden(self.track, k) {
k += 1;
}
Some(k)
}
fn read(&self, i: usize) -> crate::Result<Vec<u8>> {
self.src.read(self.track, i)
+1
View File
@@ -577,6 +577,7 @@ fn codec_label(c: &CodecConfig) -> String {
),
CodecConfig::Hevc(_) => "HEVC".into(),
CodecConfig::Vp9(c) => format!("VP9 (Profile {})", c.profile),
CodecConfig::Unknown { fourcc, .. } if fourcc.0 == *b"vp08" => "VP8".into(),
CodecConfig::Av1(c) => format!("AV1 ({} Profile)", ["Main", "High", "Professional"].get(c.seq_profile as usize).unwrap_or(&"Main")),
CodecConfig::Apv(a) => match filmcraft_apv::Profile::from_idc(a.profile_idc) {
Some(p) => p.name().into(),
+2
View File
@@ -42,6 +42,8 @@ pub fn from_codec_config(codec: &CodecConfig) -> Option<ColorCodes> {
CodecConfig::Avc(c) => avc(c),
CodecConfig::Hevc(c) => hevc(c),
CodecConfig::Av1(c) => av1(&c.config_obus),
// VP8 has no colour signalling: its one colour space is BT.601 YUV, studio swing (RFC 6386 9.2)
CodecConfig::Unknown { fourcc, .. } if fourcc.0 == *b"vp08" => Some(ColorCodes { primaries: 2, transfer: 2, matrix: 6, full_range: Some(false) }),
_ => None,
}
}
+113
View File
@@ -449,6 +449,119 @@ fn vp9_webm_alpha_layer_matches_libvpx() {
}
}
/// #432: VP8 in WebM with hidden altref frames (vpxenc, two-pass with ARNR: 11 of its 161
/// frames are not shown). Frames requested out of order (seeks back and forth, then sequential
/// playback across the hidden frames) are sample-exact against libvpx's decode, and the stream's
/// colour is BT.601 (RFC 6386 9.2). Skipped without ffmpeg or vpxenc.
#[test]
fn vp8_webm_random_access_matches_libvpx() {
let frames = 150usize;
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/codecs");
let path = dir.join("vp8_hidden_altref.webm");
if !path.exists() {
let Some(y4m) =
fixture_path("mandelbrot_352x288_150.y4m", &["-f", "lavfi", "-i", "mandelbrot=s=352x288:r=25", "-frames:v", "150", "-pix_fmt", "yuv420p"])
else {
return;
};
let tmp = dir.join(format!("vp8_hidden_altref.{}.webm", std::process::id()));
let enc = Command::new("vpxenc")
.args(["--codec=vp8", "--passes=2", "--auto-alt-ref=1", "--lag-in-frames=25", "--arnr-maxframes=7", "--arnr-strength=5"])
.args(["--target-bitrate=300", "--kf-max-dist=150", "-q", "-o"])
.arg(&tmp)
.arg(&y4m)
.status();
if !enc.is_ok_and(|s| s.success()) {
eprintln!("vpxenc not available; skipping");
return;
}
let _ = std::fs::rename(&tmp, &path);
}
let p = path.to_string_lossy().into_owned();
let Some(reference) =
fixture("vp8_hidden_altref.webm.yuv", &["-c:v", "libvpx", "-i", &p, "-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "yuv420p"])
else {
return;
};
let bytes: Arc<[u8]> = std::fs::read(&path).unwrap().into();
let src = crate::open_bytes("vp8_hidden_altref.webm", bytes).unwrap();
let info = src.info().clone();
assert_eq!(info.container, "WebM");
let v = info.video.as_ref().unwrap();
assert_eq!(v.codec, "VP8");
assert_eq!((v.width, v.height), (352, 288));
assert_eq!(v.color.matrix, filmcraft_color::Matrix::Bt601);
assert_eq!(reference.len(), frames * 352 * 288 * 3 / 2);
let frame_len = reference.len() / frames;
let rate = info.frame_rate();
let order: Vec<usize> = [147, 3, 90, 26, 24, 149, 0, 112, 61].into_iter().chain(40..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));
assert_eq!(f.color.matrix, filmcraft_color::Matrix::Bt601);
assert!(yuv_bytes(&f) == reference[k * frame_len..(k + 1) * frame_len], "frame {k} differs from libvpx");
}
}
/// #432 / #402: a VP8 WebM with an alpha layer (`AlphaMode` 1, a second VP8 stream in each
/// block's `BlockAdditional`, as browsers and GIF converters write transparent WebM) decodes
/// with that alpha; frames requested out of order match libvpx exactly, colour and alpha.
#[test]
fn vp8_webm_alpha_layer_matches_libvpx() {
let (w, h, frames) = (176usize, 144usize, 30usize);
let Some(path) = fixture_path(
"alpha_vp8.webm",
&[
"-f",
"lavfi",
"-i",
"testsrc2=s=176x144:r=25,format=yuva420p,geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='mod(X*2+N*7\\,256)'",
"-frames:v",
"30",
"-c:v",
"libvpx",
"-pix_fmt",
"yuva420p",
"-g",
"10",
"-auto-alt-ref",
"0",
"-b:v",
"600k",
],
) else {
return;
};
let src_path = path.to_string_lossy().into_owned();
let Some(reference) =
fixture("alpha_vp8.webm.yuva420p.yuv", &["-c:v", "libvpx", "-i", &src_path, "-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "yuva420p"])
else {
return;
};
let (luma, chroma) = (w * h, (w / 2) * (h / 2));
let frame_len = luma * 2 + chroma * 2;
if reference.len() != frame_len * frames {
eprintln!("libvpx reference has {} bytes, not {}; skipping", reference.len(), frame_len * frames);
return;
}
let bytes: Arc<[u8]> = std::fs::read(&path).unwrap().into();
let src = crate::open_bytes("alpha_vp8.webm", bytes).unwrap();
let info = src.info().clone();
assert_eq!(info.container, "WebM");
assert!(info.video.as_ref().unwrap().has_alpha);
let rate = info.frame_rate();
let order: Vec<usize> = [29, 3, 17, 12, 0, 25, 21, 9].into_iter().chain(10..frames).collect();
for k in order {
let f = src.video_frame(FrameRequest::full(rate.tick_of(k as i64))).unwrap();
let r = &reference[k * frame_len..(k + 1) * frame_len];
let filmcraft_frame::PixelData::Yuv8 { planes, alpha, .. } = &f.data else { panic!("frame {k}: not 8-bit YUV: {}", f.format_label()) };
assert_eq!(planes[0].as_slice(), &r[..luma], "frame {k}: luma");
assert_eq!(planes[1].as_slice(), &r[luma..luma + chroma], "frame {k}: U");
let alpha = alpha.as_ref().unwrap_or_else(|| panic!("frame {k}: no alpha plane"));
assert_eq!(alpha.as_slice(), &r[luma + 2 * chroma..], "frame {k}: alpha");
}
}
/// 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]
+54
View File
@@ -547,6 +547,60 @@ pub fn vp9_factory(e: &SampleEntry) -> Option<Result<Box<dyn VideoDecoder>>> {
}
}
/// Our pure-Rust VP8 decoder (RFC 6386; #432): WebM / Matroska `V_VP8` and MP4 `vp08`.
pub struct Vp8Decoder {
dec: filmcraft_vp8::Decoder,
}
impl Default for Vp8Decoder {
fn default() -> Self {
Self::new()
}
}
impl Vp8Decoder {
pub fn new() -> Self {
Self { dec: filmcraft_vp8::Decoder::new() }
}
fn convert(p: filmcraft_vp8::Picture) -> DecodedFrame {
use filmcraft_frame::{Chroma, PixelData};
use std::sync::Arc;
// color_space 0 is BT.601 YUV (RFC 6386 9.2), studio swing; a container's Colour wins
let color = filmcraft_color::ColorInfo { matrix: filmcraft_color::Matrix::Bt601, ..filmcraft_color::ColorInfo::REC709 };
let data = PixelData::Yuv8 { planes: [Arc::new(p.y), Arc::new(p.u), Arc::new(p.v)], chroma: Chroma::C420, alpha: None };
let frame = VideoFrame { width: p.width, height: p.height, data, color, par: (1, 1), pts: filmcraft_time::Tick::ZERO };
DecodedFrame { pts: p.pts, frame, draft: false }
}
}
impl VideoDecoder for Vp8Decoder {
fn decode(&mut self, sample: &[u8], pts: i64) -> Result<Vec<DecodedFrame>> {
let pic = self.dec.decode(sample, pts).map_err(|e| CodecError::Decode(e.to_string()))?;
Ok(pic.into_iter().map(Self::convert).collect())
}
fn flush(&mut self) -> Vec<DecodedFrame> {
// no reordering: every picture comes out with its frame
Vec::new()
}
fn reset(&mut self) {
self.dec.reset();
}
fn name(&self) -> &str {
"FilmCraft VP8"
}
fn is_random_access(&self, sample: &[u8]) -> Option<bool> {
Some(filmcraft_vp8::is_keyframe(sample))
}
fn is_disposable(&self, sample: &[u8]) -> bool {
self.dec.is_disposable(sample)
}
}
pub fn vp8_factory(e: &SampleEntry) -> Option<Result<Box<dyn VideoDecoder>>> {
(e.format.0 == *b"vp08").then(|| Ok(Box::new(Vp8Decoder::new()) as Box<dyn VideoDecoder>))
}
/// Our AV1 decoder. The `av1C` configuration OBUs (sequence header) are fed before the first
/// sample and again after every reset.
pub struct Av1Decoder {
+6 -5
View File
@@ -1,9 +1,10 @@
//! WebM alpha (#402): a VP9 track with `AlphaMode` 1 carries, in each block's `BlockAdditional`
//! with `BlockAddID` 1, a second VP9 stream coded independently of the colour one. Its luma plane,
//! WebM alpha (#402): a VP8 or VP9 track with `AlphaMode` 1 carries, in each block's
//! `BlockAdditional` with `BlockAddID` 1, a second stream of the same codec coded independently of
//! the colour one. Its luma plane,
//! full range, is the frame's alpha (the WebM convention, as libvpx-based decoders read it).
//!
//! [`AlphaDecoder`] wraps the colour decoder: every sample it decodes, it also decodes that
//! sample's alpha frame with a second (software) VP9 decoder and attaches the luma plane to the
//! sample's alpha frame with a second (software) decoder and attaches the luma plane to the
//! colour picture with the same timestamp, holding a colour picture back until its alpha is out. The alpha stream has key frames of its own, which need
//! not coincide with the colour ones: after a seek (or any other break in the order samples are
//! fed), the alpha decoder restarts from the nearest alpha key frame at or before the sample.
@@ -21,7 +22,7 @@ use crate::video::{DecodedFrame, VideoDecoder};
/// How far back the alpha decoder looks for a key frame when it resynchronises.
const MAX_RESYNC: usize = 600;
/// Decoded alpha pictures waiting for their colour picture, and colour pictures waiting for their
/// alpha (VP9 pictures come out in order, so a few are plenty; past that a picture goes out opaque
/// alpha (VP8 / VP9 pictures come out in order, so a few are plenty; past that a picture goes out opaque
/// rather than stall).
const MAX_PENDING: usize = 8;
@@ -106,7 +107,7 @@ impl AlphaDecoder {
self.alpha.reset();
self.pending.clear();
self.alpha_max = None;
let is_key = |k: usize| self.layer.read(k).is_some_and(|d| filmcraft_vp9::is_keyframe(&d));
let is_key = |k: usize| self.layer.read(k).is_some_and(|d| self.alpha.is_random_access(&d) == Some(true));
let start = (i.saturating_sub(MAX_RESYNC)..=i).rev().find(|&k| is_key(k));
if let Some(start) = start {
for k in start..i {
+1 -1
View File
@@ -1279,7 +1279,7 @@ pub fn system_report() -> Value {
"family": std::env::consts::FAMILY,
"arch": std::env::consts::ARCH,
"cpuThreads": cpus,
"decoders": ["H.264 (AVC)", "H.265 (HEVC) Main / Main 10", "VP9", "AV1 Main", "Apple ProRes", "Avid DNxHD / DNxHR", "Motion JPEG", "AAC-LC", "Opus", "PCM"],
"decoders": ["H.264 (AVC)", "H.265 (HEVC) Main / Main 10", "VP8", "VP9", "AV1 Main", "Apple ProRes", "Avid DNxHD / DNxHR", "Motion JPEG", "AAC-LC", "Opus", "PCM"],
"containers": ["MP4 / MOV (ISO BMFF)", "Matroska / WebM", "WAV", "PNG / JPEG stills"],
"exportFormats": formats,
"speechToText": crate::transcript::speech_available(),
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "filmcraft-vp8"
description = "Clean-room pure-Rust VP8 decoder (RFC 6386)"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
thiserror = { workspace = true }
+72
View File
@@ -0,0 +1,72 @@
# filmcraft-vp8
Clean-room, pure-Rust (no `unsafe`) VP8 decoder, implemented from RFC 6386, *VP8 Data Format and
Decoding Guide*, sections 1-19. It is an L0 crate: it depends only on `thiserror`, and builds for
`wasm32-unknown-unknown`.
The constant tables (coefficient and mode probabilities and their update probabilities, the
subblock mode contexts, quantizer lookups, MV probabilities, interpolation filters...) were
extracted from the text of the RFC by [`tools/extract_tables.py`](tools/extract_tables.py) into
`src/spec_tables.rs`; the script cuts the text at section 20, so nothing is read from the
reference decoder attached to the RFC. No code was taken or ported from libvpx, FFmpeg or any
other decoder; ffmpeg (with libvpx) and vpxenc are used only as external fixture generators and
test oracles.
## Status
| Area | Status |
|---|---|
| Frame tag, key frame header, frame header (segmentation, loop filter type / level / sharpness, deltas, partitions, quantizer indices, reference updates and sign bias, probability updates) | done |
| Boolean decoder, tree decoding, probability persistence across frames and `refresh_entropy_probs` | done |
| Intra prediction: 16x16 and chroma modes, the ten 4x4 subblock modes, key frame subblock mode contexts, edge rules | done |
| Inter prediction: last / golden / altref with sign bias, NEAREST / NEAR / ZERO / NEW / SPLIT (all four partitionings), MV decoding and clamping, six-tap and bilinear filters, full-pixel chroma (bitstream versions 0-3) | done |
| Coefficient tokens (1-8 partitions), dequantization (frame and segment quantizers), inverse WHT and DCT | done |
| Normal and simple loop filters, per-macroblock levels from segments and reference / mode deltas | done |
| Golden / altref copies, hidden (altref) frames, frame size changes at key frames | done |
| Damaged input: errors (never panics), decoding resumes at the next key frame | done |
| Upscaling hints of the key frame header (9.1) | not applied (reported in `KeyframeInfo`) |
Bit-exact with libvpx on the 62 libvpx VP8 test vectors checked (`vp80-00-comprehensive-001` to
`018` and the `vp80-01` to `vp80-06` intra, inter, segmentation, partition, sharpness and small
size vectors; `tests/vectors.rs` checks them when they are present) and on the generated fixtures
of `tests/conformance.rs`.
## API
```rust
use filmcraft_vp8::Decoder;
let mut dec = Decoder::new();
for (pts, frame) in frames { // one IVF frame / WebM block / MP4 sample each
let skippable = dec.is_disposable(frame); // could this frame be left out (catching up)?
if let Some(pic) = dec.decode(frame, pts)? {
// pic.width / pic.height, pic.chroma_width / chroma_height,
// pic.y / pic.u / pic.v: tight 8-bit 4:2:0 planes, pic.pts, pic.key,
// pic.color_space (0: BT.601 YUV)
}
// None: a hidden (altref) frame
}
dec.reset(); // forget the reference frames (seek): decoding resumes at the next key frame
let stats = dec.stats(); // frames, key frames, hidden frames
// Random access points
let key: bool = filmcraft_vp8::is_keyframe(frame);
let info: Option<filmcraft_vp8::KeyframeInfo> = filmcraft_vp8::keyframe_info(frame);
```
`filmcraft-codecs` wires the decoder into Matroska / WebM (`V_VP8`, including the alpha layer of
transparent WebM in `BlockAdditional`) and MP4 (`vp08`). VP8 has no colour signalling: pictures are
BT.601, studio swing (RFC 6386 9.2), unless the container's `Colour` says otherwise. VP8 has no
superframes either: a hidden altref frame is a block of its own, and the WebM reader takes a
request for its time as a request for the shown frame that follows it.
## Tests and tools
- `cargo test -p filmcraft-vp8`: unit tests, bit-exactness against libvpx on generated fixtures
(`target/fixtures/vp8-tests`, skipped without ffmpeg / libvpx), robustness (corrupted,
truncated, reordered and random input), and the libvpx test vectors when present in
`target/fixtures/vp8/conformance` (or `$VP8_TEST_VECTORS`).
- `cargo run --release -p filmcraft-vp8 --example vp8dec in.ivf out.yuv`: decode to raw I420.
- `cargo run -p filmcraft-vp8 --example fuzz -- <iterations> <seed> files.ivf...`: decode
randomly damaged variants of IVF files (in a debug build, so overflow panics too) and report
any panic.
+100
View File
@@ -0,0 +1,100 @@
//! Decode corrupted variants of IVF files and report panics (`fuzz iterations seed files...`).
//! Run in a debug build so arithmetic overflow panics too.
fn frames(data: &[u8]) -> Vec<Vec<u8>> {
let mut out = Vec::new();
let Some(h) = data.get(6..8) else { return out };
let mut pos = usize::from(u16::from_le_bytes([h[0], h[1]]));
while pos + 12 <= data.len() {
let size = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 12;
out.push(data[pos..(pos + size).min(data.len())].to_vec());
pos += size;
}
out
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let iterations: usize = args[1].parse().unwrap_or(100);
let mut seed: u64 = args[2].parse().unwrap_or(1);
let mut rnd = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
std::panic::set_hook(Box::new(|_| {}));
let mut panics = 0;
let mut decoded = 0u64;
for path in &args[3..] {
let original = frames(&std::fs::read(path).unwrap_or_default());
for it in 0..iterations {
let mut fr = original.clone();
let mode = rnd() % 6;
for f in fr.iter_mut() {
if f.is_empty() {
continue;
}
match mode {
0 => {
// a few bit flips per frame
for _ in 0..1 + rnd() % 4 {
let i = (rnd() as usize) % f.len();
f[i] ^= 1 << (rnd() % 8);
}
}
1 => {
let n = (rnd() as usize) % (f.len() + 1);
f.truncate(n);
}
2 => {
// random bytes in the header region (frame tag, sizes, first partition)
for _ in 0..1 + rnd() % 3 {
let i = (rnd() as usize) % f.len().min(24);
f[i] = rnd() as u8;
}
}
3 => {
for b in f.iter_mut().skip(10) {
if rnd() % 50 == 0 {
*b = rnd() as u8;
}
}
}
// trailing garbage on some frames
4 if rnd() % 3 == 0 => f.extend((0..rnd() % 64).map(|_| rnd() as u8)),
_ => {}
}
}
if mode == 5 {
// decode order shuffled: inter frames after the wrong references
for i in (1..fr.len()).rev() {
let j = (rnd() as usize) % (i + 1);
fr.swap(i, j);
}
}
let r = std::panic::catch_unwind(|| {
let mut dec = filmcraft_vp8::Decoder::new();
let mut n = 0u64;
for (k, f) in fr.iter().enumerate() {
let _ = dec.is_disposable(f);
if dec.decode(f, k as i64).is_ok() {
n += 1;
}
let _ = filmcraft_vp8::keyframe_info(f);
}
n
});
match r {
Ok(n) => decoded += n,
Err(e) => {
panics += 1;
let msg = e.downcast_ref::<String>().cloned().or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string())).unwrap_or_default();
eprintln!("PANIC {path} iteration {it} mode {mode}: {msg}");
}
}
}
}
eprintln!("done: {panics} panics, {decoded} frames decoded");
}
+40
View File
@@ -0,0 +1,40 @@
//! Decode an IVF file to raw I420 (`vp8dec in.ivf out.yuv`), for comparison with other decoders.
//! With `VP8DEC_SIZES` set, prints the size of each picture.
use std::io::Write;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let [_, input, output] = args.as_slice() else {
return Err("usage: vp8dec in.ivf out.yuv".into());
};
let data = std::fs::read(input)?;
let hdr_len = usize::from(u16::from_le_bytes([data[6], data[7]]));
let mut pos = hdr_len;
let mut out = std::io::BufWriter::new(std::fs::File::create(output)?);
let mut dec = filmcraft_vp8::Decoder::new();
let mut n = 0;
let t = std::time::Instant::now();
while pos + 12 <= data.len() {
let size = u32::from_le_bytes(data[pos..pos + 4].try_into()?) as usize;
let pts = i64::from_le_bytes(data[pos + 4..pos + 12].try_into()?);
pos += 12;
let frame = &data[pos..(pos + size).min(data.len())];
pos += size;
match dec.decode(frame, pts) {
Ok(Some(p)) => {
if std::env::var_os("VP8DEC_SIZES").is_some() {
println!("{} {}", p.width, p.height);
}
out.write_all(&p.y)?;
out.write_all(&p.u)?;
out.write_all(&p.v)?;
n += 1;
}
Ok(None) => {}
Err(e) => eprintln!("frame at pts {pts}: {e}"),
}
}
eprintln!("{n} pictures in {:.2}s; {:?}", t.elapsed().as_secs_f64(), dec.stats());
Ok(())
}
+218
View File
@@ -0,0 +1,218 @@
//! Boolean entropy decoder (RFC 6386 section 7).
//!
//! The specification keeps two bytes of the input in `value` and shifts in one byte every eight
//! renormalisation steps. This implementation keeps the active bits at the top of a 64-bit window
//! whose lower bits hold the next bytes of the input, which is equivalent and refills only every
//! few bytes. Reading past the end of the data yields zero bits.
pub struct BoolDecoder<'a> {
data: &'a [u8],
pos: usize,
/// The bits being decoded, most significant first: the top 8 bits correspond to the
/// specification's `value >> 8`, followed by `count` further bits of input.
value: u64,
count: i32,
range: u32,
}
impl<'a> BoolDecoder<'a> {
pub fn new(data: &'a [u8]) -> Self {
let mut d = BoolDecoder { data, pos: 0, value: 0, count: -8, range: 255 };
d.fill();
d
}
fn fill(&mut self) {
let mut shift = 48 - self.count;
while shift >= 0 {
match self.data.get(self.pos) {
Some(&b) => {
self.pos += 1;
self.value |= u64::from(b) << shift;
self.count += 8;
shift -= 8;
}
None => {
// past the end: zero bits for as long as anyone reads
self.count += 0x4000;
return;
}
}
}
}
/// `read_bool` (7.3): a bool whose probability of being zero is `p / 256`.
#[inline(always)]
pub fn read_bool(&mut self, p: u8) -> bool {
let split = 1 + (((self.range - 1) * u32::from(p)) >> 8);
if self.count < 0 {
self.fill();
}
let big_split = u64::from(split) << 56;
let bit = if self.value >= big_split {
self.range -= split;
self.value -= big_split;
true
} else {
self.range = split;
false
};
// renormalise: shift until range >= 128
let shift = self.range.leading_zeros().saturating_sub(24);
self.range <<= shift;
self.value <<= shift;
self.count -= shift as i32;
bit
}
/// `read_literal` (7.3): an `n`-bit unsigned value, most significant bit first, each bit at
/// probability 1/2.
#[inline]
pub fn read_literal(&mut self, n: u32) -> u32 {
let mut v = 0;
for _ in 0..n {
v = (v << 1) | u32::from(self.read_bool(128));
}
v
}
/// An `L(1)` flag.
#[inline]
pub fn read_flag(&mut self) -> bool {
self.read_bool(128)
}
/// A magnitude `L(n)` followed by a sign `L(1)` (1 = negative), as the frame header codes its
/// signed fields.
pub fn read_signed(&mut self, n: u32) -> i32 {
let v = self.read_literal(n) as i32;
if self.read_flag() { -v } else { v }
}
/// An optional signed field: a flag, then [`Self::read_signed`] when it is set, else 0.
pub fn read_optional_signed(&mut self, n: u32) -> i32 {
if self.read_flag() { self.read_signed(n) } else { 0 }
}
/// `treed_read` (8.1): a tree-coded value. `tree` uses the specification's layout: positive
/// entries index deeper nodes, non-positive entries `-v` are leaves of value `v`; `probs[i]`
/// is the probability of node `2 * i`.
#[inline]
pub fn read_tree(&mut self, tree: &[i8], probs: &[u8]) -> u8 {
self.read_tree_from(tree, probs, 0)
}
/// [`Self::read_tree`] starting at node `start` instead of the root.
#[inline]
pub fn read_tree_from(&mut self, tree: &[i8], probs: &[u8], start: usize) -> u8 {
let mut i = start;
loop {
let p = probs.get(i >> 1).copied().unwrap_or(128);
let next = tree.get(i + usize::from(self.read_bool(p))).copied().unwrap_or(0);
if next <= 0 {
return (-next) as u8;
}
i = next as usize;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The encoder of section 7.3, to round-trip the decoder.
struct BoolEncoder {
out: Vec<u8>,
range: u32,
bottom: u32,
bit_count: i32,
}
impl BoolEncoder {
fn new() -> Self {
BoolEncoder { out: Vec::new(), range: 255, bottom: 0, bit_count: 24 }
}
fn add_one(&mut self) {
let mut i = self.out.len();
while i > 0 {
i -= 1;
if self.out[i] == 255 {
self.out[i] = 0;
} else {
self.out[i] += 1;
break;
}
}
}
fn write(&mut self, p: u8, bit: bool) {
let split = 1 + (((self.range - 1) * u32::from(p)) >> 8);
if bit {
self.bottom = self.bottom.wrapping_add(split);
self.range -= split;
} else {
self.range = split;
}
while self.range < 128 {
self.range <<= 1;
if self.bottom & (1 << 31) != 0 {
self.add_one();
}
self.bottom <<= 1;
self.bit_count -= 1;
if self.bit_count == 0 {
self.out.push((self.bottom >> 24) as u8);
self.bottom &= (1 << 24) - 1;
self.bit_count = 8;
}
}
}
fn finish(mut self) -> Vec<u8> {
let mut c = self.bit_count;
let mut v = self.bottom;
if v & (1u32.wrapping_shl((32 - c) as u32)) != 0 {
self.add_one();
}
v <<= c & 7;
c >>= 3;
while c > 0 {
c -= 1;
v <<= 8;
}
for _ in 0..4 {
self.out.push((v >> 24) as u8);
v <<= 8;
}
self.out
}
}
#[test]
fn round_trips_bools_at_every_probability() {
let mut seed = 0x1234_5678u32;
let mut rnd = || {
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
seed
};
let bits: Vec<(u8, bool)> = (0..20_000).map(|_| (((rnd() % 255) + 1) as u8, rnd() % 3 == 0)).collect();
let mut e = BoolEncoder::new();
for &(p, b) in &bits {
e.write(p, b);
}
let data = e.finish();
let mut d = BoolDecoder::new(&data);
for (i, &(p, b)) in bits.iter().enumerate() {
assert_eq!(d.read_bool(p), b, "bool {i}");
}
}
#[test]
fn reads_zeros_past_the_end() {
let mut d = BoolDecoder::new(&[]);
for _ in 0..1000 {
assert!(!d.read_bool(128));
}
}
}
+804
View File
@@ -0,0 +1,804 @@
//! Frame decoding: macroblock prediction records (11, 16, 17), coefficients (13), reconstruction
//! (12, 14, 18), the loop filter (15) and reference frame management (9.7, 9.8).
use std::sync::Arc;
use crate::Picture;
use crate::boolcoder::BoolDecoder;
use crate::error::{Error, Result, ensure};
use crate::header::{EntropyContext, FrameHeader, FrameTag, LfDeltas, Segmentation, read_header};
use crate::loopfilter::{self, Kind, Params};
use crate::predict::{self, RefPlane};
use crate::spec_tables::*;
use crate::tables::*;
use crate::transform;
/// Largest frame dimension in macroblocks (14-bit sizes).
const MAX_MBS: usize = 16384 / 16 + 1;
/// A reconstructed frame: planes sized to whole macroblocks.
#[derive(Clone)]
struct Frame {
y: Vec<u8>,
u: Vec<u8>,
v: Vec<u8>,
/// Luma width / height (multiples of 16); chroma is half.
w: usize,
h: usize,
}
impl Frame {
fn new(w: usize, h: usize) -> Frame {
Frame { y: vec![0; w * h], u: vec![0; w * h / 4], v: vec![0; w * h / 4], w, h }
}
}
/// A motion vector in quarter luma pixels.
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
struct Mv {
row: i32,
col: i32,
}
impl Mv {
const ZERO: Mv = Mv { row: 0, col: 0 };
fn is_zero(self) -> bool {
self == Mv::ZERO
}
}
/// What later macroblocks (and the loop filter) need to know about a decoded macroblock.
#[derive(Clone, Copy, Default)]
struct MbInfo {
/// Intra mode (`DC_PRED`..`B_PRED`) or inter mode (`MV_NEAREST`..`MV_SPLIT`).
mode: u8,
/// 0 intra, 1 last, 2 golden, 3 altref.
ref_frame: u8,
mv: Mv,
/// Motion vector of each Y subblock (all `mv` unless split).
bmv: [Mv; 16],
}
/// Statistics of the frames decoded so far.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DecodeStats {
pub frames: u64,
pub key_frames: u64,
pub hidden_frames: u64,
}
/// Dequantization factors of one segment: [DC, AC] for Y, Y2 and chroma (14.1).
#[derive(Clone, Copy, Default)]
struct Dequant {
y: [i32; 2],
y2: [i32; 2],
uv: [i32; 2],
}
impl Dequant {
fn new(q: i32, h: &FrameHeader) -> Dequant {
let dc = |d: i32| i32::from(DC_QLOOKUP[(q + d).clamp(0, 127) as usize]);
let ac = |d: i32| i32::from(AC_QLOOKUP[(q + d).clamp(0, 127) as usize]);
let qi = &h.quant;
Dequant {
y: [dc(qi.y_dc_delta), ac(0)],
y2: [dc(qi.y2_dc_delta) * 2, (ac(qi.y2_ac_delta) * 155 / 100).max(8)],
uv: [dc(qi.uv_dc_delta).min(132), ac(qi.uv_ac_delta)],
}
}
}
/// VP8 decoder (RFC 6386). Feed it one compressed frame at a time ([`Decoder::decode`]).
pub struct Decoder {
ctx: EntropyContext,
seg: Segmentation,
lf: LfDeltas,
/// Segment of each macroblock (persists while the map is not updated).
seg_map: Vec<u8>,
width: usize,
height: usize,
mb_w: usize,
mb_h: usize,
/// Last, golden and altref frames.
refs: [Option<Arc<Frame>>; 3],
/// `ref_frame_sign_bias`, indexed by reference frame (0 intra, 1 last, 2 golden, 3 altref).
sign_bias: [bool; 4],
stats: DecodeStats,
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
impl Decoder {
pub fn new() -> Decoder {
Decoder {
ctx: EntropyContext::default(),
seg: Segmentation::default(),
lf: LfDeltas::default(),
seg_map: Vec::new(),
width: 0,
height: 0,
mb_w: 0,
mb_h: 0,
refs: [None, None, None],
sign_bias: [false; 4],
stats: DecodeStats::default(),
}
}
/// Forget the reference frames (after a seek): decoding resumes at the next key frame.
pub fn reset(&mut self) {
self.refs = [None, None, None];
}
pub fn stats(&self) -> DecodeStats {
self.stats
}
/// Whether leaving `frame` out, decoded next, would change no other picture: an inter frame
/// that refreshes and copies no reference buffer, does not update the segment map, and leaves
/// the probabilities, segment data and loop filter deltas as they are now (or keeps its
/// probability updates to itself). Only the frame header is read.
pub fn is_disposable(&self, frame: &[u8]) -> bool {
let Ok(tag) = FrameTag::parse(frame) else { return false };
if tag.key_frame || self.refs[0].is_none() {
return false;
}
let Some(first) = frame.get(tag.header_len..tag.header_len.saturating_add(tag.first_part_size)) else { return false };
let mut d = BoolDecoder::new(first);
let (mut seg, mut lf, mut ctx, mut saved) = (self.seg, self.lf, self.ctx.clone(), None);
let h = read_header(&mut d, false, &mut seg, &mut lf, &mut ctx, &mut saved);
let refs_untouched = !h.refresh_golden && !h.refresh_altref && !h.refresh_last && h.copy_to_golden == 0 && h.copy_to_altref == 0;
let probs_kept = !h.refresh_entropy || ctx == self.ctx;
let segments_kept = !seg.update_map && seg.absolute == self.seg.absolute && seg.quant == self.seg.quant && seg.lf_level == self.seg.lf_level;
let deltas_kept = lf.ref_deltas == self.lf.ref_deltas && lf.mode_deltas == self.lf.mode_deltas;
refs_untouched && probs_kept && segments_kept && deltas_kept
}
/// Decode one compressed frame. Returns the picture when the frame is meant to be shown
/// (`show_frame`), `None` for a hidden frame (an altref).
pub fn decode(&mut self, data: &[u8], pts: i64) -> Result<Option<Picture>> {
let tag = FrameTag::parse(data)?;
if tag.key_frame {
ensure!(tag.width > 0 && tag.height > 0, "key frame of size {}x{}", tag.width, tag.height);
let (w, h) = (usize::from(tag.width), usize::from(tag.height));
if w != self.width || h != self.height {
self.width = w;
self.height = h;
self.mb_w = w.div_ceil(16);
self.mb_h = h.div_ceil(16);
self.seg_map = vec![0; self.mb_w * self.mb_h];
}
// key frame defaults (9.3, 9.4, 9.7, 13.4, 16.1, 17.2)
self.ctx = EntropyContext::default();
self.seg.absolute = false;
self.seg.quant = [0; 4];
self.seg.lf_level = [0; 4];
self.lf.ref_deltas = [0; 4];
self.lf.mode_deltas = [0; 4];
self.sign_bias = [false; 4];
} else if self.refs[0].is_none() {
return Err(Error::MissingReference("inter frame before the first key frame".into()));
}
ensure!(self.mb_w > 0 && self.mb_w <= MAX_MBS && self.mb_h <= MAX_MBS, "no frame size");
let first_start = tag.header_len;
let first_end = first_start.saturating_add(tag.first_part_size);
ensure!(first_end <= data.len(), "first partition ({} bytes) runs past the frame ({} bytes)", tag.first_part_size, data.len());
let mut bd = BoolDecoder::new(&data[first_start..first_end]);
let mut saved = None;
let hdr = read_header(&mut bd, tag.key_frame, &mut self.seg, &mut self.lf, &mut self.ctx, &mut saved);
if !tag.key_frame {
self.sign_bias[2] = hdr.sign_bias_golden;
self.sign_bias[3] = hdr.sign_bias_altref;
}
// token partitions (9.5): sizes of all but the last, then the data
let n = hdr.partitions;
let sizes_start = first_end;
let mut pos = sizes_start.saturating_add(3 * (n - 1)).min(data.len());
let mut parts: Vec<&[u8]> = Vec::with_capacity(n);
for i in 0..n {
let size = if i + 1 < n {
let s = sizes_start + 3 * i;
match data.get(s..s + 3) {
Some(b) => usize::from(b[0]) | usize::from(b[1]) << 8 | usize::from(b[2]) << 16,
None => 0,
}
} else {
data.len() - pos
};
let end = pos.saturating_add(size).min(data.len());
parts.push(&data[pos..end]);
pos = end;
}
let mut token_decoders: Vec<BoolDecoder> = parts.into_iter().map(BoolDecoder::new).collect();
let mut frame = Frame::new(self.mb_w * 16, self.mb_h * 16);
let dequant: [Dequant; 4] = std::array::from_fn(|s| {
let q = if self.seg.enabled {
let v = i32::from(self.seg.quant[s]);
if self.seg.absolute { v } else { hdr.quant.y_ac + v }
} else {
hdr.quant.y_ac
};
Dequant::new(q.clamp(0, 127), &hdr)
});
let filters: &[i32; 48] = if tag.version == 0 || tag.version > 3 { &SIXTAP_FILTERS } else { &BILINEAR_FILTERS };
let full_pixel = tag.version == 3;
let stride_info = self.mb_w + 1;
let mut infos = vec![MbInfo::default(); stride_info * (self.mb_h + 1)];
let mut lf_info: Vec<(u8, bool)> = vec![(0, false); self.mb_w * self.mb_h];
// contexts: B_PRED modes above / left (key frames), non-zero flags above / left
let mut above_bmodes = vec![[B_DC_PRED; 4]; self.mb_w];
let mut above_nz = vec![[0u8; 9]; self.mb_w];
let refs = self.refs.clone();
for mb_y in 0..self.mb_h {
let mut left_bmodes = [B_DC_PRED; 4];
let mut left_nz = [0u8; 9];
let part = mb_y % n;
for mb_x in 0..self.mb_w {
let idx = (mb_y + 1) * stride_info + mb_x + 1;
// ---- prediction record (first partition)
let seg_idx = mb_y * self.mb_w + mb_x;
if self.seg.update_map {
let s = bd.read_tree(&SEGMENT_TREE, &self.seg.tree_probs);
if let Some(m) = self.seg_map.get_mut(seg_idx) {
*m = s;
}
}
let segment = if self.seg.enabled { self.seg_map.get(seg_idx).copied().unwrap_or(0) as usize & 3 } else { 0 };
let coeff_skip = hdr.skip_prob.is_some_and(|p| bd.read_bool(p));
let mut bmodes = [B_DC_PRED; 16];
let mut uv_mode = DC_PRED;
let mut info = MbInfo::default();
if tag.key_frame {
info.mode = bd.read_tree(&KF_YMODE_TREE, &KF_YMODE_PROBS);
if info.mode == B_PRED {
for b in 0..16 {
let a = if b < 4 { above_bmodes[mb_x][b] } else { bmodes[b - 4] };
let l = if b & 3 == 0 { left_bmodes[b >> 2] } else { bmodes[b - 1] };
let o = (usize::from(a) * 10 + usize::from(l)) * 9;
bmodes[b] = bd.read_tree(&BMODE_TREE, &KF_BMODE_PROBS[o..o + 9]);
}
} else {
bmodes = [implied_bmode(info.mode); 16];
}
uv_mode = bd.read_tree(&UV_MODE_TREE, &KF_UV_MODE_PROBS);
} else if bd.read_bool(hdr.prob_intra) {
info.ref_frame = if bd.read_bool(hdr.prob_last) { 2 + u8::from(bd.read_bool(hdr.prob_golden)) } else { 1 };
self.read_inter_modes(&mut bd, &infos, idx, stride_info, mb_x, mb_y, &mut info);
} else {
info.mode = bd.read_tree(&YMODE_TREE, &self.ctx.ymode);
if info.mode == B_PRED {
for m in &mut bmodes {
*m = bd.read_tree(&BMODE_TREE, &BMODE_PROBS);
}
}
uv_mode = bd.read_tree(&UV_MODE_TREE, &self.ctx.uv_mode);
}
above_bmodes[mb_x] = [bmodes[12], bmodes[13], bmodes[14], bmodes[15]];
left_bmodes = [bmodes[3], bmodes[7], bmodes[11], bmodes[15]];
if let Some(slot) = infos.get_mut(idx) {
*slot = info;
}
// ---- residual (token partition)
let has_y2 = info.mode != B_PRED && info.mode != MV_SPLIT;
let mut coeffs = [[0i16; 16]; 25];
let mut nonzero = [false; 25];
let td = &mut token_decoders[part];
let mut has_coeffs = false;
if coeff_skip {
let keep_y2 = if has_y2 { 0 } else { left_nz[8] };
let keep_y2_above = if has_y2 { 0 } else { above_nz[mb_x][8] };
left_nz = [0; 9];
above_nz[mb_x] = [0; 9];
left_nz[8] = keep_y2;
above_nz[mb_x][8] = keep_y2_above;
} else {
let dq = &dequant[segment];
let an = &mut above_nz[mb_x];
let first = if has_y2 {
let ctx = usize::from(an[8] + left_nz[8]);
let eob = read_coefficients(td, &self.ctx.coeff[1], ctx, 0, dq.y2, &mut coeffs[24]);
let nz = u8::from(eob > 0);
an[8] = nz;
left_nz[8] = nz;
nonzero[24] = eob > 0;
has_coeffs |= eob > 0;
1
} else {
0
};
let y_probs = if has_y2 { &self.ctx.coeff[0] } else { &self.ctx.coeff[3] };
for b in 0..16 {
let (bx, by) = (b & 3, b >> 2);
let ctx = usize::from(an[bx] + left_nz[by]);
let eob = read_coefficients(td, y_probs, ctx, first, dq.y, &mut coeffs[b]);
let nz = eob > first;
an[bx] = u8::from(nz);
left_nz[by] = u8::from(nz);
nonzero[b] = nz;
has_coeffs |= nz;
}
for b in 16..24 {
let (off, k) = if b < 20 { (4, b - 16) } else { (6, b - 20) };
let (bx, by) = (k & 1, k >> 1);
let ctx = usize::from(an[off + bx] + left_nz[off + by]);
let eob = read_coefficients(td, &self.ctx.coeff[2], ctx, 0, dq.uv, &mut coeffs[b]);
let nz = eob > 0;
an[off + bx] = u8::from(nz);
left_nz[off + by] = u8::from(nz);
nonzero[b] = nz;
has_coeffs |= nz;
}
if has_y2 && nonzero[24] {
let dc = transform::inverse_wht(&coeffs[24]);
for b in 0..16 {
coeffs[b][0] = dc[b];
nonzero[b] |= dc[b] != 0;
}
}
}
// ---- loop filter level (15.1, 9.3, 9.4)
let level = self.filter_level(&hdr, segment, &info);
let inner = !has_y2 || has_coeffs;
if let Some(l) = lf_info.get_mut(seg_idx) {
*l = (level, inner);
}
// ---- reconstruction
let (px, py) = (mb_x * 16, mb_y * 16);
let ys = frame.w;
let cs = frame.w / 2;
if info.ref_frame == 0 {
if info.mode == B_PRED {
for b in 0..16 {
predict::predict_subblock(&mut frame.y, ys, px, py, b, bmodes[b], mb_x + 1 == self.mb_w);
if nonzero[b] {
let pos = (py + 4 * (b >> 2)) * ys + px + 4 * (b & 3);
transform::add_residue(&mut frame.y, ys, pos, &transform::inverse_dct(&coeffs[b]));
}
}
} else {
predict::predict_block(&mut frame.y, ys, px, py, 16, info.mode);
add_luma_residue(&mut frame.y, ys, px, py, &coeffs, &nonzero);
}
predict::predict_block(&mut frame.u, cs, px / 2, py / 2, 8, uv_mode);
predict::predict_block(&mut frame.v, cs, px / 2, py / 2, 8, uv_mode);
} else {
let Some(r) = refs.get(usize::from(info.ref_frame) - 1).and_then(|r| r.as_deref()) else {
return Err(Error::MissingReference(format!("reference frame {}", info.ref_frame)));
};
predict_inter_mb(r, &mut frame, mb_x, mb_y, &info, filters, full_pixel);
add_luma_residue(&mut frame.y, ys, px, py, &coeffs, &nonzero);
}
for b in 16..24 {
if nonzero[b] {
let k = (b - 16) & 3;
let pos = (py / 2 + 4 * (k >> 1)) * cs + px / 2 + 4 * (k & 1);
let plane = if b < 20 { &mut frame.u } else { &mut frame.v };
transform::add_residue(plane, cs, pos, &transform::inverse_dct(&coeffs[b]));
}
}
}
}
if hdr.filter_level > 0 {
loop_filter(&mut frame, self.mb_w, self.mb_h, &lf_info, &hdr, tag.key_frame);
}
if let Some(s) = saved {
self.ctx = s;
}
// reference updates (9.7, 9.8): the copies first (altref, then golden from the possibly
// updated altref), then the refreshes with the new frame
let frame = Arc::new(frame);
let [last, golden, altref] = self.refs.clone();
let mut new_golden = golden;
let mut new_altref = altref;
match hdr.copy_to_altref {
1 => new_altref = last.clone(),
2 => new_altref = new_golden.clone(),
_ => {}
}
match hdr.copy_to_golden {
1 => new_golden = last.clone(),
2 => new_golden = new_altref.clone(),
_ => {}
}
if hdr.refresh_golden {
new_golden = Some(frame.clone());
}
if hdr.refresh_altref {
new_altref = Some(frame.clone());
}
let new_last = if hdr.refresh_last { Some(frame.clone()) } else { last };
self.refs = [new_last, new_golden, new_altref];
self.stats.frames += 1;
self.stats.key_frames += u64::from(tag.key_frame);
if !tag.show_frame {
self.stats.hidden_frames += 1;
return Ok(None);
}
Ok(Some(self.picture(&frame, pts, tag.key_frame, hdr.color_space)))
}
/// The visible part of a reconstructed frame.
fn picture(&self, f: &Frame, pts: i64, key: bool, color_space: u8) -> Picture {
let (w, h) = (self.width, self.height);
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let crop = |plane: &[u8], stride: usize, w: usize, h: usize| {
let mut out = Vec::with_capacity(w * h);
for r in 0..h {
out.extend_from_slice(plane.get(r * stride..r * stride + w).unwrap_or(&[]));
}
out
};
Picture {
width: w as u32,
height: h as u32,
chroma_width: cw as u32,
chroma_height: ch as u32,
y: crop(&f.y, f.w, w, h),
u: crop(&f.u, f.w / 2, cw, ch),
v: crop(&f.v, f.w / 2, cw, ch),
pts,
key,
color_space,
}
}
/// Loop filter level of a macroblock (15.1 with the adjustments of 9.3 and 9.4).
fn filter_level(&self, hdr: &FrameHeader, segment: usize, info: &MbInfo) -> u8 {
let mut level = i32::from(hdr.filter_level);
if self.seg.enabled {
let v = i32::from(self.seg.lf_level[segment]);
level = if self.seg.absolute { v } else { level + v }.clamp(0, 63);
}
if self.lf.enabled {
level += i32::from(self.lf.ref_deltas[usize::from(info.ref_frame)]);
let mode_delta = match (info.ref_frame, info.mode) {
(0, B_PRED) => Some(0),
(0, _) => None,
(_, MV_ZERO) => Some(1),
(_, MV_SPLIT) => Some(3),
_ => Some(2),
};
if let Some(m) = mode_delta {
level += i32::from(self.lf.mode_deltas[m]);
}
level = level.clamp(0, 63);
}
level as u8
}
/// Inter prediction record (16.2-16.4, 17) of the macroblock at `idx` in `infos`.
fn read_inter_modes(&self, bd: &mut BoolDecoder, infos: &[MbInfo], idx: usize, stride: usize, mb_x: usize, mb_y: usize, info: &mut MbInfo) {
let get = |i: usize| infos.get(i).copied().unwrap_or_default();
let above = get(idx - stride);
let left = get(idx - 1);
let above_left = get(idx - stride - 1);
// vp8_find_near_mvs (16.3)
let bias = |n: &MbInfo| {
let mut m = n.mv;
if self.sign_bias[usize::from(n.ref_frame)] != self.sign_bias[usize::from(info.ref_frame)] {
m.row = -m.row;
m.col = -m.col;
}
m
};
let mut near = [Mv::ZERO; 4];
let mut cnt = [0usize; 4];
let mut k = 0;
if above.ref_frame != 0 {
if !above.mv.is_zero() {
k += 1;
near[k] = bias(&above);
}
cnt[k] += 2;
}
if left.ref_frame != 0 {
if !left.mv.is_zero() {
let m = bias(&left);
if m != near[k] {
k += 1;
near[k] = m;
}
cnt[k] += 2;
} else {
cnt[0] += 2;
}
}
if above_left.ref_frame != 0 {
if !above_left.mv.is_zero() {
let m = bias(&above_left);
if m != near[k] {
k += 1;
near[k] = m;
}
cnt[k] += 1;
} else {
cnt[0] += 1;
}
}
// three distinct vectors: merge the above-left one into "nearest" when equal
if cnt[3] > 0 && near[3] == near[1] {
cnt[1] += 1;
}
cnt[3] = usize::from(above.mode == MV_SPLIT) * 2 + usize::from(left.mode == MV_SPLIT) * 2 + usize::from(above_left.mode == MV_SPLIT);
if cnt[2] > cnt[1] {
cnt.swap(1, 2);
near.swap(1, 2);
}
if cnt[1] >= cnt[0] {
near[0] = near[1];
}
let bounds = MvBounds::new(mb_x, mb_y, self.mb_w, self.mb_h);
let best = bounds.clamp(near[0]);
let probs: [u8; 4] = std::array::from_fn(|i| MODE_CONTEXTS[cnt[i].min(5) * 4 + i]);
info.mode = MV_NEAREST + bd.read_tree(&MV_REF_TREE, &probs);
info.mv = match info.mode {
MV_NEAREST => bounds.clamp(near[1]),
MV_NEAR => bounds.clamp(near[2]),
MV_ZERO => Mv::ZERO,
MV_NEW => {
let d = self.read_mv(bd);
Mv { row: best.row + d.row, col: best.col + d.col }
}
_ => {
self.read_split(bd, &left, &above, best, info);
return;
}
};
info.bmv = [info.mv; 16];
}
/// SPLITMV partitioning and subblock vectors (16.4).
fn read_split(&self, bd: &mut BoolDecoder, left: &MbInfo, above: &MbInfo, best: Mv, info: &mut MbInfo) {
let part = usize::from(bd.read_tree(&MVPARTITION_TREE, &MVPARTITION_PROBS)) & 3;
let map = &SPLIT_MAPS[part];
let mut part_mv = [Mv::ZERO; 16];
for j in 0..SPLIT_COUNTS[part] {
// first subblock of partition j
let first = map.iter().position(|&p| usize::from(p) == j).unwrap_or(0);
let lmv = if first & 3 == 0 { left.bmv[first + 3] } else { part_mv[usize::from(map[first - 1])] };
let amv = if first < 4 { above.bmv[first + 12] } else { part_mv[usize::from(map[first - 4])] };
let context = if lmv == amv {
if amv.is_zero() { 4 } else { 3 }
} else if amv.is_zero() {
2
} else if lmv.is_zero() {
1
} else {
0
};
let mv = match bd.read_tree(&SUB_MV_REF_TREE, &SUB_MV_REF_PROBS[context * 3..context * 3 + 3]) {
LEFT_4X4 => lmv,
ABOVE_4X4 => amv,
ZERO_4X4 => Mv::ZERO,
_ => {
let d = self.read_mv(bd);
Mv { row: best.row + d.row, col: best.col + d.col }
}
};
part_mv[j] = mv;
}
for (b, &p) in map.iter().enumerate() {
info.bmv[b] = part_mv[usize::from(p)];
}
info.mv = info.bmv[15];
}
/// `read_mv` (17.2): row, then column.
fn read_mv(&self, bd: &mut BoolDecoder) -> Mv {
let row = read_mv_component(bd, &self.ctx.mv[0]);
let col = read_mv_component(bd, &self.ctx.mv[1]);
Mv { row, col }
}
}
/// `read_mvcomponent` (17.1), in quarter pixels.
fn read_mv_component(bd: &mut BoolDecoder, p: &[u8; 19]) -> i32 {
let a = if bd.read_bool(p[MVP_IS_SHORT]) {
let mut a = 0i32;
for i in 0..3 {
a += i32::from(bd.read_bool(p[MVP_BITS + i])) << i;
}
for i in (4..10).rev() {
a += i32::from(bd.read_bool(p[MVP_BITS + i])) << i;
}
if a & 0xfff0 == 0 || bd.read_bool(p[MVP_BITS + 3]) {
a += 8;
}
a
} else {
i32::from(bd.read_tree(&SMALL_MV_TREE, &p[MVP_SHORT..MVP_SHORT + 7]))
};
if a != 0 && bd.read_bool(p[MVP_SIGN]) { -a } else { a }
}
/// The limits `vp8_clamp_mv` (16.3) keeps "nearest", "near" and "best" within: 16 pixels
/// beyond the frame's macroblocks, in quarter pixels.
struct MvBounds {
min_col: i32,
max_col: i32,
min_row: i32,
max_row: i32,
}
impl MvBounds {
fn new(mb_x: usize, mb_y: usize, mb_w: usize, mb_h: usize) -> MvBounds {
const MARGIN: i32 = 16 << 2;
MvBounds {
min_col: -((mb_x as i32 * 16) << 2) - MARGIN,
max_col: (((mb_w - 1 - mb_x) as i32 * 16) << 2) + MARGIN,
min_row: -((mb_y as i32 * 16) << 2) - MARGIN,
max_row: (((mb_h - 1 - mb_y) as i32 * 16) << 2) + MARGIN,
}
}
fn clamp(&self, m: Mv) -> Mv {
Mv { row: m.row.clamp(self.min_row, self.max_row), col: m.col.clamp(self.min_col, self.max_col) }
}
}
/// Subblock mode a 16x16 luma mode stands for in B_PRED contexts (11.3).
fn implied_bmode(mode: u8) -> u8 {
match mode {
V_PRED => B_VE_PRED,
H_PRED => B_HE_PRED,
TM_PRED => B_TM_PRED,
_ => B_DC_PRED,
}
}
/// Decode the tokens of one block (13), storing dequantized coefficients in raster order.
/// Returns the position after the last token (where the end of block was read, or 16).
fn read_coefficients(bd: &mut BoolDecoder, probs: &[[[u8; 11]; 3]; 8], ctx: usize, first: usize, dq: [i32; 2], out: &mut [i16; 16]) -> usize {
let mut ctx = ctx.min(2);
let mut prev_zero = false;
let mut i = first;
while i < 16 {
let p = &probs[usize::from(COEFF_BANDS[i])][ctx];
let token = if prev_zero { bd.read_tree_from(&COEFF_TREE, p, 2) } else { bd.read_tree(&COEFF_TREE, p) };
if token == DCT_EOB {
break;
}
let abs = match token {
DCT_0..=DCT_4 => i32::from(token),
_ => {
let cat = usize::from(token - DCT_CAT1).min(5);
let mut extra = 0i32;
for &pb in PCAT[cat] {
extra = extra + extra + i32::from(bd.read_bool(pb));
}
CAT_BASE[cat] + extra
}
};
let v = if abs != 0 && bd.read_bool(128) { -abs } else { abs };
ctx = match abs {
0 => 0,
1 => 1,
_ => 2,
};
prev_zero = token == DCT_0;
out[ZIGZAG[i]] = (v * dq[usize::from(i > 0)]) as i16;
i += 1;
}
i
}
/// Add the residue of the 16 Y subblocks of the macroblock at `(px, py)`.
fn add_luma_residue(plane: &mut [u8], stride: usize, px: usize, py: usize, coeffs: &[[i16; 16]; 25], nonzero: &[bool; 25]) {
for b in 0..16 {
if nonzero[b] {
let pos = (py + 4 * (b >> 2)) * stride + px + 4 * (b & 3);
transform::add_residue(plane, stride, pos, &transform::inverse_dct(&coeffs[b]));
}
}
}
/// Inter prediction of a macroblock (18) into `frame`.
fn predict_inter_mb(r: &Frame, frame: &mut Frame, mb_x: usize, mb_y: usize, info: &MbInfo, filters: &[i32; 48], full_pixel: bool) {
let (px, py) = (mb_x * 16, mb_y * 16);
let ys = frame.w;
let cs = frame.w / 2;
let ry = RefPlane { data: &r.y, stride: r.w, width: r.w, height: r.h };
let ru = RefPlane { data: &r.u, stride: r.w / 2, width: r.w / 2, height: r.h / 2 };
let rv = RefPlane { data: &r.v, stride: r.w / 2, width: r.w / 2, height: r.h / 2 };
let fp = |v: i32| if full_pixel { v & !7 } else { v };
if info.mode != MV_SPLIT {
let m = info.mv;
predict::predict_inter(&ry, &mut frame.y, ys, px, py, 16, 16, m.col * 2, m.row * 2, filters);
// chroma: the luma vector in quarter pixels is the chroma vector in eighth pixels
let (cx, cy) = (fp(m.col), fp(m.row));
predict::predict_inter(&ru, &mut frame.u, cs, px / 2, py / 2, 8, 8, cx, cy, filters);
predict::predict_inter(&rv, &mut frame.v, cs, px / 2, py / 2, 8, 8, cx, cy, filters);
return;
}
for b in 0..16 {
let m = info.bmv[b];
predict::predict_inter(&ry, &mut frame.y, ys, px + 4 * (b & 3), py + 4 * (b >> 2), 4, 4, m.col * 2, m.row * 2, filters);
}
for i in 0..2 {
for j in 0..2 {
let k = 8 * i + 2 * j;
let sum = |f: fn(Mv) -> i32| [k, k + 1, k + 4, k + 5].iter().map(|&b| f(info.bmv[b])).sum::<i32>();
// average of the four luma vectors, in eighth chroma pixels (18.1)
let avg = |s: i32| (s + 2 + if s < 0 { -1 } else { 0 }) >> 2;
let cx = fp(avg(sum(|m| m.col)));
let cy = fp(avg(sum(|m| m.row)));
let (bx, by) = (px / 2 + 4 * j, py / 2 + 4 * i);
predict::predict_inter(&ru, &mut frame.u, cs, bx, by, 4, 4, cx, cy, filters);
predict::predict_inter(&rv, &mut frame.v, cs, bx, by, 4, 4, cx, cy, filters);
}
}
}
/// Loop filter of the whole frame (15.1), macroblocks in raster order.
fn loop_filter(frame: &mut Frame, mb_w: usize, mb_h: usize, lf_info: &[(u8, bool)], hdr: &FrameHeader, key_frame: bool) {
let ys = frame.w;
let cs = frame.w / 2;
let simple = hdr.filter_type == 1;
for mb_y in 0..mb_h {
for mb_x in 0..mb_w {
let Some(&(level, inner)) = lf_info.get(mb_y * mb_w + mb_x) else { continue };
if level == 0 {
continue;
}
let prm = Params::new(level, hdr.sharpness, key_frame);
let y0 = mb_y * 16 * ys + mb_x * 16;
let c0 = mb_y * 8 * cs + mb_x * 8;
if simple {
if mb_x > 0 {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0, true, Kind::Simple, &prm);
}
if inner {
for x in [4, 8, 12] {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0 + x, true, Kind::SimpleInner, &prm);
}
}
if mb_y > 0 {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0, false, Kind::Simple, &prm);
}
if inner {
for y in [4, 8, 12] {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0 + y * ys, false, Kind::SimpleInner, &prm);
}
}
continue;
}
if mb_x > 0 {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0, true, Kind::NormalMb, &prm);
loopfilter::filter_edge::<8>(&mut frame.u, cs, c0, true, Kind::NormalMb, &prm);
loopfilter::filter_edge::<8>(&mut frame.v, cs, c0, true, Kind::NormalMb, &prm);
}
if inner {
for x in [4, 8, 12] {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0 + x, true, Kind::NormalSub, &prm);
}
loopfilter::filter_edge::<8>(&mut frame.u, cs, c0 + 4, true, Kind::NormalSub, &prm);
loopfilter::filter_edge::<8>(&mut frame.v, cs, c0 + 4, true, Kind::NormalSub, &prm);
}
if mb_y > 0 {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0, false, Kind::NormalMb, &prm);
loopfilter::filter_edge::<8>(&mut frame.u, cs, c0, false, Kind::NormalMb, &prm);
loopfilter::filter_edge::<8>(&mut frame.v, cs, c0, false, Kind::NormalMb, &prm);
}
if inner {
for y in [4, 8, 12] {
loopfilter::filter_edge::<16>(&mut frame.y, ys, y0 + y * ys, false, Kind::NormalSub, &prm);
}
loopfilter::filter_edge::<8>(&mut frame.u, cs, c0 + 4 * cs, false, Kind::NormalSub, &prm);
loopfilter::filter_edge::<8>(&mut frame.v, cs, c0 + 4 * cs, false, Kind::NormalSub, &prm);
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
/// Errors produced by the decoder.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
/// A syntax element had a value outside its legal range, or a size did not fit the data.
#[error("invalid bitstream: {0}")]
Invalid(String),
/// An inter frame came before any key frame (decoding must start at a 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;
+268
View File
@@ -0,0 +1,268 @@
//! Frame tag (9.1, 19.1) and the frame header in the first partition (9.2-9.11, 19.2).
use crate::boolcoder::BoolDecoder;
use crate::error::{Error, Result, ensure};
/// The uncompressed data chunk at the start of every frame (9.1).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FrameTag {
pub key_frame: bool,
/// Bitstream version (0-3): reconstruction filter and full-pixel chroma.
pub version: u8,
pub show_frame: bool,
pub first_part_size: usize,
/// Key frames only: dimensions and upscaling codes.
pub width: u16,
pub height: u16,
pub horizontal_scale: u8,
pub vertical_scale: u8,
/// Bytes of the uncompressed chunk (3, or 10 for key frames).
pub header_len: usize,
}
impl FrameTag {
pub fn parse(data: &[u8]) -> Result<FrameTag> {
let [b0, b1, b2, ..] = *data else {
return Err(Error::Invalid("frame shorter than its 3-byte tag".into()));
};
let tmp = u32::from(b0) | u32::from(b1) << 8 | u32::from(b2) << 16;
let key_frame = tmp & 1 == 0;
let mut tag = FrameTag {
key_frame,
version: ((tmp >> 1) & 7) as u8,
show_frame: (tmp >> 4) & 1 == 1,
first_part_size: (tmp >> 5) as usize,
width: 0,
height: 0,
horizontal_scale: 0,
vertical_scale: 0,
header_len: 3,
};
if key_frame {
let Some(c) = data.get(3..10) else {
return Err(Error::Invalid("key frame shorter than its 10-byte header".into()));
};
ensure!(c[0] == 0x9d && c[1] == 0x01 && c[2] == 0x2a, "bad key frame start code");
let w = u16::from_le_bytes([c[3], c[4]]);
let h = u16::from_le_bytes([c[5], c[6]]);
tag.width = w & 0x3fff;
tag.horizontal_scale = (w >> 14) as u8;
tag.height = h & 0x3fff;
tag.vertical_scale = (h >> 14) as u8;
tag.header_len = 10;
}
Ok(tag)
}
}
/// Segment-based adjustments (9.3): carried from frame to frame.
#[derive(Clone, Copy, Debug, Default)]
pub struct Segmentation {
pub enabled: bool,
pub update_map: bool,
/// Feature values are absolute (else deltas from the frame values).
pub absolute: bool,
pub quant: [i8; 4],
pub lf_level: [i8; 4],
pub tree_probs: [u8; 3],
}
/// Loop filter adjustments by reference frame and mode (9.4): carried from frame to frame.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LfDeltas {
pub enabled: bool,
/// Intra, last, golden, altref.
pub ref_deltas: [i8; 4],
/// B_PRED, ZEROMV, other MV modes, SPLITMV.
pub mode_deltas: [i8; 4],
}
/// Probabilities that persist across frames (until the next key frame), saved and restored
/// around a frame whose `refresh_entropy_probs` is 0.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntropyContext {
pub coeff: [[[[u8; 11]; 3]; 8]; 4],
pub ymode: [u8; 4],
pub uv_mode: [u8; 3],
pub mv: [[u8; 19]; 2],
}
impl Default for EntropyContext {
fn default() -> Self {
use crate::spec_tables::*;
let mut coeff = [[[[0u8; 11]; 3]; 8]; 4];
for (i, p) in coeff.iter_mut().flatten().flatten().flatten().enumerate() {
*p = DEFAULT_COEFF_PROBS[i];
}
let mut mv = [[0u8; 19]; 2];
for (i, p) in mv.iter_mut().flatten().enumerate() {
*p = DEFAULT_MV_PROBS[i];
}
EntropyContext { coeff, ymode: YMODE_PROBS, uv_mode: UV_MODE_PROBS, mv }
}
}
/// Dequantization indices (9.6).
#[derive(Clone, Copy, Debug, Default)]
pub struct QuantIndices {
pub y_ac: i32,
pub y_dc_delta: i32,
pub y2_dc_delta: i32,
pub y2_ac_delta: i32,
pub uv_dc_delta: i32,
pub uv_ac_delta: i32,
}
/// Per-frame header fields (9.2-9.11) that do not persist.
#[derive(Clone, Debug, Default)]
pub struct FrameHeader {
pub color_space: u8,
pub clamping_type: u8,
/// 0 normal, 1 simple.
pub filter_type: u8,
pub filter_level: u8,
pub sharpness: u8,
pub partitions: usize,
pub quant: QuantIndices,
pub refresh_golden: bool,
pub refresh_altref: bool,
/// 0 none, 1 last, 2 altref.
pub copy_to_golden: u8,
/// 0 none, 1 last, 2 golden.
pub copy_to_altref: u8,
pub sign_bias_golden: bool,
pub sign_bias_altref: bool,
pub refresh_entropy: bool,
pub refresh_last: bool,
/// `prob_skip_false` when `mb_no_skip_coeff` is set.
pub skip_prob: Option<u8>,
pub prob_intra: u8,
pub prob_last: u8,
pub prob_golden: u8,
}
/// Read the frame header from the first partition, updating the persistent state.
/// `saved` receives the entropy context to restore after the frame when
/// `refresh_entropy_probs` is 0.
pub fn read_header(
d: &mut BoolDecoder,
key_frame: bool,
seg: &mut Segmentation,
lf: &mut LfDeltas,
ctx: &mut EntropyContext,
saved: &mut Option<EntropyContext>,
) -> FrameHeader {
let mut h = FrameHeader::default();
if key_frame {
h.color_space = d.read_literal(1) as u8;
h.clamping_type = d.read_literal(1) as u8;
}
seg.enabled = d.read_flag();
seg.update_map = false;
if seg.enabled {
seg.update_map = d.read_flag();
let update_data = d.read_flag();
if update_data {
seg.absolute = d.read_flag();
for q in &mut seg.quant {
*q = d.read_optional_signed(7) as i8;
}
for l in &mut seg.lf_level {
*l = d.read_optional_signed(6) as i8;
}
}
if seg.update_map {
for p in &mut seg.tree_probs {
*p = if d.read_flag() { d.read_literal(8) as u8 } else { crate::tables::DEFAULT_SEGMENT_PROB };
}
}
}
h.filter_type = d.read_literal(1) as u8;
h.filter_level = d.read_literal(6) as u8;
h.sharpness = d.read_literal(3) as u8;
lf.enabled = d.read_flag();
if lf.enabled && d.read_flag() {
for v in &mut lf.ref_deltas {
if d.read_flag() {
*v = d.read_signed(6) as i8;
}
}
for v in &mut lf.mode_deltas {
if d.read_flag() {
*v = d.read_signed(6) as i8;
}
}
}
h.partitions = 1 << d.read_literal(2);
h.quant = QuantIndices {
y_ac: d.read_literal(7) as i32,
y_dc_delta: d.read_optional_signed(4),
y2_dc_delta: d.read_optional_signed(4),
y2_ac_delta: d.read_optional_signed(4),
uv_dc_delta: d.read_optional_signed(4),
uv_ac_delta: d.read_optional_signed(4),
};
if key_frame {
h.refresh_golden = true;
h.refresh_altref = true;
h.refresh_entropy = d.read_flag();
h.refresh_last = true;
} else {
h.refresh_golden = d.read_flag();
h.refresh_altref = d.read_flag();
if !h.refresh_golden {
h.copy_to_golden = d.read_literal(2) as u8;
}
if !h.refresh_altref {
h.copy_to_altref = d.read_literal(2) as u8;
}
h.sign_bias_golden = d.read_flag();
h.sign_bias_altref = d.read_flag();
h.refresh_entropy = d.read_flag();
h.refresh_last = d.read_flag();
}
// The probabilities in effect before this frame's updates come back after it.
*saved = if h.refresh_entropy { None } else { Some(ctx.clone()) };
// token_prob_update (13.4)
let mut i = 0;
for plane in ctx.coeff.iter_mut() {
for band in plane.iter_mut() {
for c in band.iter_mut() {
for p in c.iter_mut() {
if d.read_bool(crate::spec_tables::COEFF_UPDATE_PROBS[i]) {
*p = d.read_literal(8) as u8;
}
i += 1;
}
}
}
}
h.skip_prob = if d.read_flag() { Some(d.read_literal(8) as u8) } else { None };
if !key_frame {
h.prob_intra = d.read_literal(8) as u8;
h.prob_last = d.read_literal(8) as u8;
h.prob_golden = d.read_literal(8) as u8;
if d.read_flag() {
for p in &mut ctx.ymode {
*p = d.read_literal(8) as u8;
}
}
if d.read_flag() {
for p in &mut ctx.uv_mode {
*p = d.read_literal(8) as u8;
}
}
// mv_prob_update (17.2)
let mut i = 0;
for comp in ctx.mv.iter_mut() {
for p in comp.iter_mut() {
if d.read_bool(crate::spec_tables::MV_UPDATE_PROBS[i]) {
let x = d.read_literal(7) as u8;
*p = if x != 0 { x << 1 } else { 1 };
}
i += 1;
}
}
}
h
}
+78
View File
@@ -0,0 +1,78 @@
//! Clean-room pure-Rust VP8 decoder, implemented from RFC 6386 ("VP8 Data Format and Decoding
//! Guide"), sections 1-19.
//!
//! Supported: the whole format — key frames and inter frames, all intra modes (16x16, chroma,
//! the ten 4x4 subblock modes), inter prediction from the last, golden and altref frames with
//! sign bias, NEAREST / NEAR / ZERO / NEW / SPLIT motion vectors, six-tap and bilinear
//! interpolation (versions 0-3, full-pixel chroma), segmentation, the normal and simple loop
//! filters with mode / reference deltas, multiple token partitions, probability persistence
//! (`refresh_entropy_probs`), golden / altref copies and hidden (altref) frames.
//!
//! ```no_run
//! let mut dec = filmcraft_vp8::Decoder::new();
//! let frame: Vec<u8> = Vec::new(); // one IVF / WebM frame
//! if let Some(pic) = dec.decode(&frame, 0).unwrap() {
//! println!("{}x{}", pic.width, pic.height);
//! }
//! ```
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
mod boolcoder;
mod decoder;
mod error;
mod header;
mod loopfilter;
mod predict;
#[rustfmt::skip]
mod spec_tables;
mod tables;
mod transform;
pub use decoder::{DecodeStats, Decoder};
pub use error::{Error, Result};
/// A decoded picture: 8-bit 4:2:0, planes tight (stride = width).
#[derive(Clone, Debug)]
pub struct Picture {
pub width: u32,
pub height: u32,
pub chroma_width: u32,
pub chroma_height: u32,
pub y: Vec<u8>,
pub u: Vec<u8>,
pub v: Vec<u8>,
/// `pts` passed to [`Decoder::decode`] with the frame.
pub pts: i64,
pub key: bool,
/// `color_space` of the last key frame (0: BT.601 YUV, 1: reserved).
pub color_space: u8,
}
/// Stream parameters read from a key frame's uncompressed chunk (see [`keyframe_info`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyframeInfo {
pub width: u32,
pub height: u32,
/// Upscaling codes (9.1), informational only.
pub horizontal_scale: u8,
pub vertical_scale: u8,
pub version: u8,
}
/// If `frame` is a key frame (a random access point), its parameters.
pub fn keyframe_info(frame: &[u8]) -> Option<KeyframeInfo> {
let tag = header::FrameTag::parse(frame).ok()?;
(tag.key_frame && tag.width > 0 && tag.height > 0).then_some(KeyframeInfo {
width: u32::from(tag.width),
height: u32::from(tag.height),
horizontal_scale: tag.horizontal_scale,
vertical_scale: tag.vertical_scale,
version: tag.version,
})
}
/// Whether `frame` is a key frame (decoding can start there).
pub fn is_keyframe(frame: &[u8]) -> bool {
keyframe_info(frame).is_some()
}
+156
View File
@@ -0,0 +1,156 @@
//! Loop filter (15).
/// Filter parameters of one macroblock (15.4).
#[derive(Clone, Copy, Debug)]
pub struct Params {
pub mbedge_limit: i32,
pub sub_bedge_limit: i32,
pub interior_limit: i32,
pub hev_threshold: i32,
}
impl Params {
pub fn new(level: u8, sharpness: u8, key_frame: bool) -> Params {
let level = i32::from(level);
let mut interior = level;
if sharpness > 0 {
interior >>= if sharpness > 4 { 2 } else { 1 };
interior = interior.min(9 - i32::from(sharpness));
}
let interior = interior.max(1);
let hev_threshold = match (key_frame, level) {
(_, l) if l >= 40 => 2 + i32::from(!key_frame),
(false, l) if l >= 20 => 2,
(_, l) if l >= 15 => 1,
_ => 0,
};
Params { mbedge_limit: (level + 2) * 2 + interior, sub_bedge_limit: level * 2 + interior, interior_limit: interior, hev_threshold }
}
}
/// Which edge filter to apply along an edge.
#[derive(Clone, Copy)]
pub enum Kind {
/// The simple filter (15.2) with the macroblock edge limit.
Simple,
/// The simple filter with the subblock edge limit.
SimpleInner,
/// The normal macroblock edge filter (`MBfilter`, 15.3).
NormalMb,
/// The normal subblock edge filter (`subblock_filter`, 15.3).
NormalSub,
}
/// Filter `N` segments at once: `px[k][l]` is pixel `k - 4` across the edge of segment `l`
/// (p3 p2 p1 p0 | q0 q1 q2 q3). Branch-free: the conditions of 15.2 / 15.3 become masks
/// (all ones or zero) that zero the adjustment where the filter does not apply, which gives the
/// same values as the specification's procedures; each step is a plain loop over the lanes, which
/// the compiler vectorizes.
#[inline(always)]
fn filter_lanes<const N: usize, const NORMAL: bool, const MB: bool>(px: &mut [[u8; N]; 8], edge_limit: i16, prm: &Params) {
let (lim, thr) = (prm.interior_limit as i16, prm.hev_threshold as i16);
let ld = |k: usize| -> [i16; N] { std::array::from_fn(|l| i16::from(px[k][l])) };
let (p3, p2, p1, p0, q0, q1, q2, q3) = (ld(0), ld(1), ld(2), ld(3), ld(4), ld(5), ld(6), ld(7));
let mut m = [0i16; N];
let mut hev = [0i16; N];
for l in 0..N {
let edge = (p0[l] - q0[l]).abs() * 2 + (p1[l] - q1[l]).abs() / 2;
m[l] = -i16::from(edge <= edge_limit);
}
if NORMAL {
for l in 0..N {
let d = (p3[l] - p2[l])
.abs()
.max((p2[l] - p1[l]).abs())
.max((p1[l] - p0[l]).abs())
.max((q3[l] - q2[l]).abs())
.max((q2[l] - q1[l]).abs())
.max((q1[l] - q0[l]).abs());
m[l] &= -i16::from(d <= lim);
hev[l] = -i16::from((p1[l] - p0[l]).abs().max((q1[l] - q0[l]).abs()) > thr);
}
}
let c = |v: i16| v.clamp(-128, 127);
let out = |v: i16| (c(v) + 128) as u8;
for l in 0..N {
let (sp2, sp1, sp0, sq0, sq1, sq2) = (p2[l] - 128, p1[l] - 128, p0[l] - 128, q0[l] - 128, q1[l] - 128, q2[l] - 128);
if !NORMAL {
// simple_segment / common_adjust with the outer taps
let f = c(c(sp1 - sq1) + 3 * (sq0 - sp0)) & m[l];
px[4][l] = out(sq0 - (c(f + 4) >> 3));
px[3][l] = out(sp0 + (c(f + 3) >> 3));
} else if !MB {
// subblock_filter: common_adjust (outer taps on high edge variance), then half of it
// on p1 / q1 without high edge variance
let f = c((c(sp1 - sq1) & hev[l]) + 3 * (sq0 - sp0)) & m[l];
let f1 = c(f + 4) >> 3;
px[4][l] = out(sq0 - f1);
px[3][l] = out(sp0 + (c(f + 3) >> 3));
let a = ((f1 + 1) >> 1) & !hev[l];
px[5][l] = out(sq1 - a);
px[2][l] = out(sp1 + a);
} else {
// MBfilter: common_adjust with outer taps on high edge variance, the 27/18/9
// adjustments otherwise
let w = c(c(sp1 - sq1) + 3 * (sq0 - sp0)) & m[l];
let f = w & hev[l];
let q0a = c(sq0 - (c(f + 4) >> 3));
let p0a = c(sp0 + (c(f + 3) >> 3));
let w = w & !hev[l];
// 27 * w + 63 fits 16 bits: |w| <= 128
let a = |mul: i16| c((mul * w + 63) >> 7);
let (a27, a18, a9) = (a(27), a(18), a(9));
px[4][l] = out(q0a - a27);
px[3][l] = out(p0a + a27);
px[5][l] = out(sq1 - a18);
px[2][l] = out(sp1 + a18);
px[6][l] = out(sq2 - a9);
px[1][l] = out(sp2 + a9);
}
}
}
/// Filter an edge (15.2, 15.3) of `N` segments. `start` is q0 of the first segment; a vertical
/// edge (`vertical`) has its segments one row apart, a horizontal one one pixel apart. Every
/// segment must have 4 pixels inside the plane on both sides of the edge; an edge that does not
/// is left alone.
pub fn filter_edge<const N: usize>(plane: &mut [u8], stride: usize, start: usize, vertical: bool, kind: Kind, prm: &Params) {
let (step, along) = if vertical { (1, stride) } else { (stride, 1) };
if start < 4 * step || start + (N - 1) * along + 3 * step >= plane.len() {
return;
}
let mut px = [[0u8; N]; 8];
let first = start - 4 * step;
if vertical {
for l in 0..N {
let row = first + l * stride;
for k in 0..8 {
px[k][l] = plane[row + k];
}
}
} else {
for (k, row) in px.iter_mut().enumerate() {
let at = first + k * stride;
row.copy_from_slice(&plane[at..at + N]);
}
}
match kind {
Kind::Simple => filter_lanes::<N, false, false>(&mut px, prm.mbedge_limit as i16, prm),
Kind::SimpleInner => filter_lanes::<N, false, false>(&mut px, prm.sub_bedge_limit as i16, prm),
Kind::NormalMb => filter_lanes::<N, true, true>(&mut px, prm.mbedge_limit as i16, prm),
Kind::NormalSub => filter_lanes::<N, true, false>(&mut px, prm.sub_bedge_limit as i16, prm),
}
if vertical {
for l in 0..N {
let row = first + l * stride;
for k in 1..7 {
plane[row + k] = px[k][l];
}
}
} else {
for (k, row) in px.iter().enumerate().take(7).skip(1) {
let at = first + k * stride;
plane[at..at + N].copy_from_slice(row);
}
}
}
+330
View File
@@ -0,0 +1,330 @@
//! Intra prediction (12) and inter prediction (18).
//!
//! Planes are the decoder's reconstruction buffers, whose size is a whole number of macroblocks.
//! Intra prediction reads the not yet loop-filtered current frame; pixels above the frame are 127
//! and pixels left of it 129 (the corner above-left of a row's first macroblock is 127 on the top
//! row and 129 below it). Inter prediction reads a reference plane extended infinitely by
//! repeating its edge pixels.
use crate::tables::*;
/// A pixel of the current frame for intra prediction, with the out-of-frame values of section 12.
#[inline]
fn edge(plane: &[u8], stride: usize, x: isize, y: isize) -> u8 {
if y < 0 {
127
} else if x < 0 {
129
} else {
plane.get(y as usize * stride + x as usize).copied().unwrap_or(127)
}
}
/// Predict a whole `size`x`size` block (16x16 luma or 8x8 chroma) at `(x0, y0)` with a
/// macroblock-level mode (12.2, 12.3), writing the prediction into the plane.
pub fn predict_block(plane: &mut [u8], stride: usize, x0: usize, y0: usize, size: usize, mode: u8) {
let (xi, yi) = (x0 as isize, y0 as isize);
let mut above = [0u8; 16];
let mut left = [0u8; 16];
for i in 0..size {
above[i] = edge(plane, stride, xi + i as isize, yi - 1);
left[i] = edge(plane, stride, xi - 1, yi + i as isize);
}
let p = i32::from(edge(plane, stride, xi - 1, yi - 1));
let shift = size.trailing_zeros();
let fill = |plane: &mut [u8], f: &dyn Fn(usize, usize) -> u8| {
for r in 0..size {
let start = (y0 + r) * stride + x0;
if let Some(row) = plane.get_mut(start..start + size) {
for (c, px) in row.iter_mut().enumerate() {
*px = f(r, c);
}
}
}
};
match mode {
V_PRED => fill(plane, &|_, c| above[c]),
H_PRED => fill(plane, &|r, _| left[r]),
TM_PRED => fill(plane, &|r, c| (i32::from(left[r]) + i32::from(above[c]) - p).clamp(0, 255) as u8),
_ => {
// DC_PRED: only the genuinely available edges count
let sum = |v: &[u8]| v[..size].iter().map(|&x| u32::from(x)).sum::<u32>();
let dc = match (y0 > 0, x0 > 0) {
(true, true) => (sum(&above) + sum(&left) + (1 << shift)) >> (shift + 1),
(true, false) => (sum(&above) + (1 << (shift - 1))) >> shift,
(false, true) => (sum(&left) + (1 << (shift - 1))) >> shift,
(false, false) => 128,
} as u8;
fill(plane, &|_, _| dc);
}
}
}
/// Predict the 4x4 luma subblock `b` (raster index in its macroblock) of the macroblock at
/// `(mx, my)` (in pixels) with subblock mode `mode` (12.3). `last_column`: the macroblock is the
/// rightmost of its row.
pub fn predict_subblock(plane: &mut [u8], stride: usize, mx: usize, my: usize, b: usize, mode: u8, last_column: bool) {
let (bx, by) = (b & 3, b >> 2);
let x = (mx + 4 * bx) as isize;
let y = (my + 4 * by) as isize;
// A[-1] = P, A[0..8]: the 4 pixels above and the 4 above-right
let mut a = [0i32; 9];
a[0] = i32::from(edge(plane, stride, x - 1, y - 1));
for i in 0..4 {
a[1 + i] = i32::from(edge(plane, stride, x + i as isize, y - 1));
}
for i in 0..4 {
a[5 + i] = i32::from(if bx < 3 {
edge(plane, stride, x + 4 + i as isize, y - 1)
} else if my == 0 {
127
} else if last_column {
// the macroblock's above-right pixels lie outside the frame: repeat (-1, 15)
edge(plane, stride, mx as isize + 15, my as isize - 1)
} else {
// right-column subblocks all use the pixels above-right of the macroblock
edge(plane, stride, mx as isize + 16 + i as isize, my as isize - 1)
});
}
let mut l = [0i32; 4];
for (i, v) in l.iter_mut().enumerate() {
*v = i32::from(edge(plane, stride, x - 1, y + i as isize));
}
let av = |i: isize| a[(i + 1) as usize]; // A[i], i in -1..8
// E[0..9] = L[3], L[2], L[1], L[0], P, A[0..4]
let e = [l[3], l[2], l[1], l[0], a[0], a[1], a[2], a[3], a[4]];
let avg3 = |x: i32, y: i32, z: i32| (x + y + y + z + 2) >> 2;
let avg2 = |x: i32, y: i32| (x + y + 1) >> 1;
let a3 = |i: isize| avg3(av(i - 1), av(i), av(i + 1)); // avg3p(A + i)
let a2 = |i: isize| avg2(av(i), av(i + 1)); // avg2p(A + i)
let e3 = |i: usize| avg3(e[i - 1], e[i], e[i + 1]);
let e2 = |i: usize| avg2(e[i], e[i + 1]);
// L[-1] = P
let lv = |i: isize| if i < 0 { a[0] } else { l[i as usize] };
let l3 = |i: isize| avg3(lv(i - 1), lv(i), lv(i + 1));
let l2 = |i: isize| avg2(lv(i), lv(i + 1));
let mut out = [[0i32; 4]; 4];
match mode {
B_TM_PRED => {
for r in 0..4 {
for c in 0..4 {
out[r][c] = (l[r] + av(c as isize) - a[0]).clamp(0, 255);
}
}
}
B_VE_PRED => {
for c in 0..4 {
let v = a3(c as isize);
for row in &mut out {
row[c] = v;
}
}
}
B_HE_PRED => {
for r in 0..4 {
let v = if r == 3 { avg3(l[2], l[3], l[3]) } else { l3(r as isize) };
out[r] = [v; 4];
}
}
B_LD_PRED => {
for r in 0..4 {
for c in 0..4 {
let d = r + c;
out[r][c] = if d == 6 { avg3(av(6), av(7), av(7)) } else { a3(d as isize + 1) };
}
}
}
B_RD_PRED => {
for r in 0..4 {
for c in 0..4 {
// (3,0) uses E[1], each step towards the upper right one further
out[r][c] = e3((4 + c as isize - r as isize) as usize);
}
}
}
B_VR_PRED => {
out[3][0] = e3(2);
out[2][0] = e3(3);
out[3][1] = e3(4);
out[1][0] = e3(4);
out[2][1] = e2(4);
out[0][0] = e2(4);
out[3][2] = e3(5);
out[1][1] = e3(5);
out[2][2] = e2(5);
out[0][1] = e2(5);
out[3][3] = e3(6);
out[1][2] = e3(6);
out[2][3] = e2(6);
out[0][2] = e2(6);
out[1][3] = e3(7);
out[0][3] = e2(7);
}
B_VL_PRED => {
out[0][0] = a2(0);
out[1][0] = a3(1);
out[2][0] = a2(1);
out[0][1] = a2(1);
out[1][1] = a3(2);
out[3][0] = a3(2);
out[2][1] = a2(2);
out[0][2] = a2(2);
out[3][1] = a3(3);
out[1][2] = a3(3);
out[2][2] = a2(3);
out[0][3] = a2(3);
out[3][2] = a3(4);
out[1][3] = a3(4);
out[2][3] = a3(5);
out[3][3] = a3(6);
}
B_HD_PRED => {
out[3][0] = e2(0);
out[3][1] = e3(1);
out[2][0] = e2(1);
out[3][2] = e2(1);
out[2][1] = e3(2);
out[3][3] = e3(2);
out[2][2] = e2(2);
out[1][0] = e2(2);
out[2][3] = e3(3);
out[1][1] = e3(3);
out[1][2] = e2(3);
out[0][0] = e2(3);
out[1][3] = e3(4);
out[0][1] = e3(4);
out[0][2] = e3(5);
out[0][3] = e3(6);
}
B_HU_PRED => {
out[0][0] = l2(0);
out[0][1] = l3(1);
out[0][2] = l2(1);
out[1][0] = l2(1);
out[0][3] = l3(2);
out[1][1] = l3(2);
out[1][2] = l2(2);
out[2][0] = l2(2);
out[1][3] = avg3(l[2], l[3], l[3]);
out[2][1] = avg3(l[2], l[3], l[3]);
out[2][2] = l[3];
out[2][3] = l[3];
out[3] = [l[3]; 4];
}
_ => {
// B_DC_PRED
let v = (4 + (1..5).map(|i| a[i]).sum::<i32>() + l.iter().sum::<i32>()) >> 3;
out = [[v; 4]; 4];
}
}
for (r, row) in out.iter().enumerate() {
let start = (y as usize + r) * stride + x as usize;
if let Some(dst) = plane.get_mut(start..start + 4) {
for (d, &v) in dst.iter_mut().zip(row) {
*d = v as u8;
}
}
}
}
/// A reference plane for inter prediction: reads outside it repeat the nearest edge pixel.
pub struct RefPlane<'a> {
pub data: &'a [u8],
pub stride: usize,
pub width: usize,
pub height: usize,
}
impl RefPlane<'_> {
#[inline]
fn at(&self, x: isize, y: isize) -> u8 {
let x = x.clamp(0, self.width as isize - 1) as usize;
let y = y.clamp(0, self.height as isize - 1) as usize;
self.data.get(y * self.stride + x).copied().unwrap_or(0)
}
}
/// Predict the `w`x`h` block at `(bx, by)` of a plane from `reference` displaced by the motion
/// vector `(mvx, mvy)` in eighth pixels, using `filters` (8 x 6 taps: the six-tap or bilinear
/// set, 18.3), writing into `dst` (same layout as the block's plane).
pub fn predict_inter(
reference: &RefPlane,
dst: &mut [u8],
dst_stride: usize,
bx: usize,
by: usize,
w: usize,
h: usize,
mvx: i32,
mvy: i32,
filters: &[i32; 48],
) {
let x0 = bx as isize + (mvx >> 3) as isize;
let y0 = by as isize + (mvy >> 3) as isize;
let (fx, fy) = ((mvx & 7) as usize, (mvy & 7) as usize);
// source window: 2 before, 3 after in each direction
const MAX: usize = 16 + 5;
let mut src = [[0u8; MAX]; MAX];
let full_inside = x0 >= 2 && y0 >= 2 && x0 + w as isize + 3 <= reference.width as isize && y0 + h as isize + 3 <= reference.height as isize;
for r in 0..h + 5 {
let yy = y0 + r as isize - 2;
if full_inside {
let start = yy as usize * reference.stride + x0 as usize - 2;
if let Some(row) = reference.data.get(start..start + w + 5) {
src[r][..w + 5].copy_from_slice(row);
continue;
}
}
for c in 0..w + 5 {
src[r][c] = reference.at(x0 + c as isize - 2, yy);
}
}
if fx == 0 && fy == 0 {
for r in 0..h {
let start = (by + r) * dst_stride + bx;
if let Some(row) = dst.get_mut(start..start + w) {
row.copy_from_slice(&src[r + 2][2..2 + w]);
}
}
return;
}
let tap6 = |f: usize| -> [i32; 6] { std::array::from_fn(|k| filters.get(f * 6 + k).copied().unwrap_or(0)) };
let (hf, vf) = (tap6(fx), tap6(fy));
let round = |v: i32| ((v + 64) >> 7).clamp(0, 255);
// horizontal pass over the h + 5 rows (an identity when fx is 0), then vertical
let mut tmp = [[0i32; 16]; MAX];
for r in 0..h + 5 {
let s = &src[r];
let t = &mut tmp[r];
if fx == 0 {
for c in 0..w {
t[c] = i32::from(s[c + 2]);
}
} else {
for c in 0..w {
let v = i32::from(s[c]) * hf[0]
+ i32::from(s[c + 1]) * hf[1]
+ i32::from(s[c + 2]) * hf[2]
+ i32::from(s[c + 3]) * hf[3]
+ i32::from(s[c + 4]) * hf[4]
+ i32::from(s[c + 5]) * hf[5];
t[c] = round(v);
}
}
}
for r in 0..h {
let start = (by + r) * dst_stride + bx;
let Some(row) = dst.get_mut(start..start + w) else { continue };
if fy == 0 {
for (c, d) in row.iter_mut().enumerate() {
*d = tmp[r + 2][c] as u8;
}
} else {
for (c, d) in row.iter_mut().enumerate() {
let v =
tmp[r][c] * vf[0] + tmp[r + 1][c] * vf[1] + tmp[r + 2][c] * vf[2] + tmp[r + 3][c] * vf[3] + tmp[r + 4][c] * vf[4] + tmp[r + 5][c] * vf[5];
*d = round(v) as u8;
}
}
}
}
+302
View File
@@ -0,0 +1,302 @@
// Generated by tools/extract_tables.py from the text of RFC 6386 (sections 1-19). Do not edit.
// Every table is flat, row-major, in the order the specification prints it.
/// `coeff_update_probs` (RFC 6386), 1056 entries.
pub const COEFF_UPDATE_PROBS: [u8; 1056] = [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255, 223, 241, 252, 255,
255, 255, 255, 255, 255, 255, 255, 249, 253, 253, 255, 255, 255, 255, 255, 255,
255, 255, 255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255, 234, 254, 254,
255, 255, 255, 255, 255, 255, 255, 255, 253, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255, 239, 253,
254, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 254, 255, 255, 255, 255,
255, 255, 255, 255, 255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255, 251,
255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255,
251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 254, 255, 255,
255, 255, 255, 255, 255, 255, 255, 254, 253, 255, 254, 255, 255, 255, 255, 255,
255, 250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255, 254, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 217, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255, 234, 250,
241, 250, 253, 255, 253, 254, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255,
255, 255, 255, 255, 223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 238,
253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 248, 254, 255, 255, 255,
255, 255, 255, 255, 255, 249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 253, 255, 255, 255,
255, 255, 255, 255, 255, 255, 247, 254, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 253, 254, 255,
255, 255, 255, 255, 255, 255, 255, 252, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254,
255, 255, 255, 255, 255, 255, 255, 255, 253, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254,
253, 255, 255, 255, 255, 255, 255, 255, 255, 250, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255, 234, 251, 244, 254, 255,
255, 255, 255, 255, 255, 255, 251, 251, 243, 253, 254, 255, 254, 255, 255, 255,
255, 255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255, 236, 253, 254, 255,
255, 255, 255, 255, 255, 255, 255, 251, 253, 253, 254, 254, 255, 255, 255, 255,
255, 255, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254,
255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 248, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255, 248, 254,
249, 253, 255, 255, 255, 255, 255, 255, 255, 255, 253, 253, 255, 255, 255, 255,
255, 255, 255, 255, 246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255, 252,
254, 251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 252, 255, 255, 255,
255, 255, 255, 255, 255, 248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255,
253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 251, 254, 255, 255,
255, 255, 255, 255, 255, 255, 245, 251, 254, 255, 255, 255, 255, 255, 255, 255,
255, 253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 251, 253, 255,
255, 255, 255, 255, 255, 255, 255, 252, 253, 254, 255, 255, 255, 255, 255, 255,
255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 255,
255, 255, 255, 255, 255, 255, 255, 255, 249, 255, 254, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
253, 255, 255, 255, 255, 255, 255, 255, 255, 250, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
];
/// `default_coeff_probs` (RFC 6386), 1056 entries.
pub const DEFAULT_COEFF_PROBS: [u8; 1056] = [
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
128, 253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128, 189, 129, 242, 255,
227, 213, 255, 219, 128, 128, 128, 106, 126, 227, 252, 214, 209, 255, 255, 128,
128, 128, 1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128, 181, 133, 238,
254, 221, 234, 255, 154, 128, 128, 128, 78, 134, 202, 247, 198, 180, 255, 219,
128, 128, 128, 1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128, 184, 150,
247, 255, 236, 224, 128, 128, 128, 128, 128, 77, 110, 216, 255, 236, 230, 128,
128, 128, 128, 128, 1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128, 170,
139, 241, 252, 236, 209, 255, 255, 128, 128, 128, 37, 116, 196, 243, 228, 255,
255, 255, 128, 128, 128, 1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128,
207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128, 102, 103, 231, 255, 211,
171, 128, 128, 128, 128, 128, 1, 152, 252, 255, 240, 255, 128, 128, 128, 128,
128, 177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128, 80, 129, 211, 255,
194, 224, 128, 128, 128, 128, 128, 1, 1, 255, 128, 128, 128, 128, 128, 128,
128, 128, 246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128, 255, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128, 198, 35, 237, 223, 193, 187, 162, 160,
145, 155, 62, 131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1, 68, 47,
146, 208, 149, 167, 221, 162, 255, 223, 128, 1, 149, 241, 255, 221, 224, 255,
255, 128, 128, 128, 184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128, 81,
99, 181, 242, 176, 190, 249, 202, 255, 255, 128, 1, 129, 232, 253, 214, 197,
242, 196, 255, 255, 128, 99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128,
23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128, 1, 200, 246, 255, 234,
255, 128, 128, 128, 128, 128, 109, 178, 241, 255, 231, 245, 255, 255, 128, 128,
128, 44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128, 1, 132, 239, 251,
219, 209, 255, 165, 128, 128, 128, 94, 136, 225, 251, 218, 190, 255, 255, 128,
128, 128, 22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128, 1, 182, 249,
255, 232, 235, 128, 128, 128, 128, 128, 124, 143, 241, 255, 227, 234, 128, 128,
128, 128, 128, 35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128, 1, 157,
247, 255, 236, 231, 255, 255, 128, 128, 128, 121, 141, 235, 255, 225, 227, 255,
255, 128, 128, 128, 45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128, 1,
1, 251, 255, 213, 255, 128, 128, 128, 128, 128, 203, 1, 248, 255, 255, 128,
128, 128, 128, 128, 128, 137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128,
253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128, 175, 13, 224, 243, 193,
185, 249, 198, 255, 255, 128, 73, 17, 171, 221, 161, 179, 236, 167, 255, 234,
128, 1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128, 239, 90, 244, 250,
211, 209, 255, 255, 128, 128, 128, 155, 77, 195, 248, 188, 195, 255, 255, 128,
128, 128, 1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128, 201, 51, 219,
255, 196, 186, 128, 128, 128, 128, 128, 69, 46, 190, 239, 201, 218, 255, 228,
128, 128, 128, 1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128, 223, 165,
249, 255, 213, 255, 128, 128, 128, 128, 128, 141, 124, 248, 255, 255, 128, 128,
128, 128, 128, 128, 1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128, 190,
36, 230, 255, 236, 255, 128, 128, 128, 128, 128, 149, 1, 255, 128, 128, 128,
128, 128, 128, 128, 128, 1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128,
247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128, 240, 128, 255, 128, 128,
128, 128, 128, 128, 128, 128, 1, 134, 252, 255, 255, 128, 128, 128, 128, 128,
128, 213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128, 55, 93, 255, 128,
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
128, 128, 128, 128, 128, 128, 128, 128, 202, 24, 213, 235, 186, 191, 220, 160,
240, 175, 255, 126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128, 61, 46,
138, 219, 151, 178, 240, 170, 255, 216, 128, 1, 112, 230, 250, 199, 191, 247,
159, 255, 255, 128, 166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128, 39,
77, 162, 232, 172, 180, 245, 178, 255, 255, 128, 1, 52, 220, 246, 198, 199,
249, 220, 255, 255, 128, 124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128,
24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128, 1, 182, 225, 249, 219,
240, 255, 224, 128, 128, 128, 149, 150, 226, 252, 216, 205, 255, 171, 128, 128,
128, 28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128, 1, 81, 230, 252,
204, 203, 255, 192, 128, 128, 128, 123, 102, 209, 247, 188, 196, 255, 233, 128,
128, 128, 20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128, 1, 222, 248,
255, 216, 213, 128, 128, 128, 128, 128, 168, 175, 246, 252, 235, 205, 255, 255,
128, 128, 128, 47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128, 1, 121,
236, 253, 212, 214, 255, 255, 128, 128, 128, 141, 84, 213, 252, 201, 202, 255,
219, 128, 128, 128, 42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128, 1,
1, 255, 128, 128, 128, 128, 128, 128, 128, 128, 244, 1, 255, 128, 128, 128,
128, 128, 128, 128, 128, 238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128,
];
/// `kf_bmode_prob` (RFC 6386), 900 entries.
pub const KF_BMODE_PROBS: [u8; 900] = [
231, 120, 48, 89, 115, 113, 120, 152, 112, 152, 179, 64, 126, 170, 118, 46,
70, 95, 175, 69, 143, 80, 85, 82, 72, 155, 103, 56, 58, 10, 171, 218,
189, 17, 13, 152, 144, 71, 10, 38, 171, 213, 144, 34, 26, 114, 26, 17,
163, 44, 195, 21, 10, 173, 121, 24, 80, 195, 26, 62, 44, 64, 85, 170,
46, 55, 19, 136, 160, 33, 206, 71, 63, 20, 8, 114, 114, 208, 12, 9,
226, 81, 40, 11, 96, 182, 84, 29, 16, 36, 134, 183, 89, 137, 98, 101,
106, 165, 148, 72, 187, 100, 130, 157, 111, 32, 75, 80, 66, 102, 167, 99,
74, 62, 40, 234, 128, 41, 53, 9, 178, 241, 141, 26, 8, 107, 104, 79,
12, 27, 217, 255, 87, 17, 7, 74, 43, 26, 146, 73, 166, 49, 23, 157,
65, 38, 105, 160, 51, 52, 31, 115, 128, 87, 68, 71, 44, 114, 51, 15,
186, 23, 47, 41, 14, 110, 182, 183, 21, 17, 194, 66, 45, 25, 102, 197,
189, 23, 18, 22, 88, 88, 147, 150, 42, 46, 45, 196, 205, 43, 97, 183,
117, 85, 38, 35, 179, 61, 39, 53, 200, 87, 26, 21, 43, 232, 171, 56,
34, 51, 104, 114, 102, 29, 93, 77, 107, 54, 32, 26, 51, 1, 81, 43,
31, 39, 28, 85, 171, 58, 165, 90, 98, 64, 34, 22, 116, 206, 23, 34,
43, 166, 73, 68, 25, 106, 22, 64, 171, 36, 225, 114, 34, 19, 21, 102,
132, 188, 16, 76, 124, 62, 18, 78, 95, 85, 57, 50, 48, 51, 193, 101,
35, 159, 215, 111, 89, 46, 111, 60, 148, 31, 172, 219, 228, 21, 18, 111,
112, 113, 77, 85, 179, 255, 38, 120, 114, 40, 42, 1, 196, 245, 209, 10,
25, 109, 100, 80, 8, 43, 154, 1, 51, 26, 71, 88, 43, 29, 140, 166,
213, 37, 43, 154, 61, 63, 30, 155, 67, 45, 68, 1, 209, 142, 78, 78,
16, 255, 128, 34, 197, 171, 41, 40, 5, 102, 211, 183, 4, 1, 221, 51,
50, 17, 168, 209, 192, 23, 25, 82, 125, 98, 42, 88, 104, 85, 117, 175,
82, 95, 84, 53, 89, 128, 100, 113, 101, 45, 75, 79, 123, 47, 51, 128,
81, 171, 1, 57, 17, 5, 71, 102, 57, 53, 41, 49, 115, 21, 2, 10,
102, 255, 166, 23, 6, 38, 33, 13, 121, 57, 73, 26, 1, 85, 41, 10,
67, 138, 77, 110, 90, 47, 114, 101, 29, 16, 10, 85, 128, 101, 196, 26,
57, 18, 10, 102, 102, 213, 34, 20, 43, 117, 20, 15, 36, 163, 128, 68,
1, 26, 138, 31, 36, 171, 27, 166, 38, 44, 229, 67, 87, 58, 169, 82,
115, 26, 59, 179, 63, 59, 90, 180, 59, 166, 93, 73, 154, 40, 40, 21,
116, 143, 209, 34, 39, 175, 57, 46, 22, 24, 128, 1, 54, 17, 37, 47,
15, 16, 183, 34, 223, 49, 45, 183, 46, 17, 33, 183, 6, 98, 15, 32,
183, 65, 32, 73, 115, 28, 128, 23, 128, 205, 40, 3, 9, 115, 51, 192,
18, 6, 223, 87, 37, 9, 115, 59, 77, 64, 21, 47, 104, 55, 44, 218,
9, 54, 53, 130, 226, 64, 90, 70, 205, 40, 41, 23, 26, 57, 54, 57,
112, 184, 5, 41, 38, 166, 213, 30, 34, 26, 133, 152, 116, 10, 32, 134,
75, 32, 12, 51, 192, 255, 160, 43, 51, 39, 19, 53, 221, 26, 114, 32,
73, 255, 31, 9, 65, 234, 2, 15, 1, 118, 73, 88, 31, 35, 67, 102,
85, 55, 186, 85, 56, 21, 23, 111, 59, 205, 45, 37, 192, 55, 38, 70,
124, 73, 102, 1, 34, 98, 102, 61, 71, 37, 34, 53, 31, 243, 192, 69,
60, 71, 38, 73, 119, 28, 222, 37, 68, 45, 128, 34, 1, 47, 11, 245,
171, 62, 17, 19, 70, 146, 85, 55, 62, 70, 75, 15, 9, 9, 64, 255,
184, 119, 16, 37, 43, 37, 154, 100, 163, 85, 160, 1, 63, 9, 92, 136,
28, 64, 32, 201, 85, 86, 6, 28, 5, 64, 255, 25, 248, 1, 56, 8,
17, 132, 137, 255, 55, 116, 128, 58, 15, 20, 82, 135, 57, 26, 121, 40,
164, 50, 31, 137, 154, 133, 25, 35, 218, 51, 103, 44, 131, 131, 123, 31,
6, 158, 86, 40, 64, 135, 148, 224, 45, 183, 128, 22, 26, 17, 131, 240,
154, 14, 1, 209, 83, 12, 13, 54, 192, 255, 68, 47, 28, 45, 16, 21,
91, 64, 222, 7, 1, 197, 56, 21, 39, 155, 60, 138, 23, 102, 213, 85,
26, 85, 85, 128, 128, 32, 146, 171, 18, 11, 7, 63, 144, 171, 4, 4,
246, 35, 27, 10, 146, 174, 171, 12, 26, 128, 190, 80, 35, 99, 180, 80,
126, 54, 45, 85, 126, 47, 87, 176, 51, 41, 20, 32, 101, 75, 128, 139,
118, 146, 116, 128, 85, 56, 41, 15, 176, 236, 85, 37, 9, 62, 146, 36,
19, 30, 171, 255, 97, 27, 20, 71, 30, 17, 119, 118, 255, 17, 18, 138,
101, 38, 60, 138, 55, 70, 43, 26, 142, 138, 45, 61, 62, 219, 1, 81,
188, 64, 32, 41, 20, 117, 151, 142, 20, 21, 163, 112, 19, 12, 61, 195,
128, 48, 4, 24,
];
/// `dc_qlookup` (RFC 6386), 128 entries.
pub const DC_QLOOKUP: [u16; 128] = [
4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17,
18, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28,
29, 30, 31, 32, 33, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,
59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74,
75, 76, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
91, 93, 95, 96, 98, 100, 101, 102, 104, 106, 108, 110, 112, 114, 116, 118,
122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 143, 145, 148, 151, 154, 157,
];
/// `ac_qlookup` (RFC 6386), 128 entries.
pub const AC_QLOOKUP: [u16; 128] = [
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76,
78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108,
110, 112, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152,
155, 158, 161, 164, 167, 170, 173, 177, 181, 185, 189, 193, 197, 201, 205, 209,
213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259, 264, 269, 274, 279, 284,
];
/// `vp8_mode_contexts` (RFC 6386), 24 entries.
pub const MODE_CONTEXTS: [u8; 24] = [
7, 1, 1, 143, 14, 18, 14, 107, 135, 64, 57, 68, 60, 56, 128, 65,
159, 134, 128, 34, 234, 188, 128, 28,
];
/// `sub_mv_ref_prob` (RFC 6386), 15 entries.
pub const SUB_MV_REF_PROBS: [u8; 15] = [
147, 136, 18, 106, 145, 1, 179, 121, 1, 223, 1, 34, 208, 1, 1,
];
/// `vp8_mv_update_probs` (RFC 6386), 38 entries.
pub const MV_UPDATE_PROBS: [u8; 38] = [
237, 246, 253, 253, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 250, 250,
252, 254, 254, 231, 243, 245, 253, 254, 254, 254, 254, 254, 254, 254, 254, 254,
254, 251, 251, 254, 254, 254,
];
/// `default_mv_context` (RFC 6386), 38 entries.
pub const DEFAULT_MV_PROBS: [u8; 38] = [
162, 128, 225, 146, 172, 147, 214, 39, 156, 128, 129, 132, 75, 145, 178, 206,
239, 254, 254, 164, 128, 204, 170, 119, 235, 140, 230, 228, 128, 130, 130, 74,
148, 180, 203, 236, 254, 254,
];
/// `bmode_prob` (RFC 6386), 9 entries.
pub const BMODE_PROBS: [u8; 9] = [
120, 90, 79, 133, 87, 85, 80, 111, 151,
];
/// `coeff_bands` (RFC 6386), 16 entries.
pub const COEFF_BANDS: [u8; 16] = [
0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7,
];
/// `kf_ymode_prob` (RFC 6386), 4 entries.
pub const KF_YMODE_PROBS: [u8; 4] = [
145, 156, 163, 128,
];
/// `kf_uv_mode_prob` (RFC 6386), 3 entries.
pub const KF_UV_MODE_PROBS: [u8; 3] = [
142, 114, 183,
];
/// `ymode_prob` (RFC 6386), 4 entries.
pub const YMODE_PROBS: [u8; 4] = [
112, 86, 140, 37,
];
/// `uv_mode_prob` (RFC 6386), 3 entries.
pub const UV_MODE_PROBS: [u8; 3] = [
162, 101, 204,
];
/// `mvpartition_probs` (RFC 6386), 3 entries.
pub const MVPARTITION_PROBS: [u8; 3] = [
110, 111, 150,
];
/// `BilinearFilters` (RFC 6386), 48 entries.
pub const BILINEAR_FILTERS: [i32; 48] = [
0, 0, 128, 0, 0, 0, 0, 0, 112, 16, 0, 0, 0, 0, 96, 32,
0, 0, 0, 0, 80, 48, 0, 0, 0, 0, 64, 64, 0, 0, 0, 0,
48, 80, 0, 0, 0, 0, 32, 96, 0, 0, 0, 0, 16, 112, 0, 0,
];
/// `filters` (RFC 6386), 48 entries.
pub const SIXTAP_FILTERS: [i32; 48] = [
0, 0, 128, 0, 0, 0, 0, -6, 123, 12, -1, 0, 2, -11, 108, 36,
-8, 1, 0, -9, 93, 50, -6, 0, 3, -16, 77, 77, -16, 3, 0, -6,
50, 93, -9, 0, 1, -8, 36, 108, -11, 2, 0, -1, 12, 123, -6, 0,
];
+115
View File
@@ -0,0 +1,115 @@
//! Trees, enumerations and small tables of RFC 6386 that are not printed as `= { ... }` arrays
//! (the large tables are in `spec_tables.rs`).
// Macroblock luma / chroma modes (8.2), in the specification's enumeration order.
pub const DC_PRED: u8 = 0;
pub const V_PRED: u8 = 1;
pub const H_PRED: u8 = 2;
pub const TM_PRED: u8 = 3;
pub const B_PRED: u8 = 4;
// Subblock modes (11.2).
pub const B_DC_PRED: u8 = 0;
pub const B_TM_PRED: u8 = 1;
pub const B_VE_PRED: u8 = 2;
pub const B_HE_PRED: u8 = 3;
pub const B_LD_PRED: u8 = 4;
pub const B_RD_PRED: u8 = 5;
pub const B_VR_PRED: u8 = 6;
pub const B_VL_PRED: u8 = 7;
pub const B_HD_PRED: u8 = 8;
pub const B_HU_PRED: u8 = 9;
// Inter modes (16.2), numbered after the intra modes as in the specification.
pub const MV_NEAREST: u8 = 5;
pub const MV_NEAR: u8 = 6;
pub const MV_ZERO: u8 = 7;
pub const MV_NEW: u8 = 8;
pub const MV_SPLIT: u8 = 9;
// Split MV subblock modes (16.4), relative numbering.
pub const LEFT_4X4: u8 = 0;
pub const ABOVE_4X4: u8 = 1;
pub const ZERO_4X4: u8 = 2;
pub const NEW_4X4: u8 = 3;
/// `ymode_tree` (8.2).
pub const YMODE_TREE: [i8; 8] = [-(DC_PRED as i8), 2, 4, 6, -(V_PRED as i8), -(H_PRED as i8), -(TM_PRED as i8), -(B_PRED as i8)];
/// `kf_ymode_tree` (8.2).
pub const KF_YMODE_TREE: [i8; 8] = [-(B_PRED as i8), 2, 4, 6, -(DC_PRED as i8), -(V_PRED as i8), -(H_PRED as i8), -(TM_PRED as i8)];
/// `uv_mode_tree` (8.2).
pub const UV_MODE_TREE: [i8; 6] = [-(DC_PRED as i8), 2, -(V_PRED as i8), 4, -(H_PRED as i8), -(TM_PRED as i8)];
/// `bmode_tree` (11.2).
pub const BMODE_TREE: [i8; 18] = [
-(B_DC_PRED as i8),
2,
-(B_TM_PRED as i8),
4,
-(B_VE_PRED as i8),
6,
8,
12,
-(B_HE_PRED as i8),
10,
-(B_RD_PRED as i8),
-(B_VR_PRED as i8),
-(B_LD_PRED as i8),
14,
-(B_VL_PRED as i8),
16,
-(B_HD_PRED as i8),
-(B_HU_PRED as i8),
];
/// `mb_segment_tree` (10).
pub const SEGMENT_TREE: [i8; 6] = [2, 4, 0, -1, -2, -3];
/// `mv_ref_tree` (16.2), values relative to `MV_NEAREST`.
pub const MV_REF_TREE: [i8; 8] =
[-((MV_ZERO - MV_NEAREST) as i8), 2, 0, 4, -((MV_NEAR - MV_NEAREST) as i8), 6, -((MV_NEW - MV_NEAREST) as i8), -((MV_SPLIT - MV_NEAREST) as i8)];
// Split partitionings (16.4).
pub const SPLIT_TOP_BOTTOM: u8 = 0;
pub const SPLIT_LEFT_RIGHT: u8 = 1;
pub const SPLIT_QUARTERS: u8 = 2;
pub const SPLIT_16: u8 = 3;
/// `mvpartition_tree` (16.4).
pub const MVPARTITION_TREE: [i8; 6] = [-(SPLIT_16 as i8), 2, -(SPLIT_QUARTERS as i8), 4, -(SPLIT_TOP_BOTTOM as i8), -(SPLIT_LEFT_RIGHT as i8)];
/// `sub_mv_ref_tree` (16.4).
pub const SUB_MV_REF_TREE: [i8; 6] = [-(LEFT_4X4 as i8), 2, -(ABOVE_4X4 as i8), 4, -(ZERO_4X4 as i8), -(NEW_4X4 as i8)];
/// The partition of each Y subblock, per partitioning (the layouts listed with the MVpartition
/// enumeration, 16.4).
pub const SPLIT_MAPS: [[u8; 16]; 4] = [
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
[0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
];
/// Number of partitions per partitioning.
pub const SPLIT_COUNTS: [usize; 4] = [2, 2, 4, 16];
/// `small_mvtree` (17.1).
pub const SMALL_MV_TREE: [i8; 14] = [2, 8, 4, 6, 0, -1, -2, -3, 10, 12, -4, -5, -6, -7];
// Offsets in an MV_CONTEXT (17.1).
pub const MVP_IS_SHORT: usize = 0;
pub const MVP_SIGN: usize = 1;
pub const MVP_SHORT: usize = 2;
pub const MVP_BITS: usize = 9;
// DCT tokens (13.2).
pub const DCT_0: u8 = 0;
pub const DCT_4: u8 = 4;
pub const DCT_CAT1: u8 = 5;
pub const DCT_EOB: u8 = 11;
/// `coeff_tree` (13.2).
pub const COEFF_TREE: [i8; 22] = [-(DCT_EOB as i8), 2, -(DCT_0 as i8), 4, -1, 6, 8, 12, -2, 10, -3, -4, 14, 16, -5, -6, 18, 20, -7, -8, -9, -10];
/// `Pcat1` ... `Pcat6` (13.2), without the terminating zeros.
pub const PCAT: [&[u8]; 6] =
[&[159], &[165, 145], &[173, 148, 140], &[176, 155, 140, 135], &[180, 157, 141, 134, 130], &[254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129]];
/// `categoryBase` (13.3).
pub const CAT_BASE: [i32; 6] = [5, 7, 11, 19, 35, 67];
/// Coefficient scan order (13): position `i` of the token stream is coefficient
/// `ZIGZAG[i]` of the 4x4 block in raster order — the usual 4x4 zig-zag.
pub const ZIGZAG: [usize; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15];
/// Probabilities of the segment tree when not updated (9.3).
pub const DEFAULT_SEGMENT_PROB: u8 = 255;
+94
View File
@@ -0,0 +1,94 @@
//! Inverse transforms (14.3, 14.4). Inputs, the intermediate signal and the outputs are 16-bit
//! signed integers, as the specification requires; the casts to `i16` reproduce that.
const COSPI8SQRT2MINUS1: i32 = 20091;
const SINPI8SQRT2: i32 = 35468;
/// Inverse WHT of the Y2 block (14.3): `input` in raster order; `out[i]` becomes the DC of
/// Y subblock `i`.
pub fn inverse_wht(input: &[i16; 16]) -> [i16; 16] {
let mut tmp = [0i16; 16];
for i in 0..4 {
let ip = |k: usize| i32::from(input[i + 4 * k]);
let a1 = ip(0) + ip(3);
let b1 = ip(1) + ip(2);
let c1 = ip(1) - ip(2);
let d1 = ip(0) - ip(3);
tmp[i] = (a1 + b1) as i16;
tmp[i + 4] = (c1 + d1) as i16;
tmp[i + 8] = (a1 - b1) as i16;
tmp[i + 12] = (d1 - c1) as i16;
}
let mut out = [0i16; 16];
for i in 0..4 {
let ip = |k: usize| i32::from(tmp[4 * i + k]);
let a1 = ip(0) + ip(3);
let b1 = ip(1) + ip(2);
let c1 = ip(1) - ip(2);
let d1 = ip(0) - ip(3);
out[4 * i] = ((a1 + b1 + 3) >> 3) as i16;
out[4 * i + 1] = ((c1 + d1 + 3) >> 3) as i16;
out[4 * i + 2] = ((a1 - b1 + 3) >> 3) as i16;
out[4 * i + 3] = ((d1 - c1 + 3) >> 3) as i16;
}
out
}
/// Inverse DCT (14.4) of a 4x4 block in raster order, returning the residue in raster order.
pub fn inverse_dct(input: &[i16; 16]) -> [i16; 16] {
let mul_s = |x: i32| (x * SINPI8SQRT2) >> 16;
let mul_c = |x: i32| x + ((x * COSPI8SQRT2MINUS1) >> 16);
let mut tmp = [0i16; 16];
for i in 0..4 {
let ip = |k: usize| i32::from(input[i + 4 * k]);
let a1 = ip(0) + ip(2);
let b1 = ip(0) - ip(2);
let c1 = mul_s(ip(1)) - mul_c(ip(3));
let d1 = mul_c(ip(1)) + mul_s(ip(3));
tmp[i] = (a1 + d1) as i16;
tmp[i + 12] = (a1 - d1) as i16;
tmp[i + 4] = (b1 + c1) as i16;
tmp[i + 8] = (b1 - c1) as i16;
}
let mut out = [0i16; 16];
for i in 0..4 {
let ip = |k: usize| i32::from(tmp[4 * i + k]);
let a1 = ip(0) + ip(2);
let b1 = ip(0) - ip(2);
let c1 = mul_s(ip(1)) - mul_c(ip(3));
let d1 = mul_c(ip(1)) + mul_s(ip(3));
out[4 * i] = ((a1 + d1 + 4) >> 3) as i16;
out[4 * i + 3] = ((a1 - d1 + 4) >> 3) as i16;
out[4 * i + 1] = ((b1 + c1 + 4) >> 3) as i16;
out[4 * i + 2] = ((b1 - c1 + 4) >> 3) as i16;
}
out
}
/// Add `residue` to the 4x4 block at `pos` of a plane (14.5), saturating to 8 bits.
#[inline]
pub fn add_residue(plane: &mut [u8], stride: usize, pos: usize, residue: &[i16; 16]) {
for r in 0..4 {
let Some(row) = plane.get_mut(pos + r * stride..pos + r * stride + 4) else { return };
for (c, px) in row.iter_mut().enumerate() {
*px = (i32::from(*px) + i32::from(residue[4 * r + c])).clamp(0, 255) as u8;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dc_only_blocks_are_flat() {
for dc in [-2048i16, -100, -5, -4, -3, 0, 3, 4, 5, 100, 2047] {
let mut b = [0i16; 16];
b[0] = dc;
let out = inverse_dct(&b);
assert!(out.iter().all(|&v| i32::from(v) == (i32::from(dc) + 4) >> 3), "dct dc {dc}");
let out = inverse_wht(&b);
assert!(out.iter().all(|&v| i32::from(v) == (i32::from(dc) + 3) >> 3), "wht dc {dc}");
}
}
}
+197
View File
@@ -0,0 +1,197 @@
//! Test fixture generation (ffmpeg + libvpx as external encoder and oracle), a tiny IVF reader
//! and the decode helpers.
#![allow(dead_code)]
use filmcraft_vp8::{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,
/// libvpx arguments; a leading "2pass" encodes in two passes.
pub args: &'static [&'static str],
}
macro_rules! fx {
($name:expr, $src:expr, $w:expr, $h:expr, $n:expr, [$($a:expr),* $(,)?]) => {
Fixture { name: $name, source: $src, width: $w, height: $h, frames: $n, args: &[$($a),*] }
};
}
pub const FIXTURES: &[Fixture] = &[
fx!("default", "testsrc2", 352, 288, 20, ["-b:v", "600k"]),
fx!("odd_size", "testsrc2", 177, 99, 12, ["-b:v", "300k"]),
fx!("tiny", "testsrc2", 33, 17, 10, ["-b:v", "50k"]),
fx!("intra_only", "mandelbrot", 352, 288, 6, ["-g", "1", "-b:v", "2M"]),
fx!("noise_high_rate", "cellauto", 352, 288, 10, ["-b:v", "4M"]),
fx!("near_lossless", "testsrc2", 176, 144, 8, ["-qmin", "0", "-qmax", "0", "-b:v", "20M"]),
fx!("coarse", "mandelbrot", 352, 288, 12, ["-qmin", "60", "-qmax", "63", "-crf", "62", "-b:v", "100k"]),
fx!("partitions8", "testsrc2", 640, 360, 8, ["-b:v", "1M", "-slices", "8"]),
fx!("altref_2pass", "mandelbrot", 352, 288, 30, ["2pass", "-b:v", "500k", "-auto-alt-ref", "1", "-lag-in-frames", "16"]),
fx!("sharpness7", "testsrc2", 352, 288, 10, ["-b:v", "300k", "-sharpness", "7"]),
fx!("version1_bilinear_simple", "testsrc2", 352, 288, 12, ["-b:v", "500k", "-profile:v", "1"]),
fx!("version2", "testsrc2", 352, 288, 12, ["-b:v", "500k", "-profile:v", "2"]),
fx!("version3_fullpixel", "mandelbrot", 352, 288, 12, ["-b:v", "500k", "-profile:v", "3"]),
fx!("realtime_segments", "testsrc2", 352, 288, 20, ["-b:v", "300k", "-deadline", "realtime", "-cpu-used", "8", "-error-resilient", "1"]),
fx!("screen", "smptehdbars", 352, 288, 12, ["-b:v", "200k", "-screen-content-mode", "1"]),
fx!(
"temporal_layers",
"testsrc2",
352,
288,
20,
[
"-b:v",
"800k",
"-deadline",
"realtime",
"-cpu-used",
"8",
// error resilient: frames keep their probability updates to themselves, so the
// enhancement layer can be dropped
"-error-resilient",
"1",
"-ts-parameters",
"ts_number_layers=2:ts_target_bitrate=400,800:ts_rate_decimator=2,1:ts_periodicity=2:ts_layer_id=0,1:ts_layering_mode=2"
]
),
fx!("hd_1080p", "testsrc2", 1920, 1080, 3, ["-b:v", "4M", "-deadline", "realtime", "-cpu-used", "8"]),
];
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/vp8-tests");
std::fs::create_dir_all(&d).expect("create fixtures dir");
d
}
/// Unique suffix for temporary files: tests run on several threads 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: {cmd:?}\n{}", String::from_utf8_lossy(&o.stderr));
false
}
Err(e) => {
eprintln!("failed to run {cmd:?}: {e}");
false
}
}
}
/// Generate (if needed) the fixture stream and its reference decode by libvpx through ffmpeg.
/// `None` (the test skips) without ffmpeg or its libvpx encoder.
pub fn ensure(f: &Fixture) -> Option<(PathBuf, PathBuf)> {
// one generation at a time: tests share fixtures, and realtime libvpx encodes differ from run
// to run, so a stream and its reference decode must come from the same encode
static GENERATING: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = GENERATING.lock().unwrap_or_else(|e| e.into_inner());
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()));
// the source at an even size, scaled to the fixture's (possibly odd) size
let src =
format!("{}=size={}x{}:rate=25,scale={}:{},format=yuv420p", f.source, f.width.next_multiple_of(2), f.height.next_multiple_of(2), f.width, f.height);
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>| {
let mut c = Command::new(&ff);
c.args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", &src]);
c.args(["-frames:v", &f.frames.to_string(), "-c:v", "libvpx"]);
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(&tmp);
}
c
};
if two_pass && !run(&mut enc(Some(1))) {
eprintln!("SKIP {}: encode failed (ffmpeg without libvpx?)", f.name);
return None;
}
if !run(&mut enc(two_pass.then_some(2))) {
eprintln!("SKIP {}: encode failed (ffmpeg without libvpx?)", f.name);
return None;
}
if std::fs::rename(&tmp, &ivf).is_err() && !ivf.exists() {
return None;
}
}
if !yuv.exists() {
let tmp = dir.join(format!("{}.tmp.{}.yuv", f.name, unique()));
let mut c = Command::new(&ff);
c.args(["-hide_banner", "-loglevel", "error", "-y", "-c:v", "libvpx", "-i"]).arg(&ivf);
c.args(["-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", "yuv420p"]).arg(&tmp);
if !run(&mut c) {
eprintln!("SKIP {}: reference decode failed", f.name);
return None;
}
if std::fs::rename(&tmp, &yuv).is_err() && !yuv.exists() {
return None;
}
}
Some((ivf, yuv))
}
/// Frames of an IVF file.
pub fn ivf_frames(data: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let Some(h) = data.get(6..8) else { return out };
let mut pos = usize::from(u16::from_le_bytes([h[0], h[1]]));
while pos + 12 <= data.len() {
let size = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 12;
let end = (pos + size).min(data.len());
out.push(&data[pos..end]);
pos = end;
}
out
}
/// Decode every frame; the pictures in display order.
pub fn decode_all(frames: &[&[u8]]) -> Vec<Picture> {
let mut dec = Decoder::new();
frames.iter().enumerate().filter_map(|(i, f)| dec.decode(f, i as i64).expect("decode")).collect()
}
/// A picture as I420 bytes.
pub fn i420(p: &Picture) -> Vec<u8> {
[p.y.as_slice(), &p.u, &p.v].concat()
}
+91
View File
@@ -0,0 +1,91 @@
//! Bit-exactness against libvpx (through ffmpeg) on generated fixtures covering every coding
//! tool: skipped when ffmpeg or its libvpx encoder is absent.
mod common;
fn check(name: &str) {
let f = common::fixture(name);
let Some((ivf, yuv)) = common::ensure(f) else { return };
let data = std::fs::read(&ivf).unwrap();
let reference = std::fs::read(&yuv).unwrap();
let pictures = common::decode_all(&common::ivf_frames(&data));
let (w, h) = (f.width as usize, f.height as usize);
let size = w * h + 2 * w.div_ceil(2) * h.div_ceil(2);
assert_eq!(pictures.len(), reference.len() / size, "{name}: picture count");
for (i, p) in pictures.iter().enumerate() {
assert_eq!((p.width as usize, p.height as usize), (w, h), "{name}: size of picture {i}");
assert!(common::i420(p) == reference[i * size..(i + 1) * size], "{name}: picture {i} differs from libvpx");
}
}
macro_rules! fixture_tests {
($($name:ident),* $(,)?) => {
$(
#[test]
fn $name() {
check(stringify!($name));
}
)*
};
}
fixture_tests!(
default,
odd_size,
tiny,
intra_only,
noise_high_rate,
near_lossless,
coarse,
partitions8,
altref_2pass,
sharpness7,
version1_bilinear_simple,
version2,
version3_fullpixel,
realtime_segments,
screen,
temporal_layers,
hd_1080p,
);
/// Hidden (altref) frames produce no picture, and leaving out the frames the decoder calls
/// disposable changes none of the other pictures.
#[test]
fn hidden_and_disposable_frames() {
for name in ["altref_2pass", "temporal_layers"] {
let f = common::fixture(name);
let Some((ivf, _)) = common::ensure(f) else { return };
let data = std::fs::read(&ivf).unwrap();
let frames = common::ivf_frames(&data);
// every picture, and which frames could be left out (asked right before each is decoded)
let mut dec = filmcraft_vp8::Decoder::new();
let mut all = Vec::new();
let mut disposable = Vec::new();
for (i, f) in frames.iter().enumerate() {
disposable.push(dec.is_disposable(f));
if let Some(p) = dec.decode(f, i as i64).unwrap() {
all.push((p.pts, common::i420(&p)));
}
}
let stats = dec.stats();
assert_eq!(stats.frames as usize, frames.len());
assert_eq!(all.len() + stats.hidden_frames as usize, frames.len(), "{name}: one picture per shown frame");
if name == "altref_2pass" {
assert!(stats.hidden_frames > 0, "{name}: has hidden altref frames");
}
if name == "temporal_layers" {
assert!(disposable.iter().filter(|&&d| d).count() >= frames.len() / 3, "{name}: the enhancement layer is disposable: {disposable:?}");
}
let mut dec = filmcraft_vp8::Decoder::new();
for (i, f) in frames.iter().enumerate() {
if disposable[i] {
continue;
}
if let Some(p) = dec.decode(f, i as i64).unwrap() {
let reference = all.iter().find(|(pts, _)| *pts == p.pts).expect("picture of a shown frame");
assert!(common::i420(&p) == reference.1, "{name}: picture {} changed after leaving out disposable frames", p.pts);
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
//! Damaged, truncated, reordered and random input must never panic (errors are fine).
mod common;
use filmcraft_vp8::{Decoder, Error};
fn xorshift(seed: &mut u64) -> u64 {
*seed ^= *seed << 13;
*seed ^= *seed >> 7;
*seed ^= *seed << 17;
*seed
}
fn decode_all(frames: &[Vec<u8>]) {
let mut dec = Decoder::new();
for (i, f) in frames.iter().enumerate() {
let _ = dec.is_disposable(f);
let _ = dec.decode(f, i as i64);
let _ = filmcraft_vp8::keyframe_info(f);
}
}
#[test]
fn corrupted_streams_do_not_panic() {
for name in ["default", "altref_2pass", "partitions8", "realtime_segments", "version3_fullpixel", "odd_size"] {
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 = 0x9e37_79b9_7f4a_7c15u64 ^ name.len() as u64;
for round in 0..12 {
let mut d = frames.clone();
for fr in d.iter_mut() {
if fr.is_empty() {
continue;
}
match round % 4 {
0 => {
for _ in 0..1 + round {
let i = (xorshift(&mut seed) as usize) % fr.len();
fr[i] ^= 1 << (xorshift(&mut seed) % 8);
}
}
1 => {
let n = (xorshift(&mut seed) as usize) % (fr.len() + 1);
fr.truncate(n);
}
2 => {
let i = (xorshift(&mut seed) as usize) % fr.len().min(24);
fr[i] = xorshift(&mut seed) as u8;
}
// frames left intact; their order is shuffled below
_ => {}
}
}
if round % 4 == 3 {
for i in (1..d.len()).rev() {
let j = (xorshift(&mut seed) as usize) % (i + 1);
d.swap(i, j);
}
}
decode_all(&d);
}
}
}
#[test]
fn random_bytes_and_tiny_frames_do_not_panic() {
let mut seed = 42u64;
let mut dec = Decoder::new();
for len in 0..200 {
let f: Vec<u8> = (0..len).map(|_| xorshift(&mut seed) as u8).collect();
let _ = dec.decode(&f, len as i64);
}
// a key frame header claiming a large size followed by garbage
let mut key = vec![0u8; 64];
key[0] = 0x10; // key frame, version 0, shown, first partition 0 bytes
key[3..6].copy_from_slice(&[0x9d, 0x01, 0x2a]);
key[6..8].copy_from_slice(&1920u16.to_le_bytes());
key[8..10].copy_from_slice(&1080u16.to_le_bytes());
let _ = dec.decode(&key, 0);
}
#[test]
fn inter_frame_without_key_frame_is_an_error() {
let f = common::fixture("default");
let Some((ivf, _)) = common::ensure(f) else { return };
let data = std::fs::read(ivf).unwrap();
let frames = common::ivf_frames(&data);
let mut dec = Decoder::new();
assert!(matches!(dec.decode(frames[1], 1), Err(Error::MissingReference(_))));
// and decoding works from the next key frame on; after a reset again only from a key frame
assert!(dec.decode(frames[0], 0).unwrap().is_some());
assert!(dec.decode(frames[1], 1).unwrap().is_some());
dec.reset();
assert!(matches!(dec.decode(frames[2], 2), Err(Error::MissingReference(_))));
assert!(filmcraft_vp8::is_keyframe(frames[0]) && !filmcraft_vp8::is_keyframe(frames[1]));
}
+77
View File
@@ -0,0 +1,77 @@
//! The libvpx VP8 test vectors (`vp80-0*.ivf` with their `.md5` files of per-frame I420 MD5s),
//! when present in `target/fixtures/vp8/conformance` or `$VP8_TEST_VECTORS`. They are not
//! downloaded by the tests; fetch them from
//! `https://storage.googleapis.com/downloads.webmproject.org/test_data/libvpx/<name>`.
mod common;
/// MD5 (RFC 1321), for comparing with the vectors' checksums.
fn md5(data: &[u8]) -> String {
let s: [u32; 64] = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4,
11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
let k: Vec<u32> = (0..64).map(|i| ((i as f64 + 1.0).sin().abs() * 4294967296.0) as u32).collect();
let (mut a0, mut b0, mut c0, mut d0) = (0x67452301u32, 0xefcdab89u32, 0x98badcfeu32, 0x10325476u32);
let mut msg = data.to_vec();
let bits = (data.len() as u64).wrapping_mul(8);
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&bits.to_le_bytes());
for chunk in msg.chunks(64) {
let m: Vec<u32> = chunk.chunks(4).map(|w| u32::from_le_bytes([w[0], w[1], w[2], w[3]])).collect();
let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
for i in 0..64 {
let (f, g) = match i / 16 {
0 => ((b & c) | (!b & d), i),
1 => ((d & b) | (!d & c), (5 * i + 1) % 16),
2 => (b ^ c ^ d, (3 * i + 5) % 16),
_ => (c ^ (b | !d), (7 * i) % 16),
};
let f = f.wrapping_add(a).wrapping_add(k[i]).wrapping_add(m[g]);
a = d;
d = c;
c = b;
b = b.wrapping_add(f.rotate_left(s[i]));
}
a0 = a0.wrapping_add(a);
b0 = b0.wrapping_add(b);
c0 = c0.wrapping_add(c);
d0 = d0.wrapping_add(d);
}
[a0, b0, c0, d0].iter().flat_map(|v| v.to_le_bytes()).map(|b| format!("{b:02x}")).collect()
}
#[test]
fn md5_matches_rfc_1321() {
assert_eq!(md5(b""), "d41d8cd98f00b204e9800998ecf8427e");
assert_eq!(md5(b"The quick brown fox jumps over the lazy dog"), "9e107d9d372bb6826bd81d3542a419d6");
}
#[test]
fn libvpx_test_vectors() {
let dir = std::env::var_os("VP8_TEST_VECTORS")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/vp8/conformance"));
let Ok(entries) = std::fs::read_dir(&dir) else {
eprintln!("SKIP: no test vectors in {}", dir.display());
return;
};
let mut names: Vec<_> = entries.filter_map(|e| e.ok()).map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "ivf")).collect();
names.sort();
let mut checked = 0;
for ivf in names {
let Ok(sums) = std::fs::read_to_string(ivf.with_extension("ivf.md5")) else { continue };
let expected: Vec<&str> = sums.lines().filter_map(|l| l.split_whitespace().next()).collect();
let data = std::fs::read(&ivf).unwrap();
let pictures = common::decode_all(&common::ivf_frames(&data));
assert_eq!(pictures.len(), expected.len(), "{}: picture count", ivf.display());
for (i, (p, want)) in pictures.iter().zip(&expected).enumerate() {
assert_eq!(md5(&common::i420(p)), *want, "{} frame {i}", ivf.display());
}
checked += 1;
}
eprintln!("{checked} test vectors bit-exact");
}
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Extract the VP8 constant tables from the text of RFC 6386 ("VP8 Data Format and Decoding Guide").
Usage:
curl -L -o rfc6386.txt https://www.rfc-editor.org/rfc/rfc6386.txt
python3 extract_tables.py rfc6386.txt > ../src/spec_tables.rs
Only sections 1-19 (the format description) are read: the text is cut at section 20 ("Attachment
One: Reference Decoder Source Code"), so nothing comes from the reference decoder. 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()
cut = text.find("\n20. Attachment One")
if cut < 0:
sys.exit("section 20 not found")
text = text[:cut]
# page breaks: footer / header lines and form feeds
text = "\n".join(l for l in text.split("\n") if not l.startswith("Bankoski") and not l.startswith("RFC 6386 ") and "\f" not in l)
def table(name, count, occurrence=-1):
"""Integers of the table `name ... = { ... }` (by default the last definition in the text)."""
found = []
# a definition: `name [dim] [dim] ... = {` (uses of the table never have `= {` after the brackets)
for m in re.finditer(r"\b%s\s*(?:\[[^\]\n]*\]\s*)+=\s*\{" % re.escape(name), text):
brace = m.end() - 1
depth = 0
i = brace
while True:
ch = text[i]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
break
i += 1
body = re.sub(r"/\*.*?\*/", "", text[brace:i], flags=re.S)
body = re.sub(r"//[^\n]*", "", body)
found.append([int(v) for v in re.findall(r"-?\d+", body)])
if not found:
sys.exit("table %s not found" % name)
vals = found[occurrence]
if len(vals) != count:
sys.exit("table %s: %d values, expected %d" % (name, len(vals), count))
return vals
TABLES = [
# (spec name, rust name, rust type, count)
("coeff_update_probs", "COEFF_UPDATE_PROBS", "u8", 4 * 8 * 3 * 11),
("default_coeff_probs", "DEFAULT_COEFF_PROBS", "u8", 4 * 8 * 3 * 11),
("kf_bmode_prob", "KF_BMODE_PROBS", "u8", 10 * 10 * 9),
("dc_qlookup", "DC_QLOOKUP", "u16", 128),
("ac_qlookup", "AC_QLOOKUP", "u16", 128),
("vp8_mode_contexts", "MODE_CONTEXTS", "u8", 6 * 4),
("sub_mv_ref_prob", "SUB_MV_REF_PROBS", "u8", 5 * 3),
("vp8_mv_update_probs", "MV_UPDATE_PROBS", "u8", 2 * 19),
("default_mv_context", "DEFAULT_MV_PROBS", "u8", 2 * 19),
("bmode_prob", "BMODE_PROBS", "u8", 9),
("coeff_bands", "COEFF_BANDS", "u8", 16),
("kf_ymode_prob", "KF_YMODE_PROBS", "u8", 4),
("kf_uv_mode_prob", "KF_UV_MODE_PROBS", "u8", 3),
("ymode_prob", "YMODE_PROBS", "u8", 4),
("uv_mode_prob", "UV_MODE_PROBS", "u8", 3),
("mvpartition_probs", "MVPARTITION_PROBS", "u8", 3),
("BilinearFilters", "BILINEAR_FILTERS", "i32", 8 * 6),
("filters", "SIXTAP_FILTERS", "i32", 8 * 6),
]
print("// Generated by tools/extract_tables.py from the text of RFC 6386 (sections 1-19). Do not edit.")
print("// Every table is flat, row-major, in the order the specification prints it.")
print()
for spec, rust, ty, count in TABLES:
vals = table(spec, count)
print("/// `%s` (RFC 6386), %d entries." % (spec, count))
print("pub const %s: [%s; %d] = [" % (rust, ty, count))
for i in range(0, count, 16):
print(" " + ", ".join(str(v) for v in vals[i:i + 16]) + ",")
print("];")
print()
+1
View File
@@ -44,6 +44,7 @@ const LAYERS: &[(&str, u8)] = &[
("h264", 0),
("h264enc", 0),
("hevc", 0),
("vp8", 0),
("vp9", 0),
("prores", 0),
("mjpeg", 0),