M4.10: VP9 vectorised loop filter, column-parallel transforms, 16-bit 8-bit MC

The loop filter's lane masks are 0 / -1 integers with bitwise selects and min / max clipping
(bool masks and clamp kept it scalar); the 2D inverse transform's column pass runs the generated
butterflies on 4 / 8 columns at once; 8-bit sub-pixel filters accumulate in 16-bit lanes (every
sum of the VP9 filters fits modulo 2^16). Branch-free bool decoder update. Bit-exact; tested
against the per-column transform on random blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 07:12:57 -04:00
parent 842801de41
commit 1827b71891
6 changed files with 278 additions and 62 deletions

View File

@ -63,14 +63,12 @@ impl<'a> BoolDecoder<'a> {
}
let split = 1 + (((self.range - 1) * p as u32) >> 8);
let bigsplit = (split as u64) << 56;
let bit = if self.value >= bigsplit {
self.range -= split;
self.value -= bigsplit;
true
} else {
self.range = split;
false
};
// The outcome is close to random: select instead of branching on it.
let bit = self.value >= bigsplit;
let m = (bit as u64).wrapping_neg();
self.value -= bigsplit & m;
let m32 = m as u32;
self.range = ((self.range - split) & m32) | (split & !m32);
let shift = self.range.leading_zeros() - 24;
self.range <<= shift;
self.value <<= shift;

View File

