pub trait ToneMap {
#[must_use]
fn map_rgb(&self, rgb: [f32; 3]) -> [f32; 3];
#[inline]
fn map_row(&self, row: &mut [f32], channels: u8) {
match channels {
3 => map_row_cn::<3, Self>(self, row),
4 => map_row_cn::<4, Self>(self, row),
_ => panic!("channels must be 3 or 4, got {channels}"),
}
}
#[inline]
fn map_into(&self, src: &[f32], dst: &mut [f32], channels: u8) {
match channels {
3 => map_into_cn::<3, Self>(self, src, dst),
4 => map_into_cn::<4, Self>(self, src, dst),
_ => panic!("channels must be 3 or 4, got {channels}"),
}
}
#[inline]
fn map_strip_simd(&self, strip: &mut [[f32; 3]]) {
for px in strip.iter_mut() {
*px = self.map_rgb(*px);
}
}
}
#[inline]
pub(crate) fn map_row_cn<const CN: usize, T: ToneMap + ?Sized>(tm: &T, row: &mut [f32]) {
debug_assert!(CN == 3 || CN == 4);
for chunk in row.chunks_exact_mut(CN) {
let mapped = tm.map_rgb([chunk[0], chunk[1], chunk[2]]);
chunk[0] = mapped[0];
chunk[1] = mapped[1];
chunk[2] = mapped[2];
}
}
#[inline]
pub(crate) fn map_into_cn<const CN: usize, T: ToneMap + ?Sized>(
tm: &T,
src: &[f32],
dst: &mut [f32],
) {
debug_assert!(CN == 3 || CN == 4);
debug_assert_eq!(src.len(), dst.len());
for (s, d) in src.chunks_exact(CN).zip(dst.chunks_exact_mut(CN)) {
let mapped = tm.map_rgb([s[0], s[1], s[2]]);
d[0] = mapped[0];
d[1] = mapped[1];
d[2] = mapped[2];
if CN == 4 {
d[3] = s[3];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AgxLook, LUMA_BT709, ToneMapCurve};
fn synth_row_rgb(pixels: usize) -> alloc::vec::Vec<f32> {
let mut row = alloc::vec::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) -> alloc::vec::Vec<f32> {
let mut row = alloc::vec::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
}
fn sample_curves() -> [ToneMapCurve; 7] {
[
ToneMapCurve::Reinhard,
ToneMapCurve::ExtendedReinhard {
l_max: 4.0,
luma: LUMA_BT709,
},
ToneMapCurve::ReinhardJodie { luma: LUMA_BT709 },
ToneMapCurve::Narkowicz,
ToneMapCurve::HableFilmic,
ToneMapCurve::AcesAp1,
ToneMapCurve::Agx(AgxLook::Default),
]
}
#[test]
fn map_row_rgb_matches_manual_loop() {
for curve in sample_curves() {
let src = synth_row_rgb(17); let mut via_row = src.clone();
curve.map_row(&mut via_row, 3);
let mut via_manual = src.clone();
for chunk in via_manual.chunks_exact_mut(3) {
let out = curve.map_rgb([chunk[0], chunk[1], chunk[2]]);
chunk[0] = out[0];
chunk[1] = out[1];
chunk[2] = out[2];
}
for (i, (a, b)) in via_row.iter().zip(via_manual.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-6,
"curve {curve:?} diverged RGB at [{i}]: row={a}, manual={b}"
);
}
}
}
#[test]
fn map_row_rgba_matches_manual_loop_and_keeps_alpha() {
for curve in sample_curves() {
let src = synth_row_rgba(17);
let mut via_row = src.clone();
curve.map_row(&mut via_row, 4);
let mut via_manual = src.clone();
for chunk in via_manual.chunks_exact_mut(4) {
let out = curve.map_rgb([chunk[0], chunk[1], chunk[2]]);
chunk[0] = out[0];
chunk[1] = out[1];
chunk[2] = out[2];
}
for (i, (a, b)) in via_row.iter().zip(via_manual.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-6,
"curve {curve:?} diverged RGBA at [{i}]: row={a}, manual={b}"
);
}
for (i, pixel) in via_row.chunks_exact(4).enumerate() {
let expected_alpha = 0.25 + (i as f32 / 17.0) * 0.5;
assert!(
(pixel[3] - expected_alpha).abs() < 1e-6,
"curve {curve:?} pixel {i} alpha drift: {}",
pixel[3]
);
}
}
}
#[test]
fn map_into_rgb_matches_copy_then_map_row() {
for curve in sample_curves() {
let src = synth_row_rgb(17);
let mut via_copy = src.clone();
curve.map_row(&mut via_copy, 3);
let mut via_into = alloc::vec![0.0_f32; src.len()];
curve.map_into(&src, &mut via_into, 3);
for (i, (a, b)) in via_copy.iter().zip(via_into.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-5,
"curve {curve:?} map_into != map_row at [{i}]: {a} vs {b}"
);
}
}
}
#[test]
fn map_into_rgba_writes_src_alpha_to_dst() {
let curve = ToneMapCurve::Reinhard;
let src = synth_row_rgba(4);
let mut dst = alloc::vec![0.99_f32; src.len()];
curve.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,
"pixel {i}: dst alpha {} != src alpha {}",
d[3],
s[3]
);
}
}
#[test]
fn map_row_cn_direct_matches_trait_dispatch() {
let curve = ToneMapCurve::AcesAp1;
let src = synth_row_rgb(32);
let mut via_trait = src.clone();
curve.map_row(&mut via_trait, 3);
let mut via_direct = src.clone();
map_row_cn::<3, _>(&curve, &mut via_direct);
assert_eq!(via_trait, via_direct);
}
#[test]
fn dyn_tone_map_matches_concrete() {
let curve = ToneMapCurve::HableFilmic;
let src = synth_row_rgb(8);
let mut via_concrete = src.clone();
curve.map_row(&mut via_concrete, 3);
let obj: &dyn ToneMap = &curve;
let mut via_dyn = src.clone();
obj.map_row(&mut via_dyn, 3);
assert_eq!(via_concrete, via_dyn);
}
#[test]
#[should_panic(expected = "channels must be 3 or 4")]
fn map_row_panics_on_bad_channels() {
let mut row = [0.1_f32; 12];
ToneMapCurve::Reinhard.map_row(&mut row, 2);
}
#[test]
#[should_panic(expected = "channels must be 3 or 4")]
fn map_into_panics_on_bad_channels() {
let src = [0.1_f32; 12];
let mut dst = [0.0_f32; 12];
ToneMapCurve::Reinhard.map_into(&src, &mut dst, 5);
}
}