use crate::ToneMap;
pub struct Bt2446C {
k1: f32,
k2: f32,
k4: f32,
y_ip: f32, alpha: f32, }
impl Bt2446C {
pub fn new(hdr_peak_nits: f32, sdr_peak_nits: f32) -> Self {
Self::with_params(
hdr_peak_nits,
sdr_peak_nits,
0.83802,
15.09968,
0.74204,
78.99439,
0.0,
)
}
pub fn with_params(
hdr_peak_nits: f32,
sdr_peak_nits: f32,
k1: f32,
k2: f32,
_k3: f32,
k4: f32,
alpha: f32,
) -> Self {
let y_ip = k4 / k1;
let _ = (hdr_peak_nits, sdr_peak_nits); Self {
k1,
k2,
k4,
y_ip,
alpha,
}
}
#[inline]
fn crosstalk(&self, rgb: [f32; 3]) -> [f32; 3] {
if self.alpha <= 0.0 {
return rgb;
}
let a = self.alpha;
let d = 1.0 - 2.0 * a;
[
d * rgb[0] + a * rgb[1] + a * rgb[2],
a * rgb[0] + d * rgb[1] + a * rgb[2],
a * rgb[0] + a * rgb[1] + d * rgb[2],
]
}
#[inline]
fn inv_crosstalk(&self, rgb: [f32; 3]) -> [f32; 3] {
if self.alpha <= 0.0 {
return rgb;
}
let a = self.alpha;
let inv_a = -a / (1.0 - 3.0 * a);
let d = 1.0 - 2.0 * inv_a;
[
d * rgb[0] + inv_a * rgb[1] + inv_a * rgb[2],
inv_a * rgb[0] + d * rgb[1] + inv_a * rgb[2],
inv_a * rgb[0] + inv_a * rgb[1] + d * rgb[2],
]
}
#[inline]
fn tone_curve(&self, y_hdr_pct: f32) -> f32 {
if y_hdr_pct < self.y_ip {
self.k1 * y_hdr_pct
} else {
self.k2 * libm::logf(y_hdr_pct / self.y_ip) + self.k4
}
}
#[inline]
pub fn inverse_tone_curve(&self, y_sdr_pct: f32) -> f32 {
let threshold = self.k1 * self.y_ip;
if y_sdr_pct < threshold {
y_sdr_pct / self.k1
} else {
self.y_ip * libm::expf((y_sdr_pct - self.k4) / self.k2)
}
}
}
impl ToneMap for Bt2446C {
fn map_rgb(&self, rgb: [f32; 3]) -> [f32; 3] {
let rgb_pct = [
rgb[0].clamp(0.0, 100.0) * 100.0,
rgb[1].clamp(0.0, 100.0) * 100.0,
rgb[2].clamp(0.0, 100.0) * 100.0,
];
let ct = self.crosstalk(rgb_pct);
let y = 0.2627 * ct[0] + 0.6780 * ct[1] + 0.0593 * ct[2];
if y <= 0.0 {
return [0.0, 0.0, 0.0];
}
let y_sdr = self.tone_curve(y);
let ratio = y_sdr / y;
let sdr_pct = [ct[0] * ratio, ct[1] * ratio, ct[2] * ratio];
let sdr = self.inv_crosstalk(sdr_pct);
[
(sdr[0] / 100.0).clamp(0.0, 1.09),
(sdr[1] / 100.0).clamp(0.0, 1.09),
(sdr[2] / 100.0).clamp(0.0, 1.09),
]
}
fn map_strip_simd(&self, strip: &mut [[f32; 3]]) {
archmage::incant!(
crate::simd::curves::bt2446c_tier(
strip, self.k1, self.k2, self.k4, self.y_ip, self.alpha,
),
[v3, neon, wasm128, scalar]
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn black_to_black() {
let tm = Bt2446C::new(1000.0, 100.0);
assert_eq!(tm.map_rgb([0.0, 0.0, 0.0]), [0.0, 0.0, 0.0]);
}
#[test]
fn monotonic_neutral_ramp() {
let tm = Bt2446C::new(1000.0, 100.0);
let mut last = -1.0_f32;
for i in 0..=100 {
let v = i as f32 / 100.0;
let out = tm.map_rgb([v, v, v]);
assert!(out[0] >= last - 1e-5, "mono at {v}: {} < {last}", out[0]);
last = out[0];
}
}
#[test]
fn tone_curve_inverse_roundtrip() {
let tm = Bt2446C::new(1000.0, 100.0);
for &nits in &[0.0_f32, 1.0, 10.0, 50.0, 100.0, 500.0, 1000.0] {
let sdr = tm.tone_curve(nits);
let back = tm.inverse_tone_curve(sdr);
assert!((back - nits).abs() < 0.1, "roundtrip at {nits}: got {back}");
}
}
#[test]
fn colored_bounded() {
let tm = Bt2446C::new(1000.0, 100.0);
for rgb in [[0.8, 0.2, 0.05], [0.1, 0.9, 0.05], [0.3, 0.3, 0.8]] {
let out = tm.map_rgb(rgb);
for (i, c) in out.iter().enumerate() {
assert!(
c.is_finite() && *c >= 0.0 && *c <= 1.2,
"Bt2446C({rgb:?})[{i}] = {c}"
);
}
}
}
#[test]
fn crosstalk_roundtrip() {
let tm = Bt2446C::with_params(1000.0, 100.0, 0.83802, 15.09968, 0.74204, 78.99439, 0.1);
let rgb = [0.5_f32, 0.3, 0.8];
let ct = tm.crosstalk(rgb);
let back = tm.inv_crosstalk(ct);
for i in 0..3 {
assert!(
(back[i] - rgb[i]).abs() < 1e-5,
"crosstalk roundtrip[{i}]: {:.5} vs {:.5}",
back[i],
rgb[i]
);
}
}
}