#![allow(clippy::excessive_precision)]
use libm::powf;
use super::{BT2020_LB as LB, BT2020_LG as LG, BT2020_LR as LR};
const POW24_C7: f32 = 1.979_355_7e-1;
const POW24_C6: f32 = -8.261_85e-1;
const POW24_C5: f32 = 1.470_748_3;
const POW24_C4: f32 = -1.531_952;
const POW24_C3: f32 = 1.361_614_9;
const POW24_C2: f32 = 3.341_598e-1;
const POW24_C1: f32 = -6.362_703_7e-3;
const POW24_C0: f32 = 5.884_862_3e-5;
#[derive(Debug, Clone, Copy)]
pub struct Bt2446A {
pub(crate) rho_hdr: f32,
pub(crate) inv_log_rho_hdr: f32,
pub(crate) rho_sdr: f32,
pub(crate) inv_rho_sdr_minus_1: f32,
}
impl Bt2446A {
#[must_use]
pub fn new(hdr_peak_nits: f32, sdr_peak_nits: f32) -> Self {
let inv_gamma = 1.0_f32 / 2.4;
let rho_hdr = 1.0 + 32.0 * powf(hdr_peak_nits / 10000.0, inv_gamma);
let log_rho_hdr = libm::logf(rho_hdr);
let rho_sdr = 1.0 + 32.0 * powf(sdr_peak_nits / 10000.0, inv_gamma);
Self {
rho_hdr,
inv_log_rho_hdr: 1.0 / log_rho_hdr,
rho_sdr,
inv_rho_sdr_minus_1: 1.0 / (rho_sdr - 1.0),
}
}
#[must_use]
pub fn map_rgb(&self, rgb: [f32; 3]) -> [f32; 3] {
let mut buf = [rgb];
self.map_strip_simd(&mut buf);
buf[0]
}
pub fn map_strip_simd(&self, strip: &mut [[f32; 3]]) {
archmage::incant!(
bt2446a_tier(
strip,
self.rho_hdr,
self.inv_log_rho_hdr,
self.rho_sdr,
self.inv_rho_sdr_minus_1,
),
[v4(cfg(avx512)), v3, neon, wasm128, scalar]
);
}
}
#[archmage::magetypes(define(f32x16), v4(cfg(avx512)), v3, neon, wasm128, scalar)]
pub(crate) fn bt2446a_tier(
token: Token,
row: &mut [[f32; 3]],
rho_hdr: f32,
inv_log_rho_hdr: f32,
rho_sdr: f32,
inv_rho_sdr_minus_1: f32,
) {
let zero = f32x16::zero(token);
let one = f32x16::splat(token, 1.0);
let lr = f32x16::splat(token, LR);
let lg = f32x16::splat(token, LG);
let lb = f32x16::splat(token, LB);
let inv_24 = 1.0_f32 / 2.4;
let rho_hdr_minus_1 = f32x16::splat(token, rho_hdr - 1.0);
let inv_log_rho_hdr_v = f32x16::splat(token, inv_log_rho_hdr);
let inv_rho_sdr_m1_v = f32x16::splat(token, inv_rho_sdr_minus_1);
let log2_rho_sdr = f32x16::splat(token, libm::log2f(rho_sdr));
let pos_eps = f32x16::splat(token, f32::MIN_POSITIVE);
let ln2 = f32x16::splat(token, core::f32::consts::LN_2);
let t1 = f32x16::splat(token, 0.7399);
let t2 = f32x16::splat(token, 0.9909);
let a1 = f32x16::splat(token, 1.0770);
let a2_a = f32x16::splat(token, -1.1510);
let a2_b = f32x16::splat(token, 2.7811);
let a2_c = f32x16::splat(token, -0.6302);
let a3_a = f32x16::splat(token, 0.5);
let a3_b = f32x16::splat(token, 0.5);
let one_p_one = f32x16::splat(token, 1.1);
let inv_1_8814 = f32x16::splat(token, 1.0 / 1.8814);
let inv_1_4746 = f32x16::splat(token, 1.0 / 1.4746);
let mat_g_b = f32x16::splat(token, 0.16455);
let mat_g_r = f32x16::splat(token, 0.57135);
let mat_r_cr = f32x16::splat(token, 1.4746);
let mat_b_cb = f32x16::splat(token, 1.8814);
let zero_one = f32x16::splat(token, 0.1);
let c7 = f32x16::splat(token, POW24_C7);
let c6 = f32x16::splat(token, POW24_C6);
let c5 = f32x16::splat(token, POW24_C5);
let c4 = f32x16::splat(token, POW24_C4);
let c3 = f32x16::splat(token, POW24_C3);
let c2 = f32x16::splat(token, POW24_C2);
let c1 = f32x16::splat(token, POW24_C1);
let c0 = f32x16::splat(token, POW24_C0);
let mut iter = row.chunks_exact_mut(16);
for chunk in &mut iter {
let mut ra = [0.0_f32; 16];
let mut ga = [0.0_f32; 16];
let mut ba = [0.0_f32; 16];
for (i, px) in chunk.iter().enumerate() {
ra[i] = px[0];
ga[i] = px[1];
ba[i] = px[2];
}
let r = f32x16::load(token, &ra).max(pos_eps);
let g = f32x16::load(token, &ga).max(pos_eps);
let b = f32x16::load(token, &ba).max(pos_eps);
let r_p = r.pow_midp_unchecked(inv_24);
let g_p = g.pow_midp_unchecked(inv_24);
let b_p = b.pow_midp_unchecked(inv_24);
let y_p = lr * r_p + lg * g_p + lb * b_p;
let arg = (one + rho_hdr_minus_1 * y_p).max(pos_eps);
let ln_arg = arg.log2_midp_unchecked() * ln2;
let y_p_lin = ln_arg * inv_log_rho_hdr_v;
let lo_branch = a1 * y_p_lin;
let mid_branch = a2_a * y_p_lin * y_p_lin + a2_b * y_p_lin + a2_c;
let hi_branch = a3_a * y_p_lin + a3_b;
let in_lo = y_p_lin.simd_le(t1);
let in_hi = y_p_lin.simd_ge(t2);
let mid_or_hi = f32x16::blend(in_hi, hi_branch, mid_branch);
let y_c = f32x16::blend(in_lo, lo_branch, mid_or_hi);
let y_sdr = ((y_c * log2_rho_sdr).exp2_midp_unchecked() - one) * inv_rho_sdr_m1_v;
let f = y_sdr / (one_p_one * y_p);
let cb = f * (b_p - y_p) * inv_1_8814;
let cr = f * (r_p - y_p) * inv_1_4746;
let cr_pos = cr.max(zero);
let y_tmo = y_sdr - zero_one * cr_pos;
let r_prime_out = (y_tmo + mat_r_cr * cr).max(zero).min(one);
let g_prime_out = (y_tmo - mat_g_b * cb - mat_g_r * cr).max(zero).min(one);
let b_prime_out = (y_tmo + mat_b_cb * cb).max(zero).min(one);
let r2 = r_prime_out * r_prime_out;
let r4 = r2 * r2;
let r_h7 = c7 * r_prime_out + c6;
let r_h5 = c5 * r_prime_out + c4;
let r_h3 = c3 * r_prime_out + c2;
let r_h1 = c1 * r_prime_out + c0;
let r_hi = r_h7 * r2 + r_h5;
let r_lo = r_h3 * r2 + r_h1;
let r_out = r_hi * r4 + r_lo;
let g2 = g_prime_out * g_prime_out;
let g4 = g2 * g2;
let g_h7 = c7 * g_prime_out + c6;
let g_h5 = c5 * g_prime_out + c4;
let g_h3 = c3 * g_prime_out + c2;
let g_h1 = c1 * g_prime_out + c0;
let g_hi = g_h7 * g2 + g_h5;
let g_lo = g_h3 * g2 + g_h1;
let g_out = g_hi * g4 + g_lo;
let b2 = b_prime_out * b_prime_out;
let b4 = b2 * b2;
let b_h7 = c7 * b_prime_out + c6;
let b_h5 = c5 * b_prime_out + c4;
let b_h3 = c3 * b_prime_out + c2;
let b_h1 = c1 * b_prime_out + c0;
let b_hi = b_h7 * b2 + b_h5;
let b_lo = b_h3 * b2 + b_h1;
let b_out = b_hi * b4 + b_lo;
let valid = y_p.simd_gt(pos_eps);
let or_arr = f32x16::blend(valid, r_out, zero).to_array();
let og_arr = f32x16::blend(valid, g_out, zero).to_array();
let ob_arr = f32x16::blend(valid, b_out, zero).to_array();
for (i, px) in chunk.iter_mut().enumerate() {
px[0] = or_arr[i];
px[1] = og_arr[i];
px[2] = ob_arr[i];
}
}
for px in iter.into_remainder().iter_mut() {
let r_p = powf(px[0].max(0.0), 1.0 / 2.4);
let g_p = powf(px[1].max(0.0), 1.0 / 2.4);
let b_p = powf(px[2].max(0.0), 1.0 / 2.4);
let y_p = LR * r_p + LG * g_p + LB * b_p;
if y_p <= 0.0 {
*px = [0.0, 0.0, 0.0];
continue;
}
let y_p_lin = libm::logf(1.0 + (rho_hdr - 1.0) * y_p) * inv_log_rho_hdr;
let y_c = if y_p_lin <= 0.7399 {
1.0770 * y_p_lin
} else if y_p_lin < 0.9909 {
-1.1510 * y_p_lin * y_p_lin + 2.7811 * y_p_lin - 0.6302
} else {
0.5000 * y_p_lin + 0.5000
};
let y_sdr = (powf(rho_sdr, y_c) - 1.0) * inv_rho_sdr_minus_1;
let f = y_sdr / (1.1 * y_p);
let cb = f * (b_p - y_p) / 1.8814;
let cr = f * (r_p - y_p) / 1.4746;
let y_tmo = y_sdr - 0.1 * cr.max(0.0);
let r_prime_out = (y_tmo + 1.4746 * cr).clamp(0.0, 1.0);
let g_prime_out = (y_tmo - 0.16455 * cb - 0.57135 * cr).clamp(0.0, 1.0);
let b_prime_out = (y_tmo + 1.8814 * cb).clamp(0.0, 1.0);
*px = [
powf(r_prime_out, 2.4),
powf(g_prime_out, 2.4),
powf(b_prime_out, 2.4),
];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rho_hdr_matches_itu_reference_values() {
let tm1k = Bt2446A::new(1000.0, 100.0);
assert!(
(tm1k.rho_hdr - 13.260).abs() < 0.02,
"ρ_H at 1000 nits ≈ 13.26, got {}",
tm1k.rho_hdr
);
let tm10k = Bt2446A::new(10_000.0, 100.0);
assert!(
(tm10k.rho_hdr - 33.0).abs() < 0.05,
"ρ_H at 10 000 nits = 33.0, got {}",
tm10k.rho_hdr
);
assert!(
tm1k.rho_hdr > 5.0,
"ρ_H regressed toward the pre-fix value (~1.13); got {}",
tm1k.rho_hdr
);
}
#[test]
fn libplacebo_parity_end_to_end_on_gray_ramp() {
fn libplacebo_eetf_then_eotf(x: f32, hdr_peak: f32, sdr_peak: f32) -> f32 {
let x = x.clamp(0.0, 1.0);
let p_hdr = 1.0 + 32.0 * powf(hdr_peak / 10000.0, 1.0 / 2.4);
let p_sdr = 1.0 + 32.0 * powf(sdr_peak / 10000.0, 1.0 / 2.4);
let x_p = powf(x, 1.0 / 2.4);
let mut y = libm::logf(1.0 + (p_hdr - 1.0) * x_p) / libm::logf(p_hdr);
y = if y <= 0.7399 {
1.0770 * y
} else if y < 0.9909 {
-1.1510 * y * y + 2.7811 * y - 0.6302
} else {
0.5 * y + 0.5
};
let y_sdr_prime = (powf(p_sdr, y) - 1.0) / (p_sdr - 1.0);
powf(y_sdr_prime, 2.4)
}
for &(hdr, sdr) in &[(1000.0_f32, 100.0_f32), (4000.0, 100.0), (10_000.0, 100.0)] {
let tm = Bt2446A::new(hdr, sdr);
for &x in &[0.05_f32, 0.18, 0.3, 0.5, 0.7, 0.85, 0.95, 1.0] {
let got = tm.map_rgb([x, x, x])[0];
let want = libplacebo_eetf_then_eotf(x, hdr, sdr);
assert!(
(got - want).abs() < 1e-4,
"libplacebo parity at (hdr={hdr}, sdr={sdr}, x={x}): got {got}, want {want}"
);
}
}
}
#[test]
fn output_is_linear_light_not_gamma_encoded() {
let tm = Bt2446A::new(1000.0, 100.0);
let black = tm.map_rgb([0.0, 0.0, 0.0]);
assert_eq!(black, [0.0, 0.0, 0.0]);
let peak = tm.map_rgb([1.0, 1.0, 1.0]);
for c in peak {
assert!(
(c - 1.0).abs() < 1e-4,
"HDR peak should round-trip to SDR peak: {c}"
);
}
let mid = tm.map_rgb([0.18, 0.18, 0.18]);
for c in mid {
assert!(
(c - 0.370).abs() < 0.02,
"mid-grey HDR 0.18 should map to linear-light SDR ≈ 0.37, got {c}"
);
}
assert!(
mid[0] < 0.55,
"mid-grey output regressed toward the pre-fix gamma-encoded value (~0.66); got {}",
mid[0]
);
}
}