use zentone::*;
fn all_configs() -> Vec<(&'static str, Box<dyn ToneMap>)> {
vec![
("Reinhard", Box::new(ToneMapCurve::Reinhard)),
(
"ExtReinhard",
Box::new(ToneMapCurve::ExtendedReinhard {
l_max: 4.0,
luma: LUMA_BT709,
}),
),
(
"ReinhardJodie",
Box::new(ToneMapCurve::ReinhardJodie { luma: LUMA_BT709 }),
),
(
"TunedReinhard",
Box::new(ToneMapCurve::TunedReinhard {
content_max_nits: 4000.0,
display_max_nits: 250.0,
luma: LUMA_BT709,
}),
),
("Narkowicz", Box::new(ToneMapCurve::Narkowicz)),
("HableFilmic", Box::new(ToneMapCurve::HableFilmic)),
("AcesAp1", Box::new(ToneMapCurve::AcesAp1)),
(
"Bt2390",
Box::new(ToneMapCurve::Bt2390 {
source_peak: 4.0,
target_peak: 1.0,
}),
),
("AgxDefault", Box::new(ToneMapCurve::Agx(AgxLook::Default))),
("AgxPunchy", Box::new(ToneMapCurve::Agx(AgxLook::Punchy))),
("AgxGolden", Box::new(ToneMapCurve::Agx(AgxLook::Golden))),
("Clamp", Box::new(ToneMapCurve::Clamp)),
("Bt2408", Box::new(Bt2408Tonemapper::new(4000.0, 1000.0))),
(
"Bt2408_MaxRgb",
Box::new(Bt2408Tonemapper::max_rgb(4000.0, 1000.0)),
),
("Bt2446A", Box::new(Bt2446A::new(4000.0, 100.0))),
("Bt2446B", Box::new(Bt2446B::new(4000.0, 100.0))),
("Bt2446C", Box::new(Bt2446C::new(4000.0, 100.0))),
(
"FilmicSpline",
Box::new(CompiledFilmicSpline::new(&FilmicSplineConfig::default())),
),
]
}
fn synth_row(pixels: usize) -> Vec<f32> {
let mut row = Vec::with_capacity(pixels * 3);
for i in 0..pixels {
let t = i as f32 / pixels as f32;
row.push(t * 4.0);
row.push((1.0 - t) * 3.5);
row.push(t * t * 2.0);
}
row
}
fn synth_row_rgba(pixels: usize) -> Vec<f32> {
let mut row = Vec::with_capacity(pixels * 4);
for i in 0..pixels {
let t = i as f32 / pixels as f32;
row.push(t * 4.0);
row.push((1.0 - t) * 3.5);
row.push(t * t * 2.0);
row.push(0.25 + t * 0.5);
}
row
}
#[test]
fn map_row_rgb_matches_map_rgb_all_configs() {
let src = synth_row(31); for (name, tm) in all_configs() {
let mut via_row = src.clone();
tm.map_row(&mut via_row, 3);
let mut via_manual = src.clone();
for chunk in via_manual.chunks_exact_mut(3) {
let out = tm.map_rgb([chunk[0], chunk[1], chunk[2]]);
chunk[0] = out[0];
chunk[1] = out[1];
chunk[2] = out[2];
}
let tol = if name.starts_with("Agx") && name != "AgxDefault" {
0.1
} else {
1e-5
};
for (i, (a, b)) in via_row.iter().zip(via_manual.iter()).enumerate() {
assert!(
(a - b).abs() < tol,
"{name}: map_row vs map_rgb diverged at [{i}]: row={a}, manual={b}"
);
}
}
}
#[test]
fn map_row_rgba_matches_map_rgb_and_preserves_alpha_all_configs() {
let src = synth_row_rgba(31);
for (name, tm) in all_configs() {
let mut via_row = src.clone();
tm.map_row(&mut via_row, 4);
let mut via_manual = src.clone();
for chunk in via_manual.chunks_exact_mut(4) {
let out = tm.map_rgb([chunk[0], chunk[1], chunk[2]]);
chunk[0] = out[0];
chunk[1] = out[1];
chunk[2] = out[2];
}
let tol = if name.starts_with("Agx") && name != "AgxDefault" {
0.1
} else {
1e-5
};
for (i, (a, b)) in via_row.iter().zip(via_manual.iter()).enumerate() {
assert!(
(a - b).abs() < tol,
"{name}: RGBA map_row vs map_rgb diverged at [{i}]: row={a}, manual={b}"
);
}
for (i, pixel) in via_row.chunks_exact(4).enumerate() {
let expected_alpha = 0.25 + (i as f32 / 31.0) * 0.5;
assert!(
(pixel[3] - expected_alpha).abs() < 1e-6,
"{name}: pixel {i} alpha drift: {} expected {expected_alpha}",
pixel[3]
);
}
}
}
#[test]
fn map_into_matches_map_row_all_configs() {
let src = synth_row(31);
for (name, tm) in all_configs() {
let mut via_row = src.clone();
tm.map_row(&mut via_row, 3);
let mut via_into = vec![0.0_f32; src.len()];
tm.map_into(&src, &mut via_into, 3);
let tol = if name.starts_with("Agx") && name != "AgxDefault" {
0.1
} else {
1e-5
};
for (i, (a, b)) in via_row.iter().zip(via_into.iter()).enumerate() {
assert!(
(a - b).abs() < tol,
"{name}: map_into != map_row at [{i}]: {a} vs {b}"
);
}
}
}
#[test]
fn map_into_rgba_copies_alpha_all_configs() {
let src = synth_row_rgba(8);
for (name, tm) in all_configs() {
let mut dst = vec![0.99_f32; src.len()]; tm.map_into(&src, &mut dst, 4);
for (i, (s, d)) in src.chunks_exact(4).zip(dst.chunks_exact(4)).enumerate() {
assert!(
(d[3] - s[3]).abs() < 1e-6,
"{name}: pixel {i} dst alpha {} != src alpha {}",
d[3],
s[3]
);
}
}
}
#[test]
fn dyn_tonemap_matches_concrete_all_configs() {
let src = synth_row(16);
for (name, tm) in all_configs() {
let mut via_concrete = src.clone();
tm.map_row(&mut via_concrete, 3);
let obj: &dyn ToneMap = tm.as_ref();
let mut via_dyn = src.clone();
obj.map_row(&mut via_dyn, 3);
assert_eq!(via_concrete, via_dyn, "{name}: dyn dispatch != concrete");
}
}
#[test]
fn custom_pipeline_gamut_convert_and_soft_clip() {
use zentone::gamut::*;
let bt2020_pixels: Vec<f32> = (0..16)
.flat_map(|i| {
let t = i as f32 / 15.0;
[t * 2.0, (1.0 - t) * 1.5, t * t]
})
.collect();
let tm = ToneMapCurve::Reinhard;
let mut tonemapped = bt2020_pixels.clone();
tm.map_row(&mut tonemapped, 3);
for chunk in tonemapped.chunks_exact_mut(3) {
let rgb = apply_matrix(&BT2020_TO_BT709, [chunk[0], chunk[1], chunk[2]]);
let clipped = if is_out_of_gamut(rgb) {
soft_clip(rgb)
} else {
rgb
};
chunk[0] = clipped[0];
chunk[1] = clipped[1];
chunk[2] = clipped[2];
}
for (i, &v) in tonemapped.iter().enumerate() {
assert!(
(0.0..=1.0).contains(&v),
"custom pipeline: [{i}] = {v} out of [0,1]"
);
}
}
#[test]
fn pq_pipeline_all_tonemappers_in_range() {
use zentone::TonemapScratch;
use zentone::pipeline::tonemap_pq_row_simd;
let mut pq_row: Vec<[f32; 3]> = Vec::with_capacity(16);
for i in 0..16 {
let nits = 4000.0 * (i as f32 / 15.0);
let pq = linear_srgb::tf::linear_to_pq(nits / 10000.0);
pq_row.push([pq, pq, pq]);
}
let mut scratch = TonemapScratch::new();
for (name, tm) in all_configs() {
let mut out = vec![[0.0_f32; 3]; pq_row.len()];
tonemap_pq_row_simd(&mut scratch, &pq_row, &mut out, tm.as_ref());
for (i, px) in out.iter().enumerate() {
for (ch, &v) in px.iter().enumerate() {
assert!(
v.is_finite() && (-1e-6..=1.0 + 1e-6).contains(&v),
"{name}: PQ pipeline [{i}][{ch}] = {v}"
);
}
}
}
}