M13.4: av1enc sub-sample motion search, deblocking loop filter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 05:26:15 -04:00
parent 646d3fa15f
commit 917e5f7ae8
9 changed files with 590 additions and 26 deletions

View File

@ -23,8 +23,11 @@ reconstruction, so prediction never drifts.
- **Frames**: a `KEY_FRAME` every `keyint` frames, `INTER_FRAME`s otherwise. Inter frames use
slot 0 (the previous reconstruction) for all seven references and refresh slot 0 only, so
`LAST_FRAME` is always the previous frame. `primary_ref_frame = PRIMARY_REF_NONE` (default
CDFs every frame), symbol-level CDF adaptation on, frame-end CDF update off. One tile, loop
filter level 0, `TX_MODE_LARGEST`, `reduced_tx_set`, no segmentation, no delta q / lf.
CDFs every frame), symbol-level CDF adaptation on, frame-end CDF update off. One tile,
`TX_MODE_LARGEST`, `reduced_tx_set`, no segmentation, no delta q / lf.
- **Deblocking**: the normative loop filter (7.14) runs on the reconstruction; the frame level
(the same for both luma directions and both chroma planes, no deltas, sharpness 0) is searched
per frame for the lowest luma error.
- **Tiles**: the multi-symbol arithmetic encoder is the exact inverse of the spec's symbol
decoder (8.2), including the exit process padding; CDF adaptation as in 8.2.6.
- **Partitions**: 64x64 superblocks, `PARTITION_NONE` / `PARTITION_SPLIT` down to 8x8 chosen by
@ -33,9 +36,10 @@ reconstruction, so prediction never drifts.
- **Intra**: DC, V, H, Paeth, smooth, smooth-V, smooth-H for luma and chroma (chosen by SAD),
with the spec's edge preparation and above-right / below-left availability.
- **Inter**: single reference (`LAST_FRAME`), `NEWMV` / `NEARESTMV` / `NEARMV` / `GLOBALMV`
chosen from the spec's motion vector prediction stack (7.10.2, mirrored exactly), full-sample
motion search (candidates + diamond search), the normative 8-tap `EIGHTTAP` prediction for
luma and (half-sample) chroma, skip blocks, per-block intra / inter choice.
chosen from the spec's motion vector prediction stack (7.10.2, mirrored exactly); motion search
from neighbour / co-located / parent candidates, a whole-sample diamond search and half- then
quarter-sample refinement (`allow_high_precision_mv` = 0); the normative 8-tap `EIGHTTAP`
prediction for luma and chroma; skip blocks; per-block intra / inter choice.
- **Residual**: one transform per plane and block (4x4 ... 32x32): `DCT_DCT` for luma, the
mode-dependent `DCT` / `ADST` combination for intra chroma; dead-zone quantisation with the
spec's `Dc_Qlookup` / `Ac_Qlookup`; the normative dequantisation and inverse transforms in the
@ -48,8 +52,9 @@ reconstruction, so prediction never drifts.
## Limits
- One tile: width and height up to 4096 and at most 2304 superblocks (4096x2304 samples).
- No in-loop filters (deblocking, CDEF, loop restoration), so compression at low rates is below
mature encoders; no sub-sample motion search, no compound prediction, no 4xN blocks.
- No CDEF or loop restoration, no compound prediction, no rectangular or 4xN blocks, no
directional intra modes other than V / H, no CfL or palette, no transform-size splits, so
compression is below mature encoders.
- 4:2:0 only (Main profile), no 12 bit.
## Specification and provenance

View File

