use super::motion_estimation::MotionVector;
use super::reference_buffer::ReconFrame;
const LUMA_FILTER: [i32; 6] = [1, -5, 20, 20, -5, 1];
#[inline]
fn filter6(s: [i32; 6]) -> i32 {
s[0] * LUMA_FILTER[0]
+ s[1] * LUMA_FILTER[1]
+ s[2] * LUMA_FILTER[2]
+ s[3] * LUMA_FILTER[3]
+ s[4] * LUMA_FILTER[4]
+ s[5] * LUMA_FILTER[5]
}
#[inline]
fn clip1y(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
#[inline]
fn ref_luma_sample(reference: &ReconFrame, x: i32, y: i32) -> i32 {
let xc = x.clamp(0, reference.width as i32 - 1) as u32;
let yc = y.clamp(0, reference.height as i32 - 1) as u32;
reference.y_at(xc, yc) as i32
}
pub fn apply_luma_mv_block(
reference: &ReconFrame,
block_x: u32,
block_y: u32,
block_w: u32,
block_h: u32,
mv: MotionVector,
out: &mut [u8],
out_stride: usize,
) {
if super::simd::apply_luma_mv_block_dispatch(
&reference.y, reference.width, reference.height,
block_x, block_y, block_w, block_h,
mv.mv_x, mv.mv_y,
out, out_stride,
) {
return;
}
apply_luma_mv_block_scalar(reference, block_x, block_y, block_w, block_h, mv, out, out_stride);
}
#[inline]
fn apply_luma_mv_block_scalar(
reference: &ReconFrame,
block_x: u32,
block_y: u32,
block_w: u32,
block_h: u32,
mv: MotionVector,
out: &mut [u8],
out_stride: usize,
) {
for yl in 0..block_h {
for xl in 0..block_w {
let x_int = block_x as i32 + (mv.mv_x as i32 >> 2) + xl as i32;
let y_int = block_y as i32 + (mv.mv_y as i32 >> 2) + yl as i32;
let x_frac = (mv.mv_x & 3) as u8;
let y_frac = (mv.mv_y & 3) as u8;
out[(yl as usize) * out_stride + xl as usize] =
sample_luma_frac(reference, x_int, y_int, x_frac, y_frac);
}
}
}
pub fn apply_luma_mv_16x16(
reference: &ReconFrame,
block_x: u32,
block_y: u32,
mv: MotionVector,
) -> [[u8; 16]; 16] {
let mut out = [[0u8; 16]; 16];
apply_luma_mv_block(
reference,
block_x,
block_y,
16,
16,
mv,
out.as_flattened_mut(),
16,
);
out
}
fn sample_luma_frac(
reference: &ReconFrame,
x_int: i32,
y_int: i32,
x_frac: u8,
y_frac: u8,
) -> u8 {
if x_frac == 0 && y_frac == 0 {
return ref_luma_sample(reference, x_int, y_int) as u8;
}
let ref_g = |dx: i32, dy: i32| ref_luma_sample(reference, x_int + dx, y_int + dy);
let b1 = |dy: i32| -> i16 {
filter6([
ref_g(-2, dy),
ref_g(-1, dy),
ref_g(0, dy),
ref_g(1, dy),
ref_g(2, dy),
ref_g(3, dy),
]) as i16
};
let h1 = |dx: i32| -> i16 {
filter6([
ref_g(dx, -2),
ref_g(dx, -1),
ref_g(dx, 0),
ref_g(dx, 1),
ref_g(dx, 2),
ref_g(dx, 3),
]) as i16
};
let b = |dy: i32| clip1y((b1(dy) as i32 + 16) >> 5) as i32;
let h = |dx: i32| clip1y((h1(dx) as i32 + 16) >> 5) as i32;
let j = || {
let j1 = filter6([
b1(-2) as i32,
b1(-1) as i32,
b1(0) as i32,
b1(1) as i32,
b1(2) as i32,
b1(3) as i32,
]);
clip1y((j1 + 512) >> 10) as i32
};
let g_sample = ref_g(0, 0);
let h_int = ref_g(1, 0); let m_int = ref_g(0, 1);
let val = match (x_frac, y_frac) {
(0, 0) => g_sample, (1, 0) => (g_sample + b(0) + 1) >> 1, (2, 0) => b(0), (3, 0) => (h_int + b(0) + 1) >> 1, (0, 1) => (g_sample + h(0) + 1) >> 1, (1, 1) => (b(0) + h(0) + 1) >> 1, (2, 1) => (b(0) + j() + 1) >> 1, (3, 1) => (b(0) + h(1) + 1) >> 1, (0, 2) => h(0), (1, 2) => (h(0) + j() + 1) >> 1, (2, 2) => j(), (3, 2) => (j() + h(1) + 1) >> 1, (0, 3) => (m_int + h(0) + 1) >> 1, (1, 3) => (h(0) + b(1) + 1) >> 1, (2, 3) => (j() + b(1) + 1) >> 1, (3, 3) => (h(1) + b(1) + 1) >> 1, _ => unreachable!("x_frac/y_frac in [0, 3]"),
};
val.clamp(0, 255) as u8
}
#[inline]
fn ref_chroma_sample(
reference: &ReconFrame,
component: u8,
x: i32,
y: i32,
) -> i32 {
let c_width = reference.width / 2;
let c_height = reference.height / 2;
let xc = x.clamp(0, c_width as i32 - 1) as u32;
let yc = y.clamp(0, c_height as i32 - 1) as u32;
reference.chroma_at(component, xc, yc) as i32
}
pub fn apply_chroma_mv_block(
reference: &ReconFrame,
component: u8,
block_x: u32,
block_y: u32,
block_w: u32,
block_h: u32,
mv: MotionVector,
out: &mut [u8],
out_stride: usize,
) {
for yc in 0..block_h {
for xc in 0..block_w {
let x_int = block_x as i32 + (mv.mv_x as i32 >> 3) + xc as i32;
let y_int = block_y as i32 + (mv.mv_y as i32 >> 3) + yc as i32;
let x_frac = (mv.mv_x & 7) as i32;
let y_frac = (mv.mv_y & 7) as i32;
let a = ref_chroma_sample(reference, component, x_int, y_int);
let b = ref_chroma_sample(reference, component, x_int + 1, y_int);
let c = ref_chroma_sample(reference, component, x_int, y_int + 1);
let d = ref_chroma_sample(reference, component, x_int + 1, y_int + 1);
let v = ((8 - x_frac) * (8 - y_frac) * a
+ x_frac * (8 - y_frac) * b
+ (8 - x_frac) * y_frac * c
+ x_frac * y_frac * d
+ 32)
>> 6;
out[(yc as usize) * out_stride + xc as usize] = v.clamp(0, 255) as u8;
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn apply_luma_mv_block_bipred(
ref_l0: &ReconFrame,
mv_l0: MotionVector,
ref_l1: &ReconFrame,
mv_l1: MotionVector,
block_x: u32,
block_y: u32,
block_w: u32,
block_h: u32,
out: &mut [u8],
out_stride: usize,
) {
debug_assert!(block_w <= 16 && block_h <= 16, "bipred block max is 16x16 (got {block_w}x{block_h})");
debug_assert!(out.len() >= ((block_h - 1) as usize) * out_stride + block_w as usize);
let mut buf_l0 = [0u8; 16 * 16];
let mut buf_l1 = [0u8; 16 * 16];
let scratch_stride = 16usize;
apply_luma_mv_block(
ref_l0, block_x, block_y, block_w, block_h, mv_l0,
&mut buf_l0, scratch_stride,
);
apply_luma_mv_block(
ref_l1, block_x, block_y, block_w, block_h, mv_l1,
&mut buf_l1, scratch_stride,
);
for yl in 0..block_h as usize {
let l0_row_off = yl * scratch_stride;
let l1_row_off = yl * scratch_stride;
let out_row_off = yl * out_stride;
for xl in 0..block_w as usize {
let l0 = buf_l0[l0_row_off + xl] as u16;
let l1 = buf_l1[l1_row_off + xl] as u16;
out[out_row_off + xl] = ((l0 + l1 + 1) >> 1) as u8;
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn apply_chroma_mv_block_bipred(
ref_l0: &ReconFrame,
mv_l0: MotionVector,
ref_l1: &ReconFrame,
mv_l1: MotionVector,
component: u8,
block_x: u32,
block_y: u32,
block_w: u32,
block_h: u32,
out: &mut [u8],
out_stride: usize,
) {
debug_assert!(block_w <= 8 && block_h <= 8, "chroma bipred block max is 8x8 (got {block_w}x{block_h})");
debug_assert!(out.len() >= ((block_h - 1) as usize) * out_stride + block_w as usize);
let mut buf_l0 = [0u8; 8 * 8];
let mut buf_l1 = [0u8; 8 * 8];
let scratch_stride = 8usize;
apply_chroma_mv_block(
ref_l0, component, block_x, block_y, block_w, block_h, mv_l0,
&mut buf_l0, scratch_stride,
);
apply_chroma_mv_block(
ref_l1, component, block_x, block_y, block_w, block_h, mv_l1,
&mut buf_l1, scratch_stride,
);
for yc in 0..block_h as usize {
let row_off_in = yc * scratch_stride;
let row_off_out = yc * out_stride;
for xc in 0..block_w as usize {
let a = buf_l0[row_off_in + xc] as u16;
let b = buf_l1[row_off_in + xc] as u16;
out[row_off_out + xc] = ((a + b + 1) >> 1) as u8;
}
}
}
pub fn apply_chroma_mv_8x8(
reference: &ReconFrame,
component: u8,
block_x: u32,
block_y: u32,
mv: MotionVector,
) -> [[u8; 8]; 8] {
let mut out = [[0u8; 8]; 8];
apply_chroma_mv_block(
reference,
component,
block_x,
block_y,
8,
8,
mv,
out.as_flattened_mut(),
8,
);
out
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::reconstruction::ReconBuffer;
fn make_reference(width: u32, height: u32, fill_y: impl Fn(u32, u32) -> u8) -> ReconFrame {
let mut rb = ReconBuffer::new(width, height).unwrap();
for y in 0..height {
for x in 0..width {
rb.y[(y * width + x) as usize] = fill_y(x, y);
}
}
for v in rb.cb.iter_mut() { *v = 128; }
for v in rb.cr.iter_mut() { *v = 128; }
ReconFrame::snapshot(&rb)
}
#[test]
fn integer_mv_zero_returns_ref_block_unchanged() {
let reference = make_reference(64, 48, |x, y| ((x * 3 + y * 5) & 0xFF) as u8);
let mv = MotionVector { mv_x: 0, mv_y: 0 };
let block = apply_luma_mv_16x16(&reference, 16, 16, mv);
for dy in 0..16 {
for dx in 0..16 {
let expected = ((16 + dx) * 3 + (16 + dy) * 5) & 0xFF;
assert_eq!(
block[dy as usize][dx as usize],
expected as u8,
"pixel ({dx}, {dy}) mismatch"
);
}
}
}
#[test]
fn integer_mv_nonzero_shifts_block() {
let reference = make_reference(64, 48, |x, _y| x as u8);
let mv = MotionVector { mv_x: 4, mv_y: 0 }; let block = apply_luma_mv_16x16(&reference, 16, 16, mv);
for dy in 0..16 {
for dx in 0..16 {
assert_eq!(
block[dy][dx] as u32,
(16 + dx as u32 + 1),
"pixel ({dx}, {dy})"
);
}
}
}
#[test]
fn flat_reference_qpel_returns_flat() {
let reference = make_reference(64, 48, |_, _| 100);
for (mvx, mvy) in [(0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (2, 2), (3, 3)] {
let mv = MotionVector { mv_x: mvx, mv_y: mvy };
let block = apply_luma_mv_16x16(&reference, 16, 16, mv);
for row in &block {
for &v in row {
assert_eq!(v, 100, "flat expected for mv=({mvx},{mvy}), got {v}");
}
}
}
}
#[test]
fn horizontal_halfpel_applies_filter() {
let reference = make_reference(
64, 48,
|x, y| if x == 20 && y == 16 { 255 } else { 128 },
);
let mv = MotionVector { mv_x: 2, mv_y: 0 }; let block = apply_luma_mv_16x16(&reference, 16, 16, mv);
assert_eq!(block[0][3], 207);
}
#[test]
fn edge_clipping_at_top_left() {
let reference = make_reference(64, 48, |_, _| 100);
let mv = MotionVector { mv_x: -80, mv_y: -80 }; let block = apply_luma_mv_16x16(&reference, 16, 16, mv);
for row in &block {
for &v in row {
assert_eq!(v, 100);
}
}
}
#[test]
fn chroma_integer_mv_returns_ref_unchanged() {
let _reference = make_reference(64, 48, |_, _| 128);
let mut rb = ReconBuffer::new(64, 48).unwrap();
for y in 0..24 {
for x in 0..32 {
rb.cb[(y * 32 + x) as usize] = ((x * 2) & 0xFF) as u8;
}
}
let reference2 = ReconFrame::snapshot(&rb);
let mv = MotionVector { mv_x: 0, mv_y: 0 };
let block = apply_chroma_mv_8x8(&reference2, 0, 8, 8, mv);
for dy in 0..8 {
for dx in 0..8 {
assert_eq!(
block[dy][dx] as u32,
(8 + dx as u32) * 2 & 0xFF,
"Cb pixel ({dx}, {dy})"
);
}
}
}
#[test]
fn chroma_bilinear_produces_blended() {
let mut rb = ReconBuffer::new(64, 48).unwrap();
for v in rb.y.iter_mut() { *v = 128; }
for v in rb.cb.iter_mut() { *v = 128; }
rb.cb[(4 * 32 + 4) as usize] = 240;
for v in rb.cr.iter_mut() { *v = 128; }
let reference = ReconFrame::snapshot(&rb);
let mv = MotionVector { mv_x: 4, mv_y: 4 };
let block = apply_chroma_mv_8x8(&reference, 0, 0, 0, mv);
assert_eq!(block[4][4], 156);
}
#[test]
fn bipred_luma_two_zero_mvs_averages_per_sample() {
let ref_l0 = make_reference(48, 32, |_, _| 100);
let ref_l1 = make_reference(48, 32, |_, _| 200);
let mv0 = MotionVector { mv_x: 0, mv_y: 0 };
let mut out = [0u8; 16 * 16];
apply_luma_mv_block_bipred(
&ref_l0, mv0, &ref_l1, mv0, 16, 8, 16, 16, &mut out, 16,
);
for &v in &out {
assert_eq!(v, 150, "expected (100+200+1)>>1 = 150");
}
}
#[test]
fn bipred_luma_rounds_to_nearest_with_tie_to_higher() {
let ref_l0 = make_reference(32, 32, |_, _| 100);
let ref_l1 = make_reference(32, 32, |_, _| 101);
let mv0 = MotionVector { mv_x: 0, mv_y: 0 };
let mut out = [0u8; 16 * 16];
apply_luma_mv_block_bipred(
&ref_l0, mv0, &ref_l1, mv0, 8, 8, 16, 16, &mut out, 16,
);
for &v in &out {
assert_eq!(v, 101, "(100+101+1)>>1 must round up to 101");
}
}
#[test]
fn bipred_luma_partition_smaller_than_16x16() {
let ref_l0 = make_reference(48, 32, |_, _| 50);
let ref_l1 = make_reference(48, 32, |_, _| 150);
let mv0 = MotionVector { mv_x: 0, mv_y: 0 };
let mut out = [0u8; 16 * 16];
apply_luma_mv_block_bipred(
&ref_l0, mv0, &ref_l1, mv0, 0, 0, 8, 8, &mut out, 16,
);
for yl in 0..8 {
for xl in 0..8 {
assert_eq!(out[yl * 16 + xl], 100, "({xl},{yl})");
}
}
for yl in 0..16 {
for xl in 0..16 {
if xl >= 8 || yl >= 8 {
assert_eq!(out[yl * 16 + xl], 0, "out-of-rect ({xl},{yl})");
}
}
}
}
#[test]
fn bipred_chroma_averages_components() {
let mut ref_l0 = make_reference(48, 32, |_, _| 0);
for v in ref_l0.cb.iter_mut() { *v = 60; }
let mut ref_l1 = make_reference(48, 32, |_, _| 0);
for v in ref_l1.cb.iter_mut() { *v = 120; }
let mv0 = MotionVector { mv_x: 0, mv_y: 0 };
let mut out = [0u8; 8 * 8];
apply_chroma_mv_block_bipred(
&ref_l0, mv0, &ref_l1, mv0,
0, 4, 4, 8, 8, &mut out, 8,
);
for &v in &out {
assert_eq!(v, 90);
}
}
#[test]
fn bipred_luma_pred_l0_eq_pred_l1_unchanged() {
let reference = make_reference(48, 32, |x, y| ((x * 3 + y * 5) & 0xFF) as u8);
let mv0 = MotionVector { mv_x: 0, mv_y: 0 };
let mut out = [0u8; 16 * 16];
apply_luma_mv_block_bipred(
&reference, mv0, &reference, mv0, 16, 8, 16, 16, &mut out, 16,
);
for yl in 0..16 {
for xl in 0..16 {
let expected = (((16 + xl) * 3 + (8 + yl) * 5) & 0xFF) as u8;
assert_eq!(out[yl * 16 + xl], expected, "pixel ({xl},{yl})");
}
}
}
}