use crate::ToneMap;
use crate::TonemapScratch;
use crate::gamut::{BT2020_TO_BT709, apply_matrix_row_simd, soft_clip_row_simd};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HlgOotfMode {
#[default]
Exact,
LibultrahdrCompat,
}
fn chunked_in_out<I, O>(
scratch: &mut TonemapScratch,
input: &[I],
output: &mut [O],
mut process: impl FnMut(&mut TonemapScratch, &[I], &mut [O]),
) {
assert_eq!(
input.len(),
output.len(),
"pipeline: input and output strip must have equal length"
);
if input.is_empty() {
return;
}
let chunk = scratch.chunk_size();
let mut start = 0;
while start < input.len() {
let end = (start + chunk).min(input.len());
process(scratch, &input[start..end], &mut output[start..end]);
start = end;
}
}
#[inline]
pub fn tonemap_pq_row_simd(
scratch: &mut TonemapScratch,
pq_row: &[[f32; 3]],
out: &mut [[f32; 3]],
tm: &dyn ToneMap,
) {
chunked_in_out(scratch, pq_row, out, |_, in_chunk, out_chunk| {
out_chunk.copy_from_slice(in_chunk);
linear_srgb::default::pq_to_linear_slice(out_chunk.as_flattened_mut());
tm.map_strip_simd(out_chunk);
apply_matrix_and_soft_clip(out_chunk);
});
}
#[inline]
pub fn tonemap_pq_rgba_row_simd(
scratch: &mut TonemapScratch,
pq_row: &[[f32; 4]],
out: &mut [[f32; 4]],
tm: &dyn ToneMap,
) {
chunked_in_out(scratch, pq_row, out, |scratch, in_chunk, out_chunk| {
out_chunk.copy_from_slice(in_chunk);
linear_srgb::default::pq_to_linear_rgba_slice(out_chunk.as_flattened_mut());
let rgb = scratch.linear_rgb(in_chunk.len());
for (dst, src) in rgb.iter_mut().zip(out_chunk.iter()) {
*dst = [src[0], src[1], src[2]];
}
tm.map_strip_simd(rgb);
apply_matrix_and_soft_clip(rgb);
for (dst, mapped) in out_chunk.iter_mut().zip(rgb.iter()) {
dst[0] = mapped[0];
dst[1] = mapped[1];
dst[2] = mapped[2];
}
});
}
#[inline]
pub fn tonemap_hlg_row_simd(
scratch: &mut TonemapScratch,
hlg_row: &[[f32; 3]],
out: &mut [[f32; 3]],
tm: &dyn ToneMap,
display_peak_nits: f32,
) {
let gamma = crate::hlg::hlg_system_gamma(display_peak_nits);
chunked_in_out(scratch, hlg_row, out, |_, in_chunk, out_chunk| {
out_chunk.copy_from_slice(in_chunk);
linear_srgb::default::hlg_to_linear_slice(out_chunk.as_flattened_mut());
crate::hlg::hlg_ootf_row_simd(out_chunk, gamma);
tm.map_strip_simd(out_chunk);
apply_matrix_and_soft_clip(out_chunk);
});
}
#[inline]
pub fn tonemap_hlg_rgba_row_simd(
scratch: &mut TonemapScratch,
hlg_row: &[[f32; 4]],
out: &mut [[f32; 4]],
tm: &dyn ToneMap,
display_peak_nits: f32,
) {
let gamma = crate::hlg::hlg_system_gamma(display_peak_nits);
chunked_in_out(scratch, hlg_row, out, |scratch, in_chunk, out_chunk| {
out_chunk.copy_from_slice(in_chunk);
linear_srgb::default::hlg_to_linear_rgba_slice(out_chunk.as_flattened_mut());
let rgb = scratch.linear_rgb(in_chunk.len());
for (dst, src) in rgb.iter_mut().zip(out_chunk.iter()) {
*dst = [src[0], src[1], src[2]];
}
crate::hlg::hlg_ootf_row_simd(rgb, gamma);
tm.map_strip_simd(rgb);
apply_matrix_and_soft_clip(rgb);
for (dst, mapped) in out_chunk.iter_mut().zip(rgb.iter()) {
dst[0] = mapped[0];
dst[1] = mapped[1];
dst[2] = mapped[2];
}
});
}
#[inline]
pub fn tonemap_pq_to_srgb8_row_simd(
scratch: &mut TonemapScratch,
pq_row: &[[f32; 3]],
out: &mut [[u8; 3]],
tm: &dyn ToneMap,
) {
chunked_in_out(scratch, pq_row, out, |scratch, in_chunk, out_chunk| {
let linear = scratch.linear_rgb(in_chunk.len());
linear.copy_from_slice(in_chunk);
linear_srgb::default::pq_to_linear_slice(linear.as_flattened_mut());
tm.map_strip_simd(linear);
apply_matrix_and_soft_clip(linear);
linear_srgb::default::linear_to_srgb_u8_slice(
linear.as_flattened(),
out_chunk.as_flattened_mut(),
);
});
}
#[inline]
pub fn tonemap_pq_to_srgb8_rgba_row_simd(
scratch: &mut TonemapScratch,
pq_row: &[[f32; 4]],
out: &mut [[u8; 4]],
tm: &dyn ToneMap,
) {
chunked_in_out(scratch, pq_row, out, |scratch, in_chunk, out_chunk| {
let (rgb, rgb_u8) = scratch.linear_and_u8(in_chunk.len());
for (dst, src) in rgb.iter_mut().zip(in_chunk.iter()) {
*dst = [src[0], src[1], src[2]];
}
linear_srgb::default::pq_to_linear_slice(rgb.as_flattened_mut());
tm.map_strip_simd(rgb);
apply_matrix_and_soft_clip(rgb);
linear_srgb::default::linear_to_srgb_u8_slice(
rgb.as_flattened(),
rgb_u8.as_flattened_mut(),
);
for ((dst, rgb_byte), src_alpha) in
out_chunk.iter_mut().zip(rgb_u8.iter()).zip(in_chunk.iter())
{
dst[0] = rgb_byte[0];
dst[1] = rgb_byte[1];
dst[2] = rgb_byte[2];
dst[3] = (src_alpha[3] * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
});
}
fn apply_matrix_and_soft_clip(strip: &mut [[f32; 3]]) {
apply_matrix_row_simd(&BT2020_TO_BT709, strip);
soft_clip_row_simd(strip);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Bt2408Tonemapper;
use alloc::vec;
#[test]
fn pq_to_linear_srgb_black() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::new();
let pq = [[0.0_f32; 3]];
let mut out = [[0.0_f32; 3]];
tonemap_pq_row_simd(&mut scratch, &pq, &mut out, &tm);
for c in out[0] {
assert!(c.abs() < 1e-3, "black should stay black: {c}");
}
}
#[test]
fn pq_to_srgb8_produces_valid_bytes() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::new();
let pq = [[0.58_f32, 0.58, 0.58]];
let mut out = [[0u8; 3]];
tonemap_pq_to_srgb8_row_simd(&mut scratch, &pq, &mut out, &tm);
assert!(
out[0][0] > 30 && out[0][0] < 255,
"SDR white byte: {}",
out[0][0]
);
}
#[test]
fn hlg_to_linear_srgb_at_reference_white() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::new();
let hlg = [[0.75_f32, 0.75, 0.75]];
let mut out = [[0.0_f32; 3]];
tonemap_hlg_row_simd(&mut scratch, &hlg, &mut out, &tm, 1000.0);
for c in out[0] {
assert!(c.is_finite() && c > 0.0, "HLG ref white: {c}");
}
}
#[test]
fn rgba_alpha_preserved() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::new();
let pq = [[0.5_f32, 0.5, 0.5, 0.42]];
let mut out = [[0.0_f32; 4]];
tonemap_pq_rgba_row_simd(&mut scratch, &pq, &mut out, &tm);
assert!((out[0][3] - 0.42).abs() < 1e-6);
}
#[test]
fn soft_clip_pipeline_output_in_range() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::new();
let pq = [[0.6_f32, 0.6, 0.6]];
let mut out = [[0.0_f32; 3]];
tonemap_pq_row_simd(&mut scratch, &pq, &mut out, &tm);
for (i, &v) in out[0].iter().enumerate() {
assert!(
(0.0..=1.0).contains(&v),
"soft clip: ch {i} = {v} out of [0,1]"
);
}
}
#[test]
fn empty_input_is_noop() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::new();
let pq: [[f32; 3]; 0] = [];
let mut out: [[f32; 3]; 0] = [];
tonemap_pq_row_simd(&mut scratch, &pq, &mut out, &tm);
}
#[test]
fn small_chunk_handles_long_strip() {
let tm = Bt2408Tonemapper::new(4000.0, 1000.0);
let mut scratch = TonemapScratch::with_chunk_size(8);
let pq = vec![[0.58_f32, 0.58, 0.58]; 100];
let mut out = vec![[0.0_f32; 3]; 100];
tonemap_pq_row_simd(&mut scratch, &pq, &mut out, &tm);
let first = out[0];
for (i, p) in out.iter().enumerate() {
for c in 0..3 {
let err = (p[c] - first[c]).abs();
assert!(
err < 1e-5,
"chunk-boundary divergence at pixel {i}, ch {c}: {} vs {}",
p[c],
first[c]
);
}
}
}
}