use crate::xyb_simd::{K_B0, K_M00, K_M01, K_M02, K_M10, K_M11, K_M12, K_M20, K_M21, K_M22};
#[allow(clippy::excessive_precision)]
const PU21_P: [f32; 7] = [
0.353_487_901,
0.373_465_862_9,
8.277_049_286e-5,
0.906_256_262_7,
0.091_503_031_66,
0.909_951_720_4,
596.314_814_2,
];
const PU21_L_MIN: f32 = 0.005;
const PU21_L_MAX: f32 = 10000.0;
const PU_WHITE: f32 = 256.3;
const PU_X_SCALE: f32 = 4.0;
#[inline]
fn pu21_encode(y: f32) -> f32 {
let y = y.clamp(PU21_L_MIN, PU21_L_MAX);
let yp = y.powf(PU21_P[3]);
let inner = (PU21_P[0] + PU21_P[1] * yp) / (1.0 + PU21_P[2] * yp);
(PU21_P[6] * (inner.powf(PU21_P[4]) - PU21_P[5])).max(0.0)
}
pub(crate) fn linear_nits_to_pu_xyb(input: &mut [[f32; 3]]) {
for pix in input.iter_mut() {
let (r, g, b) = (pix[0], pix[1], pix[2]);
let mixed0 = K_M00
.mul_add(r, K_M01.mul_add(g, K_M02.mul_add(b, K_B0)))
.max(0.0);
let mixed1 = K_M10
.mul_add(r, K_M11.mul_add(g, K_M12.mul_add(b, K_B0)))
.max(0.0);
let mixed2 = K_M20
.mul_add(r, K_M21.mul_add(g, K_M22.mul_add(b, K_B0)))
.max(0.0);
let c0 = pu21_encode(mixed0) / PU_WHITE;
let c1 = pu21_encode(mixed1) / PU_WHITE;
let c2 = pu21_encode(mixed2) / PU_WHITE;
let x = 0.5 * (c0 - c1);
let y = 0.5 * (c0 + c1);
pix[0] = x.mul_add(PU_X_SCALE, 0.42);
pix[1] = y + 0.01;
pix[2] = (c2 - y) + 0.55;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn white_point_lands_near_one() {
let v = pu21_encode(100.0) / PU_WHITE;
assert!((v - 1.0).abs() < 0.01, "PU(100)/PU_WHITE = {v}");
}
#[test]
fn pu_xyb_outputs_are_positive_calibrated() {
for nits in [0.01, 0.5, 5.0, 100.0, 1000.0, 4000.0] {
let mut px = [[nits, nits, nits]];
linear_nits_to_pu_xyb(&mut px);
let [x, y, b] = px[0];
assert!((0.3..0.6).contains(&x), "X({nits}) = {x}");
assert!(y > 0.0 && y < 2.5, "Y({nits}) = {y}");
assert!((0.3..0.9).contains(&b), "B({nits}) = {b}");
}
let ys: Vec<f32> = [1.0f32, 10.0, 100.0, 1000.0]
.iter()
.map(|&n| {
let mut px = [[n, n, n]];
linear_nits_to_pu_xyb(&mut px);
px[0][1]
})
.collect();
assert!(ys.windows(2).all(|w| w[1] > w[0]), "Y not monotone: {ys:?}");
}
}