@ -33,6 +33,8 @@ pub(crate) struct RefFrame {
pub planes: [Plane; 3],
/// Motion vectors chosen for the frame (per 4x4), used as search candidates.
pub mvs: Vec<[i32; 2]>,
/// The frame's loop filter level.
pub lf_level: u32,
}
pub(crate) struct FrameGeom {
@ -125,8 +127,8 @@ pub(crate) struct Decider<'a> {
ac_qi: i64,
lambda: f64,
lambda_sad: f64,
sb_r: usize,
sb_c: usize,
/// Half-sample interpolations of the reference luma (visible area): (8, 0), (0, 8), (8, 8).
half: [Vec<u16>; 3],
}
const MODE_BITS_INTRA: f64 = 3.0;
@ -138,7 +140,7 @@ impl<'a> Decider<'a> {
let ac_qi = AC_QLOOKUP[bdi][qidx as usize] as i64;
// Lagrangian multiplier in squared-sample units of the coded bit depth.
let step = ac_qi as f64 / 8.0;
let lambda = 0.22 * step * step;
let lambda = 0.25 * step * step;
let rec = std::array::from_fn(|p| {
let (w, h) = g.padded(p);
Plane::new(w, h)
@ -158,15 +160,15 @@ impl<'a> Decider<'a> {
ac_qi,
lambda,
lambda_sad: lambda.sqrt(),
sb_r: 0,
sb_c: 0,
half: match refr {
Some(rf) => [(8, 0), (0, 8), (8, 8)].map(|(fx, fy)| subpel_plane(&rf.planes[0], g.width, g.height, fx, fy, bd)),
None => Default::default(),
},
}
}
/// Decides one 64x64 superblock (reconstruction is written to `rec`).
pub fn superblock(&mut self, r: usize, c: usize) -> Node {
self.sb_r = r;
self.sb_c = c;
// clear_block_decoded_flags( r, c, sbSize4 )
for plane in 0..3 {
let s = (plane > 0) as usize;
@ -475,7 +477,7 @@ impl<'a> Decider<'a> {
let cost =
|m: [i32; 2]| -> f64 { self.sad_ref(&refr.planes[0], x0 + (m[1] >> 3), y0 + (m[0] >> 3), c * 4, r * 4, n) as f64 + self.lambda_sad * mv_bits(m) };
let mut best = ([0, 0], f64::MAX);
for m in cands {
for &m in &cands {
let m = [m[0] & !7, m[1] & !7];
if !valid(m) {
continue;
@ -507,9 +509,115 @@ impl<'a> Decider<'a> {
}
step >>= 1;
}
// half- then quarter-sample refinement (plus the exact neighbour candidates)
let mut buf = vec![0u16; n * n];
let sub_cost = |m: [i32; 2], buf: &mut Vec<u16>| -> f64 {
self.search_block(m, c * 4, r * 4, n, buf);
self.sad(0, c * 4, r * 4, n, buf) as f64 + self.lambda_sad * mv_bits(m)
};
for m in cands {
if m[0] & 7 != 0 || m[1] & 7 != 0 {
let m = [m[0] & !1, m[1] & !1];
if valid(m) {
let cst = sub_cost(m, &mut buf);
if cst < best.1 {
best = (m, cst);
}
}
}
}
for step in [4, 2] {
let center = best.0;
for d in [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] {
let m = [center[0] + d[0] * step, center[1] + d[1] * step];
if !valid(m) {
continue;
}
let cst = sub_cost(m, &mut buf);
if cst < best.1 {
best = (m, cst);
}
}
}
(best.0, mv_bits(best.0))
}
/// Approximate luma prediction for motion search: exact at whole and half-sample
/// positions (precomputed planes), averaged neighbours at quarter-sample positions.
fn search_block(&self, mv: [i32; 2], x: usize, y: usize, n: usize, out: &mut [u16]) {
let refr = self.refr.expect("reference frame");
let (w, h) = (self.g.width as i32, self.g.height as i32);
// per axis: up to two (integer offset, half) taps
let axis = |v: i32| -> ([(i32, bool); 2], usize) {
let p = 2 * v; // 1/16 units
let (i, f) = (p >> 4, p & 15);
match f {
0 => ([(i, false), (i, false)], 1),
8 => ([(i, true), (i, true)], 1),
4 => ([(i, false), (i, true)], 2),
_ => ([(i, true), (i + 1, false)], 2),
}
};
let (ax, nx) = axis(mv[1]);
let (ay, ny) = axis(mv[0]);
let taps = nx.max(ny);
let fetch = |k: usize, xx: i32, yy: i32| -> u16 {
let (ox, hx) = ax[k.min(nx - 1)];
let (oy, hy) = ay[k.min(ny - 1)];
let sx = (xx + ox).clamp(0, w - 1) as usize;
let sy = (yy + oy).clamp(0, h - 1) as usize;
match (hx, hy) {
(false, false) => refr.planes[0].at(sx, sy),
(true, false) => self.half[0][sy * w as usize + sx],
(false, true) => self.half[1][sy * w as usize + sx],
(true, true) => self.half[2][sy * w as usize + sx],
}
};
let vw = n.min(self.g.width.saturating_sub(x));
let vh = n.min(self.g.height.saturating_sub(y));
// fast path: every tap inside the picture
let (x0, y0) = (x as i32, y as i32);
let inside = (0..taps).all(|k| {
let (ox, _) = ax[k.min(nx - 1)];
let (oy, _) = ay[k.min(ny - 1)];
x0 + ox >= 0 && y0 + oy >= 0 && x0 + ox + vw as i32 <= w && y0 + oy + vh as i32 <= h
});
if inside {
let src = |k: usize| -> (&[u16], usize, usize) {
let (ox, hx) = ax[k.min(nx - 1)];
let (oy, hy) = ay[k.min(ny - 1)];
let (sx, sy) = ((x0 + ox) as usize, (y0 + oy) as usize);
match (hx, hy) {
(false, false) => (&refr.planes[0].data, refr.planes[0].stride, sy * refr.planes[0].stride + sx),
(true, false) => (&self.half[0], w as usize, sy * w as usize + sx),
(false, true) => (&self.half[1], w as usize, sy * w as usize + sx),
(true, true) => (&self.half[2], w as usize, sy * w as usize + sx),
}
};
let (a, sa, oa) = src(0);
if taps == 1 {
for i in 0..vh {
out[i * n..i * n + vw].copy_from_slice(&a[oa + i * sa..oa + i * sa + vw]);
}
} else {
let (b, sb, ob) = src(1);
for i in 0..vh {
let (ra, rb) = (&a[oa + i * sa..oa + i * sa + vw], &b[ob + i * sb..ob + i * sb + vw]);
for (o, (&p, &q)) in out[i * n..i * n + vw].iter_mut().zip(ra.iter().zip(rb)) {
*o = ((p as u32 + q as u32 + 1) >> 1) as u16;
}
}
}
return;
}
for i in 0..vh {
for j in 0..vw {
let (xx, yy) = (x0 + j as i32, y0 + i as i32);
out[i * n + j] = if taps == 1 { fetch(0, xx, yy) } else { ((fetch(0, xx, yy) as u32 + fetch(1, xx, yy) as u32 + 1) >> 1) as u16 };
}
}
}
/// SAD of the source block at (x, y) (luma) against the reference block at (rx, ry).
fn sad_ref(&self, refp: &Plane, rx: i32, ry: i32, x: usize, y: usize, n: usize) -> u64 {
let (vw, vh) = (self.g.width, self.g.height);
@ -594,7 +702,7 @@ impl<'a> Decider<'a> {
let mut coef = vec![0f32; n * n];
forward_2d(&res, &mut coef, n, tx_type);
// dead-zone quantiser: rounding offset below one half
let rnd = if intra { 0.40f32 } else { 0.33f32 };
let rnd = if intra { 0.38f32 } else { 0.30f32 };
let mut levels = vec![0i32; n * n];
let mut nonzero = false;
for (k, (&cf, l)) in coef.iter().zip(levels.iter_mut()).enumerate() {

View File

@ -11,6 +11,7 @@
mod bitw;
mod cdf;
mod decide;
mod loopfilter;
mod pred;
mod symbol;
mod tables;
@ -19,6 +20,7 @@ mod writer;
use bitw::{BitWriter, obu};
use decide::{Decider, FrameGeom, RefFrame};
use loopfilter::{LfInfo, loop_filter};
use pred::Plane;
use writer::TileWriter;
@ -263,7 +265,7 @@ impl Encoder {
}
/// uncompressed_header( ) (5.9) for a shown KEY_FRAME or an INTER_FRAME referencing slot 0.
fn frame_header(&self, w: &mut BitWriter, key: bool, qidx: u8) {
fn frame_header(&self, w: &mut BitWriter, key: bool, qidx: u8, lf_level: u32) {
w.f(1, 0); // show_existing_frame
w.f(2, if key { 0 } else { 1 }); // frame_type
w.f(1, 1); // show_frame
@ -311,9 +313,13 @@ impl Encoder {
w.f(1, 0); // using_qmatrix
w.f(1, 0); // segmentation_enabled
w.f(1, 0); // delta_q_present (base_q_idx > 0)
// loop_filter_params( )
w.f(6, 0);
w.f(6, 0);
// loop_filter_params( ): the same level for every plane and direction
w.f(6, lf_level);
w.f(6, lf_level);
if lf_level != 0 {
w.f(6, lf_level);
w.f(6, lf_level);
}
w.f(3, 0); // loop_filter_sharpness
w.f(1, 0); // loop_filter_delta_enabled
w.f(1, 0); // tx_mode_select (TX_MODE_LARGEST)
@ -366,19 +372,68 @@ impl Encoder {
tw.superblock(sbr * 16, sbc * 16, &node);
}
}
let lf_level = self.pick_filter_level(&dec.rec, &tw.lf_info(), src, qidx);
loop_filter(&mut dec.rec, &tw.lf_info(), [lf_level; 4], self.cfg.bit_depth as u32, false);
let tile = tw.finish();
let mut w = BitWriter::new();
self.frame_header(&mut w, key, qidx);
self.frame_header(&mut w, key, qidx, lf_level);
w.byte_align();
// tile_group_obu( ): a single tile, no tile_start_and_end_present_flag
let mut payload = w.into_bytes();
payload.extend_from_slice(&tile);
let mut out = Vec::with_capacity(payload.len() + 8);
obu(OBU_FRAME, &payload, &mut out);
let rf = RefFrame { planes: dec.rec, mvs: dec.mvs };
let rf = RefFrame { planes: dec.rec, mvs: dec.mvs, lf_level };
(out, rf)
}
/// The loop filter level minimising the luma error (searched around a guess from the
/// quantizer; luma only).
fn pick_filter_level(&self, rec: &[Plane; 3], info: &LfInfo, src: &[Plane; 3], qidx: u8) -> u32 {
let (w, h) = (self.geom.width, self.geom.height);
let sse = |pl: &Plane| -> u64 {
let mut s = 0u64;
for y in 0..h {
for (a, b) in pl.row(y)[..w].iter().zip(&src[0].row(y)[..w]) {
let d = *a as i64 - *b as i64;
s += (d * d) as u64;
}
}
s
};
let bd = self.cfg.bit_depth as u32;
let mut cache: Vec<(u32, u64)> = vec![(0, sse(&rec[0]))];
let mut eval = |lvl: u32| -> u64 {
if let Some(&(_, e)) = cache.iter().find(|(l, _)| *l == lvl) {
return e;
}
let mut planes = [rec[0].clone(), Plane::new(0, 0), Plane::new(0, 0)];
loop_filter(&mut planes, info, [lvl; 4], bd, true);
let e = sse(&planes[0]);
cache.push((lvl, e));
e
};
let guess = ((qidx as u32 * 3) / 16).clamp(2, 50);
let mut best = (0u32, eval(0));
let mut step = (guess / 2).max(1);
let mut center = guess;
let e = eval(center);
if e < best.1 {
best = (center, e);
}
while step >= 1 {
for lvl in [center.saturating_sub(step), (center + step).min(63)] {
let e = eval(lvl);
if e < best.1 {
best = (lvl, e);
}
}
center = best.0.max(1);
step /= 2;
}
best.0
}
/// Encodes one frame.
pub fn encode(&mut self, frame: &Frame) -> Packet {
let key = self.frame_num.is_multiple_of(self.cfg.keyint.max(1) as u64);
@ -435,6 +490,12 @@ impl Encoder {
(obu, rf)
}
/// The loop filter level of the last encoded frame. For tests.
#[doc(hidden)]
pub fn last_loop_filter_level(&self) -> Option<u32> {
self.refr.as_ref().map(|r| r.lf_level)
}
/// The reconstruction of the last encoded frame (visible area, planes Y, U, V), equal to
/// what a decoder outputs. For tests.
#[doc(hidden)]

View File

@ -0,0 +1,193 @@
//! Deblocking loop filter (spec 7.14), mirrored from the decoder process so that the encoder's
//! references equal a decoder's. Segmentation and loop filter deltas are off and the sharpness
//! is 0 in the frames this encoder writes.
use crate::pred::Plane;
use crate::tables::*;
/// Mode info the loop filter reads (per 4x4 luma unit, row stride `mi_cols`), and the
/// transform size per 4x4 unit of each plane (`LoopfilterTxSizes`, row stride `mi_cols + 32`).
pub(crate) struct LfInfo<'a> {
pub mi_cols: usize,
pub mi_rows: usize,
pub width: usize,
pub height: usize,
pub mi_size: &'a [u8],
pub skip: &'a [bool],
pub is_inter: &'a [bool],
pub tx: [&'a [u8]; 3],
}
/// Applies the loop filter with `loop_filter_level[ 0..4 ]` to `planes` (only plane 0 when
/// `luma_only`).
pub(crate) fn loop_filter(planes: &mut [Plane; 3], info: &LfInfo, level: [u32; 4], bit_depth: u32, luma_only: bool) {
if level[0] == 0 && level[1] == 0 {
return;
}
let nplanes = if luma_only { 1 } else { 3 };
for (plane, pl) in planes.iter_mut().enumerate().take(nplanes) {
if plane > 0 && level[1 + plane] == 0 {
continue;
}
for pass in 0..2 {
let step = if plane == 0 { 1 } else { 2 };
let mut row = 0;
while row < info.mi_rows {
let mut col = 0;
while col < info.mi_cols {
edge(pl, info, level, bit_depth, plane, pass, row, col);
col += step;
}
row += step;
}
}
}
}
fn edge(pl: &mut Plane, info: &LfInfo, level: [u32; 4], bd: u32, plane: usize, pass: usize, row: usize, col: usize) {
let s = (plane > 0) as usize;
let (dx, dy) = if pass == 0 { (1usize, 0usize) } else { (0, 1) };
let x = col * 4;
let y = row * 4;
let row = row | s;
let col = col | s;
let on_screen = !(x >= info.width || y >= info.height || (pass == 0 && x == 0) || (pass == 1 && y == 0));
if !on_screen {
return;
}
let xp = x >> s;
let yp = y >> s;
let prev_row = row - (dy << s);
let prev_col = col - (dx << s);
let i = row * info.mi_cols + col;
let mi_size = info.mi_size[i] as usize;
let tx_stride = info.mi_cols + 32;
let tx_sz = info.tx[plane][(row >> s) * tx_stride + (col >> s)] as usize;
let plane_size = SUBSAMPLED_SIZE[mi_size][s][s] as usize;
let skip = info.skip[i];
let is_intra = !info.is_inter[i];
let prev_tx_sz = info.tx[plane][(prev_row >> s) * tx_stride + (prev_col >> s)] as usize;
let is_block_edge = if pass == 0 {
xp.is_multiple_of(4 * NUM_4X4_BLOCKS_WIDE[plane_size] as usize)
} else {
yp.is_multiple_of(4 * NUM_4X4_BLOCKS_HIGH[plane_size] as usize)
};
let is_tx_edge = if pass == 0 { xp.is_multiple_of(TX_WIDTH[tx_sz] as usize) } else { yp.is_multiple_of(TX_HEIGHT[tx_sz] as usize) };
let apply_filter = is_tx_edge && (is_block_edge || !skip || is_intra);
let base_size = if pass == 0 { TX_WIDTH[prev_tx_sz].min(TX_WIDTH[tx_sz]) } else { TX_HEIGHT[prev_tx_sz].min(TX_HEIGHT[tx_sz]) } as usize;
let filter_size = if plane == 0 { 16.min(base_size) } else { 8.min(base_size) };
// 7.14.4 with no deltas, segmentation or sharpness: the same strength for every block
let lvl = level[if plane == 0 { pass } else { plane + 1 }].min(MAX_LOOP_FILTER as u32) as i32;
if !apply_filter || lvl == 0 {
return;
}
let limit = lvl.max(1);
let blimit = 2 * (lvl + 2) + limit;
let thresh = lvl >> 4;
for k in 0..4 {
sample_filter(pl, xp + dy * k, yp + dx * k, plane, limit, blimit, thresh, dx as isize, dy as isize, filter_size, bd);
}
}
#[inline]
fn sample_filter(pl: &mut Plane, x: usize, y: usize, plane: usize, limit: i32, blimit: i32, thresh: i32, dx: isize, dy: isize, filter_size: usize, bd: u32) {
let stride = pl.stride as isize;
let step = dy * stride + dx;
let base = y as isize * stride + x as isize;
let reach: isize = if filter_size >= 16 { 7 } else { 4 };
let mut v = [0i32; 16];
for k in -reach..reach {
v[(k + 8) as usize] = pl.data[(base + k * step) as usize] as i32;
}
let at = |k: isize| -> i32 { v[(k + 8) as usize] };
let (q0, q1, q2, q3) = (at(0), at(1), at(2), at(3));
let (p0, p1, p2, p3) = (at(-1), at(-2), at(-3), at(-4));
let d = &mut pl.data;
let sh = bd - 8;
let thresh_bd = thresh << sh;
let hev = (p1 - p0).abs() > thresh_bd || (q1 - q0).abs() > thresh_bd;
let filter_len = if filter_size == 4 {
4
} else if plane != 0 {
6
} else if filter_size == 8 {
8
} else {
16
};
let limit_bd = limit << sh;
let blimit_bd = blimit << sh;
let mut mask = (p1 - p0).abs() > limit_bd || (q1 - q0).abs() > limit_bd || (p0 - q0).abs() * 2 + (p1 - q1).abs() / 2 > blimit_bd;
if filter_len >= 6 {
mask |= (p2 - p1).abs() > limit_bd || (q2 - q1).abs() > limit_bd;
}
if filter_len >= 8 {
mask |= (p3 - p2).abs() > limit_bd || (q3 - q2).abs() > limit_bd;
}
if mask {
return;
}
let t_bd = 1 << sh;
let flat = if filter_size >= 8 {
let mut m = (p1 - p0).abs() > t_bd || (q1 - q0).abs() > t_bd || (p2 - p0).abs() > t_bd || (q2 - q0).abs() > t_bd;
if filter_len >= 8 {
m |= (p3 - p0).abs() > t_bd || (q3 - q0).abs() > t_bd;
}
!m
} else {
false
};
let flat2 = if filter_size >= 16 {
let (q4, q5, q6, p4, p5, p6) = (at(4), at(5), at(6), at(-5), at(-6), at(-7));
!((p6 - p0).abs() > t_bd
|| (q6 - q0).abs() > t_bd
|| (p5 - p0).abs() > t_bd
|| (q5 - q0).abs() > t_bd
|| (p4 - p0).abs() > t_bd
|| (q4 - q0).abs() > t_bd)
} else {
false
};
if filter_size == 4 || !flat {
// narrow filter (7.14.6.3)
let half = 1i32 << (bd - 1);
let c = |v: i32| v.clamp(-half, half - 1);
let off = 0x80 << sh;
let (ps1, ps0, qs0, qs1) = (p1 - off, p0 - off, q0 - off, q1 - off);
let mut filter = if hev { c(ps1 - qs1) } else { 0 };
filter = c(filter + 3 * (qs0 - ps0));
let filter1 = c(filter + 4) >> 3;
let filter2 = c(filter + 3) >> 3;
d[base as usize] = (c(qs0 - filter1) + off) as u16;
d[(base - step) as usize] = (c(ps0 + filter2) + off) as u16;
if !hev {
let f = (filter1 + 1) >> 1;
d[(base + step) as usize] = (c(qs1 - f) + off) as u16;
d[(base - 2 * step) as usize] = (c(ps1 + f) + off) as u16;
}
} else {
// wide filter (7.14.6.4)
let log2 = if filter_size == 8 || !flat2 { 3 } else { 4 };
let n: isize = if log2 == 4 {
6
} else if plane == 0 {
3
} else {
2
};
let n2: isize = if log2 == 3 && plane == 0 { 0 } else { 1 };
let mut f = [0i32; 12];
for i in -n..n {
let mut t = 0;
for j in -n..=n {
let p = (i + j).clamp(-(n + 1), n);
let tap = if j.abs() <= n2 { 2 } else { 1 };
t += at(p) * tap;
}
f[(i + n) as usize] = (t + (1 << (log2 - 1))) >> log2;
}
for i in -n..n {
d[(base + i * step) as usize] = f[(i + n) as usize] as u16;
}
}
}

View File

@ -249,10 +249,68 @@ pub(crate) fn predict_inter(
}
}
/// The visible `w x h` area of a reference plane interpolated at the sub-sample phase
/// (`fx`, `fy`) (1/16 units) with the EIGHTTAP filter, as `predict_inter` computes it for
/// blocks wider and taller than 4. Used for motion search.
pub(crate) fn subpel_plane(refp: &Plane, w: usize, h: usize, fx: usize, fy: usize, bit_depth: u32) -> Vec<u16> {
let kh = &SUBPEL_FILTERS[EIGHTTAP][fx];
let kv = &SUBPEL_FILTERS[EIGHTTAP][fy];
let max = (1i32 << bit_depth) - 1;
let (lx, ly) = (w as i32 - 1, h as i32 - 1);
let mut inter = vec![0i32; w * h];
for y in 0..h {
let row = refp.row(y);
for x in 0..w {
let mut s = 0i32;
for t in 0..8 {
s += kh[t] as i32 * row[(x as i32 + t as i32 - 3).clamp(0, lx) as usize] as i32;
}
inter[y * w + x] = round2(s, 3);
}
}
let mut out = vec![0u16; w * h];
for y in 0..h {
for x in 0..w {
let mut s = 0i32;
for t in 0..8 {
s += kv[t] as i32 * inter[(y as i32 + t as i32 - 3).clamp(0, ly) as usize * w + x];
}
out[y * w + x] = round2(s, 11).clamp(0, max) as u16;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
/// Whole-plane interpolation equals block inter prediction at the same phase.
#[test]
fn subpel_plane_matches_block_prediction() {
let (w, h) = (37, 29);
let mut p = Plane::new(64, 64);
for (i, v) in p.data.iter_mut().enumerate() {
*v = ((i * 7919) % 256) as u16;
}
for (fx, fy) in [(8, 0), (0, 8), (8, 8), (4, 12)] {
let plane = subpel_plane(&p, w, h, fx, fy, 8);
for (bx, by) in [(0i32, 0i32), (8, 8), (24, 16), (-5, 20), (30, -3)] {
let mut pred = vec![0u16; 64];
let mv = [by * 8 + fy as i32 / 2, bx * 8 + fx as i32 / 2];
predict_inter(&p, w as i32 - 1, h as i32 - 1, 0, 0, 8, 8, mv, 0, 8, &mut pred);
for r in 0..8 {
for c in 0..8 {
let (x, y) = ((bx + c).clamp(0, w as i32 - 1), (by + r).clamp(0, h as i32 - 1));
if bx + c == x && by + r == y {
assert_eq!(pred[(r * 8 + c) as usize], plane[y as usize * w + x as usize], "phase ({fx}, {fy}) at ({}, {})", bx + c, by + r);
}
}
}
}
}
}
#[test]
fn phase_zero_filters_are_impulses() {
for f in SUBPEL_FILTERS.iter() {

View File

@ -6,6 +6,7 @@
use crate::cdf::Cdfs;
use crate::decide::{FrameGeom, Leaf, Node, intra_uv_tx_type, scan_for, tx_in_set, tx_set, tx_size_for};
use crate::loopfilter::LfInfo;
use crate::symbol::SymbolWriter;
use crate::tables::*;
@ -59,6 +60,8 @@ pub(crate) struct TileWriter<'a> {
above_dc: [Vec<u8>; 3],
left_level: [Vec<u8>; 3],
left_dc: [Vec<u8>; 3],
/// LoopfilterTxSizes per plane (row stride mi_cols + 32).
lf_tx: [Vec<u8>; 3],
// current block
r: usize,
c: usize,
@ -101,6 +104,7 @@ impl<'a> TileWriter<'a> {
above_dc: std::array::from_fn(|_| vec![0; g.mi_cols + 64]),
left_level: std::array::from_fn(|_| vec![0; g.mi_rows + 64]),
left_dc: std::array::from_fn(|_| vec![0; g.mi_rows + 64]),
lf_tx: std::array::from_fn(|_| vec![0; (g.mi_cols + 32) * (g.mi_rows + 32)]),
r: 0,
c: 0,
bsize: 0,
@ -114,6 +118,20 @@ impl<'a> TileWriter<'a> {
self.sw.finish()
}
/// The mode info the loop filter needs.
pub fn lf_info(&self) -> LfInfo<'_> {
LfInfo {
mi_cols: self.g.mi_cols,
mi_rows: self.g.mi_rows,
width: self.g.width,
height: self.g.height,
mi_size: &self.mi.mi_size,
skip: &self.mi.skip,
is_inter: &self.mi.is_inter,
tx: [&self.lf_tx[0], &self.lf_tx[1], &self.lf_tx[2]],
}
}
/// Start of a superblock row (clear_left_context).
pub fn start_sb_row(&mut self) {
for p in 0..3 {
@ -277,6 +295,17 @@ impl<'a> TileWriter<'a> {
self.mi.mi_size[i] = bsize as u8;
}
}
// LoopfilterTxSizes: one transform block per plane
let log2 = (n4 * 4).trailing_zeros();
let stride = self.g.mi_cols + 32;
for plane in 0..3 {
let s = (plane > 0) as usize;
let tx = tx_size_for(log2 - s as u32) as u8;
let (r0, c0, m) = (r >> s, c >> s, n4 >> s);
for y in r0..r0 + m {
self.lf_tx[plane][y * stride + c0..y * stride + c0 + m].iter_mut().for_each(|v| *v = tx);
}
}
}
fn intra_angle_and_uv(&mut self, b: &Leaf) {

View File

@ -0,0 +1,95 @@
//! Speed / efficiency measurements on ffmpeg-generated test sources (ignored by default):
//! `cargo test -p effectcraft-av1enc --test bench -- --ignored --nocapture`.
mod common;
use std::process::Command;
use std::time::Instant;
use common::*;
use effectcraft_av1enc::{Encoder, EncoderConfig, RateControl};
/// Raw 8-bit 4:2:0 frames of an ffmpeg lavfi source, or None without ffmpeg.
fn lavfi(src: &str, w: usize, h: usize, frames: usize) -> Option<Vec<Pic>> {
let out = std::env::temp_dir().join(format!("effectcraft-av1enc-bench-{}-{}.yuv", std::process::id(), src.split('=').next().unwrap_or("src")));
let st = Command::new("ffmpeg")
.args(["-v", "error", "-y", "-f", "lavfi", "-i"])
.arg(format!("{src}=size={w}x{h}:rate=30"))
.args(["-frames:v", &frames.to_string(), "-pix_fmt", "yuv420p", "-f", "rawvideo"])
.arg(&out)
.status()
.ok()?;
if !st.success() {
return None;
}
let raw = std::fs::read(&out).ok()?;
let _ = std::fs::remove_file(&out);
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let fs = w * h + 2 * cw * ch;
Some(
raw.chunks_exact(fs)
.map(|f| Pic {
w,
h,
y: f[..w * h].iter().map(|&b| b as u16).collect(),
u: f[w * h..w * h + cw * ch].iter().map(|&b| b as u16).collect(),
v: f[w * h + cw * ch..].iter().map(|&b| b as u16).collect(),
})
.collect(),
)
}
fn run(name: &str, pics: &[Pic], rate: RateControl, keyint: u32) {
let (w, h) = (pics[0].w, pics[0].h);
let mut cfg = EncoderConfig::new(w as u32, h as u32, 30, 1);
cfg.rate = rate;
cfg.keyint = keyint;
let mut enc = Encoder::new(cfg).unwrap();
let mut bytes = 0;
let mut psnr_sum = 0.0;
let mut t_key = 0.0;
let mut t_inter = 0.0;
let mut sizes = Vec::new();
for p in pics {
let t = Instant::now();
let pkt = enc.encode(&p.frame());
let dt = t.elapsed().as_secs_f64();
if pkt.keyframe {
t_key += dt;
} else {
t_inter += dt;
}
bytes += pkt.data.len();
sizes.push(pkt.data.len());
let rec = enc.last_reconstruction().unwrap();
psnr_sum += psnr(&p.y, &rec[0], 8);
}
let n = pics.len() as f64;
let n_key = pics.len().div_ceil(keyint as usize) as f64;
eprintln!(
"{name} {w}x{h} {rate:?}: {:.0} kbit/s, PSNR-Y {:.2} dB, key {:.2} s/frame, inter {:.2} s/frame, sizes {:?}",
bytes as f64 * 8.0 * 30.0 / n / 1000.0,
psnr_sum / n,
t_key / n_key,
if n > n_key { t_inter / (n - n_key) } else { 0.0 },
&sizes[..sizes.len().min(6)]
);
}
#[test]
#[ignore]
fn bench_lavfi() {
if std::env::var("AV1ENC_BENCH_QUICK").is_err() {
let Some(pics) = lavfi("testsrc2", 1920, 1080, 6) else {
eprintln!("ffmpeg not available");
return;
};
run("testsrc2", &pics, RateControl::ConstantQ(100), 30);
}
if let Some(pics) = lavfi("mandelbrot", 640, 360, 20) {
for q in [40, 80, 120, 160] {
run("mandelbrot", &pics, RateControl::ConstantQ(q), 30);
}
run("mandelbrot", &pics, RateControl::Bitrate { kbps: 1000 }, 30);
}
}

View File

@ -83,6 +83,7 @@ pub struct Run {
pub packets: Vec<Packet>,
pub recons: Vec<[Vec<u16>; 3]>,
pub psnr_y: Vec<f64>,
pub lf_levels: Vec<u32>,
}
/// Encodes `pics`, checks every packet decodes (filmcraft-av1) to exactly the encoder's
@ -91,7 +92,7 @@ pub fn encode_and_check(cfg: EncoderConfig, pics: &[Pic]) -> Run {
let bd = cfg.bit_depth as u32;
let mut enc = Encoder::new(cfg).expect("config");
let mut dec = filmcraft_av1::Decoder::new();
let mut run = Run { packets: Vec::new(), recons: Vec::new(), psnr_y: Vec::new() };
let mut run = Run { packets: Vec::new(), recons: Vec::new(), psnr_y: Vec::new(), lf_levels: Vec::new() };
for (i, p) in pics.iter().enumerate() {
let pkt = enc.encode(&p.frame());
let rec = enc.last_reconstruction().expect("reconstruction");
@ -107,6 +108,7 @@ pub fn encode_and_check(cfg: EncoderConfig, pics: &[Pic]) -> Run {
}
}
run.psnr_y.push(psnr(&p.y, &rec[0], bd));
run.lf_levels.push(enc.last_loop_filter_level().expect("level"));
run.recons.push(rec);
run.packets.push(pkt);
}

View File

@ -45,14 +45,17 @@ fn ivf(w: usize, h: usize, packets: &[Packet]) -> Vec<u8> {
/// Encodes `pics`, decodes the IVF stream with `ffmpeg -c:v libdav1d` and compares every
/// decoded frame with the encoder's reconstruction.
fn check(name: &str, cfg: EncoderConfig, pics: &[Pic]) {
/// Returns the loop filter levels used.
fn check(name: &str, cfg: EncoderConfig, pics: &[Pic]) -> Vec<u32> {
let (w, h, bd) = (cfg.width as usize, cfg.height as usize, cfg.bit_depth);
let mut enc = Encoder::new(cfg).expect("config");
let mut packets = Vec::new();
let mut recons = Vec::new();
let mut levels = Vec::new();
for p in pics {
packets.push(enc.encode(&p.frame()));
recons.push(enc.last_reconstruction().expect("reconstruction"));
levels.push(enc.last_loop_filter_level().expect("level"));
}
let dir = std::env::temp_dir().join(format!("effectcraft-av1enc-{}-{name}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
@ -87,7 +90,12 @@ fn check(name: &str, cfg: EncoderConfig, pics: &[Pic]) {
}
}
}
eprintln!("{name}: libdav1d bit-exact over {} frames ({} bytes)", pics.len(), packets.iter().map(|p| p.data.len()).sum::<usize>());
eprintln!(
"{name}: libdav1d bit-exact over {} frames ({} bytes, loop filter levels {levels:?})",
pics.len(),
packets.iter().map(|p| p.data.len()).sum::<usize>()
);
levels
}
fn cfg(w: usize, h: usize, bd: u8, rate: RateControl, keyint: u32) -> EncoderConfig {
@ -116,7 +124,12 @@ fn libdav1d_decodes_bit_exact() {
check("odd10", cfg(101, 75, 10, RateControl::ConstantQ(110), 4), &pics);
// a noisy moving picture, bitrate mode
let pics: Vec<Pic> = (0..6).map(|t| noisy(128, 80, t)).collect();
check("bitrate", cfg(128, 80, 8, RateControl::Bitrate { kbps: 300 }, 10), &pics);
let levels = check("bitrate", cfg(128, 80, 8, RateControl::Bitrate { kbps: 300 }, 10), &pics);
assert!(levels.iter().any(|&l| l > 0), "the deblocking filter is exercised");
// a coarse quantizer: strong deblocking, 10 bit
let pics: Vec<Pic> = (0..4).map(|t| synth(120, 88, 10, t)).collect();
let levels = check("deblock10", cfg(120, 88, 10, RateControl::ConstantQ(220), 2), &pics);
assert!(levels.iter().any(|&l| l > 0), "the deblocking filter is exercised");
}
#[test]