static T4: [[i32; 4]; 4] = [
[64, 64, 64, 64],
[83, 36, -36, -83],
[64, -64, -64, 64],
[36, -83, 83, -36],
];
static T8: [[i32; 8]; 8] = [
[64, 64, 64, 64, 64, 64, 64, 64],
[89, 75, 50, 18, -18, -50, -75, -89],
[83, 36, -36, -83, -83, -36, 36, 83],
[75, -18, -89, -50, 50, 89, 18, -75],
[64, -64, -64, 64, 64, -64, -64, 64],
[50, -89, 18, 75, -75, -18, 89, -50],
[36, -83, 83, -36, -36, 83, -83, 36],
[18, -50, 75, -89, 89, -75, 50, -18],
];
#[rustfmt::skip]
static T16: [[i32; 16]; 16] = [
[64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64],
[90, 87, 80, 70, 57, 43, 25, 9, -9, -25, -43, -57, -70, -80, -87, -90],
[89, 75, 50, 18, -18, -50, -75, -89, -89, -75, -50, -18, 18, 50, 75, 89],
[87, 57, 9, -43, -80, -90, -70, -25, 25, 70, 90, 80, 43, -9, -57, -87],
[83, 36, -36, -83, -83, -36, 36, 83, 83, 36, -36, -83, -83, -36, 36, 83],
[80, 9, -70, -87, -25, 57, 90, 43, -43, -90, -57, 25, 87, 70, -9, -80],
[75, -18, -89, -50, 50, 89, 18, -75, -75, 18, 89, 50, -50, -89, -18, 75],
[70, -43, -87, 9, 90, 25, -80, -57, 57, 80, -25, -90, -9, 87, 43, -70],
[64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64, 64, -64, -64, 64],
[57, -80, -25, 90, -9, -87, 43, 70, -70, -43, 87, 9, -90, 25, 80, -57],
[50, -89, 18, 75, -75, -18, 89, -50, -50, 89, -18, -75, 75, 18, -89, 50],
[43, -90, 57, 25, -87, 70, 9, -80, 80, -9, -70, 87, -25, -57, 90, -43],
[36, -83, 83, -36, -36, 83, -83, 36, 36, -83, 83, -36, -36, 83, -83, 36],
[25, -70, 90, -80, 43, 9, -57, 87, -87, 57, -9, -43, 80, -90, 70, -25],
[18, -50, 75, -89, 89, -75, 50, -18, -18, 50, -75, 89, -89, 75, -50, 18],
[9, -25, 43, -57, 70, -80, 87, -90, 90, -87, 80, -70, 57, -43, 25, -9],
];
static QUANT_SCALE: [i64; 6] = [26214, 23302, 20560, 18396, 16384, 14564];
const MAX_TB: usize = 256;
static DEQUANT_SCALE: [i64; 6] = [40, 45, 51, 57, 64, 72];
pub(crate) fn fwd_transform(res: &[i32], n: usize, bit_depth: u8) -> [i32; MAX_TB] {
let mut out = [0i32; MAX_TB];
match n {
4 => fwd_transform_n::<4>(res, &T4, bit_depth, &mut out),
8 => fwd_transform_n::<8>(res, &T8, bit_depth, &mut out),
16 => fwd_transform_n::<16>(res, &T16, bit_depth, &mut out),
_ => panic!("unsupported transform size {n}"),
}
out
}
#[inline]
fn fwd_transform_n<const N: usize>(
res: &[i32],
t: &[[i32; N]; N],
bit_depth: u8,
out: &mut [i32; MAX_TB],
) {
let log2n = N.trailing_zeros() as i32;
let bd = bit_depth as i32;
let shift1 = log2n + bd - 9;
let add1 = if shift1 > 0 { 1i32 << (shift1 - 1) } else { 0 };
let mut tmp = [0i32; MAX_TB]; for (j, res_row) in res.as_chunks::<N>().0.iter().enumerate().take(N) {
for (i, trow) in t.iter().enumerate() {
let mut s = 0i32;
for k in 0..N {
s += trow[k] * res_row[k];
}
tmp[j * N + i] = if shift1 > 0 { (s + add1) >> shift1 } else { s };
}
}
let shift2 = log2n + 6;
let add2 = 1i32 << (shift2 - 1);
let mut colv = [0i32; N];
for j in 0..N {
for (k, cv) in colv.iter_mut().enumerate() {
*cv = tmp[k * N + j];
}
for (i, trow) in t.iter().enumerate() {
let mut s = 0i32;
for k in 0..N {
s += trow[k] * colv[k];
}
out[i * N + j] = (s + add2) >> shift2;
}
}
}
pub(crate) fn quantize(coeff: &[i32], n: usize, qp: u8, bit_depth: u8) -> [i16; MAX_TB] {
let log2n = n.trailing_zeros() as i64;
let bd = bit_depth as i64;
let q_bits = 14 + (qp as i64) / 6 + (15 - bd - log2n);
let q_scale = QUANT_SCALE[(qp % 6) as usize];
let offset = 171i64 << (q_bits - 9); let mut out = [0i16; MAX_TB];
for (o, &c) in out[..n * n].iter_mut().zip(coeff) {
let c = c as i64;
let level = (c.abs() * q_scale + offset) >> q_bits;
let level = if c < 0 { -level } else { level };
*o = level.clamp(-32768, 32767) as i16;
}
out
}
pub(crate) fn dequantize(level: &[i16], n: usize, qp: u8, bit_depth: u8) -> [i32; MAX_TB] {
let log2n = n.trailing_zeros() as i64;
let bd = bit_depth as i64;
let bd_shift = bd + log2n - 5;
let add = 1i64 << (bd_shift - 1);
let scale = DEQUANT_SCALE[(qp % 6) as usize];
let per = 1i64 << ((qp as i64) / 6);
let factor = scale * per * 16;
let mut out = [0i32; MAX_TB];
for (o, &l) in out[..n * n].iter_mut().zip(level) {
*o = ((l as i64 * factor + add) >> bd_shift).clamp(-32768, 32767) as i32;
}
out
}
pub(crate) fn inv_transform(coeff: &[i32], n: usize, bit_depth: u8) -> [i32; MAX_TB] {
let mut out = [0i32; MAX_TB];
match n {
4 => inv_transform_n::<4>(coeff, &T4, bit_depth, &mut out),
8 => inv_transform_n::<8>(coeff, &T8, bit_depth, &mut out),
16 => inv_transform_n::<16>(coeff, &T16, bit_depth, &mut out),
_ => panic!("unsupported transform size {n}"),
}
out
}
#[inline]
fn inv_transform_n<const N: usize>(
coeff: &[i32],
t: &[[i32; N]; N],
bit_depth: u8,
out: &mut [i32; MAX_TB],
) {
let bd = bit_depth as i32;
let shift1 = 7i32;
let add1 = 1i32 << (shift1 - 1);
let shift2 = 20 - bd;
let add2 = 1i32 << (shift2 - 1);
let mut tmp = [0i32; MAX_TB];
let mut acc = [0i32; N];
for c in 0..N {
acc[..N].fill(0);
for k in 0..N {
let ck = coeff[k * N + c];
if ck == 0 {
continue;
}
let trow = &t[k];
for m in 0..N {
acc[m] += trow[m] * ck;
}
}
for m in 0..N {
tmp[m * N + c] = ((acc[m] + add1) >> shift1).clamp(-32768, 32767);
}
}
for r in 0..N {
acc[..N].fill(0);
let rowv = &tmp[r * N..r * N + N];
for k in 0..N {
let rk = rowv[k];
if rk == 0 {
continue;
}
let trow = &t[k];
for m in 0..N {
acc[m] += trow[m] * rk;
}
}
for m in 0..N {
out[r * N + m] = (acc[m] + add2) >> shift2;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn check_orthogonal<const N: usize>(t: &[[i32; N]; N]) {
let ideal = (N as i64) * 64 * 64;
for (i, row) in t.iter().enumerate() {
let ni: i64 = row.iter().map(|&v| (v as i64) * (v as i64)).sum();
assert!(
(ni - ideal).abs() <= ideal / 500,
"row {i} norm {ni} too far from ideal {ideal}"
);
for j in (i + 1)..N {
let dot: i64 = (0..N).map(|k| (t[i][k] as i64) * (t[j][k] as i64)).sum();
assert!(
dot.abs() < ideal / 50,
"rows {i},{j} insufficiently orthogonal: dot={dot} (limit {})",
ideal / 50
);
}
}
}
#[test]
fn t16_is_orthogonal() {
check_orthogonal(&T16);
check_orthogonal(&T8);
check_orthogonal(&T4);
}
fn flat_is_dc_only(n: usize) {
let c = 100i32;
let res = vec![c; n * n];
let coeff = fwd_transform(&res, n, 8);
assert_ne!(coeff[0], 0, "N={n}: DC should be non-zero");
for (i, &v) in coeff[..n * n].iter().enumerate().skip(1) {
assert_eq!(v, 0, "N={n}: AC coeff {i} should be zero, got {v}");
}
}
#[test]
fn flat_residual_dc_only() {
flat_is_dc_only(4);
flat_is_dc_only(8);
flat_is_dc_only(16);
}
fn roundtrip_bounded(n: usize, qp: u8, max_mean_abs: f64) {
let mut res = vec![0i32; n * n];
for r in 0..n {
for col in 0..n {
let v = 8 * (r as i32 + col as i32) + 20 * (((r + col) % 4) as i32) - 60;
res[r * n + col] = v;
}
}
let coeff = fwd_transform(&res, n, 8);
let level = quantize(&coeff[..n * n], n, qp, 8);
let dq = dequantize(&level[..n * n], n, qp, 8);
let rec = inv_transform(&dq[..n * n], n, 8);
let mean_abs: f64 = (0..n * n)
.map(|i| (res[i] - rec[i]).unsigned_abs() as f64)
.sum::<f64>()
/ (n * n) as f64;
assert!(
mean_abs <= max_mean_abs,
"N={n} qp={qp}: mean|err|={mean_abs:.3} exceeds {max_mean_abs}"
);
}
#[test]
fn pipeline_roundtrip_low_qp() {
for &n in &[4usize, 8, 16] {
roundtrip_bounded(n, 4, 3.0);
}
}
#[test]
fn pipeline_roundtrip_scales_with_qp() {
for &n in &[4usize, 8, 16] {
roundtrip_bounded(n, 22, 12.0);
}
}
#[test]
fn zigzag16_is_permutation() {
let mut seen = [false; 256];
for &(r, c) in crate::dct::ZIGZAG_16X16.iter() {
assert!(r < 16 && c < 16);
let idx = r * 16 + c;
assert!(!seen[idx], "duplicate scan position ({r},{c})");
seen[idx] = true;
}
assert!(seen.iter().all(|&b| b), "scan does not cover all positions");
}
}