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:
parent
646d3fa15f
commit
917e5f7ae8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in New Issue