use crate::film_grain_tables::GAUSSIAN_SEQUENCE;
use crate::loop_filter::PlaneBuffer;
use crate::uncompressed_header_tail::FilmGrainParams;
pub const LUMA_GRAIN_W: usize = 82;
pub const LUMA_GRAIN_H: usize = 73;
pub const MC_IDENTITY: u8 = 0;
#[derive(Debug, Clone, Copy)]
pub struct RandomRegister {
pub state: u16,
}
impl RandomRegister {
#[must_use]
pub const fn new(seed: u16) -> Self {
Self { state: seed }
}
#[must_use]
pub fn next(&mut self, bits: u32) -> u32 {
let r = u32::from(self.state);
let bit = (r ^ (r >> 1) ^ (r >> 3) ^ (r >> 12)) & 1;
let r = (r >> 1) | (bit << 15);
self.state = r as u16;
(r >> (16 - bits)) & ((1u32 << bits) - 1)
}
}
#[inline]
fn round2(x: i32, n: u32) -> i32 {
if n == 0 {
x
} else {
(x + (1 << (n - 1))) >> n
}
}
#[inline]
fn clip3(a: i32, b: i32, x: i32) -> i32 {
x.clamp(a, b)
}
#[inline]
fn clip1(bit_depth: u8, x: i32) -> i32 {
clip3(0, (1 << bit_depth) - 1, x)
}
#[derive(Debug, Clone)]
pub struct GrainArrays {
pub luma: Vec<i32>,
pub cb: Vec<i32>,
pub cr: Vec<i32>,
pub chroma_w: usize,
pub chroma_h: usize,
}
impl GrainArrays {
#[inline]
#[must_use]
pub fn luma(&self, y: usize, x: usize) -> i32 {
self.luma[y * LUMA_GRAIN_W + x]
}
#[inline]
#[must_use]
pub fn cb(&self, y: usize, x: usize) -> i32 {
self.cb[y * self.chroma_w + x]
}
#[inline]
#[must_use]
pub fn cr(&self, y: usize, x: usize) -> i32 {
self.cr[y * self.chroma_w + x]
}
}
#[must_use]
pub fn generate_grain(fg: &FilmGrainParams, bit_depth: u8, sub_x: u8, sub_y: u8) -> GrainArrays {
let grain_center = 128i32 << (bit_depth - 8);
let grain_min = -grain_center;
let grain_max = (256i32 << (bit_depth - 8)) - 1 - grain_center;
let init_shift = 12u32.wrapping_sub(u32::from(bit_depth)) + u32::from(fg.grain_scale_shift);
let mut luma = vec![0i32; LUMA_GRAIN_H * LUMA_GRAIN_W];
{
let mut rr = RandomRegister::new(fg.grain_seed);
for y in 0..LUMA_GRAIN_H {
for x in 0..LUMA_GRAIN_W {
let g = if fg.num_y_points > 0 {
let idx = rr.next(11) as usize;
i32::from(GAUSSIAN_SEQUENCE[idx])
} else {
0
};
luma[y * LUMA_GRAIN_W + x] = round2(g, init_shift);
}
}
}
if fg.num_y_points > 0 {
let ar_shift = u32::from(fg.ar_coeff_shift);
let lag = i32::from(fg.ar_coeff_lag);
for y in 3..LUMA_GRAIN_H as i32 {
for x in 3..(LUMA_GRAIN_W as i32 - 3) {
let mut s: i32 = 0;
let mut pos: usize = 0;
'outer: for d_row in -lag..=0 {
for d_col in -lag..=lag {
if d_row == 0 && d_col == 0 {
break 'outer;
}
let c = i32::from(fg.ar_coeffs_y_plus_128[pos]) - 128;
let yi = (y + d_row) as usize;
let xi = (x + d_col) as usize;
s += luma[yi * LUMA_GRAIN_W + xi] * c;
pos += 1;
}
}
let yi = y as usize;
let xi = x as usize;
let v = luma[yi * LUMA_GRAIN_W + xi] + round2(s, ar_shift);
luma[yi * LUMA_GRAIN_W + xi] = clip3(grain_min, grain_max, v);
}
}
}
let chroma_w = if sub_x != 0 { 44 } else { 82 };
let chroma_h = if sub_y != 0 { 38 } else { 73 };
let mut cb = vec![0i32; chroma_h * chroma_w];
{
let mut rr = RandomRegister::new(fg.grain_seed ^ 0xb524);
for y in 0..chroma_h {
for x in 0..chroma_w {
let g = if fg.num_cb_points > 0 || fg.chroma_scaling_from_luma {
let idx = rr.next(11) as usize;
i32::from(GAUSSIAN_SEQUENCE[idx])
} else {
0
};
cb[y * chroma_w + x] = round2(g, init_shift);
}
}
}
let mut cr = vec![0i32; chroma_h * chroma_w];
{
let mut rr = RandomRegister::new(fg.grain_seed ^ 0x49d8);
for y in 0..chroma_h {
for x in 0..chroma_w {
let g = if fg.num_cr_points > 0 || fg.chroma_scaling_from_luma {
let idx = rr.next(11) as usize;
i32::from(GAUSSIAN_SEQUENCE[idx])
} else {
0
};
cr[y * chroma_w + x] = round2(g, init_shift);
}
}
}
if fg.chroma_scaling_from_luma || fg.num_cb_points > 0 || fg.num_cr_points > 0 {
let ar_shift = u32::from(fg.ar_coeff_shift);
let lag = i32::from(fg.ar_coeff_lag);
for y in 3..chroma_h as i32 {
for x in 3..(chroma_w as i32 - 3) {
let mut s0: i32 = 0;
let mut s1: i32 = 0;
let mut pos: usize = 0;
'outer: for d_row in -lag..=0 {
for d_col in -lag..=lag {
let c0 = i32::from(fg.ar_coeffs_cb_plus_128[pos]) - 128;
let c1 = i32::from(fg.ar_coeffs_cr_plus_128[pos]) - 128;
if d_row == 0 && d_col == 0 {
if fg.num_y_points > 0 {
let luma_x = ((x - 3) << sub_x) + 3;
let luma_y = ((y - 3) << sub_y) + 3;
let mut l: i32 = 0;
for i in 0..=i32::from(sub_y) {
for j in 0..=i32::from(sub_x) {
let yi = (luma_y + i) as usize;
let xi = (luma_x + j) as usize;
l += luma[yi * LUMA_GRAIN_W + xi];
}
}
let n = u32::from(sub_x) + u32::from(sub_y);
let l = round2(l, n);
s0 += l * c0;
s1 += l * c1;
}
break 'outer;
}
let yi = (y + d_row) as usize;
let xi = (x + d_col) as usize;
s0 += cb[yi * chroma_w + xi] * c0;
s1 += cr[yi * chroma_w + xi] * c1;
pos += 1;
}
}
let yi = y as usize;
let xi = x as usize;
let v0 = cb[yi * chroma_w + xi] + round2(s0, ar_shift);
let v1 = cr[yi * chroma_w + xi] + round2(s1, ar_shift);
cb[yi * chroma_w + xi] = clip3(grain_min, grain_max, v0);
cr[yi * chroma_w + xi] = clip3(grain_min, grain_max, v1);
}
}
}
GrainArrays {
luma,
cb,
cr,
chroma_w,
chroma_h,
}
}
#[derive(Debug, Clone)]
pub struct ScalingLut {
pub tables: [[u8; 256]; 3],
}
impl Default for ScalingLut {
fn default() -> Self {
Self {
tables: [[0u8; 256]; 3],
}
}
}
#[must_use]
pub fn scaling_lookup_init(fg: &FilmGrainParams, num_planes: u8) -> ScalingLut {
let mut lut = ScalingLut::default();
for plane in 0..num_planes.min(3) {
let (num_points, get_x, get_y): (
u8,
&[u8; crate::uncompressed_header_tail::MAX_NUM_Y_POINTS],
&[u8; crate::uncompressed_header_tail::MAX_NUM_Y_POINTS],
) = if plane == 0 || fg.chroma_scaling_from_luma {
(fg.num_y_points, &fg.point_y_value, &fg.point_y_scaling)
} else if plane == 1 {
let np = fg.num_cb_points;
scaling_lookup_fill_chroma(
&mut lut.tables[plane as usize],
np,
&fg.point_cb_value,
&fg.point_cb_scaling,
);
continue;
} else {
let np = fg.num_cr_points;
scaling_lookup_fill_chroma(
&mut lut.tables[plane as usize],
np,
&fg.point_cr_value,
&fg.point_cr_scaling,
);
continue;
};
scaling_lookup_fill_luma(&mut lut.tables[plane as usize], num_points, get_x, get_y);
}
lut
}
fn scaling_lookup_fill_luma(
table: &mut [u8; 256],
num_points: u8,
point_value: &[u8; crate::uncompressed_header_tail::MAX_NUM_Y_POINTS],
point_scaling: &[u8; crate::uncompressed_header_tail::MAX_NUM_Y_POINTS],
) {
let np = num_points as usize;
scaling_lookup_fill(table, np, |i| point_value[i], |i| point_scaling[i]);
}
fn scaling_lookup_fill_chroma(
table: &mut [u8; 256],
num_points: u8,
point_value: &[u8; crate::uncompressed_header_tail::MAX_NUM_CHROMA_POINTS],
point_scaling: &[u8; crate::uncompressed_header_tail::MAX_NUM_CHROMA_POINTS],
) {
let np = num_points as usize;
scaling_lookup_fill(table, np, |i| point_value[i], |i| point_scaling[i]);
}
fn scaling_lookup_fill<F, G>(table: &mut [u8; 256], num_points: usize, get_x: F, get_y: G)
where
F: Fn(usize) -> u8,
G: Fn(usize) -> u8,
{
if num_points == 0 {
*table = [0u8; 256];
return;
}
let x0 = get_x(0) as usize;
let y0 = get_y(0);
for v in &mut table[..x0.min(256)] {
*v = y0;
}
for i in 0..num_points.saturating_sub(1) {
let xa = get_x(i) as i32;
let xb = get_x(i + 1) as i32;
let ya = get_y(i) as i32;
let yb = get_y(i + 1) as i32;
let delta_x = xb - xa;
if delta_x <= 0 {
continue;
}
let delta_y = yb - ya;
let delta = delta_y * ((65536 + (delta_x >> 1)) / delta_x);
for x in 0..delta_x {
let v = ya + ((x * delta + 32768) >> 16);
let idx = (xa + x) as usize;
if idx < 256 {
table[idx] = v.clamp(0, 255) as u8;
}
}
}
let xn = get_x(num_points - 1) as usize;
let yn = get_y(num_points - 1);
for v in &mut table[xn.min(256)..] {
*v = yn;
}
}
#[inline]
#[must_use]
pub fn scale_lut(lut: &ScalingLut, plane: u8, index: i32, bit_depth: u8) -> i32 {
let shift = u32::from(bit_depth) - 8;
let x = (index >> shift) as usize;
let table = &lut.tables[plane as usize];
if bit_depth == 8 || x >= 255 {
i32::from(table[x.min(255)])
} else {
let rem = index - ((x as i32) << shift);
let start = i32::from(table[x]);
let end = i32::from(table[x + 1]);
start + round2((end - start) * rem, shift)
}
}
#[allow(clippy::too_many_arguments)]
pub fn add_noise_synthesis(
fg: &FilmGrainParams,
grain: &GrainArrays,
lut: &ScalingLut,
bit_depth: u8,
num_planes: u8,
sub_x: u8,
sub_y: u8,
matrix_coefficients: u8,
out_planes: &mut [PlaneBuffer<'_>],
) {
if out_planes.is_empty() {
return;
}
let w = out_planes[0].cols;
let h = out_planes[0].rows;
if w == 0 || h == 0 {
return;
}
let grain_center = 128i32 << (bit_depth - 8);
let grain_min = -grain_center;
let grain_max = (256i32 << (bit_depth - 8)) - 1 - grain_center;
let np = num_planes.min(3) as usize;
let num_luma_stripes = h.div_ceil(2).div_ceil(16);
let mut stripe_h = [0usize; 3];
let mut stripe_w = [0usize; 3];
for plane in 0..np {
let psx = if plane > 0 { sub_x } else { 0 };
let psy = if plane > 0 { sub_y } else { 0 };
stripe_h[plane] = 34usize >> psy;
let plane_w = ((w + u32::from(psx)) >> psx) as usize;
stripe_w[plane] = plane_w + 34;
}
let mut noise_stripe: Vec<Vec<Vec<i32>>> = (0..num_luma_stripes)
.map(|_| {
(0..np)
.map(|plane| vec![0i32; stripe_h[plane] * stripe_w[plane]])
.collect()
})
.collect();
let mut luma_num = 0usize;
let mut y_stripe = 0u32;
while y_stripe < h.div_ceil(2) {
let mut rr = RandomRegister::new(fg.grain_seed);
let xor_hi = ((luma_num as u32).wrapping_mul(37).wrapping_add(178) & 255) << 8;
let xor_lo = (luma_num as u32).wrapping_mul(173).wrapping_add(105) & 255;
rr.state ^= xor_hi as u16;
rr.state ^= xor_lo as u16;
let mut x_block = 0u32;
while x_block < w.div_ceil(2) {
let rand = rr.next(8);
let offset_x = (rand >> 4) as i32;
let offset_y = (rand & 15) as i32;
for plane in 0..np {
let psx = if plane > 0 { sub_x } else { 0 };
let psy = if plane > 0 { sub_y } else { 0 };
let plane_off_x = if psx != 0 {
6 + offset_x
} else {
9 + offset_x * 2
};
let plane_off_y = if psy != 0 {
6 + offset_y
} else {
9 + offset_y * 2
};
let h_steps = 34i32 >> psy;
let w_steps = 34i32 >> psx;
for i in 0..h_steps {
for j in 0..w_steps {
let yi = (plane_off_y + i) as usize;
let xi = (plane_off_x + j) as usize;
let g0 = match plane {
0 => grain.luma[yi * LUMA_GRAIN_W + xi],
1 => grain.cb[yi * grain.chroma_w + xi],
_ => grain.cr[yi * grain.chroma_w + xi],
};
let mut g = g0;
if psx == 0 {
let col = (x_block as i32) * 2 + j;
if j < 2 && fg.overlap_flag && x_block > 0 {
let old = noise_stripe[luma_num][plane]
[(i as usize) * stripe_w[plane] + col as usize];
g = if j == 0 {
old * 27 + g * 17
} else {
old * 17 + g * 27
};
g = clip3(grain_min, grain_max, round2(g, 5));
}
if (col as usize) < stripe_w[plane] {
noise_stripe[luma_num][plane]
[(i as usize) * stripe_w[plane] + col as usize] = g;
}
} else {
let col = (x_block as i32) + j;
if j == 0 && fg.overlap_flag && x_block > 0 {
let old = noise_stripe[luma_num][plane]
[(i as usize) * stripe_w[plane] + col as usize];
g = old * 23 + g * 22;
g = clip3(grain_min, grain_max, round2(g, 5));
}
if (col as usize) < stripe_w[plane] {
noise_stripe[luma_num][plane]
[(i as usize) * stripe_w[plane] + col as usize] = g;
}
}
}
}
}
x_block += 16;
}
luma_num += 1;
y_stripe += 16;
}
let mut noise_image: Vec<Vec<i32>> = Vec::with_capacity(np);
for plane in 0..np {
let psx = if plane > 0 { sub_x } else { 0 };
let psy = if plane > 0 { sub_y } else { 0 };
let plane_h = ((h + u32::from(psy)) >> psy) as usize;
let plane_w = ((w + u32::from(psx)) >> psx) as usize;
let mut img = vec![0i32; plane_h * plane_w];
for yy in 0..plane_h {
let stripe_idx = yy >> (5 - psy);
let i = yy - (stripe_idx << (5 - psy));
if stripe_idx >= noise_stripe.len() {
continue;
}
for xx in 0..plane_w {
let mut g = noise_stripe[stripe_idx][plane][i * stripe_w[plane] + xx];
if psy == 0 {
if i < 2 && stripe_idx > 0 && fg.overlap_flag {
let old =
noise_stripe[stripe_idx - 1][plane][(i + 32) * stripe_w[plane] + xx];
g = if i == 0 {
old * 27 + g * 17
} else {
old * 17 + g * 27
};
g = clip3(grain_min, grain_max, round2(g, 5));
}
} else if i < 1 && stripe_idx > 0 && fg.overlap_flag {
let old = noise_stripe[stripe_idx - 1][plane][(i + 16) * stripe_w[plane] + xx];
g = old * 23 + g * 22;
g = clip3(grain_min, grain_max, round2(g, 5));
}
img[yy * plane_w + xx] = g;
}
}
noise_image.push(img);
}
let (min_value, max_luma, max_chroma) = if fg.clip_to_restricted_range {
let mv = 16i32 << (bit_depth - 8);
let ml = 235i32 << (bit_depth - 8);
let mc = if matrix_coefficients == MC_IDENTITY {
ml
} else {
240i32 << (bit_depth - 8)
};
(mv, ml, mc)
} else {
let mx = (256i32 << (bit_depth - 8)) - 1;
(0, mx, mx)
};
let scaling_shift = u32::from(fg.grain_scaling);
if np >= 3 && (fg.num_cb_points > 0 || fg.num_cr_points > 0 || fg.chroma_scaling_from_luma) {
let plane_w_c = ((w + u32::from(sub_x)) >> sub_x) as usize;
let plane_h_c = ((h + u32::from(sub_y)) >> sub_y) as usize;
for yy in 0..plane_h_c {
for xx in 0..plane_w_c {
let luma_x = xx << sub_x;
let luma_y = yy << sub_y;
let luma_next_x = (luma_x + 1).min((w as usize) - 1);
let out_y = &out_planes[0];
let y_at =
|x: usize, y: usize| -> i32 { out_y.samples[y * (out_y.cols as usize) + x] };
let avg_luma = if sub_x != 0 {
round2(y_at(luma_x, luma_y) + y_at(luma_next_x, luma_y), 1)
} else {
y_at(luma_x, luma_y)
};
if fg.num_cb_points > 0 || fg.chroma_scaling_from_luma {
let orig = out_planes[1].samples[yy * (out_planes[1].cols as usize) + xx];
let merged = if fg.chroma_scaling_from_luma {
avg_luma
} else {
let combined = avg_luma * (i32::from(fg.cb_luma_mult) - 128)
+ orig * (i32::from(fg.cb_mult) - 128);
clip1(
bit_depth,
(combined >> 6) + ((i32::from(fg.cb_offset) - 256) << (bit_depth - 8)),
)
};
let noise = noise_image[1][yy * plane_w_c + xx];
let noise = round2(scale_lut(lut, 1, merged, bit_depth) * noise, scaling_shift);
out_planes[1].samples[yy * (out_planes[1].cols as usize) + xx] =
clip3(min_value, max_chroma, orig + noise);
}
if fg.num_cr_points > 0 || fg.chroma_scaling_from_luma {
let orig = out_planes[2].samples[yy * (out_planes[2].cols as usize) + xx];
let merged = if fg.chroma_scaling_from_luma {
avg_luma
} else {
let combined = avg_luma * (i32::from(fg.cr_luma_mult) - 128)
+ orig * (i32::from(fg.cr_mult) - 128);
clip1(
bit_depth,
(combined >> 6) + ((i32::from(fg.cr_offset) - 256) << (bit_depth - 8)),
)
};
let noise = noise_image[2][yy * plane_w_c + xx];
let noise = round2(scale_lut(lut, 2, merged, bit_depth) * noise, scaling_shift);
out_planes[2].samples[yy * (out_planes[2].cols as usize) + xx] =
clip3(min_value, max_chroma, orig + noise);
}
}
}
}
if fg.num_y_points > 0 {
for yy in 0..(h as usize) {
for xx in 0..(w as usize) {
let orig = out_planes[0].samples[yy * (out_planes[0].cols as usize) + xx];
let noise = noise_image[0][yy * (w as usize) + xx];
let noise = round2(scale_lut(lut, 0, orig, bit_depth) * noise, scaling_shift);
out_planes[0].samples[yy * (out_planes[0].cols as usize) + xx] =
clip3(min_value, max_luma, orig + noise);
}
}
}
}
pub fn film_grain_synthesis(
fg: &FilmGrainParams,
bit_depth: u8,
num_planes: u8,
sub_x: u8,
sub_y: u8,
matrix_coefficients: u8,
out_planes: &mut [PlaneBuffer<'_>],
) {
if !fg.apply_grain {
return;
}
let grain = generate_grain(fg, bit_depth, sub_x, sub_y);
let lut = scaling_lookup_init(fg, num_planes);
add_noise_synthesis(
fg,
&grain,
&lut,
bit_depth,
num_planes,
sub_x,
sub_y,
matrix_coefficients,
out_planes,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uncompressed_header_tail::FilmGrainParams;
#[test]
fn lfsr_first_three_draws_for_seed_one() {
let mut rr = RandomRegister::new(1);
let v0 = rr.next(11);
assert_eq!(v0, 0x400);
let v1 = rr.next(11);
assert_eq!(v1, 0x200);
}
#[test]
fn lfsr_eight_bit_draw_matches_top_byte() {
let mut rr = RandomRegister::new(0xff00);
assert_eq!(rr.next(8), 0xff);
assert_eq!(rr.state, 0xff80);
}
#[test]
fn scaling_lookup_zero_num_points_is_zero_table() {
let fg = FilmGrainParams::reset();
let lut = scaling_lookup_init(&fg, 3);
for plane in 0..3 {
for x in 0..256 {
assert_eq!(lut.tables[plane][x], 0);
}
}
}
#[test]
fn scaling_lookup_two_point_ramp_interpolates() {
let mut fg = FilmGrainParams::reset();
fg.apply_grain = true;
fg.num_y_points = 2;
fg.point_y_value[0] = 0;
fg.point_y_scaling[0] = 0;
fg.point_y_value[1] = 255;
fg.point_y_scaling[1] = 255;
let lut = scaling_lookup_init(&fg, 1);
assert_eq!(lut.tables[0][0], 0);
assert!((lut.tables[0][128] as i32 - 128).abs() <= 1);
assert_eq!(lut.tables[0][255], 255);
}
#[test]
fn scaling_lookup_left_plateau_holds() {
let mut fg = FilmGrainParams::reset();
fg.apply_grain = true;
fg.num_y_points = 2;
fg.point_y_value[0] = 32;
fg.point_y_scaling[0] = 64;
fg.point_y_value[1] = 255;
fg.point_y_scaling[1] = 200;
let lut = scaling_lookup_init(&fg, 1);
for x in 0..32 {
assert_eq!(lut.tables[0][x], 64, "left plateau at {}", x);
}
assert_eq!(lut.tables[0][255], 200);
}
#[test]
fn generate_grain_zero_when_num_y_points_zero() {
let mut fg = FilmGrainParams::reset();
fg.apply_grain = true;
fg.grain_seed = 12345;
fg.grain_scale_shift = 0;
let g = generate_grain(&fg, 8, 1, 1);
assert!(g.luma.iter().all(|&v| v == 0));
assert!(g.cb.iter().all(|&v| v == 0));
assert!(g.cr.iter().all(|&v| v == 0));
assert_eq!(g.chroma_w, 44);
assert_eq!(g.chroma_h, 38);
}
#[test]
fn generate_grain_luma_first_sample_matches_round2_of_first_gauss_draw() {
let mut fg = FilmGrainParams::reset();
fg.apply_grain = true;
fg.grain_seed = 1;
fg.num_y_points = 2;
fg.point_y_value[0] = 0;
fg.point_y_scaling[0] = 0;
fg.point_y_value[1] = 255;
fg.point_y_scaling[1] = 255;
fg.grain_scale_shift = 0;
fg.ar_coeff_lag = 0;
fg.ar_coeff_shift = 6;
let g = generate_grain(&fg, 8, 1, 1);
let mut rr = RandomRegister::new(1);
let draw0 = rr.next(11) as usize;
let gauss = i32::from(GAUSSIAN_SEQUENCE[draw0]);
let expected = round2(gauss, 4);
assert_eq!(g.luma(0, 0), expected);
}
#[test]
fn film_grain_synthesis_no_apply_is_noop() {
let fg = FilmGrainParams::reset();
let cols = 64u32;
let rows = 32u32;
let n = (cols * rows) as usize;
let mut y = vec![100i32; n];
let mut u = vec![100i32; n];
let mut v = vec![100i32; n];
let mut planes = [
PlaneBuffer {
rows,
cols,
samples: &mut y,
},
PlaneBuffer {
rows,
cols,
samples: &mut u,
},
PlaneBuffer {
rows,
cols,
samples: &mut v,
},
];
film_grain_synthesis(&fg, 8, 3, 0, 0, MC_IDENTITY, &mut planes);
for &s in planes[0].samples.iter() {
assert_eq!(s, 100);
}
}
#[test]
fn film_grain_synthesis_zero_y_points_leaves_luma_unchanged() {
let mut fg = FilmGrainParams::reset();
fg.apply_grain = true;
fg.grain_seed = 9999;
fg.grain_scaling = 8;
fg.ar_coeff_shift = 6;
let cols = 32u32;
let rows = 32u32;
let n = (cols * rows) as usize;
let mut y = vec![128i32; n];
let mut u = vec![64i32; n];
let mut v = vec![64i32; n];
let mut planes = [
PlaneBuffer {
rows,
cols,
samples: &mut y,
},
PlaneBuffer {
rows,
cols,
samples: &mut u,
},
PlaneBuffer {
rows,
cols,
samples: &mut v,
},
];
film_grain_synthesis(&fg, 8, 3, 0, 0, MC_IDENTITY, &mut planes);
for &s in planes[0].samples.iter() {
assert_eq!(s, 128);
}
for &s in planes[1].samples.iter() {
assert_eq!(s, 64);
}
for &s in planes[2].samples.iter() {
assert_eq!(s, 64);
}
}
#[test]
fn scale_lut_8bit_is_direct_indexing() {
let mut lut = ScalingLut::default();
lut.tables[0][7] = 42;
lut.tables[0][255] = 99;
assert_eq!(scale_lut(&lut, 0, 7, 8), 42);
assert_eq!(scale_lut(&lut, 0, 255, 8), 99);
}
#[test]
fn scale_lut_10bit_interpolates() {
let mut lut = ScalingLut::default();
lut.tables[0][2] = 100;
lut.tables[0][3] = 200;
assert_eq!(scale_lut(&lut, 0, 9, 10), 125);
}
#[test]
fn film_grain_synthesis_luma_ramp_mutates_samples() {
let mut fg = FilmGrainParams::reset();
fg.apply_grain = true;
fg.grain_seed = 7;
fg.num_y_points = 2;
fg.point_y_value[0] = 0;
fg.point_y_scaling[0] = 64;
fg.point_y_value[1] = 255;
fg.point_y_scaling[1] = 64;
fg.grain_scaling = 8;
fg.ar_coeff_lag = 0;
fg.ar_coeff_shift = 6;
let cols = 64u32;
let rows = 64u32;
let n = (cols * rows) as usize;
let mut y = vec![128i32; n];
let mut planes = [PlaneBuffer {
rows,
cols,
samples: &mut y,
}];
film_grain_synthesis(&fg, 8, 1, 0, 0, MC_IDENTITY, &mut planes);
let mutated = planes[0].samples.iter().any(|&s| s != 128);
assert!(mutated, "film grain blend must touch at least some samples");
for &s in planes[0].samples.iter() {
assert!((0..256).contains(&s));
}
}
}