use linear_srgb::tf;
use zenpixels::{ChannelType, ColorPrimaries, PixelSlice, TransferFunction};
const PEAK_SRGB_NITS: f32 = 80.0;
const PEAK_PQ_NITS: f32 = 10_000.0;
const PEAK_HLG_NITS: f32 = 1_000.0;
const SDR_THRESHOLD_NITS: f32 = 100.0;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct DepthStats {
pub peak_nits: f32,
pub p99_nits: f32,
pub headroom_stops: f32,
pub hdr_pixel_fraction: f32,
pub wide_gamut_peak: f32,
pub wide_gamut_fraction: f32,
pub effective_bit_depth: u32,
pub hdr_present: bool,
pub gamut_coverage_srgb: f32,
pub gamut_coverage_p3: f32,
}
const HIST_BINS: usize = 256;
#[inline]
fn nits_to_bin(nits: f32) -> usize {
if !nits.is_finite() || nits <= 0.0 {
return 0;
}
let v = (1.0_f32 + nits).log2() / 14.0;
let i = (v * HIST_BINS as f32) as usize;
i.min(HIST_BINS - 1)
}
#[inline]
fn bin_to_nits(bin: usize) -> f32 {
(((bin as f32 + 0.5) / HIST_BINS as f32) * 14.0).exp2() - 1.0
}
#[inline]
fn eotf(tf_kind: TransferFunction, signal: f32) -> f32 {
match tf_kind {
TransferFunction::Linear => signal,
TransferFunction::Srgb | TransferFunction::Unknown => tf::srgb_to_linear(signal),
TransferFunction::Bt709 => tf::bt709_to_linear(signal),
TransferFunction::Gamma22 => signal.max(0.0).powf(2.2),
TransferFunction::Pq => tf::pq_to_linear(signal),
TransferFunction::Hlg => tf::hlg_to_linear(signal),
_ => signal, }
}
#[inline]
fn peak_nits_for(tf_kind: TransferFunction) -> f32 {
match tf_kind {
TransferFunction::Pq => PEAK_PQ_NITS,
TransferFunction::Hlg => PEAK_HLG_NITS,
_ => PEAK_SRGB_NITS,
}
}
const M_DISPLAYP3_TO_SRGB: [[f32; 3]; 3] = [
[1.224_940_2, -0.224_940_4, 0.000_000_0],
[-0.042_056_9, 1.042_057_1, 0.000_000_0],
[-0.019_637_6, -0.078_636_1, 1.098_273_7],
];
const M_BT2020_TO_SRGB: [[f32; 3]; 3] = [
[1.660_491_0, -0.587_641_1, -0.072_849_9],
[-0.124_550_5, 1.132_899_9, -0.008_349_4],
[-0.018_150_8, -0.100_578_9, 1.118_729_7],
];
const M_ADOBERGB_TO_SRGB: [[f32; 3]; 3] = [
[1.398_287_7, -0.398_287_8, 0.000_000_0],
[0.000_000_0, 1.000_000_0, 0.000_000_0],
[0.000_000_0, -0.042_969_2, 1.042_969_3],
];
const M_BT2020_TO_DISPLAYP3: [[f32; 3]; 3] = [
[1.343_578_8, -0.282_855_8, -0.060_722_6],
[-0.077_876_4, 1.083_393_2, -0.005_516_5],
[0.000_307_5, -0.027_209_2, 1.026_901_8],
];
const M_IDENTITY: [[f32; 3]; 3] = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
#[inline]
fn mat3_mul(m: &[[f32; 3]; 3], r: f32, g: f32, b: f32) -> (f32, f32, f32) {
(
m[0][0] * r + m[0][1] * g + m[0][2] * b,
m[1][0] * r + m[1][1] * g + m[1][2] * b,
m[2][0] * r + m[2][1] * g + m[2][2] * b,
)
}
#[inline]
fn primaries_to_srgb_matrix(src: ColorPrimaries) -> Option<&'static [[f32; 3]; 3]> {
match src {
ColorPrimaries::Bt709 => None,
ColorPrimaries::DisplayP3 => Some(&M_DISPLAYP3_TO_SRGB),
ColorPrimaries::Bt2020 => Some(&M_BT2020_TO_SRGB),
ColorPrimaries::AdobeRgb => Some(&M_ADOBERGB_TO_SRGB),
_ => None, }
}
#[inline]
fn primaries_to_displayp3_matrix(src: ColorPrimaries) -> &'static [[f32; 3]; 3] {
match src {
ColorPrimaries::DisplayP3 => &M_IDENTITY,
ColorPrimaries::Bt2020 => &M_BT2020_TO_DISPLAYP3,
ColorPrimaries::Bt709 | ColorPrimaries::AdobeRgb => &M_IDENTITY,
_ => &M_IDENTITY,
}
}
#[inline]
fn is_hdr_capable_tf(tf_kind: TransferFunction) -> bool {
matches!(
tf_kind,
TransferFunction::Pq | TransferFunction::Hlg | TransferFunction::Linear
)
}
#[inline]
fn read_sample(ch: ChannelType, bytes: &[u8]) -> f32 {
match ch {
ChannelType::U8 => bytes[0] as f32 / 255.0,
ChannelType::U16 => u16::from_le_bytes([bytes[0], bytes[1]]) as f32 / 65535.0,
ChannelType::F32 => f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
ChannelType::F16 => {
let raw = u16::from_le_bytes([bytes[0], bytes[1]]);
f16_to_f32(raw)
}
_ => 0.0,
}
}
#[inline]
fn f16_to_f32(half: u16) -> f32 {
let sign = ((half >> 15) & 0x1) as u32;
let exp = ((half >> 10) & 0x1f) as u32;
let mant = (half & 0x3ff) as u32;
let bits = if exp == 0 {
if mant == 0 {
sign << 31
} else {
let mut m = mant;
let mut e: i32 = -14;
while (m & 0x400) == 0 {
m <<= 1;
e -= 1;
}
m &= 0x3ff;
(sign << 31) | (((e + 127) as u32) << 23) | (m << 13)
}
} else if exp == 31 {
(sign << 31) | (0xff << 23) | (mant << 13)
} else {
(sign << 31) | (((exp + 127 - 15) & 0xff) << 23) | (mant << 13)
};
f32::from_bits(bits)
}
pub(crate) fn scan_depth(slice: &PixelSlice<'_>, pixel_budget: usize) -> DepthStats {
let desc = slice.descriptor();
let width = slice.width() as usize;
let height = slice.rows() as usize;
if width == 0 || height == 0 {
return DepthStats::default();
}
let layout = desc.layout();
let ch = desc.channel_type();
let color_channels = desc.color_model().color_channels() as usize;
if color_channels == 0 {
return DepthStats::default();
}
let total_channels = layout.channels();
let bpp = total_channels * ch.byte_size();
let ch_bytes = ch.byte_size();
let tf_kind = desc.transfer();
let peak_nits_unit = peak_nits_for(tf_kind);
let hdr_capable = is_hdr_capable_tf(tf_kind);
if matches!(ch, ChannelType::U8) && !hdr_capable {
let trivial_srgb_cover =
if matches!(desc.primaries, ColorPrimaries::Bt709) { 1.0 } else { 0.0 };
let trivial_p3_cover =
if matches!(desc.primaries, ColorPrimaries::Bt709 | ColorPrimaries::DisplayP3) {
1.0
} else {
0.0
};
return DepthStats {
peak_nits: PEAK_SRGB_NITS,
p99_nits: PEAK_SRGB_NITS,
headroom_stops: 0.0,
hdr_pixel_fraction: 0.0,
wide_gamut_peak: 1.0,
wide_gamut_fraction: 0.0,
effective_bit_depth: 8,
hdr_present: false,
gamut_coverage_srgb: trivial_srgb_cover,
gamut_coverage_p3: trivial_p3_cover,
};
}
let pixels_per_row = width.max(1);
let target_rows = (pixel_budget / pixels_per_row).max(1).min(height);
let row_step = (height / target_rows).max(1);
let mut hist = [0u32; HIST_BINS];
let mut total: u32 = 0;
let mut hdr_pixels: u32 = 0;
let mut wide_gamut_pixels: u32 = 0;
let mut peak_nits: f32 = 0.0;
let mut wide_gamut_peak: f32 = 0.0;
let m_to_srgb = primaries_to_srgb_matrix(desc.primaries);
let m_to_p3 = primaries_to_displayp3_matrix(desc.primaries);
let mut srgb_in: u32 = 0;
let mut p3_in: u32 = 0;
const GAMUT_LO: f32 = -0.005;
const GAMUT_HI: f32 = 1.005;
#[inline]
fn in_gamut(r: f32, g: f32, b: f32) -> bool {
let range = GAMUT_LO..=GAMUT_HI;
range.contains(&r) && range.contains(&g) && range.contains(&b)
}
const WL_R: f32 = 0.2627;
const WL_G: f32 = 0.6780;
const WL_B: f32 = 0.0593;
let mut low_byte_seen = [false; 256];
let mut low_byte_distinct: u32 = 0;
let probe_bits = matches!(ch, ChannelType::U16);
let mut y = 0usize;
while y < height {
let row = slice.row(y as u32);
let row_pixel_stride = ((width as u32) / 1024).max(1) as usize;
let mut x = 0usize;
while x < width {
let off = x * bpp;
if off + color_channels * ch_bytes > row.len() {
break;
}
let mut linear_max: f32 = 0.0;
let mut linears = [0.0_f32; 4]; for c in 0..color_channels.min(4) {
let s = read_sample(ch, &row[off + c * ch_bytes..]);
let l = eotf(tf_kind, s);
linears[c] = l;
if l > linear_max {
linear_max = l;
}
}
let linear_luma = if color_channels >= 3 {
WL_R * linears[0] + WL_G * linears[1] + WL_B * linears[2]
} else {
linears[0]
};
if probe_bits && color_channels >= 1 {
let low = row[off]; if !low_byte_seen[low as usize] {
low_byte_seen[low as usize] = true;
low_byte_distinct += 1;
}
}
let nits = linear_luma * peak_nits_unit;
if nits > peak_nits {
peak_nits = nits;
}
if linear_max > wide_gamut_peak {
wide_gamut_peak = linear_max;
}
if linear_max > 1.0 {
wide_gamut_pixels += 1;
}
if nits > SDR_THRESHOLD_NITS {
hdr_pixels += 1;
}
if color_channels >= 3 {
let (sr_r, sr_g, sr_b) = match m_to_srgb {
Some(m) => mat3_mul(m, linears[0], linears[1], linears[2]),
None => (linears[0], linears[1], linears[2]),
};
if in_gamut(sr_r, sr_g, sr_b) {
srgb_in += 1;
}
let (p3_r, p3_g, p3_b) =
mat3_mul(m_to_p3, linears[0], linears[1], linears[2]);
if in_gamut(p3_r, p3_g, p3_b) {
p3_in += 1;
}
} else {
srgb_in += 1;
p3_in += 1;
}
hist[nits_to_bin(nits)] += 1;
total += 1;
x += row_pixel_stride;
}
y += row_step;
}
if total == 0 {
return DepthStats::default();
}
let total_f = total as f32;
let target = (total / 100).max(1);
let mut cum: u32 = 0;
let mut p99_bin = 0usize;
for b in (0..HIST_BINS).rev() {
cum += hist[b];
if cum >= target {
p99_bin = b;
break;
}
}
let p99_nits = bin_to_nits(p99_bin).min(peak_nits);
let headroom_stops = if peak_nits > 0.0 {
(peak_nits / PEAK_SRGB_NITS).max(1.0).log2()
} else {
0.0
};
let effective_bit_depth = match ch {
ChannelType::U8 => 8,
ChannelType::F32 | ChannelType::F16 => {
if matches!(ch, ChannelType::F32) { 32 } else { 16 }
}
ChannelType::U16 => effective_depth_from_low_byte(low_byte_distinct, total),
_ => 0,
};
let hdr_pixel_fraction = hdr_pixels as f32 / total_f;
let wide_gamut_fraction = wide_gamut_pixels as f32 / total_f;
let hdr_present = hdr_capable
&& peak_nits > 1.5 * SDR_THRESHOLD_NITS
&& hdr_pixel_fraction > 0.001;
let gamut_coverage_srgb = srgb_in as f32 / total_f;
let gamut_coverage_p3 = p3_in as f32 / total_f;
DepthStats {
peak_nits,
p99_nits,
headroom_stops,
hdr_pixel_fraction,
wide_gamut_peak,
wide_gamut_fraction,
effective_bit_depth,
hdr_present,
gamut_coverage_srgb,
gamut_coverage_p3,
}
}
#[inline]
fn effective_depth_from_low_byte(distinct: u32, total: u32) -> u32 {
if total < 64 {
return 16;
}
match distinct {
0..=15 => 8,
16..=63 => 10,
64..=191 => 12,
_ => 14, }
}
#[cfg(test)]
mod tests {
use super::*;
use zenpixels::PixelDescriptor;
#[test]
fn solid_srgb_u8_takes_fast_path_canonical_sdr_profile() {
let buf = vec![128u8; 32 * 32 * 3];
let s = PixelSlice::new(&buf, 32, 32, 32 * 3, PixelDescriptor::RGB8_SRGB).unwrap();
let d = scan_depth(&s, 100_000);
assert_eq!(d.peak_nits, PEAK_SRGB_NITS);
assert_eq!(d.headroom_stops, 0.0);
assert!(!d.hdr_present);
assert_eq!(d.effective_bit_depth, 8);
assert_eq!(d.wide_gamut_fraction, 0.0);
}
#[test]
fn solid_white_srgb_u8_at_sdr_reference() {
let buf = vec![255u8; 32 * 32 * 3];
let s = PixelSlice::new(&buf, 32, 32, 32 * 3, PixelDescriptor::RGB8_SRGB).unwrap();
let d = scan_depth(&s, 100_000);
assert_eq!(d.peak_nits, PEAK_SRGB_NITS);
assert_eq!(d.hdr_pixel_fraction, 0.0);
assert!(!d.hdr_present);
}
#[test]
fn solid_pq_full_signal_is_high_dynamic_range() {
let mut buf = vec![0u8; 32 * 32 * 3 * 4];
let one = 1.0_f32.to_le_bytes();
for px in buf.chunks_exact_mut(12) {
px[0..4].copy_from_slice(&one);
px[4..8].copy_from_slice(&one);
px[8..12].copy_from_slice(&one);
}
let desc = PixelDescriptor::RGBF32_LINEAR.with_transfer(TransferFunction::Pq);
let s = PixelSlice::new(&buf, 32, 32, 32 * 12, desc).unwrap();
let d = scan_depth(&s, 100_000);
assert!(
(d.peak_nits - 10_000.0).abs() < 5.0,
"peak={} expected ~10000",
d.peak_nits
);
assert!(d.headroom_stops > 6.0, "headroom={}", d.headroom_stops);
assert_eq!(d.hdr_pixel_fraction, 1.0);
assert!(d.hdr_present);
}
#[test]
fn solid_hlg_full_signal_is_hdr_at_1000_nits() {
let mut buf = vec![0u8; 16 * 16 * 12];
let one = 1.0_f32.to_le_bytes();
for px in buf.chunks_exact_mut(12) {
px[0..4].copy_from_slice(&one);
px[4..8].copy_from_slice(&one);
px[8..12].copy_from_slice(&one);
}
let desc = PixelDescriptor::RGBF32_LINEAR.with_transfer(TransferFunction::Hlg);
let s = PixelSlice::new(&buf, 16, 16, 16 * 12, desc).unwrap();
let d = scan_depth(&s, 100_000);
assert!(
(d.peak_nits - 1_000.0).abs() < 1.0,
"peak={} expected ~1000",
d.peak_nits
);
assert!(d.hdr_present);
}
#[test]
fn linear_f32_above_one_is_wide_gamut_signal() {
let mut buf = vec![0u8; 16 * 16 * 12];
let two = 2.0_f32.to_le_bytes();
for px in buf.chunks_exact_mut(12) {
px[0..4].copy_from_slice(&two);
px[4..8].copy_from_slice(&two);
px[8..12].copy_from_slice(&two);
}
let s = PixelSlice::new(&buf, 16, 16, 16 * 12, PixelDescriptor::RGBF32_LINEAR).unwrap();
let d = scan_depth(&s, 100_000);
assert!((d.wide_gamut_peak - 2.0).abs() < 1e-3);
assert_eq!(d.wide_gamut_fraction, 1.0);
}
#[test]
fn u8_promoted_u16_reads_as_8bit_effective_depth() {
let mut buf = vec![0u8; 16 * 16 * 6];
for (i, px) in buf.chunks_exact_mut(2).enumerate() {
let v = ((i % 4) * 64) as u8;
let u = (v as u16) * 257;
px.copy_from_slice(&u.to_le_bytes());
}
let s = PixelSlice::new(&buf, 16, 16, 16 * 6, PixelDescriptor::RGB16_SRGB).unwrap();
let d = scan_depth(&s, 100_000);
assert_eq!(d.effective_bit_depth, 8);
}
#[test]
fn genuine_16bit_u16_reads_as_high_effective_depth() {
let mut buf = vec![0u8; 64 * 64 * 6];
let mut state = 0xC001_u32;
for px in buf.chunks_exact_mut(2) {
state = state.wrapping_mul(1_103_515_245).wrapping_add(12345);
let u = (state & 0xFFFF) as u16;
px.copy_from_slice(&u.to_le_bytes());
}
let s = PixelSlice::new(&buf, 64, 64, 64 * 6, PixelDescriptor::RGB16_SRGB).unwrap();
let d = scan_depth(&s, 100_000);
assert!(
d.effective_bit_depth >= 14,
"expected ≥14, got {}",
d.effective_bit_depth
);
}
#[test]
fn empty_slice_returns_default_stats() {
let buf: Vec<u8> = Vec::new();
let s = PixelSlice::new(&buf, 0, 0, 0, PixelDescriptor::RGB8_SRGB).unwrap();
let d = scan_depth(&s, 100_000);
assert_eq!(d.peak_nits, 0.0);
assert_eq!(d.effective_bit_depth, 0);
assert!(!d.hdr_present);
}
}