mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M10.2: VP9 inter frames (motion search, MV prediction, loop filter, forward probability updates, rate control)
effectcraft-vp9enc gains inter frames: single LAST reference, 16x16/8x8 blocks, candidate + diamond search refined to quarter samples with the regular 8-tap filters, NEARESTMV/NEARMV/ZEROMV/NEWMV with spec MV prediction, skip blocks, the in-loop deblocking filter, per-frame forward probability updates, a key frame interval and target-bitrate rate control. Decoded output matches the encoder's reconstruction exactly through FilmCraft's decoder and ffmpeg. WebM export now encodes sequentially with a key frame every two seconds. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7c9d865d19
commit
b26fbb6247
14 files changed
+2511
-732
No files matched your search
@@ -7,7 +7,7 @@
|
||||
//! | PNG / JPEG / TIFF sequence | one file per frame | 8-bit | PNG, TIFF | — |
|
||||
//! | OpenEXR sequence | one file per frame | 32-bit float, linear light, premultiplied | yes | — |
|
||||
//! | Animated GIF | GIF89a | 256-colour palette per frame (NeuQuant) | 1-bit | — |
|
||||
//! | WebM | WebM (Matroska) | VP9 profile 0 intra frames (`effectcraft-vp9enc`), 8-bit 4:2:0 | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`), 48 kHz stereo |
|
||||
//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0 | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`), 48 kHz stereo |
|
||||
//! | WAV / AIFF | RIFF WAVE / AIFF | — | — | 16-bit PCM stereo (audio only) |
|
||||
//!
|
||||
//! Frames are rendered in parallel batches (rayon; one batch ≈ one frame per core) and handed to
|
||||
|
||||
+50
-22
@@ -5,8 +5,10 @@
|
||||
//! (size patched at the end), Info, Tracks and one Cluster per render batch. Video frames are
|
||||
//! SimpleBlocks; with alpha (Channels: RGB + Alpha) each frame is a BlockGroup whose
|
||||
//! BlockAdditional (BlockAddID 1) carries a second VP9 frame coding the alpha channel as luma,
|
||||
//! with `AlphaMode` 1 on the track, as WebM defines. VP9 comes from `effectcraft-vp9enc` (every
|
||||
//! frame a key frame), Opus from `effectcraft-opusenc` (CELT, 48 kHz stereo, 20 ms packets,
|
||||
//! with `AlphaMode` 1 on the track, as WebM defines. VP9 comes from `effectcraft-vp9enc` (key
|
||||
//! frames every two seconds, inter frames between them: SimpleBlocks carry the key flag, inter
|
||||
//! BlockGroups a ReferenceBlock; the alpha stream's key frames line up with the colour stream's),
|
||||
//! Opus from `effectcraft-opusenc` (CELT, 48 kHz stereo, 20 ms packets,
|
||||
//! `CodecDelay` = pre-skip). WAV (RIFF, 16-bit PCM) and AIFF (big-endian 16-bit, 80-bit
|
||||
//! extended sample rate) follow their published file layouts.
|
||||
|
||||
@@ -82,6 +84,10 @@ fn block(track: u64, rel: i16, flags: u8, data: &[u8]) -> Vec<u8> {
|
||||
struct Frame {
|
||||
ms: i64,
|
||||
track: u64,
|
||||
/// A key frame (VP9) or any audio packet.
|
||||
key: bool,
|
||||
/// Timestamp of the previous video frame (ReferenceBlock of inter frames).
|
||||
prev_ms: Option<i64>,
|
||||
data: Vec<u8>,
|
||||
alpha: Option<Vec<u8>>,
|
||||
}
|
||||
@@ -94,10 +100,15 @@ fn cluster(frames: &mut [Frame]) -> Vec<u8> {
|
||||
for f in frames.iter() {
|
||||
let rel = (f.ms - base).clamp(i16::MIN as i64, i16::MAX as i64) as i16;
|
||||
match &f.alpha {
|
||||
None => el(&mut body, 0xA3, &block(f.track, rel, 0x80, &f.data)),
|
||||
None => el(&mut body, 0xA3, &block(f.track, rel, if f.key { 0x80 } else { 0x00 }, &f.data)),
|
||||
Some(a) => {
|
||||
let mut g = vec![];
|
||||
el(&mut g, 0xA1, &block(f.track, rel, 0x00, &f.data));
|
||||
if let (false, Some(p)) = (f.key, f.prev_ms) {
|
||||
// ReferenceBlock: a signed timestamp relative to this block (an inter frame).
|
||||
let d = (p - f.ms).clamp(i16::MIN as i64, -1) as i16;
|
||||
el(&mut g, 0xFB, &d.to_be_bytes());
|
||||
}
|
||||
let mut more = vec![];
|
||||
el_uint(&mut more, 0xEE, 1);
|
||||
el(&mut more, 0xA5, a);
|
||||
@@ -117,7 +128,19 @@ pub(crate) fn webm(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> Re
|
||||
let rate = job.settings.rate(comp);
|
||||
let alpha = job.output.channels == Channels::Rgba;
|
||||
let quality = job.output.quality.clamp(1, 100);
|
||||
let cfg = effectcraft_vp9enc::EncoderConfig { width: w, height: h, quality, full_range: false };
|
||||
let fps = rate.as_f64().max(1.0);
|
||||
let cfg = effectcraft_vp9enc::EncoderConfig {
|
||||
width: w,
|
||||
height: h,
|
||||
quality,
|
||||
full_range: false,
|
||||
keyframe_interval: (fps * 2.0).round().max(1.0) as u32,
|
||||
target_kbps: None,
|
||||
frame_rate: fps,
|
||||
loop_filter: true,
|
||||
};
|
||||
let mut color_enc = effectcraft_vp9enc::Vp9Encoder::new(cfg.clone());
|
||||
let mut alpha_enc = alpha.then(|| effectcraft_vp9enc::Vp9Encoder::new(cfg.clone()));
|
||||
let with_audio = wants_audio(job);
|
||||
let mut opus = with_audio.then(|| effectcraft_opusenc::OpusEncoder::new(2, OPUS_BITRATE));
|
||||
let total = st.total;
|
||||
@@ -209,7 +232,7 @@ pub(crate) fn webm(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> Re
|
||||
while pcm_left.len() >= fs {
|
||||
let pkt = enc.encode_float(&pcm_left[..fs]);
|
||||
pcm_left.drain(..fs);
|
||||
frames.push(Frame { ms: packets as i64 * 20, track: 2, data: pkt, alpha: None });
|
||||
frames.push(Frame { ms: packets as i64 * 20, track: 2, key: true, prev_ms: None, data: pkt, alpha: None });
|
||||
packets += 1;
|
||||
}
|
||||
};
|
||||
@@ -218,23 +241,28 @@ pub(crate) fn webm(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> Re
|
||||
let mut i = 0;
|
||||
while i < total {
|
||||
let end = (i + batch).min(total);
|
||||
let encoded: Vec<(Vec<u8>, Option<Vec<u8>>)> = (i..end)
|
||||
.into_par_iter()
|
||||
.map(|k| {
|
||||
let px = rgba8(&render_frame(job, comp, k), comp, if alpha { Channels::Rgba } else { Channels::Rgb }, w, h);
|
||||
let mut enc = effectcraft_vp9enc::Vp9Encoder::new(cfg.clone());
|
||||
let (y, u, vv) = effectcraft_vp9enc::rgba_to_yuv420(&px, w, h, false);
|
||||
let color = enc.encode_yuv420(&y, &u, &vv);
|
||||
let a = alpha.then(|| {
|
||||
let mut ea = effectcraft_vp9enc::Vp9Encoder::new(cfg.clone());
|
||||
let (y, u, vv) = effectcraft_vp9enc::alpha_to_yuv420(&px, w, h);
|
||||
ea.encode_yuv420(&y, &u, &vv)
|
||||
});
|
||||
(color, a)
|
||||
})
|
||||
.collect();
|
||||
let mut frames: Vec<Frame> =
|
||||
encoded.into_iter().enumerate().map(|(j, (data, alpha))| Frame { ms: frame_ms(i + j as u64), track: 1, data, alpha }).collect();
|
||||
// Frames render in parallel; the VP9 streams (inter-coded) encode in order, colour and
|
||||
// alpha side by side.
|
||||
let pixels: Vec<Vec<u8>> =
|
||||
(i..end).into_par_iter().map(|k| rgba8(&render_frame(job, comp, k), comp, if alpha { Channels::Rgba } else { Channels::Rgb }, w, h)).collect();
|
||||
let mut frames: Vec<Frame> = Vec::with_capacity(pixels.len());
|
||||
for (j, px) in pixels.iter().enumerate() {
|
||||
let (color, a) = rayon::join(
|
||||
|| {
|
||||
let (y, u, vv) = effectcraft_vp9enc::rgba_to_yuv420(px, w, h, false);
|
||||
color_enc.encode_frame(&y, &u, &vv, false)
|
||||
},
|
||||
|| {
|
||||
alpha_enc.as_mut().map(|ea| {
|
||||
let (y, u, vv) = effectcraft_vp9enc::alpha_to_yuv420(px, w, h);
|
||||
ea.encode_frame(&y, &u, &vv, false).data
|
||||
})
|
||||
},
|
||||
);
|
||||
let k = i + j as u64;
|
||||
frames.push(Frame { ms: frame_ms(k), track: 1, key: color.key, prev_ms: (k > 0).then(|| frame_ms(k - 1)), data: color.data, alpha: a });
|
||||
}
|
||||
drop(pixels);
|
||||
let t_end = if end >= total { span_end } else { job.settings.frame_time(comp, end) };
|
||||
audio_until(t_end, &mut frames, end >= total);
|
||||
let c = cluster(&mut frames);
|
||||
|
||||
@@ -61,7 +61,7 @@ fn lossless_is_exact_for_odd_sizes() {
|
||||
for (w, h) in [(1, 1), (33, 17), (64, 64), (200, 120), (7, 70)] {
|
||||
for kind in 0..3 {
|
||||
let (y, u, v) = source(w, h, kind);
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 100, full_range: false });
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 100, full_range: false, ..Default::default() });
|
||||
let f = e.encode_yuv420(&y, &u, &v);
|
||||
let p = decode(&f);
|
||||
assert_eq!((p.width, p.height), (w as u32, h as u32));
|
||||
@@ -80,7 +80,7 @@ fn lossy_quality_and_size() {
|
||||
let (y, u, v) = source(w, h, kind);
|
||||
let mut last = 0usize;
|
||||
for q in [30u8, 60, 80, 95] {
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: q, full_range: false });
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: q, full_range: false, ..Default::default() });
|
||||
let f = e.encode_yuv420(&y, &u, &v);
|
||||
let p = decode(&f);
|
||||
let got = crop(&plane_u8(&p.y), p.y_stride, w, h);
|
||||
@@ -103,7 +103,7 @@ fn wide_frames_use_tile_columns() {
|
||||
// 4160 px > 64 superblocks: two tile columns are required.
|
||||
let (w, h) = (4160, 16);
|
||||
let (y, u, v) = source(w, h, 0);
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 90, full_range: false });
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 90, full_range: false, ..Default::default() });
|
||||
let f = e.encode_yuv420(&y, &u, &v);
|
||||
let p = decode(&f);
|
||||
assert!(psnr(&crop(&plane_u8(&p.y), p.y_stride, w, h), &y) > 35.0);
|
||||
@@ -113,7 +113,7 @@ fn wide_frames_use_tile_columns() {
|
||||
fn ffmpeg_decodes_our_stream() {
|
||||
let (w, h) = (96, 64);
|
||||
let (y, u, v) = source(w, h, 0);
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 100, full_range: false });
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 100, full_range: false, ..Default::default() });
|
||||
let frame = e.encode_yuv420(&y, &u, &v);
|
||||
// IVF container (one frame).
|
||||
let mut ivf = Vec::new();
|
||||
@@ -147,3 +147,176 @@ fn ffmpeg_decodes_our_stream() {
|
||||
assert_eq!(&out.stdout[..w * h], &y[..], "ffmpeg luma matches (lossless)");
|
||||
assert_eq!(&out.stdout[w * h..w * h + u.len()], &u[..]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- inter frames
|
||||
|
||||
/// Frame `t` of a moving sequence: a diagonal gradient with a sinusoidal texture and bright
|
||||
/// squares, all translating by a fractional amount per frame (`kind` 1 adds noise that changes
|
||||
/// every frame).
|
||||
fn moving(w: usize, h: usize, t: usize, kind: u32) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
let (dx, dy) = (1.75 * t as f64, 0.6 * t as f64);
|
||||
let mut s = 1234u32 + t as u32 * 77;
|
||||
let mut rnd = || {
|
||||
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
(s >> 28) as f64
|
||||
};
|
||||
let lum = |x: f64, y: f64| -> f64 {
|
||||
let g = 40.0 + 120.0 * (x / w as f64) + 50.0 * (y / h as f64);
|
||||
let tex = 18.0 * ((x * 0.31).sin() * (y * 0.23).cos());
|
||||
let sq = if (x - 20.0).rem_euclid(64.0) < 18.0 && (y - 10.0).rem_euclid(48.0) < 14.0 { 45.0 } else { 0.0 };
|
||||
g + tex + sq
|
||||
};
|
||||
let mut y = vec![0u8; w * h];
|
||||
for yy in 0..h {
|
||||
for x in 0..w {
|
||||
let n = if kind == 1 { rnd() - 7.5 } else { 0.0 };
|
||||
y[yy * w + x] = (lum(x as f64 - dx, yy as f64 - dy) + n).round().clamp(0.0, 255.0) as u8;
|
||||
}
|
||||
}
|
||||
let u: Vec<u8> = (0..cw * ch).map(|i| (100.0 + 40.0 * (((i % cw) as f64 * 2.0 - dx) / w as f64)).round() as u8).collect();
|
||||
let v: Vec<u8> = (0..cw * ch).map(|i| (150.0 - 30.0 * (((i / cw) as f64 * 2.0 - dy) / h as f64)).round() as u8).collect();
|
||||
(y, u, v)
|
||||
}
|
||||
|
||||
fn planes_of(p: &filmcraft_vp9::Picture, w: usize, h: usize) -> [Vec<u8>; 3] {
|
||||
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
|
||||
[crop(&plane_u8(&p.y), p.y_stride, w, h), crop(&plane_u8(&p.u), p.uv_stride, cw, ch), crop(&plane_u8(&p.v), p.uv_stride, cw, ch)]
|
||||
}
|
||||
|
||||
/// Encode `n` frames and decode them with one decoder: every decoded frame must equal the
|
||||
/// encoder's reconstruction. Returns (total bytes, mean luma PSNR vs the source).
|
||||
fn run_sequence(cfg: EncoderConfig, n: usize, kind: u32) -> (usize, f64) {
|
||||
let (w, h) = (cfg.width as usize, cfg.height as usize);
|
||||
let mut e = Vp9Encoder::new(cfg);
|
||||
let mut d = Decoder::new();
|
||||
let (mut bytes, mut db) = (0usize, 0.0);
|
||||
for t in 0..n {
|
||||
let (y, u, v) = moving(w, h, t, kind);
|
||||
let f = e.encode_frame(&y, &u, &v, false);
|
||||
bytes += f.data.len();
|
||||
let kind_s = if f.key { "key" } else { "inter" };
|
||||
let mut pics = d.decode(&f.data, t as i64).unwrap_or_else(|err| panic!("{w}x{h} frame {t} ({kind_s}): {err:?}"));
|
||||
assert_eq!(pics.len(), 1);
|
||||
let got = planes_of(&pics.remove(0), w, h);
|
||||
let rec = e.reconstruction().expect("reconstruction");
|
||||
for k in 0..3 {
|
||||
if got[k] != rec[k] {
|
||||
let first = got[k].iter().zip(&rec[k]).position(|(a, b)| a != b).unwrap_or(0);
|
||||
let pw = if k == 0 { w } else { w.div_ceil(2) };
|
||||
panic!("{w}x{h} frame {t} ({kind_s}, q{} lf{}): plane {k} differs first at ({}, {})", f.q_idx, f.filter_level, first % pw, first / pw);
|
||||
}
|
||||
}
|
||||
db += psnr(&got[0], &y);
|
||||
}
|
||||
(bytes, db / n as f64)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inter_frames_decode_exactly() {
|
||||
for (w, h) in [(64, 64), (96, 72), (33, 17), (130, 66)] {
|
||||
for (q, kind) in [(30u8, 0u32), (70, 1), (92, 0), (100, 0)] {
|
||||
let cfg = EncoderConfig { width: w as u32, height: h as u32, quality: q, keyframe_interval: 6, ..Default::default() };
|
||||
run_sequence(cfg, 8, kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inter_frames_with_tile_columns_decode_exactly() {
|
||||
let cfg = EncoderConfig { width: 4160, height: 24, quality: 80, keyframe_interval: 0, ..Default::default() };
|
||||
run_sequence(cfg, 3, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inter_psnr_rises_with_bitrate() {
|
||||
let (w, h) = (160, 96);
|
||||
let mut last = (0usize, 0.0f64);
|
||||
let mut rows = vec![];
|
||||
for kbps in [60u32, 150, 400, 1200] {
|
||||
let cfg = EncoderConfig { width: w, height: h, quality: 60, target_kbps: Some(kbps), frame_rate: 30.0, keyframe_interval: 30, ..Default::default() };
|
||||
let (bytes, db) = run_sequence(cfg, 20, 0);
|
||||
rows.push(format!("{kbps} kbps target: {:.0} kbps actual, {db:.2} dB", bytes as f64 * 8.0 * 30.0 / 20.0 / 1000.0));
|
||||
assert!(bytes > last.0 && db > last.1, "{rows:#?}");
|
||||
last = (bytes, db);
|
||||
}
|
||||
eprintln!("{rows:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inter_frames_are_much_smaller_than_intra() {
|
||||
let (w, h) = (192u32, 108u32);
|
||||
let mut report = vec![];
|
||||
for q in [50u8, 75, 90] {
|
||||
let intra = EncoderConfig { width: w, height: h, quality: q, keyframe_interval: 1, ..Default::default() };
|
||||
let inter = EncoderConfig { keyframe_interval: 0, ..intra.clone() };
|
||||
let (bi, pi) = run_sequence(intra, 24, 0);
|
||||
let (bp, pp) = run_sequence(inter, 24, 0);
|
||||
report.push(format!("q{q}: intra {bi} B {pi:.2} dB, inter {bp} B {pp:.2} dB, ratio {:.2}", bi as f64 / bp as f64));
|
||||
assert!(pp > pi - 1.0, "{report:#?}");
|
||||
assert!(bi as f64 >= 3.0 * bp as f64, "{report:#?}");
|
||||
}
|
||||
eprintln!("{report:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffmpeg_decodes_inter_frames_like_the_encoder() {
|
||||
let (w, h) = (96usize, 64usize);
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 75, keyframe_interval: 5, ..Default::default() });
|
||||
let n = 7;
|
||||
let mut ivf = Vec::new();
|
||||
ivf.extend_from_slice(b"DKIF");
|
||||
ivf.extend_from_slice(&0u16.to_le_bytes());
|
||||
ivf.extend_from_slice(&32u16.to_le_bytes());
|
||||
ivf.extend_from_slice(b"VP90");
|
||||
ivf.extend_from_slice(&(w as u16).to_le_bytes());
|
||||
ivf.extend_from_slice(&(h as u16).to_le_bytes());
|
||||
ivf.extend_from_slice(&30u32.to_le_bytes());
|
||||
ivf.extend_from_slice(&1u32.to_le_bytes());
|
||||
ivf.extend_from_slice(&(n as u32).to_le_bytes());
|
||||
ivf.extend_from_slice(&0u32.to_le_bytes());
|
||||
let mut recon = vec![];
|
||||
for t in 0..n {
|
||||
let (y, u, v) = moving(w, h, t, 0);
|
||||
let f = e.encode_frame(&y, &u, &v, false);
|
||||
ivf.extend_from_slice(&(f.data.len() as u32).to_le_bytes());
|
||||
ivf.extend_from_slice(&(t as u64).to_le_bytes());
|
||||
ivf.extend_from_slice(&f.data);
|
||||
recon.push(e.reconstruction().expect("recon"));
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("effectcraft-vp9-inter-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("a.ivf");
|
||||
std::fs::write(&path, &ivf).unwrap();
|
||||
let out = std::process::Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-loglevel", "error", "-i"])
|
||||
.arg(&path)
|
||||
.args(["-f", "rawvideo", "-pix_fmt", "yuv420p", "-"])
|
||||
.output();
|
||||
let Ok(out) = out else {
|
||||
eprintln!("ffmpeg not found: skipping oracle");
|
||||
return;
|
||||
};
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let fsz = w * h + 2 * w.div_ceil(2) * h.div_ceil(2);
|
||||
assert_eq!(out.stdout.len(), fsz * n);
|
||||
for (t, r) in recon.iter().enumerate() {
|
||||
let frame = &out.stdout[t * fsz..(t + 1) * fsz];
|
||||
assert_eq!(&frame[..w * h], &r[0][..], "ffmpeg luma of frame {t}");
|
||||
assert_eq!(&frame[w * h..w * h + r[1].len()], &r[1][..], "ffmpeg U of frame {t}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoder speed on 720p (run with `--ignored --nocapture`).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_720p() {
|
||||
let (w, h) = (1280usize, 720usize);
|
||||
let mut e = Vp9Encoder::new(EncoderConfig { width: w as u32, height: h as u32, quality: 80, keyframe_interval: 0, ..Default::default() });
|
||||
for t in 0..6 {
|
||||
let (y, u, v) = moving(w, h, t, 0);
|
||||
let t0 = std::time::Instant::now();
|
||||
let f = e.encode_frame(&y, &u, &v, false);
|
||||
eprintln!("frame {t} ({}): {} B in {:.0} ms", if f.key { "key" } else { "inter" }, f.data.len(), t0.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "effectcraft-vp9enc"
|
||||
description = "VP9 intra-frame encoder (profile 0, 8-bit 4:2:0) for WebM export, from the VP9 Bitstream & Decoding Process Specification"
|
||||
description = "VP9 encoder (profile 0, 8-bit 4:2:0; intra + inter frames, loop filter, rate control) for WebM export, from the VP9 Bitstream & Decoding Process Specification"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
+42
-10
@@ -1,15 +1,47 @@
|
||||
# effectcraft-vp9enc
|
||||
|
||||
A small VP9 encoder for WebM export: every frame is a key frame (profile 0, 8-bit 4:2:0,
|
||||
BT.709), coded with 8×8 intra blocks (DC / V / H / TM prediction chosen per block), 4×4
|
||||
DCT/ADST transforms with default probabilities, or the Walsh–Hadamard transform at quality 100
|
||||
(mathematically lossless).
|
||||
A small VP9 encoder for WebM export (profile 0, 8-bit 4:2:0, BT.709).
|
||||
|
||||
* **Key frames**: 8×8 intra blocks (DC / V / H / TM prediction chosen per block), 4×4 DCT/ADST
|
||||
transforms, or the Walsh–Hadamard transform at quality 100 (mathematically lossless).
|
||||
* **Inter frames**: one reference (the previous frame, LAST), 16×16 or 8×8 blocks chosen by a
|
||||
cost comparison; motion search from the predicted vectors (NearestMv / NearMv / last frame's
|
||||
co-located vector) with a whole-sample cross/diamond search refined to half and quarter
|
||||
samples with the regular 8-tap interpolation filters; NEARESTMV / NEARMV / ZEROMV / NEWMV
|
||||
chosen by bit cost; blocks with no coefficients worth coding are skipped; 4×4 DCT residual
|
||||
with a rate–distortion check per transform block. Inter frames are coded error resilient
|
||||
(`error_resilient_mode` = 1: no dependence on earlier probability contexts or motion vectors),
|
||||
so `UsePrevFrameMvs` is never needed. No high-precision (1/8) vectors, no compound prediction,
|
||||
no intra blocks inside inter frames.
|
||||
* **Loop filter**: the in-loop deblocking filter (§8.8) on every lossy frame; the level is the
|
||||
best of three trials against the source (sharpness 0, no mode/reference deltas).
|
||||
* **Probabilities**: per-frame forward updates in the compressed header (`diff_update_prob` /
|
||||
`update_mv_prob`) of the coefficient, skip, inter-mode, is-inter, reference, partition and
|
||||
motion-vector probabilities. Tiles are recorded as symbols first, the frame's own counts pick
|
||||
the updates that pay for their signalling, then the symbols are entropy coded with the updated
|
||||
probabilities. There is no backward adaptation (`refresh_frame_context` = 0).
|
||||
* **Rate control**: constant quality (quality → `base_q_idx`) or a target bitrate: a per-frame
|
||||
q index from a bits ≈ complexity / step model per frame type, key frames getting a larger share
|
||||
of each key-frame interval, with the running over/undershoot paid back over about a second.
|
||||
* **Key frame interval** is configurable (1 = every frame a key frame, the old intra-only mode).
|
||||
|
||||
The encoder reconstructs exactly what a decoder reconstructs (inter prediction with the
|
||||
decoder's vector clamping and edge-clamped reference fetches, integer inverse transforms, the
|
||||
loop filter) and exposes it (`Vp9Encoder::reconstruction`); the tests require FilmCraft's VP9
|
||||
decoder (and ffmpeg, when installed) to output exactly that reconstruction, frame by frame, for
|
||||
moving synthetic sequences at several sizes and qualities, with tile columns, and lossless.
|
||||
|
||||
On a moving-gradient sequence (192×108, 24 frames) inter coding is 3.9–7× smaller than all-intra
|
||||
at the same quality setting with equal or better PSNR.
|
||||
|
||||
Written from the **VP9 Bitstream & Decoding Process Specification, version 0.6 (31 March
|
||||
2016)**: the encoder emits the uncompressed and compressed headers (§6.2, §6.3), partition, mode
|
||||
info and token syntax (§6.4) and reconstructs with the specification's intra prediction (§8.5.1)
|
||||
and integer inverse transforms (§8.7) so it stays in step with any conformant decoder. The
|
||||
boolean encoder is the inverse of the §9.2 decoder in the form given in RFC 6386 §7.3. Spec
|
||||
tables (§10) were transcribed from FilmCraft's first-party decoder `filmcraft-vp9` (MIT OR
|
||||
Apache-2.0), which is also the decoder the tests round-trip through; ffmpeg is used only as an
|
||||
external test oracle. No libvpx or other third-party encoder/decoder source was consulted.
|
||||
info, motion vector and token syntax (§6.4), derives motion vector candidates with the §6.4
|
||||
`find_mv_refs` / `find_best_ref_mvs` processes, and reconstructs with the specification's intra
|
||||
and inter prediction (§8.5), integer inverse transforms (§8.7) and loop filter (§8.8) so it stays
|
||||
in step with any conformant decoder. The boolean encoder is the inverse of the §9.2 decoder in
|
||||
the form given in RFC 6386 §7.3. Spec tables (§10) are the specification's; the key-frame tables
|
||||
were transcribed from FilmCraft's first-party decoder `filmcraft-vp9` (MIT OR Apache-2.0), which
|
||||
is also the decoder the tests round-trip through and whose behaviour (loop filter edge rules,
|
||||
vector candidate order) the encoder was checked against; ffmpeg is used only as an external test
|
||||
oracle. No libvpx or other third-party encoder/decoder source was consulted.
|
||||
@@ -64,31 +64,6 @@ impl BoolEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `value` with a tree (leaves stored negated, as in the decoder's tables).
|
||||
pub fn tree(&mut self, tree: &[i8], probs: &[u8], value: u8) {
|
||||
// Find the path to the leaf.
|
||||
fn path(tree: &[i8], node: usize, value: u8, out: &mut Vec<(usize, bool)>) -> bool {
|
||||
for b in 0..2 {
|
||||
let t = tree[node + b];
|
||||
out.push((node >> 1, b == 1));
|
||||
if t <= 0 {
|
||||
if (-t) as u8 == value {
|
||||
return true;
|
||||
}
|
||||
} else if path(tree, t as usize, value, out) {
|
||||
return true;
|
||||
}
|
||||
out.pop();
|
||||
}
|
||||
false
|
||||
}
|
||||
let mut p = Vec::with_capacity(8);
|
||||
path(tree, 0, value, &mut p);
|
||||
for (i, bit) in p {
|
||||
self.write(bit, probs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush (RFC 6386 §7.3) and return the bytes.
|
||||
pub fn finish(mut self) -> Vec<u8> {
|
||||
let mut c = self.bit_count;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! The in-loop deblocking filter (spec 8.8), as the decoder applies it to a frame coded with 4×4
|
||||
//! transforms, 8×8 / 16×16 blocks, no segmentation and no reference/mode deltas: superblocks in
|
||||
//! raster order, for each plane the vertical edges (pass 0) then the horizontal edges (pass 1).
|
||||
//! Transform edges inside a skipped inter block are left alone; every 4×4 edge uses the 4-tap
|
||||
//! filter except edges on a 32-sample boundary, which get the 8-tap (flat) filter.
|
||||
|
||||
use crate::{BLOCK_16X16, Mi, Plane};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Limits {
|
||||
limit: i32,
|
||||
blimit: i32,
|
||||
thresh: i32,
|
||||
}
|
||||
|
||||
/// Filter the reconstructed planes (Y, U, V, or a prefix of them) in place at `level` (0 = off;
|
||||
/// sharpness 0).
|
||||
pub fn filter_frame(planes: &mut [Plane], mi: &[Mi], mi_rows: usize, mi_cols: usize, level: u8) {
|
||||
if level == 0 {
|
||||
return;
|
||||
}
|
||||
let l = level as i32;
|
||||
let limit = l.max(1);
|
||||
let lim = Limits { limit, blimit: 2 * (l + 2) + limit, thresh: l >> 4 };
|
||||
for sbr in 0..mi_rows.div_ceil(8) {
|
||||
for sbc in 0..mi_cols.div_ceil(8) {
|
||||
for (plane, p) in planes.iter_mut().enumerate() {
|
||||
for pass in 0..2 {
|
||||
sb_plane(p, mi, mi_rows, mi_cols, plane, pass, sbr * 8, sbc * 8, lim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn sb_plane(p: &mut Plane, mi: &[Mi], mi_rows: usize, mi_cols: usize, plane: usize, pass: usize, row: usize, col: usize, lim: Limits) {
|
||||
let (sx, sy) = if plane > 0 { (1, 1) } else { (0, 0) };
|
||||
let (sub, edge_len) = if pass == 0 { (sx, 64 >> sy) } else { (sy, 64 >> sx) };
|
||||
let stride = p.w;
|
||||
for edge in 0..(16usize >> sub) {
|
||||
let mut i = 0;
|
||||
while i < edge_len {
|
||||
let (x, y) = if pass == 0 { (col * 8 + edge * (4 << sx), row * 8 + (i << sy)) } else { (col * 8 + (i << sx), row * 8 + edge * (4 << sy)) };
|
||||
if x >= 8 * mi_cols || y >= 8 * mi_rows || (pass == 0 && x == 0) || (pass == 1 && y == 0) {
|
||||
i += 8;
|
||||
continue;
|
||||
}
|
||||
let lc = ((x >> 3) >> sx) << sx;
|
||||
let lr = ((y >> 3) >> sy) << sy;
|
||||
let m = mi[lr * mi_cols + lc];
|
||||
let size = if sub == 0 { m.size } else { m.size.max(BLOCK_16X16) };
|
||||
let n8 = if size >= BLOCK_16X16 { 2 } else { 1 };
|
||||
let is_block_edge = if pass == 0 { x & (8 * n8 - 1) == 0 } else { y & (8 * n8 - 1) == 0 };
|
||||
let tx_ok = !m.inter || !m.skip;
|
||||
if !is_block_edge && !tx_ok {
|
||||
i += 8;
|
||||
continue;
|
||||
}
|
||||
let wide = edge % 8 == 0;
|
||||
let n = if pass == 0 { (8 * mi_rows - y).div_ceil(1 << sy) } else { (8 * mi_cols - x).div_ceil(1 << sx) }.min(8).min(edge_len - i);
|
||||
// A transform edge is not filtered in the last odd chroma column.
|
||||
let odd_last = pass == 1 && sx == 1 && mi_cols & 1 == 1 && edge & 1 == 1;
|
||||
let (px, py) = (x >> sx, y >> sy);
|
||||
for l in 0..n {
|
||||
let is_tx_edge = tx_ok && !(odd_last && x + (l << sx) + 8 >= mi_cols * 8);
|
||||
if !(is_block_edge || is_tx_edge) {
|
||||
continue;
|
||||
}
|
||||
let (pos, step) = if pass == 0 { ((py + l) * stride + px, 1) } else { (py * stride + px + l, stride) };
|
||||
filter_lane(&mut p.rec, pos, step, wide, lim);
|
||||
}
|
||||
i += 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter mask, narrow (4-tap) and 8-tap flat filters of spec 8.8.5 across one edge position;
|
||||
/// `pos` is q0, samples across the edge are `step` apart.
|
||||
fn filter_lane(d: &mut [u8], pos: usize, step: usize, wide: bool, lim: Limits) {
|
||||
let at = |k: isize| d[(pos as isize + k * step as isize) as usize] as i32;
|
||||
let (p3, p2, p1, p0) = (at(-4), at(-3), at(-2), at(-1));
|
||||
let (q0, q1, q2, q3) = (at(0), at(1), at(2), at(3));
|
||||
let Limits { limit, blimit, thresh } = lim;
|
||||
let mask = (p3 - p2).abs() <= limit
|
||||
&& (p2 - p1).abs() <= limit
|
||||
&& (p1 - p0).abs() <= limit
|
||||
&& (q1 - q0).abs() <= limit
|
||||
&& (q2 - q1).abs() <= limit
|
||||
&& (q3 - q2).abs() <= limit
|
||||
&& (p0 - q0).abs() * 2 + (p1 - q1).abs() / 2 <= blimit;
|
||||
if !mask {
|
||||
return;
|
||||
}
|
||||
let flat =
|
||||
wide && (p1 - p0).abs() <= 1 && (q1 - q0).abs() <= 1 && (p2 - p0).abs() <= 1 && (q2 - q0).abs() <= 1 && (p3 - p0).abs() <= 1 && (q3 - q0).abs() <= 1;
|
||||
let mut set = |k: isize, v: i32| d[(pos as isize + k * step as isize) as usize] = v as u8;
|
||||
if flat {
|
||||
// 8-tap: positions p2..q2, each the rounded mean of a 7-wide clamped window plus itself.
|
||||
let s = [p3, p2, p1, p0, q0, q1, q2, q3];
|
||||
let px = |k: isize| s[(k.clamp(-4, 3) + 4) as usize];
|
||||
for k in -3isize..3 {
|
||||
let mut sum = px(k);
|
||||
for j in -3..=3 {
|
||||
sum += px(k + j);
|
||||
}
|
||||
set(k, (sum + 4) >> 3);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let c = |v: i32| v.clamp(-128, 127);
|
||||
let hev = (p1 - p0).abs() > thresh || (q1 - q0).abs() > thresh;
|
||||
let (ps1, ps0, qs0, qs1) = (p1 - 128, p0 - 128, q0 - 128, q1 - 128);
|
||||
let filter = c(if hev { c(ps1 - qs1) } else { 0 } + 3 * (qs0 - ps0));
|
||||
let f1 = c(filter + 4) >> 3;
|
||||
let f2 = c(filter + 3) >> 3;
|
||||
set(0, c(qs0 - f1) + 128);
|
||||
set(-1, c(ps0 + f2) + 128);
|
||||
if !hev {
|
||||
let f = (f1 + 1) >> 1;
|
||||
set(1, c(qs1 - f) + 128);
|
||||
set(-2, c(ps1 + f) + 128);
|
||||
}
|
||||
}
|
||||
+266
-668
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,207 @@
|
||||
//! Inter prediction exactly as the decoder forms it: motion vector clamping (spec 8.5.2.2) and
|
||||
//! the unscaled block inter prediction process (8.5.2.4) with the regular 8-tap filters and
|
||||
//! reference samples clamped to the reference frame's visible area.
|
||||
|
||||
use crate::tables::SUBPEL_REGULAR;
|
||||
|
||||
/// A motion vector in 1/8 luma samples.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Mv {
|
||||
pub row: i32,
|
||||
pub col: i32,
|
||||
}
|
||||
|
||||
impl Mv {
|
||||
pub const ZERO: Mv = Mv { row: 0, col: 0 };
|
||||
pub fn new(row: i32, col: i32) -> Mv {
|
||||
Mv { row, col }
|
||||
}
|
||||
pub fn minus(self, o: Mv) -> Mv {
|
||||
Mv::new(self.row - o.row, self.col - o.col)
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference plane: `w`×`h` visible samples at `stride`.
|
||||
pub struct RefPlane<'a> {
|
||||
pub data: &'a [u8],
|
||||
pub stride: usize,
|
||||
pub w: usize,
|
||||
pub h: usize,
|
||||
}
|
||||
|
||||
/// The block's motion vector for `plane` in 1/16 samples of that plane, clamped as the
|
||||
/// decoder clamps it. `n8`: block size in 8×8 units.
|
||||
pub fn clamp_for_plane(mv: Mv, r: usize, c: usize, n8: usize, mi_rows: usize, mi_cols: usize, chroma: bool) -> (i32, i32) {
|
||||
let s = chroma as i32;
|
||||
let (r, c, b) = (r as i32, c as i32, n8 as i32);
|
||||
let to_top = (-(r * 8 * 16)) >> s;
|
||||
let to_bottom = (((mi_rows as i32 - b - r) * 8) * 16) >> s;
|
||||
let to_left = (-(c * 8 * 16)) >> s;
|
||||
let to_right = (((mi_cols as i32 - b - c) * 8) * 16) >> s;
|
||||
let spel = (4 + ((b * 8) >> s)) << 4;
|
||||
let row = ((2 * mv.row) >> s).clamp(to_top - spel, (to_bottom + spel - 16).max(to_top - spel));
|
||||
let col = ((2 * mv.col) >> s).clamp(to_left - spel, (to_right + spel - 16).max(to_left - spel));
|
||||
(row, col)
|
||||
}
|
||||
|
||||
/// Predict a `w`×`h` block whose top-left sample is at (`x16`, `y16`) (1/16 samples) into `out`
|
||||
/// (stride `w`).
|
||||
pub fn predict(r: &RefPlane, x16: i32, y16: i32, w: usize, h: usize, out: &mut [u8]) {
|
||||
let (x0, y0) = (x16 >> 4, y16 >> 4);
|
||||
let (fx, fy) = ((x16 & 15) as usize, (y16 & 15) as usize);
|
||||
let (lx, ly) = (r.w as i32 - 1, r.h as i32 - 1);
|
||||
let inside = x0 - 3 >= 0 && y0 - 3 >= 0 && x0 + w as i32 + 4 <= lx && y0 + h as i32 + 4 <= ly;
|
||||
let at = |x: i32, y: i32| -> i32 {
|
||||
if inside { r.data[y as usize * r.stride + x as usize] as i32 } else { r.data[y.clamp(0, ly) as usize * r.stride + x.clamp(0, lx) as usize] as i32 }
|
||||
};
|
||||
let fh = &SUBPEL_REGULAR[fx];
|
||||
let fv = &SUBPEL_REGULAR[fy];
|
||||
if fx == 0 && fy == 0 {
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
out[i * w + j] = at(x0 + j as i32, y0 + i as i32) as u8;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Horizontal pass over h + 7 rows (from y0 - 3), then vertical.
|
||||
let mut tmp = [0u8; 23 * 16];
|
||||
for i in 0..h + 7 {
|
||||
let y = y0 - 3 + i as i32;
|
||||
for j in 0..w {
|
||||
let x = x0 + j as i32 - 3;
|
||||
let mut acc = 64;
|
||||
for t in 0..8 {
|
||||
acc += fh[t] * at(x + t as i32, y);
|
||||
}
|
||||
tmp[i * w + j] = (acc >> 7).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
for i in 0..h {
|
||||
for j in 0..w {
|
||||
let mut acc = 64;
|
||||
for t in 0..8 {
|
||||
acc += fv[t] * tmp[(i + t) * w + j] as i32;
|
||||
}
|
||||
out[i * w + j] = (acc >> 7).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The reference luma pre-interpolated at the 16 quarter-sample phases the encoder uses (motion
|
||||
/// vectors are even in 1/8 units: fractions 0, 4, 8, 12 in 1/16), over the visible area plus a
|
||||
/// border. Each phase plane holds exactly what [`predict`] returns at that phase (the decoder's
|
||||
/// separable filter with edge clamping), so motion search reads predictions directly.
|
||||
pub struct PhasePlanes {
|
||||
border: usize,
|
||||
stride: usize,
|
||||
rows: usize,
|
||||
planes: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl PhasePlanes {
|
||||
pub fn new(r: &RefPlane, border: usize) -> PhasePlanes {
|
||||
const M: usize = 4;
|
||||
let (pw, ph) = (r.w + 2 * border, r.h + 2 * border);
|
||||
// Edge-replicated source with M more samples on every side for the filter taps.
|
||||
let rs = pw + 2 * M;
|
||||
let rh = ph + 2 * M;
|
||||
let off = (border + M) as i32;
|
||||
let mut raw = vec![0u8; rs * rh];
|
||||
for y in 0..rh {
|
||||
let sy = (y as i32 - off).clamp(0, r.h as i32 - 1) as usize;
|
||||
let src = &r.data[sy * r.stride..sy * r.stride + r.w];
|
||||
for x in 0..rs {
|
||||
raw[y * rs + x] = src[(x as i32 - off).clamp(0, r.w as i32 - 1) as usize];
|
||||
}
|
||||
}
|
||||
// Horizontal phases over every raw row (inner columns).
|
||||
let hpass = |fx: usize| -> Vec<u8> {
|
||||
let f = &SUBPEL_REGULAR[fx];
|
||||
let mut out = vec![0u8; pw * rh];
|
||||
for y in 0..rh {
|
||||
let row = &raw[y * rs..y * rs + rs];
|
||||
for x in 0..pw {
|
||||
let s = &row[x + M - 3..x + M + 5];
|
||||
let acc: i32 = 64 + (0..8).map(|t| f[t] * s[t] as i32).sum::<i32>();
|
||||
out[y * pw + x] = (acc >> 7).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
let unfiltered: Vec<u8> = (0..rh).flat_map(|y| raw[y * rs + M..y * rs + M + pw].iter().copied()).collect();
|
||||
let h: Vec<Vec<u8>> = (0..4).map(|k| if k == 0 { unfiltered.clone() } else { hpass(k * 4) }).collect();
|
||||
let mut planes = Vec::with_capacity(16);
|
||||
for fy in 0..4 {
|
||||
for hp in &h {
|
||||
if fy == 0 {
|
||||
planes.push(hp[M * pw..(M + ph) * pw].to_vec());
|
||||
continue;
|
||||
}
|
||||
let f = &SUBPEL_REGULAR[fy * 4];
|
||||
let mut out = vec![0u8; pw * ph];
|
||||
for y in 0..ph {
|
||||
for x in 0..pw {
|
||||
let mut acc = 64;
|
||||
for t in 0..8 {
|
||||
acc += f[t] * hp[(y + M + t - 3) * pw + x] as i32;
|
||||
}
|
||||
out[y * pw + x] = (acc >> 7).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
planes.push(out);
|
||||
}
|
||||
}
|
||||
PhasePlanes { border, stride: pw, rows: ph, planes }
|
||||
}
|
||||
|
||||
/// The prediction of the `n`×`n` block at (`x16`, `y16`) (fractions multiples of 4) as rows of
|
||||
/// a phase plane: (plane, offset of the first sample, stride). `None` beyond the border.
|
||||
pub fn block(&self, x16: i32, y16: i32, n: usize) -> Option<(&[u8], usize, usize)> {
|
||||
let (x0, y0) = ((x16 >> 4) + self.border as i32, (y16 >> 4) + self.border as i32);
|
||||
let (fx, fy) = ((x16 & 15) as usize, (y16 & 15) as usize);
|
||||
if fx % 4 != 0 || fy % 4 != 0 || x0 < 0 || y0 < 0 || x0 as usize + n > self.stride || y0 as usize + n > self.rows {
|
||||
return None;
|
||||
}
|
||||
Some((&self.planes[(fy / 4) * 4 + fx / 4], y0 as usize * self.stride + x0 as usize, self.stride))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn phase_planes_match_predict() {
|
||||
let (w, h) = (37usize, 29usize);
|
||||
let data: Vec<u8> = (0..w * h).map(|i| ((i * 7919) % 251) as u8).collect();
|
||||
let r = RefPlane { data: &data, stride: w, w, h };
|
||||
let pp = PhasePlanes::new(&r, 24);
|
||||
let mut out = [0u8; 64];
|
||||
for y16 in (-20 * 16..(h as i32 + 10) * 16).step_by(4 * 7) {
|
||||
for x16 in (-20 * 16..(w as i32 + 10) * 16).step_by(4 * 5) {
|
||||
let Some((p, o, s)) = pp.block(x16, y16, 8) else { continue };
|
||||
predict(&r, x16, y16, 8, 8, &mut out);
|
||||
for i in 0..8 {
|
||||
assert_eq!(&p[o + i * s..o + i * s + 8], &out[i * 8..i * 8 + 8], "({x16}, {y16}) row {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn whole_sample_and_linear_ramps() {
|
||||
let (w, h) = (40usize, 30usize);
|
||||
let data: Vec<u8> = (0..w * h).map(|i| ((i % w) * 4 + (i / w)) as u8).collect();
|
||||
let r = RefPlane { data: &data, stride: w, w, h };
|
||||
let mut out = [0u8; 64];
|
||||
predict(&r, 10 << 4, 8 << 4, 8, 8, &mut out);
|
||||
assert_eq!(out[0], data[8 * w + 10]);
|
||||
// Half a sample right on a linear ramp: exact midpoint.
|
||||
predict(&r, (10 << 4) + 8, 8 << 4, 8, 8, &mut out);
|
||||
assert_eq!(out[0], data[8 * w + 10] + 2);
|
||||
// Off the left edge: clamped to column 0.
|
||||
predict(&r, -(20 << 4), 0, 4, 4, &mut out);
|
||||
assert_eq!(out[0], data[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
//! Probabilities: the frame's adaptable probability table (one flat array laid out in the order
|
||||
//! the compressed header updates them), a symbol recorder that codes every boolean decision
|
||||
//! against either a fixed probability or an entry of that table, bit-cost estimates, and the
|
||||
//! per-frame forward updates (spec 6.3: `diff_update_prob` with `decode_term_subexp` /
|
||||
//! `inv_remap_prob`, and `update_mv_prob`).
|
||||
//!
|
||||
//! Tiles are first recorded, then the encoder counts how often each table entry coded a 0 or a
|
||||
//! 1, picks updates that pay for themselves, writes them in the compressed header and finally
|
||||
//! entropy codes the recorded symbols with the updated probabilities.
|
||||
|
||||
use crate::bool::BoolEncoder;
|
||||
use crate::tables::*;
|
||||
|
||||
/// Coefficient probabilities `[ptype][ref][band][ctx][3]` for 4×4 transforms (band 0 uses
|
||||
/// contexts 0..3 only).
|
||||
pub const COEF: usize = 0;
|
||||
pub const SKIP: usize = 432;
|
||||
pub const INTER_MODE: usize = SKIP + 3;
|
||||
pub const IS_INTER: usize = INTER_MODE + 21;
|
||||
pub const SINGLE_REF: usize = IS_INTER + 4;
|
||||
pub const PARTITION: usize = SINGLE_REF + 10;
|
||||
pub const MV_JOINT: usize = PARTITION + 48;
|
||||
pub const MV_SIGN: usize = MV_JOINT + 3;
|
||||
pub const MV_CLASS: usize = MV_SIGN + 2;
|
||||
pub const MV_CLASS0_BIT: usize = MV_CLASS + 20;
|
||||
pub const MV_BITS: usize = MV_CLASS0_BIT + 2;
|
||||
pub const MV_CLASS0_FR: usize = MV_BITS + 20;
|
||||
pub const MV_FR: usize = MV_CLASS0_FR + 12;
|
||||
pub const NPROBS: usize = MV_FR + 6;
|
||||
|
||||
/// Index of the first of the three coefficient probabilities of a context.
|
||||
pub fn coef_index(ptype: usize, inter: bool, band: usize, ctx: usize) -> usize {
|
||||
COEF + (((ptype * 2 + inter as usize) * 6 + band) * 6 + ctx) * 3
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Probs(pub [u8; NPROBS]);
|
||||
|
||||
impl Probs {
|
||||
/// The default probabilities (setup_past_independence / key frames).
|
||||
pub fn defaults() -> Probs {
|
||||
let mut p = [128u8; NPROBS];
|
||||
p[COEF..COEF + 432].copy_from_slice(&DEFAULT_COEF_PROBS);
|
||||
p[SKIP..SKIP + 3].copy_from_slice(&DEFAULT_SKIP_PROB);
|
||||
p[INTER_MODE..INTER_MODE + 21].copy_from_slice(&DEFAULT_INTER_MODE_PROBS);
|
||||
p[IS_INTER..IS_INTER + 4].copy_from_slice(&DEFAULT_IS_INTER_PROB);
|
||||
p[SINGLE_REF..SINGLE_REF + 10].copy_from_slice(&DEFAULT_SINGLE_REF_PROB);
|
||||
p[PARTITION..PARTITION + 48].copy_from_slice(&DEFAULT_PARTITION_PROBS);
|
||||
p[MV_JOINT..MV_JOINT + 3].copy_from_slice(&DEFAULT_MV_JOINT_PROBS);
|
||||
p[MV_SIGN..MV_SIGN + 2].copy_from_slice(&DEFAULT_MV_SIGN_PROB);
|
||||
p[MV_CLASS..MV_CLASS + 20].copy_from_slice(&DEFAULT_MV_CLASS_PROBS);
|
||||
p[MV_CLASS0_BIT..MV_CLASS0_BIT + 2].copy_from_slice(&DEFAULT_MV_CLASS0_BIT_PROB);
|
||||
p[MV_BITS..MV_BITS + 20].copy_from_slice(&DEFAULT_MV_BITS_PROB);
|
||||
p[MV_CLASS0_FR..MV_CLASS0_FR + 12].copy_from_slice(&DEFAULT_MV_CLASS0_FR_PROBS);
|
||||
p[MV_FR..MV_FR + 6].copy_from_slice(&DEFAULT_MV_FR_PROBS);
|
||||
Probs(p)
|
||||
}
|
||||
}
|
||||
|
||||
/// Probabilities of the full token tree (9.3.2) for model probability `p`.
|
||||
pub fn pareto(p: u8) -> [u8; 8] {
|
||||
let p = p.max(1) as usize;
|
||||
let x = (p - 1) / 2;
|
||||
let mut t = [0u8; 8];
|
||||
for n in 0..8 {
|
||||
t[n] = if p & 1 == 1 { PARETO_TABLE[x * 8 + n] } else { ((PARETO_TABLE[x * 8 + n] as u32 + PARETO_TABLE[(x + 1) * 8 + n] as u32) >> 1) as u8 };
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
/// The probability a recorded symbol is coded with.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum P {
|
||||
Fixed(u8),
|
||||
/// An entry of the frame's [`Probs`].
|
||||
Ctx(u16),
|
||||
/// Node `n` of the Pareto token tree derived from the coefficient probability at the index.
|
||||
Pareto(u16, u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Sym {
|
||||
bit: bool,
|
||||
p: P,
|
||||
}
|
||||
|
||||
/// Records the boolean decisions of a tile.
|
||||
#[derive(Default)]
|
||||
pub struct Writer {
|
||||
syms: Vec<Sym>,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
#[inline]
|
||||
pub fn put(&mut self, bit: bool, p: P) {
|
||||
self.syms.push(Sym { bit, p });
|
||||
}
|
||||
#[inline]
|
||||
pub fn fixed(&mut self, bit: bool, prob: u8) {
|
||||
self.put(bit, P::Fixed(prob));
|
||||
}
|
||||
#[inline]
|
||||
pub fn ctx(&mut self, bit: bool, idx: usize) {
|
||||
self.put(bit, P::Ctx(idx as u16));
|
||||
}
|
||||
/// Write `value` with `tree` (leaves stored negated); node `i` uses `prob(i)`.
|
||||
pub fn tree(&mut self, tree: &[i8], value: u8, prob: impl Fn(usize) -> P) {
|
||||
for (node, bit) in tree_path(tree, value) {
|
||||
self.put(bit, prob(node));
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the 0/1 counts of every table entry this tile used.
|
||||
pub fn count(&self, counts: &mut [[u32; 2]]) {
|
||||
for s in &self.syms {
|
||||
if let P::Ctx(i) = s.p {
|
||||
counts[i as usize][s.bit as usize] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Entropy code the recorded symbols.
|
||||
pub fn emit(&self, probs: &Probs) -> Vec<u8> {
|
||||
let mut bc = BoolEncoder::new();
|
||||
bc.write(false, 128); // marker bit
|
||||
for s in &self.syms {
|
||||
let p = match s.p {
|
||||
P::Fixed(p) => p,
|
||||
P::Ctx(i) => probs.0[i as usize],
|
||||
P::Pareto(i, n) => pareto(probs.0[i as usize])[n as usize],
|
||||
};
|
||||
bc.write(s.bit, p);
|
||||
}
|
||||
bc.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// The (node, bit) path to `value` in a tree (node = tree index / 2).
|
||||
pub fn tree_path(tree: &[i8], value: u8) -> impl Iterator<Item = (usize, bool)> {
|
||||
struct Path {
|
||||
steps: [(u8, bool); 16],
|
||||
len: usize,
|
||||
}
|
||||
fn walk(tree: &[i8], node: usize, value: u8, out: &mut Path) -> bool {
|
||||
for b in 0..2 {
|
||||
let t = tree[node + b];
|
||||
out.steps[out.len] = ((node >> 1) as u8, b == 1);
|
||||
out.len += 1;
|
||||
if t <= 0 {
|
||||
if (-t) as u8 == value {
|
||||
return true;
|
||||
}
|
||||
} else if walk(tree, t as usize, value, out) {
|
||||
return true;
|
||||
}
|
||||
out.len -= 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
let mut p = Path { steps: [(0, false); 16], len: 0 };
|
||||
walk(tree, 0, value, &mut p);
|
||||
let Path { steps, len } = p;
|
||||
steps.into_iter().take(len).map(|(n, b)| (n as usize, b))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- costs
|
||||
|
||||
/// Cost in 1/256 bit of coding a 0 (`[p][0]`) or a 1 (`[p][1]`) with probability `p`.
|
||||
pub fn cost_table() -> &'static [[u32; 2]; 256] {
|
||||
use std::sync::OnceLock;
|
||||
static T: OnceLock<[[u32; 2]; 256]> = OnceLock::new();
|
||||
T.get_or_init(|| {
|
||||
let mut t = [[0u32; 2]; 256];
|
||||
for (p, e) in t.iter_mut().enumerate() {
|
||||
let q = (p.max(1) as f64) / 256.0;
|
||||
e[0] = (-q.log2() * 256.0).round() as u32;
|
||||
e[1] = (-(1.0 - q).log2() * 256.0).round() as u32;
|
||||
}
|
||||
t
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn cost(bit: bool, p: u8) -> u32 {
|
||||
cost_table()[p as usize][bit as usize]
|
||||
}
|
||||
|
||||
/// Cost of `value` in a tree with probabilities `probs` (node i → probs[i]).
|
||||
pub fn tree_cost(tree: &[i8], probs: &[u8], value: u8) -> u32 {
|
||||
tree_path(tree, value).map(|(n, b)| cost(b, probs[n])).sum()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- forward updates
|
||||
|
||||
fn inv_recenter_nonneg(v: i32, m: i32) -> i32 {
|
||||
if v > 2 * m {
|
||||
v
|
||||
} else if v & 1 != 0 {
|
||||
m - ((v + 1) >> 1)
|
||||
} else {
|
||||
m + (v >> 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// `inv_remap_prob` (6.3.5).
|
||||
fn inv_remap_prob(delta: usize, prob: u8) -> u8 {
|
||||
let v = inv_map_table()[delta.min(254)] as i32;
|
||||
let m = prob as i32 - 1;
|
||||
let r = if (m << 1) <= 255 { 1 + inv_recenter_nonneg(v, m) } else { 255 - inv_recenter_nonneg(v, 255 - 1 - m) };
|
||||
r.clamp(1, 255) as u8
|
||||
}
|
||||
|
||||
/// The `decode_term_subexp` value that moves `old` to `new`.
|
||||
fn delta_for(old: u8, new: u8) -> Option<usize> {
|
||||
(0..255).find(|&d| inv_remap_prob(d, old) == new)
|
||||
}
|
||||
|
||||
/// Bits of `decode_term_subexp` for `d`.
|
||||
fn subexp_bits(d: usize) -> u32 {
|
||||
match d {
|
||||
0..16 => 5,
|
||||
16..32 => 6,
|
||||
32..64 => 8,
|
||||
64..129 => 10,
|
||||
_ => 11,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_subexp(bc: &mut BoolEncoder, d: usize) {
|
||||
let d = d as u32;
|
||||
if d < 16 {
|
||||
bc.literal(0, 1);
|
||||
bc.literal(d, 4);
|
||||
} else if d < 32 {
|
||||
bc.literal(2, 2);
|
||||
bc.literal(d - 16, 4);
|
||||
} else if d < 64 {
|
||||
bc.literal(6, 3);
|
||||
bc.literal(d - 32, 5);
|
||||
} else {
|
||||
bc.literal(7, 3);
|
||||
if d < 129 {
|
||||
bc.literal(d - 64, 7);
|
||||
} else {
|
||||
let v = (d + 1) >> 1;
|
||||
bc.literal(v, 7);
|
||||
bc.literal((d + 1) & 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn counts_cost(c: [u32; 2], p: u8) -> u64 {
|
||||
c[0] as u64 * cost(false, p) as u64 + c[1] as u64 * cost(true, p) as u64
|
||||
}
|
||||
|
||||
/// Whether table entry `i` is an MV probability (7-bit `update_mv_prob` rather than
|
||||
/// `diff_update_prob`).
|
||||
fn is_mv(i: usize) -> bool {
|
||||
i >= MV_JOINT
|
||||
}
|
||||
|
||||
/// Choose updated probabilities: an entry changes when the bits saved on this frame's symbols
|
||||
/// exceed the cost of signalling the change. `inter`: the inter-frame tables are updatable.
|
||||
pub fn plan_updates(old: &Probs, counts: &[[u32; 2]], inter: bool) -> Probs {
|
||||
let mut new = old.clone();
|
||||
let flag = cost(true, 252) as i64 - cost(false, 252) as i64;
|
||||
let limit = if inter { NPROBS } else { INTER_MODE };
|
||||
for i in 0..limit {
|
||||
let c = counts[i];
|
||||
let n = c[0] + c[1];
|
||||
if n == 0 {
|
||||
continue;
|
||||
}
|
||||
let o = old.0[i];
|
||||
let ideal = ((c[0] as f64 * 256.0 / n as f64).round() as i32).clamp(1, 255) as u8;
|
||||
let cands: Vec<u8> = if is_mv(i) { vec![ideal | 1, ideal.saturating_sub(1).max(1) | 1] } else { vec![ideal] };
|
||||
let base = counts_cost(c, o) as i64;
|
||||
let mut best = (0i64, o);
|
||||
for p in cands {
|
||||
if p == o {
|
||||
continue;
|
||||
}
|
||||
let signal = if is_mv(i) {
|
||||
7 * 256
|
||||
} else {
|
||||
match delta_for(o, p) {
|
||||
Some(d) => subexp_bits(d) as i64 * 256,
|
||||
None => continue,
|
||||
}
|
||||
};
|
||||
let gain = base - counts_cost(c, p) as i64 - signal - flag;
|
||||
if gain > best.0 {
|
||||
best = (gain, p);
|
||||
}
|
||||
}
|
||||
new.0[i] = best.1;
|
||||
}
|
||||
new
|
||||
}
|
||||
|
||||
/// `diff_update_prob` for one entry.
|
||||
fn diff_update(bc: &mut BoolEncoder, old: &Probs, new: &Probs, i: usize) {
|
||||
let (o, n) = (old.0[i], new.0[i]);
|
||||
match (o != n).then(|| delta_for(o, n)).flatten() {
|
||||
Some(d) => {
|
||||
bc.write(true, 252);
|
||||
write_subexp(bc, d);
|
||||
}
|
||||
None => bc.write(false, 252),
|
||||
}
|
||||
}
|
||||
|
||||
/// `update_mv_prob` for one entry.
|
||||
fn mv_update(bc: &mut BoolEncoder, old: &Probs, new: &Probs, i: usize) {
|
||||
let n = new.0[i];
|
||||
if n != old.0[i] && n & 1 == 1 {
|
||||
bc.write(true, 252);
|
||||
bc.literal(n as u32 >> 1, 7);
|
||||
} else {
|
||||
bc.write(false, 252);
|
||||
}
|
||||
}
|
||||
|
||||
/// The compressed header (6.3) of a frame coded with ONLY_4X4 transforms, single LAST
|
||||
/// references and a fixed interpolation filter, moving the probabilities from `old` to `new`.
|
||||
/// Entries `new` cannot signal are left at `old` (and must be coded with `old`): the caller
|
||||
/// codes the tiles with the probabilities this returns.
|
||||
pub fn compressed_header(old: &Probs, new: &Probs, lossless: bool, inter: bool) -> (Vec<u8>, Probs) {
|
||||
let mut bc = BoolEncoder::new();
|
||||
bc.write(false, 128);
|
||||
if !lossless {
|
||||
bc.literal(0, 2); // tx_mode = ONLY_4X4
|
||||
}
|
||||
// read_coef_probs: TX_4X4 only.
|
||||
let coef_changed = (COEF..COEF + 432).any(|i| new.0[i] != old.0[i]);
|
||||
bc.literal(coef_changed as u32, 1);
|
||||
if coef_changed {
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
for k in 0..6 {
|
||||
for l in 0..if k == 0 { 3 } else { 6 } {
|
||||
for m in 0..3 {
|
||||
diff_update(&mut bc, old, new, coef_index(i, j == 1, k, l) + m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..3 {
|
||||
diff_update(&mut bc, old, new, SKIP + i);
|
||||
}
|
||||
if inter {
|
||||
for i in 0..21 {
|
||||
diff_update(&mut bc, old, new, INTER_MODE + i);
|
||||
}
|
||||
for i in 0..4 {
|
||||
diff_update(&mut bc, old, new, IS_INTER + i);
|
||||
}
|
||||
// reference_mode is SINGLE_REFERENCE (no compound: equal sign biases).
|
||||
for i in 0..10 {
|
||||
diff_update(&mut bc, old, new, SINGLE_REF + i);
|
||||
}
|
||||
for _ in 0..36 {
|
||||
bc.write(false, 252); // y mode probabilities (no intra blocks in inter frames)
|
||||
}
|
||||
for i in 0..48 {
|
||||
diff_update(&mut bc, old, new, PARTITION + i);
|
||||
}
|
||||
for j in 0..3 {
|
||||
mv_update(&mut bc, old, new, MV_JOINT + j);
|
||||
}
|
||||
for i in 0..2 {
|
||||
mv_update(&mut bc, old, new, MV_SIGN + i);
|
||||
for j in 0..10 {
|
||||
mv_update(&mut bc, old, new, MV_CLASS + i * 10 + j);
|
||||
}
|
||||
mv_update(&mut bc, old, new, MV_CLASS0_BIT + i);
|
||||
for j in 0..10 {
|
||||
mv_update(&mut bc, old, new, MV_BITS + i * 10 + j);
|
||||
}
|
||||
}
|
||||
for i in 0..2 {
|
||||
for j in 0..6 {
|
||||
mv_update(&mut bc, old, new, MV_CLASS0_FR + i * 6 + j);
|
||||
}
|
||||
for j in 0..3 {
|
||||
mv_update(&mut bc, old, new, MV_FR + i * 3 + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
// What the decoder ends up with.
|
||||
let mut used = old.clone();
|
||||
for i in 0..NPROBS {
|
||||
let ok = if is_mv(i) { new.0[i] & 1 == 1 } else { delta_for(old.0[i], new.0[i]).is_some() };
|
||||
if ok && (inter || i < INTER_MODE) {
|
||||
used.0[i] = new.0[i];
|
||||
}
|
||||
}
|
||||
(bc.finish(), used)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn remap_reaches_every_probability() {
|
||||
for old in 1..=255u8 {
|
||||
for new in 1..=255u8 {
|
||||
if new != old {
|
||||
assert!(delta_for(old, new).is_some(), "{old} -> {new}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inv_map_table_shape() {
|
||||
let t = inv_map_table();
|
||||
assert_eq!(&t[..4], &[7, 20, 33, 46]);
|
||||
assert_eq!(t[19], 254);
|
||||
assert_eq!(&t[20..27], &[1, 2, 3, 4, 5, 6, 8]);
|
||||
assert_eq!(&t[252..], &[252, 253, 253]);
|
||||
}
|
||||
}
|
||||
@@ -128,3 +128,76 @@ pub const DEFAULT_COEF_PROBS: [u8; 432] = [
|
||||
79, 1, 20, 39, 99, 167, 239, 47, 141, 224, 10, 104, 178, 2, 73, 133, 1, 44, 85, 1, 22, 47, 127, 145, 243, 71, 129, 228, 17, 93, 177, 3, 61, 124, 1, 41, 84,
|
||||
1, 21, 52, 157, 78, 244, 140, 72, 231, 69, 58, 184, 31, 44, 137, 14, 38, 105, 8, 23, 61,
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------- inter frames (spec section 10.5)
|
||||
|
||||
/// `inv_map_table` (used by `inv_remap_prob`): the 20 values 7 + 13k first, then every other value
|
||||
/// 1..=253 in increasing order, then 253 again.
|
||||
pub fn inv_map_table() -> &'static [u8; 255] {
|
||||
use std::sync::OnceLock;
|
||||
static T: OnceLock<[u8; 255]> = OnceLock::new();
|
||||
T.get_or_init(|| {
|
||||
let mut t = [253u8; 255];
|
||||
let mut n = 0;
|
||||
for k in 0..20 {
|
||||
t[n] = 7 + 13 * k;
|
||||
n += 1;
|
||||
}
|
||||
for v in 1..=253u8 {
|
||||
if v < 7 || (v - 7) % 13 != 0 {
|
||||
t[n] = v;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
t
|
||||
})
|
||||
}
|
||||
|
||||
/// The regular 8-tap sub-sample interpolation filters (`subpel_filters[EIGHTTAP]`), 16 positions.
|
||||
pub const SUBPEL_REGULAR: [[i32; 8]; 16] = [
|
||||
[0, 0, 0, 128, 0, 0, 0, 0],
|
||||
[0, 1, -5, 126, 8, -3, 1, 0],
|
||||
[-1, 3, -10, 122, 18, -6, 2, 0],
|
||||
[-1, 4, -13, 118, 27, -9, 3, -1],
|
||||
[-1, 4, -16, 112, 37, -11, 4, -1],
|
||||
[-1, 5, -18, 105, 48, -14, 4, -1],
|
||||
[-1, 5, -19, 97, 58, -16, 5, -1],
|
||||
[-1, 6, -19, 88, 68, -18, 5, -1],
|
||||
[-1, 6, -19, 78, 78, -19, 6, -1],
|
||||
[-1, 5, -18, 68, 88, -19, 6, -1],
|
||||
[-1, 5, -16, 58, 97, -19, 5, -1],
|
||||
[-1, 4, -14, 48, 105, -18, 5, -1],
|
||||
[-1, 4, -11, 37, 112, -16, 4, -1],
|
||||
[-1, 3, -9, 27, 118, -13, 4, -1],
|
||||
[0, 2, -6, 18, 122, -10, 3, -1],
|
||||
[0, 1, -3, 8, 126, -5, 1, 0],
|
||||
];
|
||||
|
||||
/// `mv_ref_blocks` (row, column offsets) for BLOCK_8X8 and BLOCK_16X16.
|
||||
pub const MV_REF_8X8: [(i32, i32); 8] = [(-1, 0), (0, -1), (-1, -1), (-2, 0), (0, -2), (-2, -1), (-1, -2), (-2, -2)];
|
||||
pub const MV_REF_16X16: [(i32, i32); 8] = [(-1, 0), (0, -1), (-1, 1), (1, -1), (-1, -1), (-3, 0), (0, -3), (-3, -3)];
|
||||
|
||||
/// `mode_2_counter` indexed by y mode (intra modes 0..=9, then NEARESTMV, NEARMV, ZEROMV, NEWMV).
|
||||
pub const MODE_2_COUNTER: [u8; 14] = [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 3, 1];
|
||||
|
||||
/// `counter_to_context`; INVALID_CASE entries are 9.
|
||||
pub const COUNTER_TO_CONTEXT: [u8; 19] = [2, 3, 4, 1, 3, 9, 0, 9, 9, 5, 5, 9, 5, 9, 9, 9, 9, 9, 6];
|
||||
|
||||
pub const DEFAULT_PARTITION_PROBS: [u8; 48] = [
|
||||
199, 122, 141, 147, 63, 159, 148, 133, 118, 121, 104, 114, 174, 73, 87, 92, 41, 83, 82, 99, 50, 53, 39, 39, 177, 58, 59, 68, 26, 63, 52, 79, 25, 17, 14,
|
||||
12, 222, 34, 30, 72, 16, 44, 58, 32, 12, 10, 7, 6,
|
||||
];
|
||||
|
||||
pub const DEFAULT_IS_INTER_PROB: [u8; 4] = [9, 102, 187, 225];
|
||||
|
||||
pub const DEFAULT_SINGLE_REF_PROB: [u8; 10] = [33, 16, 77, 74, 142, 142, 172, 170, 238, 247];
|
||||
|
||||
pub const DEFAULT_INTER_MODE_PROBS: [u8; 21] = [2, 173, 34, 7, 145, 85, 7, 166, 63, 7, 94, 66, 8, 64, 46, 17, 81, 31, 25, 29, 30];
|
||||
|
||||
pub const DEFAULT_MV_JOINT_PROBS: [u8; 3] = [32, 64, 96];
|
||||
pub const DEFAULT_MV_SIGN_PROB: [u8; 2] = [128, 128];
|
||||
pub const DEFAULT_MV_CLASS_PROBS: [u8; 20] = [224, 144, 192, 168, 192, 176, 192, 198, 198, 245, 216, 128, 176, 160, 176, 176, 192, 198, 198, 208];
|
||||
pub const DEFAULT_MV_CLASS0_BIT_PROB: [u8; 2] = [216, 208];
|
||||
pub const DEFAULT_MV_BITS_PROB: [u8; 20] = [136, 140, 148, 160, 176, 192, 224, 234, 234, 240, 136, 140, 148, 160, 176, 192, 224, 234, 234, 240];
|
||||
pub const DEFAULT_MV_CLASS0_FR_PROBS: [u8; 12] = [128, 128, 64, 96, 112, 64, 128, 128, 64, 96, 112, 64];
|
||||
pub const DEFAULT_MV_FR_PROBS: [u8; 6] = [64, 96, 64, 64, 96, 64];
|
||||
@@ -0,0 +1,894 @@
|
||||
//! Tile coding: partitions, intra blocks (key frames) and inter blocks (motion search, mode
|
||||
//! decision, motion vector prediction, residual), recorded as symbols (see [`crate::probs`]).
|
||||
//!
|
||||
//! Mode info, motion vector candidate lists and contexts follow the decoding process of the
|
||||
//! VP9 specification (6.4, 6.4.x `find_mv_refs` / `find_best_ref_mvs`) for the subset of the
|
||||
//! syntax this encoder emits: 8×8 and 16×16 blocks, single LAST references, no
|
||||
//! `UsePrevFrameMvs` (inter frames are error resilient), no high-precision vectors.
|
||||
|
||||
use crate::mc::{Mv, PhasePlanes, RefPlane, clamp_for_plane, predict};
|
||||
use crate::probs::{self, P, Writer, cost, tree_cost};
|
||||
use crate::tables::*;
|
||||
use crate::transform::{ADST_ADST, ADST_DCT, DCT_ADST, DCT_DCT, Quant};
|
||||
use crate::{BLOCK_8X8, BLOCK_16X16, Mi, Plane};
|
||||
|
||||
pub const DC_PRED: u8 = 0;
|
||||
pub const V_PRED: u8 = 1;
|
||||
pub const H_PRED: u8 = 2;
|
||||
pub const TM_PRED: u8 = 9;
|
||||
const MODES: [u8; 4] = [DC_PRED, V_PRED, H_PRED, TM_PRED];
|
||||
pub const NEARESTMV: u8 = 10;
|
||||
pub const NEARMV: u8 = 11;
|
||||
pub const ZEROMV: u8 = 12;
|
||||
pub const NEWMV: u8 = 13;
|
||||
|
||||
const PARTITION_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3];
|
||||
const INTRA_MODE_TREE: [i8; 18] = [0, 2, -9, 4, -1, 6, 8, 12, -2, 10, -4, -5, -3, 14, -8, 16, -6, -7];
|
||||
/// Leaves are `mode - NEARESTMV`.
|
||||
const INTER_MODE_TREE: [i8; 6] = [-2, 2, 0, 4, -1, -3];
|
||||
const MV_JOINT_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3];
|
||||
const MV_CLASS_TREE: [i8; 20] = [0, 2, -1, 4, 6, 8, -2, -3, 10, 12, -4, -5, -6, 14, 16, 18, -7, -8, -9, -10];
|
||||
const MV_FR_TREE: [i8; 6] = [0, 2, -1, 4, -2, -3];
|
||||
|
||||
/// extra_bits[token] = (cat, numExtra, base).
|
||||
const EXTRA_BITS: [(u8, u8, u16); 11] =
|
||||
[(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3), (0, 0, 4), (1, 1, 5), (2, 2, 7), (3, 3, 11), (4, 4, 19), (5, 5, 35), (6, 14, 67)];
|
||||
const CAT_PROBS: [&[u8]; 7] = [
|
||||
&[],
|
||||
&[159],
|
||||
&[165, 145],
|
||||
&[173, 148, 140],
|
||||
&[176, 155, 140, 135],
|
||||
&[180, 157, 141, 134, 130],
|
||||
&[254, 254, 254, 252, 249, 243, 230, 196, 177, 153, 140, 133, 130, 129],
|
||||
];
|
||||
|
||||
/// mode2txfm_map for the intra modes used here (inter blocks use DCT_DCT).
|
||||
fn mode_tx_type(mode: u8) -> u8 {
|
||||
match mode {
|
||||
V_PRED => ADST_DCT,
|
||||
H_PRED => DCT_ADST,
|
||||
TM_PRED => ADST_ADST,
|
||||
_ => DCT_DCT,
|
||||
}
|
||||
}
|
||||
|
||||
fn scan(tx_type: u8) -> &'static [u16; 16] {
|
||||
match tx_type {
|
||||
ADST_DCT => &ROW_SCAN_4X4,
|
||||
DCT_ADST => &COL_SCAN_4X4,
|
||||
_ => &DEFAULT_SCAN_4X4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Coefficient context neighbours (9.3.2) of each scan position.
|
||||
fn neighbors(tx_type: u8) -> [(usize, usize); 16] {
|
||||
let sc = scan(tx_type);
|
||||
let mut nb = [(0, 0); 16];
|
||||
for (c, &pos) in sc.iter().enumerate().skip(1) {
|
||||
let pos = pos as usize;
|
||||
let (i, j) = (pos / 4, pos % 4);
|
||||
nb[c] = if i > 0 && j > 0 {
|
||||
let a = (i - 1) * 4 + j;
|
||||
let b = i * 4 + j - 1;
|
||||
match tx_type {
|
||||
DCT_ADST => (a, a),
|
||||
ADST_DCT => (b, b),
|
||||
_ => (a, b),
|
||||
}
|
||||
} else if i > 0 {
|
||||
((i - 1) * 4 + j, (i - 1) * 4 + j)
|
||||
} else {
|
||||
(j - 1, j - 1)
|
||||
};
|
||||
}
|
||||
nb
|
||||
}
|
||||
|
||||
fn eob_of(levels: &[i32; 16], tx_type: u8) -> usize {
|
||||
let sc = scan(tx_type);
|
||||
(0..16).rev().find(|&i| levels[sc[i] as usize] != 0).map_or(0, |i| i + 1)
|
||||
}
|
||||
|
||||
/// Rough coefficient bit cost (1/256 bit) for rate-distortion decisions.
|
||||
fn levels_cost(levels: &[i32; 16], eob: usize) -> u32 {
|
||||
if eob == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut bits = 256;
|
||||
for &l in levels {
|
||||
let a = l.unsigned_abs();
|
||||
bits += if a == 0 { 160 } else { 640 + 384 * (32 - a.leading_zeros()) };
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
/// Intra prediction (8.5.1) of a square block for DC / V / H / TM.
|
||||
fn predict_intra(p: &Plane, x: usize, y: usize, log2: usize, mode: u8, have_left: bool, have_above: bool) -> [i32; 64] {
|
||||
let size = 1usize << log2;
|
||||
let base = 128i32;
|
||||
let buf = &p.rec;
|
||||
let w = p.w;
|
||||
let mut above = [0i32; 8];
|
||||
let mut left = [0i32; 8];
|
||||
if have_above {
|
||||
for i in 0..size {
|
||||
above[i] = buf[(y - 1) * w + (x + i).min(p.max_x)] as i32;
|
||||
}
|
||||
} else {
|
||||
above[..size].fill(base - 1);
|
||||
}
|
||||
let top_left = if have_above && have_left {
|
||||
buf[(y - 1) * w + (x - 1).min(p.max_x)] as i32
|
||||
} else if have_above {
|
||||
base + 1
|
||||
} else {
|
||||
base - 1
|
||||
};
|
||||
if have_left {
|
||||
for i in 0..size {
|
||||
left[i] = buf[(y + i).min(p.max_y) * w + x - 1] as i32;
|
||||
}
|
||||
} else {
|
||||
left[..size].fill(base + 1);
|
||||
}
|
||||
let mut out = [0i32; 64];
|
||||
for i in 0..size {
|
||||
for j in 0..size {
|
||||
out[i * size + j] = match mode {
|
||||
V_PRED => above[j],
|
||||
H_PRED => left[i],
|
||||
TM_PRED => (above[j] + left[i] - top_left).clamp(0, 255),
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
if mode == DC_PRED {
|
||||
let v = match (have_left, have_above) {
|
||||
(true, true) => (left[..size].iter().sum::<i32>() + above[..size].iter().sum::<i32>() + size as i32) >> (log2 + 1),
|
||||
(true, false) => (left[..size].iter().sum::<i32>() + (1 << (log2 - 1))) >> log2,
|
||||
(false, true) => (above[..size].iter().sum::<i32>() + (1 << (log2 - 1))) >> log2,
|
||||
(false, false) => base,
|
||||
};
|
||||
out[..size * size].fill(v);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- motion vector coding
|
||||
|
||||
/// `mv_class` of an offset (|component| - 1).
|
||||
fn mv_class(offset: u32) -> u32 {
|
||||
if offset < 16 { 0 } else { (31 - offset.leading_zeros()) - 3 }
|
||||
}
|
||||
|
||||
fn mv_component_cost(i: usize, v: i32) -> u32 {
|
||||
let d = &probs_defaults().0;
|
||||
let offset = v.unsigned_abs() - 1;
|
||||
let c = mv_class(offset);
|
||||
let mut bits = cost(v < 0, d[probs::MV_SIGN + i]);
|
||||
bits += tree_cost(&MV_CLASS_TREE, &d[probs::MV_CLASS + i * 10..], c as u8);
|
||||
let fr = ((offset >> 1) & 3) as u8;
|
||||
if c == 0 {
|
||||
let b = offset >> 3;
|
||||
bits += cost(b == 1, d[probs::MV_CLASS0_BIT + i]);
|
||||
bits += tree_cost(&MV_FR_TREE, &d[probs::MV_CLASS0_FR + i * 6 + b as usize * 3..], fr);
|
||||
} else {
|
||||
let dd = (offset - (1 << (c + 3))) >> 3;
|
||||
for b in 0..c {
|
||||
bits += cost((dd >> b) & 1 == 1, d[probs::MV_BITS + i * 10 + b as usize]);
|
||||
}
|
||||
bits += tree_cost(&MV_FR_TREE, &d[probs::MV_FR + i * 3..], fr);
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
const MV_TABLE: i32 = 8192;
|
||||
|
||||
/// Component costs for |v| ≤ MV_TABLE (index v + MV_TABLE) and the four joint costs.
|
||||
fn mv_tables() -> &'static ([Vec<u32>; 2], [u32; 4]) {
|
||||
use std::sync::OnceLock;
|
||||
static T: OnceLock<([Vec<u32>; 2], [u32; 4])> = OnceLock::new();
|
||||
T.get_or_init(|| {
|
||||
let comp = std::array::from_fn(|i| (-MV_TABLE..=MV_TABLE).map(|v| if v == 0 { 0 } else { mv_component_cost(i, v) }).collect());
|
||||
let d = &probs_defaults().0;
|
||||
let joints = std::array::from_fn(|j| tree_cost(&MV_JOINT_TREE, &d[probs::MV_JOINT..], j as u8));
|
||||
(comp, joints)
|
||||
})
|
||||
}
|
||||
|
||||
/// Bits (1/256) of a NEWMV difference with the default probabilities.
|
||||
pub fn mv_cost(diff: Mv) -> u32 {
|
||||
let (comp, joints) = mv_tables();
|
||||
let joint = (diff.row != 0) as usize * 2 + (diff.col != 0) as usize;
|
||||
let c = |i: usize, v: i32| if v.abs() <= MV_TABLE { comp[i][(v + MV_TABLE) as usize] } else { mv_component_cost(i, v) };
|
||||
joints[joint] + if diff.row != 0 { c(0, diff.row) } else { 0 } + if diff.col != 0 { c(1, diff.col) } else { 0 }
|
||||
}
|
||||
|
||||
fn probs_defaults() -> &'static probs::Probs {
|
||||
use std::sync::OnceLock;
|
||||
static D: OnceLock<probs::Probs> = OnceLock::new();
|
||||
D.get_or_init(probs::Probs::defaults)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the tile
|
||||
|
||||
/// Per-frame coding parameters shared by the tiles.
|
||||
pub struct FrameParams<'a> {
|
||||
pub key: bool,
|
||||
pub quant: Quant,
|
||||
pub mi_cols: usize,
|
||||
pub mi_rows: usize,
|
||||
/// Reference planes (inter frames).
|
||||
pub refs: Option<[RefPlane<'a>; 3]>,
|
||||
/// The reference luma at every quarter-sample phase (motion search).
|
||||
pub phases: Option<PhasePlanes>,
|
||||
/// Encoder-side motion hints from the previous frame (by MI position).
|
||||
pub prev_mvs: Option<&'a [Mv]>,
|
||||
/// Lagrangian multipliers: per bit, in SAD and in squared-error units.
|
||||
pub lambda_sad: f64,
|
||||
pub lambda_sse: f64,
|
||||
}
|
||||
|
||||
pub struct Tile<'a> {
|
||||
pub f: &'a FrameParams<'a>,
|
||||
pub w: Writer,
|
||||
pub planes: &'a mut [Plane; 3],
|
||||
pub mi: &'a mut [Mi],
|
||||
pub col_start: usize,
|
||||
pub col_end: usize,
|
||||
above_nz: [Vec<u8>; 3],
|
||||
left_nz: [Vec<u8>; 3],
|
||||
above_part: Vec<u8>,
|
||||
left_part: Vec<u8>,
|
||||
/// Motion vectors of the four 8×8 blocks evaluated for the current 16×16 area.
|
||||
pending: [(usize, usize, Mv); 4],
|
||||
}
|
||||
|
||||
impl<'a> Tile<'a> {
|
||||
pub fn new(f: &'a FrameParams<'a>, planes: &'a mut [Plane; 3], mi: &'a mut [Mi], col_start: usize, col_end: usize) -> Tile<'a> {
|
||||
let n = f.mi_cols * 2 + 16;
|
||||
Tile {
|
||||
f,
|
||||
w: Writer::default(),
|
||||
planes,
|
||||
mi,
|
||||
col_start,
|
||||
col_end,
|
||||
above_nz: [vec![0; n], vec![0; n], vec![0; n]],
|
||||
left_nz: [vec![0; 16], vec![0; 16], vec![0; 16]],
|
||||
above_part: vec![0; f.mi_cols + 8],
|
||||
left_part: vec![0; 8],
|
||||
pending: [(usize::MAX, 0, Mv::ZERO); 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// Code the tile's superblocks.
|
||||
pub fn code(&mut self) {
|
||||
for r in (0..self.f.mi_rows).step_by(8) {
|
||||
for p in 0..3 {
|
||||
self.left_nz[p].fill(0);
|
||||
}
|
||||
self.left_part.fill(0);
|
||||
for c in (self.col_start..self.col_end).step_by(8) {
|
||||
self.partition(r, c, 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mi_at(&self, r: usize, c: usize) -> Mi {
|
||||
self.mi[r * self.f.mi_cols + c]
|
||||
}
|
||||
|
||||
fn set_mi(&mut self, r: usize, c: usize, n8: usize, m: Mi) {
|
||||
for dr in 0..n8 {
|
||||
for dc in 0..n8 {
|
||||
if r + dr < self.f.mi_rows && c + dc < self.f.mi_cols {
|
||||
self.mi[(r + dr) * self.f.mi_cols + c + dc] = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn partition(&mut self, r: usize, c: usize, bsize: u8) {
|
||||
let (mi_rows, mi_cols) = (self.f.mi_rows, self.f.mi_cols);
|
||||
if r >= mi_rows || c >= mi_cols {
|
||||
return;
|
||||
}
|
||||
let num8 = [1usize, 1, 1, 1, 1, 2, 2, 2, 4, 4, 4, 8, 8][bsize as usize];
|
||||
let half = num8 >> 1;
|
||||
let has_rows = (r + half) < mi_rows;
|
||||
let has_cols = (c + half) < mi_cols;
|
||||
let bsl = [0usize, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3][bsize as usize];
|
||||
let boffset = 3 - bsl;
|
||||
let mut above = 0u8;
|
||||
let mut left = 0u8;
|
||||
for i in 0..num8 {
|
||||
above |= self.above_part[c + i];
|
||||
left |= self.left_part[(r & 7) + i];
|
||||
}
|
||||
let ctx = bsl * 4 + (((left >> boffset) & 1) as usize) * 2 + ((above >> boffset) & 1) as usize;
|
||||
let key = self.f.key;
|
||||
let prob = |i: usize| if key { P::Fixed(KF_PARTITION_PROBS[ctx * 3 + i]) } else { P::Ctx((probs::PARTITION + ctx * 3 + i) as u16) };
|
||||
if bsize == BLOCK_8X8 {
|
||||
self.w.put(false, prob(0)); // PARTITION_NONE
|
||||
if key {
|
||||
self.intra_block(r, c);
|
||||
} else {
|
||||
let mv = self.pending.iter().find(|p| p.0 == r && p.1 == c).map(|p| p.2);
|
||||
let mv = mv.unwrap_or_else(|| self.search(r, c, BLOCK_8X8).0);
|
||||
self.inter_block(r, c, BLOCK_8X8, mv);
|
||||
}
|
||||
self.above_part[c] = 15 >> 1;
|
||||
self.left_part[r & 7] = 15 >> 1;
|
||||
return;
|
||||
}
|
||||
if !key && bsize == BLOCK_16X16 && has_rows && has_cols {
|
||||
if let Some(mv) = self.decide16(r, c) {
|
||||
self.w.put(false, prob(0)); // PARTITION_NONE
|
||||
self.inter_block(r, c, BLOCK_16X16, mv);
|
||||
for i in 0..2 {
|
||||
self.above_part[c + i] = 15 >> 2;
|
||||
self.left_part[(r & 7) + i] = 15 >> 2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if !key && bsize == BLOCK_16X16 {
|
||||
self.pending = [(usize::MAX, 0, Mv::ZERO); 4];
|
||||
}
|
||||
// PARTITION_SPLIT.
|
||||
if has_rows && has_cols {
|
||||
self.w.tree(&PARTITION_TREE, 3, prob);
|
||||
} else if has_cols {
|
||||
self.w.put(true, prob(1));
|
||||
} else if has_rows {
|
||||
self.w.put(true, prob(2));
|
||||
}
|
||||
let sub = bsize - 3;
|
||||
self.partition(r, c, sub);
|
||||
self.partition(r, c + half, sub);
|
||||
self.partition(r + half, c, sub);
|
||||
self.partition(r + half, c + half, sub);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ intra (key frames)
|
||||
|
||||
fn intra_block(&mut self, r: usize, c: usize) {
|
||||
let avail_u = r > 0;
|
||||
let avail_l = c > self.col_start;
|
||||
let y_mode = self.choose_intra(0, c * 8, r * 8, 3, avail_l, avail_u);
|
||||
let uv_mode = {
|
||||
let mut best = (i64::MAX, DC_PRED);
|
||||
for m in MODES {
|
||||
let cost: i64 = (1..3).map(|p| self.intra_cost(p, c * 4, r * 4, 2, m, avail_l, avail_u)).sum();
|
||||
if cost < best.0 {
|
||||
best = (cost, m);
|
||||
}
|
||||
}
|
||||
best.1
|
||||
};
|
||||
// skip = 0 (neighbours never skip in key frames: context 0); tx_size implied (4×4).
|
||||
self.w.ctx(false, probs::SKIP);
|
||||
let above_mode = if avail_u { self.mi_at(r - 1, c).mode } else { DC_PRED };
|
||||
let left_mode = if avail_l { self.mi_at(r, c - 1).mode } else { DC_PRED };
|
||||
let o = (above_mode as usize * 10 + left_mode as usize) * 9;
|
||||
self.w.tree(&INTRA_MODE_TREE, y_mode, |i| P::Fixed(KF_Y_MODE_PROBS[o + i]));
|
||||
let o = y_mode as usize * 9;
|
||||
self.w.tree(&INTRA_MODE_TREE, uv_mode, |i| P::Fixed(KF_UV_MODE_PROBS[o + i]));
|
||||
self.set_mi(r, c, 1, Mi { inter: false, mode: y_mode, mv: Mv::ZERO, skip: false, size: BLOCK_8X8 });
|
||||
// Residual: luma 2×2 transform blocks, then one 4×4 block per chroma plane.
|
||||
for (dy, dx) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
|
||||
let tx = if self.f.quant.lossless { DCT_DCT } else { mode_tx_type(y_mode) };
|
||||
self.intra_tx(0, c * 8 + dx * 4, r * 8 + dy * 4, y_mode, tx, avail_l || dx > 0, avail_u || dy > 0);
|
||||
}
|
||||
for p in 1..3 {
|
||||
self.intra_tx(p, c * 4, r * 4, uv_mode, DCT_DCT, avail_l, avail_u);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn intra_cost(&self, plane: usize, x: usize, y: usize, log2: usize, mode: u8, have_left: bool, have_above: bool) -> i64 {
|
||||
let p = &self.planes[plane];
|
||||
let pred = predict_intra(p, x, y, log2, mode, have_left, have_above);
|
||||
let size = 1 << log2;
|
||||
let mut s = 0i64;
|
||||
for i in 0..size {
|
||||
for j in 0..size {
|
||||
s += (p.src[(y + i) * p.w + x + j] as i32 - pred[i * size + j]).abs() as i64;
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn choose_intra(&self, plane: usize, x: usize, y: usize, log2: usize, have_left: bool, have_above: bool) -> u8 {
|
||||
let mut best = (i64::MAX, DC_PRED);
|
||||
for m in MODES {
|
||||
let cost = self.intra_cost(plane, x, y, log2, m, have_left, have_above);
|
||||
if cost < best.0 {
|
||||
best = (cost, m);
|
||||
}
|
||||
}
|
||||
best.1
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn intra_tx(&mut self, plane: usize, x: usize, y: usize, mode: u8, tx_type: u8, have_left: bool, have_above: bool) {
|
||||
let ss = (plane > 0) as usize;
|
||||
let max_x = (self.f.mi_cols * 8) >> ss;
|
||||
let max_y = (self.f.mi_rows * 8) >> ss;
|
||||
let (x4, y4) = (x >> 2, y >> 2);
|
||||
let ly = y4 & (15 >> ss);
|
||||
if x >= max_x || y >= max_y {
|
||||
self.above_nz[plane][x4] = 0;
|
||||
self.left_nz[plane][ly] = 0;
|
||||
return;
|
||||
}
|
||||
let p = &self.planes[plane];
|
||||
let pred = predict_intra(p, x, y, 2, mode, have_left, have_above);
|
||||
let mut res = [0i32; 16];
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
res[i * 4 + j] = p.src[(y + i) * p.w + x + j] as i32 - pred[i * 4 + j];
|
||||
}
|
||||
}
|
||||
let q = Quant { round_dc: 0.5, round_ac: 0.38, ..self.f.quant };
|
||||
let levels = q.levels(&res, tx_type);
|
||||
let eob = eob_of(&levels, tx_type);
|
||||
let ctx0 = (self.above_nz[plane][x4] + self.left_nz[plane][ly]) as usize;
|
||||
self.tokens(plane, false, tx_type, &levels, eob, ctx0);
|
||||
let resid = q.residual(&levels, tx_type, eob);
|
||||
let p = &mut self.planes[plane];
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
p.rec[(y + i) * p.w + x + j] = (pred[i * 4 + j] + resid[i * 4 + j]).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
let nz = (eob > 0) as u8;
|
||||
self.above_nz[plane][x4] = nz;
|
||||
self.left_nz[plane][ly] = nz;
|
||||
}
|
||||
|
||||
fn tokens(&mut self, plane: usize, inter: bool, tx_type: u8, levels: &[i32; 16], eob: usize, ctx0: usize) {
|
||||
let sc = scan(tx_type);
|
||||
let nb = neighbors(tx_type);
|
||||
let ptype = (plane > 0) as usize;
|
||||
let mut tc = [0u8; 16];
|
||||
let mut check_eob = true;
|
||||
let mut c = 0;
|
||||
while c < 16 {
|
||||
let pos = sc[c] as usize;
|
||||
let band = COEFBAND_4X4[c] as usize;
|
||||
let ctx = if c == 0 { ctx0 } else { ((1 + tc[nb[c].0] + tc[nb[c].1]) >> 1) as usize };
|
||||
let o = probs::coef_index(ptype, inter, band, ctx);
|
||||
if check_eob {
|
||||
let more = c < eob;
|
||||
self.w.ctx(more, o);
|
||||
if !more {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let v = levels[pos].unsigned_abs();
|
||||
if v == 0 {
|
||||
self.w.ctx(false, o + 1);
|
||||
tc[pos] = 0;
|
||||
check_eob = false;
|
||||
c += 1;
|
||||
continue;
|
||||
}
|
||||
self.w.ctx(true, o + 1);
|
||||
check_eob = true;
|
||||
let token = match v {
|
||||
1 => 1,
|
||||
2 => 2,
|
||||
3 => 3,
|
||||
4 => 4,
|
||||
5..=6 => 5,
|
||||
7..=10 => 6,
|
||||
11..=18 => 7,
|
||||
19..=34 => 8,
|
||||
35..=66 => 9,
|
||||
_ => 10,
|
||||
};
|
||||
if token == 1 {
|
||||
self.w.ctx(false, o + 2);
|
||||
} else {
|
||||
self.w.ctx(true, o + 2);
|
||||
let path: &[(u8, bool)] = match token {
|
||||
2 => &[(0, false), (1, false)],
|
||||
3 => &[(0, false), (1, true), (2, false)],
|
||||
4 => &[(0, false), (1, true), (2, true)],
|
||||
5 => &[(0, true), (3, false), (4, false)],
|
||||
6 => &[(0, true), (3, false), (4, true)],
|
||||
7 => &[(0, true), (3, true), (5, false), (6, false)],
|
||||
8 => &[(0, true), (3, true), (5, false), (6, true)],
|
||||
9 => &[(0, true), (3, true), (5, true), (7, false)],
|
||||
_ => &[(0, true), (3, true), (5, true), (7, true)],
|
||||
};
|
||||
for &(n, b) in path {
|
||||
self.w.put(b, P::Pareto((o + 2) as u16, n));
|
||||
}
|
||||
let (cat, num_extra, base_v) = EXTRA_BITS[token];
|
||||
let extra = (v - base_v as u32).min((1 << num_extra) - 1);
|
||||
let cp = CAT_PROBS[cat as usize];
|
||||
for e in 0..num_extra as usize {
|
||||
self.w.fixed((extra >> (num_extra as usize - 1 - e)) & 1 != 0, cp[e]);
|
||||
}
|
||||
}
|
||||
tc[pos] = ENERGY_CLASS[token];
|
||||
self.w.fixed(levels[pos] < 0, 128);
|
||||
c += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ inter
|
||||
|
||||
fn is_inside(&self, r: i32, c: i32) -> bool {
|
||||
r >= 0 && (r as usize) < self.f.mi_rows && c >= self.col_start as i32 && (c as usize) < self.col_end
|
||||
}
|
||||
|
||||
/// `find_mv_refs` + `find_best_ref_mvs` for LAST_FRAME: (NearestMv, NearMv, mode context).
|
||||
fn mv_refs(&self, r: usize, c: usize, bsize: u8) -> (Mv, Mv, usize) {
|
||||
let search = if bsize == BLOCK_16X16 { &MV_REF_16X16 } else { &MV_REF_8X8 };
|
||||
let mut list = [Mv::ZERO; 2];
|
||||
let mut count = 0;
|
||||
let add = |mv: Mv, list: &mut [Mv; 2], count: &mut usize| {
|
||||
if *count >= 2 || (*count > 0 && mv == list[0]) {
|
||||
return;
|
||||
}
|
||||
list[*count] = mv;
|
||||
*count += 1;
|
||||
};
|
||||
let mut counter = 0usize;
|
||||
for (i, &(dr, dc)) in search.iter().enumerate() {
|
||||
let (cr, cc) = (r as i32 + dr, c as i32 + dc);
|
||||
if !self.is_inside(cr, cc) {
|
||||
continue;
|
||||
}
|
||||
let m = self.mi_at(cr as usize, cc as usize);
|
||||
if i < 2 {
|
||||
counter += MODE_2_COUNTER[m.mode as usize] as usize;
|
||||
}
|
||||
if m.inter {
|
||||
add(m.mv, &mut list, &mut count);
|
||||
}
|
||||
}
|
||||
let ctx = COUNTER_TO_CONTEXT[counter.min(18)] as usize;
|
||||
let n8 = if bsize == BLOCK_16X16 { 2 } else { 1 };
|
||||
let clamp = |mv: Mv, border: i32| -> Mv {
|
||||
let (r, c, b) = (r as i32, c as i32, n8);
|
||||
let to_top = -(r * 64);
|
||||
let to_bottom = (self.f.mi_rows as i32 - b - r) * 64;
|
||||
let to_left = -(c * 64);
|
||||
let to_right = (self.f.mi_cols as i32 - b - c) * 64;
|
||||
Mv::new(
|
||||
mv.row.clamp(to_top - border, (to_bottom + border).max(to_top - border)),
|
||||
mv.col.clamp(to_left - border, (to_right + border).max(to_left - border)),
|
||||
)
|
||||
};
|
||||
for mv in list.iter_mut() {
|
||||
*mv = clamp(*mv, 128);
|
||||
// No high precision: odd components move toward zero.
|
||||
let even = |v: i32| if v & 1 != 0 { v + if v > 0 { -1 } else { 1 } } else { v };
|
||||
*mv = clamp(Mv::new(even(mv.row), even(mv.col)), (160 - 4) << 3);
|
||||
}
|
||||
(list[0], list[1], ctx)
|
||||
}
|
||||
|
||||
/// The allowed motion vector range of a block (no decoder clamping inside it).
|
||||
fn mv_range(&self, r: usize, c: usize, n: i32) -> (i32, i32, i32, i32) {
|
||||
let n8 = n / 8;
|
||||
let (r, c) = (r as i32, c as i32);
|
||||
let lo_c = -(c * 64) - (4 + n) * 8;
|
||||
let hi_c = (self.f.mi_cols as i32 - n8 - c) * 64 + (4 + n) * 8 - 8;
|
||||
let lo_r = -(r * 64) - (4 + n) * 8;
|
||||
let hi_r = (self.f.mi_rows as i32 - n8 - r) * 64 + (4 + n) * 8 - 8;
|
||||
let lim = 8 * 1000;
|
||||
(lo_r.max(-lim), hi_r.min(lim), lo_c.max(-lim), hi_c.min(lim))
|
||||
}
|
||||
|
||||
/// SAD of the luma prediction at `mv` against the source.
|
||||
fn sad(&self, r: usize, c: usize, n: usize, mv: Mv) -> u32 {
|
||||
let p = &self.planes[0];
|
||||
let (x16, y16) = (((c * 8) << 4) as i32 + 2 * mv.col, ((r * 8) << 4) as i32 + 2 * mv.row);
|
||||
let mut s = 0u32;
|
||||
if let Some((pl, o, st)) = self.f.phases.as_ref().and_then(|ph| ph.block(x16, y16, n)) {
|
||||
for i in 0..n {
|
||||
let a = &p.src[(r * 8 + i) * p.w + c * 8..][..n];
|
||||
let b = &pl[o + i * st..o + i * st + n];
|
||||
s += a.iter().zip(b).map(|(a, b)| (*a as i32 - *b as i32).unsigned_abs()).sum::<u32>();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let refs = self.f.refs.as_ref().expect("reference");
|
||||
let mut pred = [0u8; 256];
|
||||
predict(&refs[0], x16, y16, n, n, &mut pred);
|
||||
for i in 0..n {
|
||||
let a = &p.src[(r * 8 + i) * p.w + c * 8..][..n];
|
||||
s += a.iter().zip(&pred[i * n..i * n + n]).map(|(a, b)| (*a as i32 - *b as i32).unsigned_abs()).sum::<u32>();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Motion search for the block at (`r`, `c`): the best vector and its cost
|
||||
/// (SAD + λ·bits).
|
||||
fn search(&self, r: usize, c: usize, bsize: u8) -> (Mv, f64) {
|
||||
let n = if bsize == BLOCK_16X16 { 16 } else { 8 };
|
||||
let (nearest, near, mctx) = self.mv_refs(r, c, bsize);
|
||||
let (lo_r, hi_r, lo_c, hi_c) = self.mv_range(r, c, n as i32);
|
||||
let inside = |mv: Mv| mv.row >= lo_r && mv.row <= hi_r && mv.col >= lo_c && mv.col <= hi_c;
|
||||
let lambda = self.f.lambda_sad / 256.0;
|
||||
let d = &probs_defaults().0;
|
||||
let mb: [u32; 4] = std::array::from_fn(|m| tree_cost(&INTER_MODE_TREE, &d[probs::INTER_MODE + mctx * 3..], m as u8));
|
||||
let mode_bits = |m: u8| mb[(m - NEARESTMV) as usize];
|
||||
let cost = |mv: Mv| -> f64 {
|
||||
let bits = if mv == nearest {
|
||||
mode_bits(NEARESTMV)
|
||||
} else if mv == Mv::ZERO {
|
||||
mode_bits(ZEROMV)
|
||||
} else if mv == near {
|
||||
mode_bits(NEARMV)
|
||||
} else {
|
||||
mode_bits(NEWMV) + mv_cost(mv.minus(nearest))
|
||||
};
|
||||
self.sad(r, c, n, mv) as f64 + lambda * bits as f64
|
||||
};
|
||||
let mut best = (Mv::ZERO, cost(Mv::ZERO));
|
||||
let prev = self.f.prev_mvs.map(|p| p[r * self.f.mi_cols + c]);
|
||||
for cand in [Some(nearest), Some(near), prev].into_iter().flatten() {
|
||||
if cand != Mv::ZERO && inside(cand) {
|
||||
let j = cost(cand);
|
||||
if j < best.1 {
|
||||
best = (cand, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A perfect match needs no search.
|
||||
let good_enough = lambda * 256.0 * 0.5;
|
||||
if best.1 > good_enough {
|
||||
// Whole-sample search from the best candidate: a coarse cross, then a small diamond.
|
||||
let round8 = |v: i32| (v + 4).div_euclid(8) * 8;
|
||||
let mut cur = Mv::new(round8(best.0.row), round8(best.0.col));
|
||||
let mut cur_j = if inside(cur) { cost(cur) } else { f64::MAX };
|
||||
if cur_j < best.1 {
|
||||
best = (cur, cur_j);
|
||||
} else {
|
||||
cur = best.0;
|
||||
cur_j = best.1;
|
||||
}
|
||||
for (step, max_iters) in [(16, 1), (8, 2), (4, 3), (2, 4), (1, 8)] {
|
||||
let s = step * 8;
|
||||
for _ in 0..max_iters {
|
||||
let mut moved = false;
|
||||
let center = cur;
|
||||
for (dr, dc) in [(-s, 0), (s, 0), (0, -s), (0, s)] {
|
||||
let cand = Mv::new(center.row + dr, center.col + dc);
|
||||
if !inside(cand) {
|
||||
continue;
|
||||
}
|
||||
let j = cost(cand);
|
||||
if j < cur_j {
|
||||
cur = cand;
|
||||
cur_j = j;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
if !moved {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if cur_j < best.1 {
|
||||
best = (cur, cur_j);
|
||||
}
|
||||
}
|
||||
// Sub-sample refinement: half then quarter samples (1/8 units, even: no high precision).
|
||||
for s in [4, 2] {
|
||||
let center = best.0;
|
||||
for (dr, dc) in [(-s, 0), (s, 0), (0, -s), (0, s), (-s, -s), (-s, s), (s, -s), (s, s)] {
|
||||
let cand = Mv::new(center.row + dr, center.col + dc);
|
||||
if !inside(cand) {
|
||||
continue;
|
||||
}
|
||||
let j = cost(cand);
|
||||
if j < best.1 {
|
||||
best = (cand, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Choose between one 16×16 block and four 8×8 blocks at (`r`, `c`): `Some(mv)` for 16×16.
|
||||
fn decide16(&mut self, r: usize, c: usize) -> Option<Mv> {
|
||||
let (mv16, j16) = self.search(r, c, BLOCK_16X16);
|
||||
let lambda = self.f.lambda_sad;
|
||||
// A near-perfect 16×16 prediction needs no split.
|
||||
if j16 < 2.0 * 256.0 / 256.0 * 16.0 {
|
||||
return Some(mv16);
|
||||
}
|
||||
let mut j8 = lambda * 4.0; // partition split + three more blocks of mode info (≈ 4 bits)
|
||||
for (k, (dr, dc)) in [(0, 0), (0, 1), (1, 0), (1, 1)].into_iter().enumerate() {
|
||||
let (rr, cc) = (r + dr, c + dc);
|
||||
let (mv, j) = self.search(rr, cc, BLOCK_8X8);
|
||||
j8 += j;
|
||||
self.pending[k] = (rr, cc, mv);
|
||||
// Tentative mode info so the next 8×8 blocks see this one as a candidate.
|
||||
self.set_mi(rr, cc, 1, Mi { inter: true, mode: NEWMV, mv, skip: false, size: BLOCK_8X8 });
|
||||
}
|
||||
if j16 <= j8 {
|
||||
self.pending = [(usize::MAX, 0, Mv::ZERO); 4];
|
||||
Some(mv16)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Code an inter block with motion vector `mv`.
|
||||
fn inter_block(&mut self, r: usize, c: usize, bsize: u8, mv: Mv) {
|
||||
let (mi_rows, mi_cols) = (self.f.mi_rows, self.f.mi_cols);
|
||||
let n8 = if bsize == BLOCK_16X16 { 2 } else { 1 };
|
||||
let n = 8 * n8;
|
||||
let (nearest, near, mctx) = self.mv_refs(r, c, bsize);
|
||||
let d = &probs_defaults().0;
|
||||
let mode_bits = |m: u8| tree_cost(&INTER_MODE_TREE, &d[probs::INTER_MODE + mctx * 3..], m - NEARESTMV);
|
||||
let mut mode = (NEWMV, mode_bits(NEWMV) + mv_cost(mv.minus(nearest)));
|
||||
for (m, ok) in [(NEARESTMV, mv == nearest), (NEARMV, mv == near), (ZEROMV, mv == Mv::ZERO)] {
|
||||
if ok && mode_bits(m) < mode.1 {
|
||||
mode = (m, mode_bits(m));
|
||||
}
|
||||
}
|
||||
let mode = mode.0;
|
||||
// Prediction of every plane (written straight into the reconstruction).
|
||||
let refs = self.f.refs.as_ref().expect("reference");
|
||||
for plane in 0..3 {
|
||||
let ch = plane > 0;
|
||||
let (row16, col16) = clamp_for_plane(mv, r, c, n8, mi_rows, mi_cols, ch);
|
||||
let bn = if ch { n / 2 } else { n };
|
||||
let (bx, by) = if ch { (c * 4, r * 4) } else { (c * 8, r * 8) };
|
||||
let mut pred = [0u8; 256];
|
||||
predict(&refs[plane], ((bx as i32) << 4) + col16, ((by as i32) << 4) + row16, bn, bn, &mut pred);
|
||||
let p = &mut self.planes[plane];
|
||||
for i in 0..bn {
|
||||
p.rec[(by + i) * p.w + bx..][..bn].copy_from_slice(&pred[i * bn..i * bn + bn]);
|
||||
}
|
||||
}
|
||||
// Residual: quantise every 4×4, dropping blocks whose coefficients don't pay for themselves.
|
||||
let q = Quant { round_dc: 0.5, round_ac: 0.42, ..self.f.quant };
|
||||
// Weight of the rate term when dropping a transform block's coefficients (low: keep quality on par
|
||||
// with key frames at the same q index).
|
||||
let zl = 0.07;
|
||||
let tx = DCT_DCT;
|
||||
let mut blocks: Vec<(usize, usize, usize, [i32; 16], usize)> = Vec::with_capacity(24);
|
||||
let mut any = false;
|
||||
for plane in 0..3 {
|
||||
let ch = plane > 0;
|
||||
let bn = if ch { n / 2 } else { n };
|
||||
let (bx, by) = if ch { (c * 4, r * 4) } else { (c * 8, r * 8) };
|
||||
let p = &self.planes[plane];
|
||||
for ty in (0..bn).step_by(4) {
|
||||
for tx4 in (0..bn).step_by(4) {
|
||||
let (x, y) = (bx + tx4, by + ty);
|
||||
let mut res = [0i32; 16];
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
let k = (y + i) * p.w + x + j;
|
||||
res[i * 4 + j] = p.src[k] as i32 - p.rec[k] as i32;
|
||||
}
|
||||
}
|
||||
let mut levels = q.levels(&res, tx);
|
||||
let mut eob = eob_of(&levels, tx);
|
||||
if eob > 0 && !q.lossless {
|
||||
let back = q.residual(&levels, tx, eob);
|
||||
let d_skip: i64 = res.iter().map(|v| (*v as i64) * (*v as i64)).sum();
|
||||
let d_code: i64 = res.iter().zip(&back).map(|(a, b)| ((a - b) as i64).pow(2)).sum();
|
||||
if d_skip as f64 <= d_code as f64 + zl * self.f.lambda_sse * levels_cost(&levels, eob) as f64 / 256.0 {
|
||||
levels = [0; 16];
|
||||
eob = 0;
|
||||
}
|
||||
}
|
||||
any |= eob > 0;
|
||||
blocks.push((plane, x, y, levels, eob));
|
||||
}
|
||||
}
|
||||
}
|
||||
let skip = !any;
|
||||
// Mode info.
|
||||
let avail_u = r > 0;
|
||||
let avail_l = c > self.col_start;
|
||||
let above = avail_u.then(|| self.mi_at(r - 1, c));
|
||||
let left = avail_l.then(|| self.mi_at(r, c - 1));
|
||||
let skip_ctx = above.is_some_and(|m| m.skip) as usize + left.is_some_and(|m| m.skip) as usize;
|
||||
self.w.ctx(skip, probs::SKIP + skip_ctx);
|
||||
// is_inter context: neighbours are inter (no intra blocks in inter frames).
|
||||
let intra = |m: Option<Mi>| m.is_some_and(|m| !m.inter);
|
||||
let inter_ctx = match (above, left) {
|
||||
(Some(_), Some(_)) => {
|
||||
if intra(above) && intra(left) {
|
||||
3
|
||||
} else {
|
||||
(intra(above) || intra(left)) as usize
|
||||
}
|
||||
}
|
||||
(Some(_), None) => 2 * intra(above) as usize,
|
||||
(None, Some(_)) => 2 * intra(left) as usize,
|
||||
(None, None) => 0,
|
||||
};
|
||||
self.w.ctx(true, probs::IS_INTER + inter_ctx);
|
||||
// single_ref_p1 = 0 (LAST_FRAME); context for single-reference LAST neighbours.
|
||||
let ref_ctx = match (above, left) {
|
||||
(Some(a), Some(l)) => match (a.inter, l.inter) {
|
||||
(false, false) => 2,
|
||||
(false, true) => 4,
|
||||
(true, false) => 4,
|
||||
(true, true) => 4,
|
||||
},
|
||||
(Some(m), None) | (None, Some(m)) => {
|
||||
if m.inter {
|
||||
4
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
(None, None) => 2,
|
||||
};
|
||||
self.w.ctx(false, probs::SINGLE_REF + ref_ctx * 2);
|
||||
self.w.tree(&INTER_MODE_TREE, mode - NEARESTMV, |i| P::Ctx((probs::INTER_MODE + mctx * 3 + i) as u16));
|
||||
if mode == NEWMV {
|
||||
self.write_mv(mv.minus(nearest));
|
||||
}
|
||||
self.set_mi(r, c, n8, Mi { inter: true, mode, mv, skip, size: bsize });
|
||||
// Tokens and reconstruction.
|
||||
for (plane, x, y, levels, eob) in blocks {
|
||||
let ss = (plane > 0) as usize;
|
||||
let (x4, ly) = (x >> 2, (y >> 2) & (15 >> ss));
|
||||
if !skip {
|
||||
let ctx0 = (self.above_nz[plane][x4] + self.left_nz[plane][ly]) as usize;
|
||||
self.tokens(plane, true, tx, &levels, eob, ctx0);
|
||||
if eob > 0 {
|
||||
let resid = q.residual(&levels, tx, eob);
|
||||
let p = &mut self.planes[plane];
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
let k = (y + i) * p.w + x + j;
|
||||
p.rec[k] = (p.rec[k] as i32 + resid[i * 4 + j]).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let nz = (!skip && eob > 0) as u8;
|
||||
self.above_nz[plane][x4] = nz;
|
||||
self.left_nz[plane][ly] = nz;
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mv(&mut self, diff: Mv) {
|
||||
let joint = (diff.row != 0) as u8 * 2 + (diff.col != 0) as u8;
|
||||
self.w.tree(&MV_JOINT_TREE, joint, |i| P::Ctx((probs::MV_JOINT + i) as u16));
|
||||
if diff.row != 0 {
|
||||
self.write_mv_component(0, diff.row);
|
||||
}
|
||||
if diff.col != 0 {
|
||||
self.write_mv_component(1, diff.col);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_mv_component(&mut self, i: usize, v: i32) {
|
||||
debug_assert!(v != 0 && v & 1 == 0, "vectors are even without high precision");
|
||||
let offset = v.unsigned_abs() - 1;
|
||||
let c = mv_class(offset);
|
||||
self.w.ctx(v < 0, probs::MV_SIGN + i);
|
||||
self.w.tree(&MV_CLASS_TREE, c as u8, |k| P::Ctx((probs::MV_CLASS + i * 10 + k) as u16));
|
||||
let fr = ((offset >> 1) & 3) as u8;
|
||||
if c == 0 {
|
||||
let b = (offset >> 3) as usize;
|
||||
self.w.ctx(b == 1, probs::MV_CLASS0_BIT + i);
|
||||
self.w.tree(&MV_FR_TREE, fr, |k| P::Ctx((probs::MV_CLASS0_FR + i * 6 + b * 3 + k) as u16));
|
||||
} else {
|
||||
let dd = (offset - (1 << (c + 3))) >> 3;
|
||||
for b in 0..c {
|
||||
self.w.ctx((dd >> b) & 1 == 1, probs::MV_BITS + i * 10 + b as usize);
|
||||
}
|
||||
self.w.tree(&MV_FR_TREE, fr, |k| P::Ctx((probs::MV_FR + i * 3 + k) as u16));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! 4×4 transforms: the decoder's integer inverse DCT / ADST (spec 8.7.1) and Walsh–Hadamard
|
||||
//! transform (8.7.1.10), and their forward counterparts (the exact inverse matrices of the
|
||||
//! decoder's linear transforms, so `inverse4(forward(res)) ≈ res`; the WHT inverse is exact).
|
||||
|
||||
pub const DCT_DCT: u8 = 0;
|
||||
pub const ADST_DCT: u8 = 1;
|
||||
pub const DCT_ADST: u8 = 2;
|
||||
pub const ADST_ADST: u8 = 3;
|
||||
|
||||
fn r14(v: i32) -> i32 {
|
||||
(v + (1 << 13)) >> 14
|
||||
}
|
||||
|
||||
fn idct4(x: [i32; 4]) -> [i32; 4] {
|
||||
let v0 = r14(x[0] * 11585 - x[2] * 11585);
|
||||
let v1 = r14(x[0] * 11585 + x[2] * 11585);
|
||||
let v2 = r14(x[1] * 6270 - x[3] * 15137);
|
||||
let v3 = r14(x[1] * 15137 + x[3] * 6270);
|
||||
[v1 + v3, v0 + v2, v0 - v2, v1 - v3]
|
||||
}
|
||||
|
||||
fn iadst4(x: [i32; 4]) -> [i32; 4] {
|
||||
let (s19, s29, s39, s49) = (5283i64, 9929i64, 13377i64, 15212i64);
|
||||
let x: [i64; 4] = x.map(|v| v as i64);
|
||||
let s0 = s19 * x[0];
|
||||
let s1 = s29 * x[0];
|
||||
let s2 = s39 * x[1];
|
||||
let s3 = s49 * x[2];
|
||||
let s4 = s19 * x[2];
|
||||
let s5 = s29 * x[3];
|
||||
let s6 = s49 * x[3];
|
||||
let s7 = s39 * (x[0] - x[2] + x[3]);
|
||||
let x0 = s0 + s3 + s5;
|
||||
let x1 = s1 - s4 - s6;
|
||||
let x2 = s7;
|
||||
let x3 = s2;
|
||||
let r = |v: i64| ((v + (1 << 13)) >> 14) as i32;
|
||||
[r(x0 + x3), r(x1 + x3), r(x2), r(x0 + x1 - x3)]
|
||||
}
|
||||
|
||||
/// The decoder's 4×4 inverse transform of dequantised coefficients (row-major) to a residual.
|
||||
pub fn inverse4(coefs: &[i32; 16], tx_type: u8) -> [i32; 16] {
|
||||
let row_adst = matches!(tx_type, DCT_ADST | ADST_ADST);
|
||||
let col_adst = matches!(tx_type, ADST_DCT | ADST_ADST);
|
||||
let mut t = [[0i32; 4]; 4];
|
||||
for i in 0..4 {
|
||||
let r = [coefs[i * 4], coefs[i * 4 + 1], coefs[i * 4 + 2], coefs[i * 4 + 3]];
|
||||
t[i] = if row_adst { iadst4(r) } else { idct4(r) };
|
||||
}
|
||||
let mut out = [0i32; 16];
|
||||
for j in 0..4 {
|
||||
let c = [t[0][j], t[1][j], t[2][j], t[3][j]];
|
||||
let c = if col_adst { iadst4(c) } else { idct4(c) };
|
||||
for i in 0..4 {
|
||||
out[i * 4 + j] = (c[i] + 8) >> 4;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Inverse Walsh–Hadamard (8.7.1.10) on one line.
|
||||
fn iwht(x: [i32; 4], shift: u32) -> [i32; 4] {
|
||||
let mut a = x[0] >> shift;
|
||||
let mut c = x[1] >> shift;
|
||||
let mut d = x[2] >> shift;
|
||||
let mut b = x[3] >> shift;
|
||||
a += c;
|
||||
d -= b;
|
||||
let e = (a - d) >> 1;
|
||||
b = e - b;
|
||||
c = e - c;
|
||||
a -= b;
|
||||
d += c;
|
||||
[a, b, c, d]
|
||||
}
|
||||
|
||||
/// The exact inverse of [`iwht`] (with shift 0): inputs that produce outputs `y`.
|
||||
fn fwht(y: [i32; 4]) -> [i32; 4] {
|
||||
let (aa, bb, cc, dd) = (y[0], y[1], y[2], y[3]);
|
||||
let d1 = dd - cc;
|
||||
let a1 = aa + bb;
|
||||
let e = (a1 - d1) >> 1;
|
||||
let b0 = e - bb;
|
||||
let c0 = e - cc;
|
||||
let a0 = a1 - c0;
|
||||
let d0 = d1 + b0;
|
||||
[a0, c0, d0, b0]
|
||||
}
|
||||
|
||||
pub fn inverse_wht(coefs: &[i32; 16]) -> [i32; 16] {
|
||||
let mut t = [[0i32; 4]; 4];
|
||||
for i in 0..4 {
|
||||
t[i] = iwht([coefs[i * 4], coefs[i * 4 + 1], coefs[i * 4 + 2], coefs[i * 4 + 3]], 2);
|
||||
}
|
||||
let mut out = [0i32; 16];
|
||||
for j in 0..4 {
|
||||
let r = iwht([t[0][j], t[1][j], t[2][j], t[3][j]], 0);
|
||||
for i in 0..4 {
|
||||
out[i * 4 + j] = r[i];
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Lossless forward transform: levels (dequantised by 4) whose inverse is exactly `res`.
|
||||
pub fn forward_wht(res: &[i32; 16]) -> [i32; 16] {
|
||||
let mut t = [[0i32; 4]; 4];
|
||||
for j in 0..4 {
|
||||
let col = fwht([res[j], res[4 + j], res[8 + j], res[12 + j]]);
|
||||
for i in 0..4 {
|
||||
t[i][j] = col[i];
|
||||
}
|
||||
}
|
||||
let mut lv = [0i32; 16];
|
||||
for i in 0..4 {
|
||||
let r = fwht(t[i]);
|
||||
lv[i * 4..i * 4 + 4].copy_from_slice(&r);
|
||||
}
|
||||
lv
|
||||
}
|
||||
|
||||
/// Forward transform matrices: the inverses of the decoder's (linear part of the) inverse
|
||||
/// transforms.
|
||||
pub fn forward_matrices() -> &'static [[[f64; 16]; 16]; 4] {
|
||||
use std::sync::OnceLock;
|
||||
static M: OnceLock<[[[f64; 16]; 16]; 4]> = OnceLock::new();
|
||||
M.get_or_init(|| {
|
||||
let mut out = [[[0.0; 16]; 16]; 4];
|
||||
for (ty, o) in out.iter_mut().enumerate() {
|
||||
// Columns of the inverse: response to a scaled unit coefficient.
|
||||
let k = 1 << 16;
|
||||
let mut inv = [[0.0f64; 16]; 16];
|
||||
for c in 0..16 {
|
||||
let mut e = [0i32; 16];
|
||||
e[c] = k;
|
||||
let r = inverse4(&e, ty as u8);
|
||||
for p in 0..16 {
|
||||
inv[p][c] = r[p] as f64 / k as f64;
|
||||
}
|
||||
}
|
||||
*o = invert16(inv);
|
||||
}
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
fn invert16(m: [[f64; 16]; 16]) -> [[f64; 16]; 16] {
|
||||
let mut a = m;
|
||||
let mut inv = [[0.0; 16]; 16];
|
||||
for (i, r) in inv.iter_mut().enumerate() {
|
||||
r[i] = 1.0;
|
||||
}
|
||||
for col in 0..16 {
|
||||
let piv = (col..16).max_by(|&x, &y| a[x][col].abs().total_cmp(&a[y][col].abs())).unwrap_or(col);
|
||||
a.swap(col, piv);
|
||||
inv.swap(col, piv);
|
||||
let d = a[col][col];
|
||||
for j in 0..16 {
|
||||
a[col][j] /= d;
|
||||
inv[col][j] /= d;
|
||||
}
|
||||
for r in 0..16 {
|
||||
if r != col {
|
||||
let f = a[r][col];
|
||||
if f != 0.0 {
|
||||
for j in 0..16 {
|
||||
a[r][j] -= f * a[col][j];
|
||||
inv[r][j] -= f * inv[col][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
inv
|
||||
}
|
||||
|
||||
/// Quantisation settings of one block.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Quant {
|
||||
pub dcq: i32,
|
||||
pub acq: i32,
|
||||
pub lossless: bool,
|
||||
/// Rounding offsets (fractions of a step) for DC and AC; smaller = wider dead zone.
|
||||
pub round_dc: f64,
|
||||
pub round_ac: f64,
|
||||
}
|
||||
|
||||
impl Quant {
|
||||
/// Quantised levels (raster order) of a residual.
|
||||
pub fn levels(&self, res: &[i32; 16], tx_type: u8) -> [i32; 16] {
|
||||
if self.lossless {
|
||||
return forward_wht(res);
|
||||
}
|
||||
let f = &forward_matrices()[tx_type as usize];
|
||||
let mut lv = [0i32; 16];
|
||||
for k in 0..16 {
|
||||
let coef: f64 = (0..16).map(|q| f[k][q] * res[q] as f64).sum();
|
||||
let (qs, rnd) = if k == 0 { (self.dcq, self.round_dc) } else { (self.acq, self.round_ac) };
|
||||
let l = (coef.abs() / qs as f64 + rnd).floor() as i32;
|
||||
lv[k] = if coef < 0.0 { -l } else { l };
|
||||
}
|
||||
lv
|
||||
}
|
||||
|
||||
/// The decoder's residual for `levels` (all zero when `eob` is 0).
|
||||
pub fn residual(&self, levels: &[i32; 16], tx_type: u8, eob: usize) -> [i32; 16] {
|
||||
if eob == 0 {
|
||||
return [0; 16];
|
||||
}
|
||||
let mut deq = [0i32; 16];
|
||||
for k in 0..16 {
|
||||
deq[k] = levels[k] * if k == 0 { self.dcq } else { self.acq };
|
||||
}
|
||||
if self.lossless { inverse_wht(&deq) } else { inverse4(&deq, tx_type) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wht_is_exactly_invertible() {
|
||||
let mut s = 7u32;
|
||||
for _ in 0..2000 {
|
||||
let mut r = [0i32; 16];
|
||||
for v in &mut r {
|
||||
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
*v = (s >> 16) as i32 % 511 - 255;
|
||||
}
|
||||
let lv = forward_wht(&r);
|
||||
let deq = lv.map(|l| l * 4);
|
||||
assert_eq!(inverse_wht(&deq), r);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_transforms_invert_the_decoder() {
|
||||
for ty in 0..4u8 {
|
||||
let f = &forward_matrices()[ty as usize];
|
||||
let r: [i32; 16] = std::array::from_fn(|i| (i as i32 * 37 % 61) - 30);
|
||||
let c: [i32; 16] = std::array::from_fn(|k| (0..16).map(|q| f[k][q] * r[q] as f64).sum::<f64>().round() as i32);
|
||||
let back = inverse4(&c, ty);
|
||||
for i in 0..16 {
|
||||
assert!((back[i] - r[i]).abs() <= 1, "type {ty}: {back:?} vs {r:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -110,7 +110,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
||||
| Area | Weighted parity | Remaining (agent-hours) | Biggest gaps |
|
||||
|---|---|---|---|
|
||||
| Layers | 88% | 4.8 | frame blending, collapse transformations, slip edit |
|
||||
| Output | 90% | 3.0 | multiple output modules, pre-render (WebM with VP9 alpha + Opus and WAV/AIFF audio-only landed: VP9 is intra-only, Opus CELT-only) |
|
||||
| Output | 90% | 3.0 | multiple output modules, pre-render (WebM with VP9 alpha + Opus and WAV/AIFF audio-only landed: VP9 key + inter frames with motion search, loop filter and rate control; Opus CELT-only) |
|
||||
| Audio | 85% | 0.5 | audio to keyframes |
|
||||
| Import | 88% | 2.0 | AI/EPS/PDF vector footage, PSD smart objects and 3D layers (PSD as footage/composition/retain layer sizes, SVG footage and Create Shapes from Vector Layer landed) |
|
||||
| Automation | ≈ 90% | 0.8 | scripting covers the documented core object model (AUT-2, M14.4: `app`, project items, comps, layers, properties and keyframes, text documents, markers, render queue, Script Console, `effectcraft-cli script`, MCP `run_script`); still missing: ScriptUI panels/dialogs, `.jsxbin`, sockets |
|
||||
|
||||
Reference in new issue
Block a user