@ -142,6 +142,18 @@ fn filter_h(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
#[inline(always)]
fn filter_h_n<const W: usize>(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
let o: &mut [u16; W] = (&mut o[..W]).try_into().expect("row");
if max == 255 {
let mut acc = [64u16; W];
for t in 0..8 {
let c = f[t] as u16;
let p: &[u16; W] = s[t..t + W].try_into().expect("row");
for j in 0..W {
acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j]));
}
}
narrow8(&acc, o);
return;
}
let mut acc = [64i32; W];
for t in 0..8 {
let c = f[t] as i32;
@ -155,6 +167,18 @@ fn filter_h_n<const W: usize>(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
}
}
/// Round2(sum, 7) clipped to 8 bits, from 8-bit filter sums kept modulo 2^16 (8 lanes per
/// vector instead of 4). With 8-bit samples every sum of the VP9 filters (whose positive taps
/// add up to at most 182 and negative taps to at most -54) plus the rounding term 64 lies in
/// [-13706, 46474]: wrapped, the non-negative ones stay below 0xC000 and the negative ones
/// (which clip to 0) land above it.
#[inline(always)]
fn narrow8<const W: usize>(acc: &[u16; W], o: &mut [u16; W]) {
for j in 0..W {
o[j] = if acc[j] < 0xC000 { (acc[j] >> 7).min(255) } else { 0 };
}
}
/// Vertical 8-tap filter of one row: `o[j]` from `s[j + t * ss]`, t = 0..8.
#[inline(always)]
fn filter_v(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
@ -176,6 +200,18 @@ fn filter_v(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
#[inline(always)]
fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
let o: &mut [u16; W] = (&mut o[..W]).try_into().expect("row");
if max == 255 {
let mut acc = [64u16; W];
for t in 0..8 {
let c = f[t] as u16;
let p: &[u16; W] = s[t * ss..t * ss + W].try_into().expect("row");
for j in 0..W {
acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j]));
}
}
narrow8(&acc, o);
return;
}
let mut acc = [64i32; W];
for t in 0..8 {
let c = f[t] as i32;

View File

@ -218,72 +218,77 @@ macro_rules! group_filter {
let thresh_bd = thresh << shift;
let one = 1 << shift;
let (p3, p2, p1, p0, q0, q1, q2, q3) = (&v[4], &v[5], &v[6], &v[7], &v[8], &v[9], &v[10], &v[11]);
// Filter mask process (8.8.5.1), with non-short-circuit `&` so that it vectorizes.
let mut mask = [false; LANES];
let mut hev = [false; LANES];
let mut flat = [false; LANES];
let mut flat2 = [false; LANES];
let can_flat = filter_size >= TX_8X8;
// Filter mask process (8.8.5.1). Masks are 0 / -1 per lane, selects are bitwise
// and clipping uses min / max (`clamp` asserts its bounds), so every lane loop
// below compiles to vector instructions.
let le = |a: T, b: T| -((a <= b) as T);
let sel = |m: T, a: T, b: T| (a & m) | (b & !m);
let mut mask = [0 as T; LANES];
let mut hev = [0 as T; LANES];
let mut flat = [0 as T; LANES];
let mut flat2 = [0 as T; LANES];
let can_flat = -((filter_size >= TX_8X8) as T);
let can_flat2 = R == 8 && filter_size >= TX_16X16;
let apply: [T; LANES] = apply.map(|a| -(a as T));
for l in 0..LANES {
let d_p1p0 = (p1[l] - p0[l]).abs();
let d_q1q0 = (q1[l] - q0[l]).abs();
mask[l] = apply[l]
& ((p3[l] - p2[l]).abs() <= limit_bd)
& ((p2[l] - p1[l]).abs() <= limit_bd)
& (d_p1p0 <= limit_bd)
& (d_q1q0 <= limit_bd)
& ((q2[l] - q1[l]).abs() <= limit_bd)
& ((q3[l] - q2[l]).abs() <= limit_bd)
& ((p0[l] - q0[l]).abs() * 2 + (p1[l] - q1[l]).abs() / 2 <= blimit_bd);
hev[l] = (d_p1p0 > thresh_bd) | (d_q1q0 > thresh_bd);
& le((p3[l] - p2[l]).abs(), limit_bd)
& le((p2[l] - p1[l]).abs(), limit_bd)
& le(d_p1p0, limit_bd)
& le(d_q1q0, limit_bd)
& le((q2[l] - q1[l]).abs(), limit_bd)
& le((q3[l] - q2[l]).abs(), limit_bd)
& le((p0[l] - q0[l]).abs() * 2 + ((p1[l] - q1[l]).abs() >> 1), blimit_bd);
hev[l] = !(le(d_p1p0, thresh_bd) & le(d_q1q0, thresh_bd));
flat[l] = can_flat
& mask[l]
& (d_p1p0 <= one)
& (d_q1q0 <= one)
& ((p2[l] - p0[l]).abs() <= one)
& ((q2[l] - q0[l]).abs() <= one)
& ((p3[l] - p0[l]).abs() <= one)
& ((q3[l] - q0[l]).abs() <= one);
& le(d_p1p0, one)
& le(d_q1q0, one)
& le((p2[l] - p0[l]).abs(), one)
& le((q2[l] - q0[l]).abs(), one)
& le((p3[l] - p0[l]).abs(), one)
& le((q3[l] - q0[l]).abs(), one);
}
if !mask.iter().fold(false, |a, &m| a | m) {
if mask.iter().fold(0, |a, &m| a | m) == 0 {
return;
}
if can_flat2 {
for l in 0..LANES {
flat2[l] = flat[l]
& ((v[0][l] - p0[l]).abs() <= one)
& ((v[1][l] - p0[l]).abs() <= one)
& ((v[2][l] - p0[l]).abs() <= one)
& ((v[3][l] - p0[l]).abs() <= one)
& ((v[12][l] - q0[l]).abs() <= one)
& ((v[13][l] - q0[l]).abs() <= one)
& ((v[14][l] - q0[l]).abs() <= one)
& ((v[15][l] - q0[l]).abs() <= one);
& le((v[0][l] - p0[l]).abs(), one)
& le((v[1][l] - p0[l]).abs(), one)
& le((v[2][l] - p0[l]).abs(), one)
& le((v[3][l] - p0[l]).abs(), one)
& le((v[12][l] - q0[l]).abs(), one)
& le((v[13][l] - q0[l]).abs(), one)
& le((v[14][l] - q0[l]).abs(), one)
& le((v[15][l] - q0[l]).abs(), one);
}
}
let any_flat = flat.iter().fold(false, |a, &f| a | f);
let any_flat2 = flat2.iter().fold(false, |a, &f| a | f);
let any_flat = flat.iter().fold(0, |a, &f| a | f) != 0;
let any_flat2 = flat2.iter().fold(0, |a, &f| a | f) != 0;
// Results per position across the edge; start from the unfiltered samples.
let mut out = v;
// Narrow filter (8.8.5.2) for lanes that are masked but not flat.
{
let lo = -(1 << (bit_depth - 1)) as T;
let hi = ((1 << (bit_depth - 1)) - 1) as T;
let c = |x: T| x.clamp(lo, hi);
let c = |x: T| x.max(lo).min(hi);
let off: T = 0x80 << shift;
for l in 0..LANES {
let (ps1, ps0, qs0, qs1) = (p1[l] - off, p0[l] - off, q0[l] - off, q1[l] - off);
let filter = c(if hev[l] { c(ps1 - qs1) } else { 0 } + 3 * (qs0 - ps0));
let filter = c((c(ps1 - qs1) & hev[l]) + 3 * (qs0 - ps0));
let filter1 = c(filter + 4) >> 3;
let filter2 = c(filter + 3) >> 3;
let f = (filter1 + 1) >> 1;
let narrow = mask[l] && !flat[l];
let outer = narrow && !hev[l];
out[8][l] = if narrow { c(qs0 - filter1) + off } else { out[8][l] };
out[7][l] = if narrow { c(ps0 + filter2) + off } else { out[7][l] };
out[9][l] = if outer { c(qs1 - f) + off } else { out[9][l] };
out[6][l] = if outer { c(ps1 + f) + off } else { out[6][l] };
let narrow = mask[l] & !flat[l];
let outer = narrow & !hev[l];
out[8][l] = sel(narrow, c(qs0 - filter1) + off, out[8][l]);
out[7][l] = sel(narrow, c(ps0 + filter2) + off, out[7][l]);
out[9][l] = sel(outer, c(qs1 - f) + off, out[9][l]);
out[6][l] = sel(outer, c(ps1 + f) + off, out[6][l]);
}
}
// Wide filter (8.8.5.3) with 8 taps for flat lanes (p2..q2) ...
@ -291,9 +296,7 @@ macro_rules! group_filter {
let w8 = wide_filter::<3>(&v);
for k in 5..11 {
for l in 0..LANES {
if flat[l] && !flat2[l] {
out[k][l] = w8[k][l];
}
out[k][l] = sel(flat[l] & !flat2[l], w8[k][l], out[k][l]);
}
}
}
@ -302,9 +305,7 @@ macro_rules! group_filter {
let w16 = wide_filter::<4>(&v);
for k in 1..15 {
for l in 0..LANES {
if flat2[l] {
out[k][l] = w16[k][l];
}
out[k][l] = sel(flat2[l], w16[k][l], out[k][l]);
}
}
}

View File

@ -202,19 +202,123 @@ fn itx_narrow<const N: usize>(coefs: &[i32], p: &TxParams, dst: &mut [u16], stri
*v = sat(*v, lim);
}
}
let mut col = [0i32; N];
for j in 0..N {
for i in 0..N {
col[i] = t[i][j];
}
tx1d_narrow(&mut col, N, p.col_adst);
for i in 0..N {
let d = &mut dst[i * stride + j];
*d = (*d as i32 + (col[i].wrapping_add(p.round) >> p.shift).clamp(-(1 << 20), 1 << 20)).clamp(0, p.max) as u16;
// Column pass across 4 / 8 columns at once (the same butterflies on lane vectors), then
// the rounding, residual add and clipping per row of lanes.
if N == 4 {
let x: [Cols<4>; N] = std::array::from_fn(|i| Cols(t[i][..4].try_into().expect("4 columns")));
let y = col_tx4(x, p.col_adst);
add_cols(&y, 0, p, dst, stride);
} else {
for c0 in (0..N).step_by(8) {
let x: [Cols<8>; N] = std::array::from_fn(|i| Cols(t[i][c0..c0 + 8].try_into().expect("8 columns")));
let y = col_tx8(x, p.col_adst);
add_cols(&y, c0, p, dst, stride);
}
}
}
/// Round the column pass output `y` (columns c0..c0 + W of every row) and add it to `dst`.
#[inline(always)]
fn add_cols<const N: usize, const W: usize>(y: &[Cols<W>; N], c0: usize, p: &TxParams, dst: &mut [u16], stride: usize) {
for (i, v) in y.iter().enumerate() {
let d: &mut [u16; W] = (&mut dst[i * stride + c0..i * stride + c0 + W]).try_into().expect("row");
for j in 0..W {
let r = (v.0[j].wrapping_add(p.round) >> p.shift).clamp(-(1 << 20), 1 << 20);
d[j] = (d[j] as i32 + r).max(0).min(p.max) as u16;
}
}
}
/// `W` columns of 32-bit values processed together: the generated butterflies
/// (`transform_gen::cols4` / `cols8`) run on these lane vectors with the scalar arithmetic of
/// `narrow` applied per lane, so the column pass vectorises.
#[derive(Clone, Copy)]
pub struct Cols<const W: usize>(pub [i32; W]);
impl<const W: usize> From<i32> for Cols<W> {
#[inline(always)]
fn from(v: i32) -> Self {
Cols([v; W])
}
}
impl<const W: usize> Cols<W> {
#[inline(always)]
pub fn wrapping_add<O: Into<Self>>(self, o: O) -> Self {
let o = o.into();
Cols(std::array::from_fn(|i| self.0[i].wrapping_add(o.0[i])))
}
#[inline(always)]
pub fn wrapping_sub(self, o: Self) -> Self {
Cols(std::array::from_fn(|i| self.0[i].wrapping_sub(o.0[i])))
}
#[inline(always)]
pub fn wrapping_mul(self, c: i32) -> Self {
Cols(self.0.map(|v| v.wrapping_mul(c)))
}
#[inline(always)]
pub fn wrapping_neg(self) -> Self {
Cols(self.0.map(|v| v.wrapping_neg()))
}
}
impl<const W: usize> std::ops::Shr<i32> for Cols<W> {
type Output = Self;
#[inline(always)]
fn shr(self, s: i32) -> Self {
Cols(self.0.map(|v| v >> s))
}
}
/// `x` (N rows of 4 columns) through the 1D column transform.
#[inline(always)]
fn col_tx4<const N: usize>(x: [Cols<4>; N], adst: bool) -> [Cols<4>; N] {
use crate::transform_gen::cols4;
let r = if adst { iadst4_cols(fixed(&x)) } else { cols4::idct4(fixed(&x)) };
std::array::from_fn(|i| r[i])
}
/// `x` (N rows of 8 columns) through the 1D column transform.
#[inline(always)]
fn col_tx8<const N: usize>(x: [Cols<8>; N], adst: bool) -> [Cols<8>; N] {
use crate::transform_gen::cols8;
let mut out = x;
match (N, adst) {
(8, false) => out.copy_from_slice(&cols8::idct8(fixed(&x))),
(8, true) => out.copy_from_slice(&cols8::iadst8(fixed(&x))),
(16, false) => out.copy_from_slice(&cols8::idct16(fixed(&x))),
(16, true) => out.copy_from_slice(&cols8::iadst16(fixed(&x))),
_ => out.copy_from_slice(&cols8::idct32(fixed(&x))),
}
out
}
/// A slice of known length as an array.
#[inline(always)]
fn fixed<T: Copy, const M: usize>(x: &[T]) -> [T; M] {
x[..M].try_into().expect("transform size")
}
/// ADST4 (8.7.1.6) on 4 columns.
#[inline(always)]
fn iadst4_cols(x: [Cols<4>; 4]) -> [Cols<4>; 4] {
let s0 = x[0].wrapping_mul(SINPI_1_9 as i32);
let s1 = x[0].wrapping_mul(SINPI_2_9 as i32);
let s2 = x[1].wrapping_mul(SINPI_3_9 as i32);
let s3 = x[2].wrapping_mul(SINPI_4_9 as i32);
let s4 = x[2].wrapping_mul(SINPI_1_9 as i32);
let s5 = x[3].wrapping_mul(SINPI_2_9 as i32);
let s6 = x[3].wrapping_mul(SINPI_4_9 as i32);
let v = x[0].wrapping_sub(x[2]).wrapping_add(x[3]);
let s7 = v.wrapping_mul(SINPI_3_9 as i32);
let x0 = s0.wrapping_add(s3).wrapping_add(s5);
let x1 = s1.wrapping_sub(s4).wrapping_sub(s6);
let x2 = s7;
let x3 = s2;
let r = |v: Cols<4>| v.wrapping_add(1 << 13) >> 14;
[r(x0.wrapping_add(x3)), r(x1.wrapping_add(x3)), r(x2), r(x0.wrapping_add(x1).wrapping_sub(x3))]
}
/// 2D inverse transform of size N with 64-bit intermediates (high bit depth).
fn itx_wide<const N: usize>(coefs: &[i32], p: &TxParams, dst: &mut [u16], stride: usize) {
let lim = 1i64 << 40;
@ -327,6 +431,65 @@ mod tests {
}
}
/// The per-column formulation the vectorised column pass replaced.
fn itx_narrow_ref<const N: usize>(coefs: &[i32], p: &TxParams, dst: &mut [u16], stride: usize) {
let lim = 1 << 24;
let mut t = [[0i32; N]; N];
for (i, row) in t.iter_mut().enumerate().take(p.rows.min(N)) {
for (d, s) in row.iter_mut().zip(&coefs[i * N..i * N + N]) {
*d = sat(*s, lim);
}
tx1d_narrow(row, N, p.row_adst);
for v in row.iter_mut() {
*v = sat(*v, lim);
}
}
let mut col = [0i32; N];
for j in 0..N {
for i in 0..N {
col[i] = t[i][j];
}
tx1d_narrow(&mut col, N, p.col_adst);
for i in 0..N {
let d = &mut dst[i * stride + j];
*d = (*d as i32 + (col[i].wrapping_add(p.round) >> p.shift).clamp(-(1 << 20), 1 << 20)).clamp(0, p.max) as u16;
}
}
}
fn check_cols<const N: usize>(seed: &mut u32) {
let mut rnd = || {
*seed ^= *seed << 13;
*seed ^= *seed >> 17;
*seed ^= *seed << 5;
*seed
};
let tx_size = N.trailing_zeros() - 2;
let shift = (tx_size + 4).min(6);
for case in 0..200 {
let (row_adst, col_adst) = if N == 32 { (false, false) } else { (case & 1 == 1, case & 2 == 2) };
let rows = 1 + rnd() as usize % N;
let big = if case % 7 == 0 { 1 << 22 } else { 2000 };
let coefs: Vec<i32> = (0..N * N).map(|i| if i / N < rows && rnd() % 3 == 0 { (rnd() % (2 * big)) as i32 - big as i32 } else { 0 }).collect();
let p = TxParams { row_adst, col_adst, rows, shift, round: 1 << (shift - 1), max: 255 };
let stride = N + 5;
let base: Vec<u16> = (0..N * stride).map(|_| (rnd() % 256) as u16).collect();
let (mut a, mut b) = (base.clone(), base);
itx_narrow::<N>(&coefs, &p, &mut a, stride);
itx_narrow_ref::<N>(&coefs, &p, &mut b, stride);
assert_eq!(a, b, "N {N} case {case}");
}
}
#[test]
fn column_pass_matches_per_column_transform() {
let mut seed = 0x9e37_79b9u32;
check_cols::<4>(&mut seed);
check_cols::<8>(&mut seed);
check_cols::<16>(&mut seed);
check_cols::<32>(&mut seed);
}
#[test]
fn wht_round_trip_identity() {
// A single DC coefficient of 4 * v spreads v / 4 ... check invertibility on a simple case:

View File

@ -478,3 +478,12 @@ pub(crate) mod narrow {
pub(crate) mod wide {
transforms_1d!(i64);
}
/// The same butterflies on 4 / 8 columns at once (column pass of the 2D transforms).
pub(crate) mod cols4 {
transforms_1d!(crate::transform::Cols<4>);
}
pub(crate) mod cols8 {
transforms_1d!(crate::transform::Cols<8>);
}

View File

@ -247,3 +247,12 @@ print()
print("pub(crate) mod wide {")
print(" transforms_1d!(i64);")
print("}")
print()
print("/// The same butterflies on 4 / 8 columns at once (column pass of the 2D transforms).")
print("pub(crate) mod cols4 {")
print(" transforms_1d!(crate::transform::Cols<4>);")
print("}")
print()
print("pub(crate) mod cols8 {")
print(" transforms_1d!(crate::transform::Cols<8>);")
print("}")