use crate::math::powf;
const LR: f32 = 0.2627;
const LG: f32 = 0.6780;
const LB: f32 = 0.0593;
#[inline]
pub fn hlg_system_gamma(display_peak_nits: f32) -> f32 {
1.2 + 0.42 * libm::log10f(display_peak_nits / 1000.0)
}
pub fn hlg_ootf(rgb: [f32; 3], gamma: f32) -> [f32; 3] {
let ys = LR * rgb[0] + LG * rgb[1] + LB * rgb[2];
if ys <= 0.0 {
return [0.0, 0.0, 0.0];
}
let scale = powf(ys, gamma - 1.0);
[rgb[0] * scale, rgb[1] * scale, rgb[2] * scale]
}
pub fn hlg_inverse_ootf(rgb: [f32; 3], gamma: f32) -> [f32; 3] {
let yd = LR * rgb[0] + LG * rgb[1] + LB * rgb[2];
if yd <= 0.0 {
return [0.0, 0.0, 0.0];
}
let inv_gamma = 1.0 / gamma;
let scale = powf(yd, (1.0 - gamma) * inv_gamma);
[rgb[0] * scale, rgb[1] * scale, rgb[2] * scale]
}
pub fn hlg_ootf_approx(rgb: [f32; 3], gamma: f32) -> [f32; 3] {
[
powf(rgb[0], gamma),
powf(rgb[1], gamma),
powf(rgb[2], gamma),
]
}
pub fn hlg_inverse_ootf_approx(rgb: [f32; 3], gamma: f32) -> [f32; 3] {
let inv = 1.0 / gamma;
[powf(rgb[0], inv), powf(rgb[1], inv), powf(rgb[2], inv)]
}
pub fn hlg_to_display(hlg_signal: [f32; 3], display_peak_nits: f32) -> [f32; 3] {
let gamma = hlg_system_gamma(display_peak_nits);
let scene = [
linear_srgb::tf::hlg_to_linear(hlg_signal[0]),
linear_srgb::tf::hlg_to_linear(hlg_signal[1]),
linear_srgb::tf::hlg_to_linear(hlg_signal[2]),
];
hlg_ootf(scene, gamma)
}
#[inline]
pub fn hlg_ootf_row_simd(row: &mut [[f32; 3]], gamma: f32) {
let k = gamma - 1.0;
archmage::incant!(
crate::simd::blocks::hlg_ootf_exact_tier(row, k),
[v3, neon, wasm128, scalar]
);
}
#[inline]
pub fn hlg_inverse_ootf_row_simd(row: &mut [[f32; 3]], gamma: f32) {
let k = (1.0 - gamma) / gamma;
archmage::incant!(
crate::simd::blocks::hlg_ootf_exact_tier(row, k),
[v3, neon, wasm128, scalar]
);
}
#[inline]
pub fn hlg_ootf_approx_row_simd(row: &mut [[f32; 3]], gamma: f32) {
archmage::incant!(
crate::simd::blocks::hlg_ootf_approx_tier(row, gamma),
[v3, neon, wasm128, scalar]
);
}
#[inline]
pub fn hlg_inverse_ootf_approx_row_simd(row: &mut [[f32; 3]], gamma: f32) {
let inv = 1.0 / gamma;
archmage::incant!(
crate::simd::blocks::hlg_ootf_approx_tier(row, inv),
[v3, neon, wasm128, scalar]
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_gamma_reference_1000_nits() {
let g = hlg_system_gamma(1000.0);
assert!(
(g - 1.2).abs() < 1e-5,
"1000 nits should give gamma=1.2, got {g}"
);
}
#[test]
fn system_gamma_4000_nits() {
let g = hlg_system_gamma(4000.0);
assert!((g - 1.453).abs() < 0.01, "4000 nits gamma ≈ 1.453, got {g}");
}
#[test]
fn ootf_preserves_black() {
let out = hlg_ootf([0.0, 0.0, 0.0], 1.2);
assert_eq!(out, [0.0, 0.0, 0.0]);
}
#[test]
fn ootf_adds_contrast_for_gamma_gt_1() {
let mid = hlg_ootf([0.5, 0.5, 0.5], 1.2);
let mid_lum = LR * mid[0] + LG * mid[1] + LB * mid[2];
assert!(
mid_lum < 0.5 && mid_lum > 0.3,
"OOTF should darken mid-gray: got {mid_lum}"
);
let peak = hlg_ootf([1.0, 1.0, 1.0], 1.2);
let peak_lum = LR * peak[0] + LG * peak[1] + LB * peak[2];
assert!(
(peak_lum - 1.0).abs() < 1e-5,
"OOTF should preserve peak: got {peak_lum}"
);
}
#[test]
fn ootf_roundtrip() {
let rgb = [0.3, 0.6, 0.1];
let gamma = 1.2;
let display = hlg_ootf(rgb, gamma);
let back = hlg_inverse_ootf(display, gamma);
for i in 0..3 {
assert!(
(back[i] - rgb[i]).abs() < 1e-5,
"OOTF roundtrip[{i}]: {:.6} vs {:.6}",
back[i],
rgb[i]
);
}
}
#[test]
fn ootf_approx_roundtrip() {
for &gamma in &[1.0_f32, 1.033, 1.2, 1.453, 1.5] {
for &rgb in &[[0.3_f32, 0.6, 0.1], [0.001, 0.5, 0.999], [0.18, 0.18, 0.18]] {
let display = hlg_ootf_approx(rgb, gamma);
let back = hlg_inverse_ootf_approx(display, gamma);
for i in 0..3 {
assert!(
(back[i] - rgb[i]).abs() < 5e-6,
"approx OOTF roundtrip[{i}] gamma={gamma} rgb={rgb:?}: \
{:.6} vs {:.6}",
back[i],
rgb[i]
);
}
}
}
}
#[test]
fn ootf_approx_diverges_from_exact_on_saturated_color() {
let gamma = 1.2;
let grey = [0.5_f32, 0.5, 0.5];
let exact_grey = hlg_ootf(grey, gamma);
let approx_grey = hlg_ootf_approx(grey, gamma);
for i in 0..3 {
assert!(
(exact_grey[i] - approx_grey[i]).abs() < 1e-5,
"exact and approx must agree on greys: {exact_grey:?} vs {approx_grey:?}"
);
}
let red = [0.9_f32, 0.1, 0.1];
let exact_red = hlg_ootf(red, gamma);
let approx_red = hlg_ootf_approx(red, gamma);
let max_diff = (0..3)
.map(|i| (exact_red[i] - approx_red[i]).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff > 1e-3,
"exact and approx should differ on saturated red: {exact_red:?} vs {approx_red:?}"
);
}
#[test]
fn hlg_to_display_at_reference_white() {
let out = hlg_to_display([0.75, 0.75, 0.75], 1000.0);
for c in out {
assert!(c.is_finite() && c > 0.0 && c < 1.0, "ref white: {c}");
}
}
}