diff --git a/crates/av1enc/README.md b/crates/av1enc/README.md index f0756766..88b2a6c6 100644 --- a/crates/av1enc/README.md +++ b/crates/av1enc/README.md @@ -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 diff --git a/crates/av1enc/src/decide.rs b/crates/av1enc/src/decide.rs index e1697c31..939cabaa 100644 --- a/crates/av1enc/src/decide.rs +++ b/crates/av1enc/src/decide.rs @@ -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; 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| -> 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() { diff --git a/crates/av1enc/src/lib.rs b/crates/av1enc/src/lib.rs index 9a0889ca..99212361 100644 --- a/crates/av1enc/src/lib.rs +++ b/crates/av1enc/src/lib.rs @@ -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 { + 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)] diff --git a/crates/av1enc/src/loopfilter.rs b/crates/av1enc/src/loopfilter.rs new file mode 100644 index 00000000..6e74e411 --- /dev/null +++ b/crates/av1enc/src/loopfilter.rs @@ -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; + } + } +} diff --git a/crates/av1enc/src/pred.rs b/crates/av1enc/src/pred.rs index ab7e0d0d..48d48ad8 100644 --- a/crates/av1enc/src/pred.rs +++ b/crates/av1enc/src/pred.rs @@ -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 { + 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() { diff --git a/crates/av1enc/src/writer.rs b/crates/av1enc/src/writer.rs index 729a0887..0f0262a0 100644 --- a/crates/av1enc/src/writer.rs +++ b/crates/av1enc/src/writer.rs @@ -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; 3], left_level: [Vec; 3], left_dc: [Vec; 3], + /// LoopfilterTxSizes per plane (row stride mi_cols + 32). + lf_tx: [Vec; 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) { diff --git a/crates/av1enc/tests/bench.rs b/crates/av1enc/tests/bench.rs new file mode 100644 index 00000000..164c6f66 --- /dev/null +++ b/crates/av1enc/tests/bench.rs @@ -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> { + 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); + } +} diff --git a/crates/av1enc/tests/common/mod.rs b/crates/av1enc/tests/common/mod.rs index 9f191cd0..cf5dba73 100644 --- a/crates/av1enc/tests/common/mod.rs +++ b/crates/av1enc/tests/common/mod.rs @@ -83,6 +83,7 @@ pub struct Run { pub packets: Vec, pub recons: Vec<[Vec; 3]>, pub psnr_y: Vec, + pub lf_levels: Vec, } /// 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); } diff --git a/crates/av1enc/tests/ffmpeg_oracle.rs b/crates/av1enc/tests/ffmpeg_oracle.rs index 231ed7bc..dc13aed6 100644 --- a/crates/av1enc/tests/ffmpeg_oracle.rs +++ b/crates/av1enc/tests/ffmpeg_oracle.rs @@ -45,14 +45,17 @@ fn ivf(w: usize, h: usize, packets: &[Packet]) -> Vec { /// 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 { 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::()); + eprintln!( + "{name}: libdav1d bit-exact over {} frames ({} bytes, loop filter levels {levels:?})", + pics.len(), + packets.iter().map(|p| p.data.len()).sum::() + ); + 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 = (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 = (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]