use alloc::boxed::Box;
use alloc::vec;
use zenpixels::hdr::{ContentLightLevel, DiffuseWhite};
use zenpixels::{PixelFormat, PixelSlice, TransferFunction};
#[inline]
fn nits_to_u16(nits: f64) -> u16 {
(nits + 0.5) as u16
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LightLevelMethod {
#[default]
MaxRgb,
LuminanceBt2020,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
#[doc(hidden)]
pub struct LightLevelHistogram {
bins: Box<[u32]>,
total: u64,
sum_nits: f64,
literal_max_nits: f32,
method: LightLevelMethod,
}
impl LightLevelHistogram {
pub const BIN_MIN_NITS: f32 = 0.005;
pub const BIN_MAX_NITS: f32 = 10_000.0;
pub const NUM_BINS: usize = 1024;
const LOG2_MIN: f32 = -7.643_856;
const LOG2_MAX: f32 = 13.287_712;
#[inline(always)]
const fn log2_step() -> f32 {
(Self::LOG2_MAX - Self::LOG2_MIN) / (Self::NUM_BINS as f32)
}
#[inline(always)]
const fn inv_log2_step() -> f32 {
1.0 / Self::log2_step()
}
pub fn max(&self) -> f32 {
self.literal_max_nits
}
pub fn mean(&self) -> f32 {
if self.total == 0 {
return 0.0;
}
(self.sum_nits / self.total as f64) as f32
}
pub fn percentile(&self, percentile: f32) -> f32 {
if self.total == 0 {
return 0.0;
}
let p = if percentile.is_nan() {
0.0
} else {
percentile.clamp(0.0, 1.0)
};
if p >= 1.0 {
return self.literal_max_nits;
}
if p <= 0.0 {
return 0.0;
}
let threshold = (p as f64 * self.total as f64) as u64;
let mut cum: u64 = 0;
let inv_step = Self::inv_log2_step();
for (i, &count) in self.bins.iter().enumerate() {
let count_u64 = count as u64;
cum += count_u64;
if cum >= threshold {
let count_before = cum - count_u64;
let fraction = if count_u64 > 0 {
let inside = threshold.saturating_sub(count_before) as f64;
let f = (inside / count_u64 as f64).clamp(0.0, 1.0);
f as f32
} else {
0.0
};
let log2_interp = Self::LOG2_MIN + (i as f32 + fraction) / inv_step;
let interp = fast_exp2(log2_interp).max(0.0);
return interp.min(self.literal_max_nits);
}
}
self.literal_max_nits
}
pub fn method(&self) -> LightLevelMethod {
self.method
}
pub fn total_pixels(&self) -> u64 {
self.total
}
pub fn bins(&self) -> &[u32] {
&self.bins
}
}
#[inline]
fn fast_log2(x: f32) -> f32 {
use core::f32::consts::LOG2_E;
if let Some(core::cmp::Ordering::Greater) = x.partial_cmp(&0.0) {
let bits = x.to_bits();
let exponent = ((bits >> 23) & 0xFF) as i32 - 127;
let mantissa = f32::from_bits((bits & 0x7F_FFFF) | (127 << 23));
let f = mantissa - 1.0;
let log2_mantissa = f * (LOG2_E - (LOG2_E - 1.0) * f);
(exponent as f32) + log2_mantissa
} else {
f32::NEG_INFINITY
}
}
#[inline]
fn fast_exp2(x: f32) -> f32 {
if !x.is_finite() {
return if x > 0.0 { f32::INFINITY } else { 0.0 };
}
let mut i = x as i32;
if (i as f32) > x {
i -= 1;
}
let f = x - (i as f32);
let pf = 1.0 + f * (core::f32::consts::LN_2 + f * (0.240_226_4 + f * 0.055_497_6));
let biased = i + 127;
if biased <= 0 {
return 0.0;
}
if biased >= 255 {
return f32::INFINITY;
}
let two_i = f32::from_bits((biased as u32) << 23);
two_i * pf
}
#[inline(always)]
fn bin_for_nits(value_nits: f32) -> usize {
if value_nits <= LightLevelHistogram::BIN_MIN_NITS {
return 0;
}
if value_nits >= LightLevelHistogram::BIN_MAX_NITS {
return LightLevelHistogram::NUM_BINS - 1;
}
let log2 = fast_log2(value_nits);
let bin =
((log2 - LightLevelHistogram::LOG2_MIN) * LightLevelHistogram::inv_log2_step()) as usize;
if bin >= LightLevelHistogram::NUM_BINS {
LightLevelHistogram::NUM_BINS - 1
} else {
bin
}
}
#[inline]
fn scan_row_max_mean_smoothed<const N: usize>(row: &[f32], method: LightLevelMethod) -> (f32, f64) {
const ONE_THIRD: f32 = 1.0 / 3.0;
let pixel_count = row.len() / N;
if pixel_count == 0 {
return (0.0, 0.0);
}
let reduce_at = |i: usize| -> f32 {
let px: &[f32; N] = row[i * N..(i + 1) * N].try_into().unwrap();
match method {
LightLevelMethod::MaxRgb => 0.0_f32.max(px[0]).max(px[1]).max(px[2]),
LightLevelMethod::LuminanceBt2020 => {
let r = 0.0_f32.max(px[0]);
let g = 0.0_f32.max(px[1]);
let b = 0.0_f32.max(px[2]);
0.262_7 * r + 0.678_0 * g + 0.059_3 * b
}
}
};
if pixel_count == 1 {
let m = reduce_at(0);
return (m, f64::from(m));
}
if pixel_count == 2 {
let m0 = reduce_at(0);
let m1 = reduce_at(1);
let s0 = (2.0 * m0 + m1) * ONE_THIRD;
let s1 = (m0 + 2.0 * m1) * ONE_THIRD;
return (s0.max(s1), f64::from(m0) + f64::from(m1));
}
let m0 = reduce_at(0);
let m1 = reduce_at(1);
let mut prev = m0;
let mut curr = m1;
let mut max_x3 = 2.0 * m0 + m1;
let mut sum = f64::from(m0);
for i in 2..pixel_count {
let next = reduce_at(i);
let s = prev + curr + next;
if s > max_x3 {
max_x3 = s;
}
sum += f64::from(curr);
prev = curr;
curr = next;
}
let s_last = prev + 2.0 * curr;
if s_last > max_x3 {
max_x3 = s_last;
}
sum += f64::from(curr);
(max_x3 * ONE_THIRD, sum)
}
pub trait CllMeasure {
fn measure_max(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel>;
#[doc(hidden)]
fn measure_max_smoothed(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel>;
#[doc(hidden)]
fn measure_robust(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel>;
#[doc(hidden)]
fn measure_percentile(
px: PixelSlice<'_>,
white: DiffuseWhite,
percentile: f32,
method: LightLevelMethod,
) -> Option<ContentLightLevel>;
#[doc(hidden)]
fn measure_histogram(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<LightLevelHistogram>;
}
impl CllMeasure for ContentLightLevel {
fn measure_max(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel> {
let desc = px.descriptor();
let channels = match desc.pixel_format() {
PixelFormat::RgbF32 => 3,
PixelFormat::RgbaF32 => 4,
_ => return None,
};
if desc.transfer != TransferFunction::Linear {
return None;
}
let w = px.width() as usize;
let h = px.rows() as usize;
if w == 0 || h == 0 {
return Some(ContentLightLevel::new(0, 0));
}
let stride = px.stride();
let bytes = px.as_strided_bytes();
let row_len = w * channels * 4;
let white_nits = white.nits();
let (row_max, row_sum) =
simd_kernel::scan_max_mean_simd(bytes, h, stride, channels, row_len, method);
let wn = f64::from(white_nits);
let max_nits = f64::from(row_max) * wn;
let fall_nits = row_sum / (w as f64 * h as f64) * wn;
Some(ContentLightLevel::new(
nits_to_u16(max_nits),
nits_to_u16(fall_nits),
))
}
fn measure_max_smoothed(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel> {
let desc = px.descriptor();
let channels = match desc.pixel_format() {
PixelFormat::RgbF32 => 3,
PixelFormat::RgbaF32 => 4,
_ => return None,
};
if desc.transfer != TransferFunction::Linear {
return None;
}
let w = px.width() as usize;
let h = px.rows() as usize;
if w == 0 || h == 0 {
return Some(ContentLightLevel::new(0, 0));
}
let stride = px.stride();
let bytes = px.as_strided_bytes();
let row_len = w * channels * 4;
let white_nits = white.nits();
let mut max_rel = 0.0_f32;
let mut sum_rel = 0.0_f64;
for row in 0..h {
let row_bytes = &bytes[row * stride..row * stride + row_len];
let floats: &[f32] = bytemuck::cast_slice(row_bytes);
let (rm, rs) = if channels == 3 {
scan_row_max_mean_smoothed::<3>(floats, method)
} else {
scan_row_max_mean_smoothed::<4>(floats, method)
};
max_rel = max_rel.max(rm);
sum_rel += rs;
}
let wn = f64::from(white_nits);
let max_nits = f64::from(max_rel) * wn;
let fall_nits = sum_rel / (w as f64 * h as f64) * wn;
Some(ContentLightLevel::new(
nits_to_u16(max_nits),
nits_to_u16(fall_nits),
))
}
fn measure_robust(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel> {
<ContentLightLevel as CllMeasure>::measure_percentile(
px,
white,
ContentLightLevel::DEFAULT_PERCENTILE,
method,
)
}
fn measure_percentile(
px: PixelSlice<'_>,
white: DiffuseWhite,
percentile: f32,
method: LightLevelMethod,
) -> Option<ContentLightLevel> {
let h = <ContentLightLevel as CllMeasure>::measure_histogram(px, white, method)?;
Some(ContentLightLevel::new(
nits_to_u16(f64::from(h.percentile(percentile))),
nits_to_u16(f64::from(h.mean())),
))
}
fn measure_histogram(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<LightLevelHistogram> {
let desc = px.descriptor();
let channels = match desc.pixel_format() {
PixelFormat::RgbF32 => 3,
PixelFormat::RgbaF32 => 4,
_ => return None,
};
if desc.transfer != TransferFunction::Linear {
return None;
}
let w = px.width() as usize;
let h = px.rows() as usize;
if w == 0 || h == 0 {
return Some(LightLevelHistogram {
bins: vec![0u32; LightLevelHistogram::NUM_BINS].into_boxed_slice(),
total: 0,
sum_nits: 0.0,
literal_max_nits: 0.0,
method,
});
}
let stride = px.stride();
let bytes = px.as_strided_bytes();
let row_len = w * channels * 4;
let white_nits = white.nits();
Some(simd_kernel::measure_histogram_simd(
bytes, w, h, stride, channels, row_len, white_nits, method,
))
}
}
#[cfg(test)]
fn measure_max_via_histogram_for_test(
px: PixelSlice<'_>,
white: DiffuseWhite,
method: LightLevelMethod,
) -> Option<ContentLightLevel> {
let h = <ContentLightLevel as CllMeasure>::measure_histogram(px, white, method)?;
Some(ContentLightLevel::new(
nits_to_u16(f64::from(h.max())),
nits_to_u16(f64::from(h.mean())),
))
}
mod simd_kernel {
use super::{LightLevelHistogram, LightLevelMethod, bin_for_nits};
const LANES: usize = 8;
const SUM_FLUSH_CHUNKS: u32 = 256;
use crate::hdr::{BT2020_LB as KB, BT2020_LG as KG, BT2020_LR as KR};
type SubHists = alloc::boxed::Box<[u32]>;
fn zero_subhists() -> SubHists {
alloc::vec![0u32; LANES * LightLevelHistogram::NUM_BINS].into_boxed_slice()
}
fn merge_subhists(sub: &SubHists, out: &mut [u32]) {
debug_assert_eq!(out.len(), LightLevelHistogram::NUM_BINS);
for bin in 0..LightLevelHistogram::NUM_BINS {
let mut total: u32 = 0;
for lane in 0..LANES {
total = total.wrapping_add(sub[lane * LightLevelHistogram::NUM_BINS + bin]);
}
out[bin] = total;
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn measure_histogram_simd(
bytes: &[u8],
w: usize,
h: usize,
stride: usize,
channels: usize,
row_len: usize,
white_nits: f32,
method: LightLevelMethod,
) -> LightLevelHistogram {
let mut sub = zero_subhists();
let mut sum_nits = 0.0_f64;
let mut literal_max_nits = 0.0_f32;
for row in 0..h {
let row_bytes = &bytes[row * stride..row * stride + row_len];
let floats: &[f32] = bytemuck::cast_slice(row_bytes);
match method {
LightLevelMethod::MaxRgb => {
if channels == 3 {
archmage::incant!(
accumulate_strip_max_rgb_tier::<3>(
floats,
white_nits,
&mut sub,
&mut sum_nits,
&mut literal_max_nits,
),
[v3, neon, wasm128, scalar]
);
} else {
archmage::incant!(
accumulate_strip_max_rgb_tier::<4>(
floats,
white_nits,
&mut sub,
&mut sum_nits,
&mut literal_max_nits,
),
[v3, neon, wasm128, scalar]
);
}
}
LightLevelMethod::LuminanceBt2020 => {
if channels == 3 {
archmage::incant!(
accumulate_strip_luma_bt2020_tier::<3>(
floats,
white_nits,
&mut sub,
&mut sum_nits,
&mut literal_max_nits,
),
[v3, neon, wasm128, scalar]
);
} else {
archmage::incant!(
accumulate_strip_luma_bt2020_tier::<4>(
floats,
white_nits,
&mut sub,
&mut sum_nits,
&mut literal_max_nits,
),
[v3, neon, wasm128, scalar]
);
}
}
}
}
let mut bins = alloc::vec![0u32; LightLevelHistogram::NUM_BINS].into_boxed_slice();
merge_subhists(&sub, &mut bins);
LightLevelHistogram {
bins,
total: (w as u64) * (h as u64),
sum_nits,
literal_max_nits,
method,
}
}
#[archmage::magetypes(define(f32x8), v3, neon, wasm128, scalar)]
pub(crate) fn accumulate_strip_max_rgb_tier<const N: usize>(
token: Token,
row: &[f32],
white_nits: f32,
sub_hists: &mut [u32],
sum_nits: &mut f64,
literal_max_nits: &mut f32,
) {
let zero = f32x8::zero(token);
let wn = f32x8::splat(token, white_nits);
let log2_min = f32x8::splat(token, LightLevelHistogram::LOG2_MIN);
let inv_step = f32x8::splat(token, LightLevelHistogram::inv_log2_step());
let bin_min_nits = f32x8::splat(token, LightLevelHistogram::BIN_MIN_NITS);
let num_bins_minus_1 = f32x8::splat(token, (LightLevelHistogram::NUM_BINS - 1) as f32);
let mut local_max = zero;
let mut local_sum = zero;
let mut chunks_since_flush = 0u32;
let mut iter = row.chunks_exact(LANES * N);
for chunk in &mut iter {
let mut ra = [0.0_f32; LANES];
let mut ga = [0.0_f32; LANES];
let mut ba = [0.0_f32; LANES];
for i in 0..LANES {
let base = i * N;
ra[i] = chunk[base];
ga[i] = chunk[base + 1];
ba[i] = chunk[base + 2];
}
let r = f32x8::load(token, &ra);
let g = f32x8::load(token, &ga);
let b = f32x8::load(token, &ba);
let r = f32x8::blend(r.simd_gt(zero), r, zero);
let g = f32x8::blend(g.simd_gt(zero), g, zero);
let b = f32x8::blend(b.simd_gt(zero), b, zero);
let m_rel = r.max(g).max(b);
let m_nits = m_rel * wn;
local_max = local_max.max(m_nits);
local_sum += m_nits;
chunks_since_flush += 1;
if chunks_since_flush == SUM_FLUSH_CHUNKS {
*sum_nits += f64::from(local_sum.reduce_add());
local_sum = zero;
chunks_since_flush = 0;
}
let safe = m_nits.max(bin_min_nits);
let log2 = safe.log2_midp();
let bin_f = ((log2 - log2_min) * inv_step)
.max(zero)
.min(num_bins_minus_1);
let nits_arr = m_nits.to_array();
let bin_arr = bin_f.to_array();
for i in 0..LANES {
let bin = saturating_bin_scalar(nits_arr[i], bin_arr[i]);
sub_hists[i * LightLevelHistogram::NUM_BINS + bin] += 1;
}
}
let row_max = local_max.reduce_max();
if row_max > *literal_max_nits {
*literal_max_nits = row_max;
}
*sum_nits += f64::from(local_sum.reduce_add());
let remainder = iter.remainder();
for chunk in remainder.chunks_exact(N) {
let r = chunk[0].max(0.0);
let g = chunk[1].max(0.0);
let b = chunk[2].max(0.0);
let m_rel = r.max(g).max(b);
let m_nits = m_rel * white_nits;
if m_nits > *literal_max_nits {
*literal_max_nits = m_nits;
}
*sum_nits += f64::from(m_nits);
sub_hists[bin_for_nits(m_nits)] += 1;
}
}
#[archmage::magetypes(define(f32x8), v3, neon, wasm128, scalar)]
pub(crate) fn accumulate_strip_luma_bt2020_tier<const N: usize>(
token: Token,
row: &[f32],
white_nits: f32,
sub_hists: &mut [u32],
sum_nits: &mut f64,
literal_max_nits: &mut f32,
) {
let zero = f32x8::zero(token);
let wn = f32x8::splat(token, white_nits);
let kr = f32x8::splat(token, KR);
let kg = f32x8::splat(token, KG);
let kb = f32x8::splat(token, KB);
let log2_min = f32x8::splat(token, LightLevelHistogram::LOG2_MIN);
let inv_step = f32x8::splat(token, LightLevelHistogram::inv_log2_step());
let bin_min_nits = f32x8::splat(token, LightLevelHistogram::BIN_MIN_NITS);
let num_bins_minus_1 = f32x8::splat(token, (LightLevelHistogram::NUM_BINS - 1) as f32);
let mut local_max = zero;
let mut local_sum = zero;
let mut chunks_since_flush = 0u32;
let mut iter = row.chunks_exact(LANES * N);
for chunk in &mut iter {
let mut ra = [0.0_f32; LANES];
let mut ga = [0.0_f32; LANES];
let mut ba = [0.0_f32; LANES];
for i in 0..LANES {
let base = i * N;
ra[i] = chunk[base];
ga[i] = chunk[base + 1];
ba[i] = chunk[base + 2];
}
let r = f32x8::load(token, &ra);
let g = f32x8::load(token, &ga);
let b = f32x8::load(token, &ba);
let r = f32x8::blend(r.simd_gt(zero), r, zero);
let g = f32x8::blend(g.simd_gt(zero), g, zero);
let b = f32x8::blend(b.simd_gt(zero), b, zero);
let y_rel = kr * r + kg * g + kb * b;
let y_nits = y_rel * wn;
local_max = local_max.max(y_nits);
local_sum += y_nits;
chunks_since_flush += 1;
if chunks_since_flush == SUM_FLUSH_CHUNKS {
*sum_nits += f64::from(local_sum.reduce_add());
local_sum = zero;
chunks_since_flush = 0;
}
let safe = y_nits.max(bin_min_nits);
let log2 = safe.log2_midp();
let bin_f = ((log2 - log2_min) * inv_step)
.max(zero)
.min(num_bins_minus_1);
let nits_arr = y_nits.to_array();
let bin_arr = bin_f.to_array();
for i in 0..LANES {
let bin = saturating_bin_scalar(nits_arr[i], bin_arr[i]);
sub_hists[i * LightLevelHistogram::NUM_BINS + bin] += 1;
}
}
let row_max = local_max.reduce_max();
if row_max > *literal_max_nits {
*literal_max_nits = row_max;
}
*sum_nits += f64::from(local_sum.reduce_add());
let remainder = iter.remainder();
for chunk in remainder.chunks_exact(N) {
let r = chunk[0].max(0.0);
let g = chunk[1].max(0.0);
let b = chunk[2].max(0.0);
let y_rel = KR * r + KG * g + KB * b;
let y_nits = y_rel * white_nits;
if y_nits > *literal_max_nits {
*literal_max_nits = y_nits;
}
*sum_nits += f64::from(y_nits);
sub_hists[bin_for_nits(y_nits)] += 1;
}
}
#[inline(always)]
fn saturating_bin_scalar(nits: f32, bin_f: f32) -> usize {
if nits <= LightLevelHistogram::BIN_MIN_NITS {
return 0;
}
if nits >= LightLevelHistogram::BIN_MAX_NITS {
return LightLevelHistogram::NUM_BINS - 1;
}
let b = bin_f as usize;
if b >= LightLevelHistogram::NUM_BINS {
LightLevelHistogram::NUM_BINS - 1
} else {
b
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn scan_max_mean_simd(
bytes: &[u8],
h: usize,
stride: usize,
channels: usize,
row_len: usize,
method: LightLevelMethod,
) -> (f32, f64) {
let mut max_rel = 0.0_f32;
let mut sum_rel = 0.0_f64;
for row in 0..h {
let row_bytes = &bytes[row * stride..row * stride + row_len];
let floats: &[f32] = bytemuck::cast_slice(row_bytes);
let (rm, rs) = match method {
LightLevelMethod::MaxRgb => {
if channels == 3 {
let mut rm = 0.0_f32;
let mut rs = 0.0_f64;
archmage::incant!(
scan_row_max_rgb_tier::<3>(floats, &mut rm, &mut rs),
[v3, neon, wasm128, scalar]
);
(rm, rs)
} else {
let mut rm = 0.0_f32;
let mut rs = 0.0_f64;
archmage::incant!(
scan_row_max_rgb_tier::<4>(floats, &mut rm, &mut rs),
[v3, neon, wasm128, scalar]
);
(rm, rs)
}
}
LightLevelMethod::LuminanceBt2020 => {
if channels == 3 {
let mut rm = 0.0_f32;
let mut rs = 0.0_f64;
archmage::incant!(
scan_row_luma_bt2020_tier::<3>(floats, &mut rm, &mut rs),
[v3, neon, wasm128, scalar]
);
(rm, rs)
} else {
let mut rm = 0.0_f32;
let mut rs = 0.0_f64;
archmage::incant!(
scan_row_luma_bt2020_tier::<4>(floats, &mut rm, &mut rs),
[v3, neon, wasm128, scalar]
);
(rm, rs)
}
}
};
max_rel = max_rel.max(rm);
sum_rel += rs;
}
(max_rel, sum_rel)
}
#[archmage::magetypes(define(f32x8), v3, neon, wasm128, scalar)]
pub(crate) fn scan_row_max_rgb_tier<const N: usize>(
token: Token,
row: &[f32],
row_max_rel: &mut f32,
row_sum_rel: &mut f64,
) {
let zero = f32x8::zero(token);
let mut local_max = zero;
let mut local_sum = zero;
let mut chunks_since_flush = 0u32;
let mut iter = row.chunks_exact(LANES * N);
for chunk in &mut iter {
let mut ra = [0.0_f32; LANES];
let mut ga = [0.0_f32; LANES];
let mut ba = [0.0_f32; LANES];
for i in 0..LANES {
let base = i * N;
ra[i] = chunk[base];
ga[i] = chunk[base + 1];
ba[i] = chunk[base + 2];
}
let r = f32x8::load(token, &ra);
let g = f32x8::load(token, &ga);
let b = f32x8::load(token, &ba);
let r = f32x8::blend(r.simd_gt(zero), r, zero);
let g = f32x8::blend(g.simd_gt(zero), g, zero);
let b = f32x8::blend(b.simd_gt(zero), b, zero);
let m = r.max(g).max(b);
local_max = local_max.max(m);
local_sum += m;
chunks_since_flush += 1;
if chunks_since_flush == SUM_FLUSH_CHUNKS {
*row_sum_rel += f64::from(local_sum.reduce_add());
local_sum = zero;
chunks_since_flush = 0;
}
}
*row_max_rel = local_max.reduce_max().max(*row_max_rel);
*row_sum_rel += f64::from(local_sum.reduce_add());
for chunk in iter.remainder().chunks_exact(N) {
let m = 0.0_f32.max(chunk[0]).max(chunk[1]).max(chunk[2]);
if m > *row_max_rel {
*row_max_rel = m;
}
*row_sum_rel += f64::from(m);
}
}
#[archmage::magetypes(define(f32x8), v3, neon, wasm128, scalar)]
pub(crate) fn scan_row_luma_bt2020_tier<const N: usize>(
token: Token,
row: &[f32],
row_max_rel: &mut f32,
row_sum_rel: &mut f64,
) {
let zero = f32x8::zero(token);
let kr = f32x8::splat(token, KR);
let kg = f32x8::splat(token, KG);
let kb = f32x8::splat(token, KB);
let mut local_max = zero;
let mut local_sum = zero;
let mut chunks_since_flush = 0u32;
let mut iter = row.chunks_exact(LANES * N);
for chunk in &mut iter {
let mut ra = [0.0_f32; LANES];
let mut ga = [0.0_f32; LANES];
let mut ba = [0.0_f32; LANES];
for i in 0..LANES {
let base = i * N;
ra[i] = chunk[base];
ga[i] = chunk[base + 1];
ba[i] = chunk[base + 2];
}
let r = f32x8::load(token, &ra);
let g = f32x8::load(token, &ga);
let b = f32x8::load(token, &ba);
let r = f32x8::blend(r.simd_gt(zero), r, zero);
let g = f32x8::blend(g.simd_gt(zero), g, zero);
let b = f32x8::blend(b.simd_gt(zero), b, zero);
let y = kr * r + kg * g + kb * b;
local_max = local_max.max(y);
local_sum += y;
chunks_since_flush += 1;
if chunks_since_flush == SUM_FLUSH_CHUNKS {
*row_sum_rel += f64::from(local_sum.reduce_add());
local_sum = zero;
chunks_since_flush = 0;
}
}
*row_max_rel = local_max.reduce_max().max(*row_max_rel);
*row_sum_rel += f64::from(local_sum.reduce_add());
for chunk in iter.remainder().chunks_exact(N) {
let r = chunk[0].max(0.0);
let g = chunk[1].max(0.0);
let b = chunk[2].max(0.0);
let y = KR * r + KG * g + KB * b;
if y > *row_max_rel {
*row_max_rel = y;
}
*row_sum_rel += f64::from(y);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;
use zenpixels::{PixelBuffer, PixelDescriptor};
fn rgbf32(pixels: &[[f32; 3]], w: u32, h: u32) -> PixelBuffer {
let mut data = Vec::with_capacity(pixels.len() * 12);
for p in pixels {
for c in p {
data.extend_from_slice(&c.to_ne_bytes());
}
}
PixelBuffer::from_vec(data, w, h, PixelDescriptor::RGBF32_LINEAR).unwrap()
}
#[test]
fn fast_log2_round_trips_through_fast_exp2_at_bin_edges() {
let inv_step = LightLevelHistogram::inv_log2_step();
for &i in &[0_usize, 1, 100, 500, 1023] {
let log2_edge = LightLevelHistogram::LOG2_MIN + (i as f32) / inv_step;
let recovered = fast_exp2(log2_edge);
let want = libm_pow2_oracle(f64::from(log2_edge));
let rel = (f64::from(recovered) - want).abs() / want;
assert!(
rel < 0.005,
"bin {i}: fast_exp2 mismatch: got {recovered} want {want}"
);
}
}
#[cfg(feature = "std")]
fn libm_pow2_oracle(x: f64) -> f64 {
x.exp2()
}
#[cfg(not(feature = "std"))]
fn libm_pow2_oracle(x: f64) -> f64 {
let i = x.floor() as i32;
let f = x - (i as f64);
let pf = 1.0
+ f * (0.693_147_180_559_945_3
+ f * (0.240_226_506_959_100_7 + f * 0.055_504_108_664_821_58));
let two_i = (1u64 << (i + 1023)) as f64 / (1u64 << 1023) as f64;
two_i * pf
}
#[test]
fn measure_histogram_empty_input_returns_zero_readouts() {
let owned: Vec<f32> = Vec::new();
let bytes: &[u8] = bytemuck::cast_slice(&owned);
let px = PixelSlice::new(bytes, 1, 0, 12, PixelDescriptor::RGBF32_LINEAR).unwrap();
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
px,
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(h.total_pixels(), 0);
assert_eq!(h.max(), 0.0);
assert_eq!(h.mean(), 0.0);
assert_eq!(h.percentile(0.5), 0.0);
}
#[test]
fn measure_max_matches_cta_literal_spec() {
let buf = rgbf32(&[[1.0; 3], [2.0; 3]], 2, 1);
let cll = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(cll.max_content_light_level, 406);
assert_eq!(cll.max_frame_average_light_level, 305);
}
#[test]
fn measure_max_luminance_bt2020_method_uses_luma_weights() {
let buf = rgbf32(&[[1.0, 0.0, 0.0]], 1, 1);
let cll = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::LuminanceBt2020,
)
.unwrap();
assert_eq!(cll.max_content_light_level, 53);
assert_eq!(cll.max_frame_average_light_level, 53);
let cll_max_rgb = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(cll_max_rgb.max_content_light_level, 203);
}
#[test]
fn defect_spike_percentile_drops_lone_outlier() {
let mut pixels = alloc::vec![[0.5_f32; 3]; 100];
pixels[0] = [50.0; 3]; let buf = rgbf32(&pixels, 10, 10);
let lit = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
lit.max_content_light_level >= 9000,
"defect spike: spec literal MaxCLL = {} (expected near 10000)",
lit.max_content_light_level
);
let pct = <ContentLightLevel as CllMeasure>::measure_percentile(
buf.as_slice(),
DiffuseWhite::BT2408,
0.99,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
pct.max_content_light_level < 200,
"p99 should drop the lone defect: got {}",
pct.max_content_light_level
);
}
#[test]
fn night_stars_literal_max_preserves_sparse_bright_content() {
let mut pixels: Vec<[f32; 3]> = alloc::vec![[0.005_f32; 3]; 1100];
for star in pixels.iter_mut().take(100) {
*star = [5.0; 3];
}
let buf = rgbf32(&pixels, 100, 11);
let lit = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
lit.max_content_light_level > 900 && lit.max_content_light_level < 1100,
"night stars: spec literal MaxCLL = {} (expected near 1015)",
lit.max_content_light_level
);
let pct_high = <ContentLightLevel as CllMeasure>::measure_percentile(
buf.as_slice(),
DiffuseWhite::BT2408,
0.9999,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
pct_high.max_content_light_level > 900,
"p99.99 must keep the stars (none are defects): got {}",
pct_high.max_content_light_level
);
let pct_low = <ContentLightLevel as CllMeasure>::measure_percentile(
buf.as_slice(),
DiffuseWhite::BT2408,
0.90,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
pct_low.max_content_light_level < 100,
"p90 demonstrably loses sparse-bright content: got {}",
pct_low.max_content_light_level
);
}
#[test]
fn percentile_zero_and_one_are_well_defined() {
let buf = rgbf32(&[[0.0; 3], [0.5; 3], [1.0; 3]], 3, 1);
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!((h.percentile(1.0) - 203.0).abs() < 0.01);
assert_eq!(h.percentile(0.0), 0.0);
}
#[test]
fn percentile_interpolates_within_bin_when_threshold_lands_high() {
let buf = rgbf32(&[[5.0_f32; 3]; 10_000], 100, 100);
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let p = h.percentile(0.9999);
assert!(
(1010.0..=1015.0).contains(&p),
"p99.99 interpolated within bin: expected ≈1015, got {p}"
);
}
#[test]
fn percentile_interpolation_never_exceeds_literal_max() {
let buf = rgbf32(&[[5.0; 3]; 1000], 100, 10);
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
for &p in &[0.5_f32, 0.9, 0.95, 0.99, 0.999, 0.9999, 0.99999] {
let v = h.percentile(p);
assert!(
v <= h.max() + 1e-3,
"p={p}: percentile {v} must not exceed literal max {}",
h.max()
);
}
}
#[test]
fn percentile_interpolation_beats_floor_precision_on_dense_content() {
let pixels: Vec<[f32; 3]> = alloc::vec![[5.0_f32; 3]; 1024 * 1024];
let buf = rgbf32(&pixels, 1024, 1024);
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let p = h.percentile(0.9999);
assert!(
(p - h.max()).abs() < 2.0,
"1 MP solid: interpolated p99.99 = {p}, literal max = {} \
(expected within ~1 nit; floor-of-bin would be ~1002)",
h.max()
);
}
#[test]
fn percentile_clamps_nan_and_out_of_range_inputs() {
let buf = rgbf32(&[[0.5; 3]], 1, 1);
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(h.percentile(f32::NAN), 0.0); assert!(h.percentile(2.0) > 0.0); assert_eq!(h.percentile(-0.5), 0.0); }
#[test]
fn measure_histogram_rejects_non_linear_or_non_rgb_f32() {
let desc = PixelDescriptor::RGBF32_LINEAR.with_transfer(TransferFunction::Srgb);
let mut data = Vec::new();
for c in [0.5_f32; 3] {
data.extend_from_slice(&c.to_ne_bytes());
}
let buf = PixelBuffer::from_vec(data, 1, 1, desc).unwrap();
assert!(
<ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.is_none()
);
let desc = PixelDescriptor::RGB8_SRGB;
let buf = PixelBuffer::from_vec(alloc::vec![0u8; 3], 1, 1, desc).unwrap();
assert!(
<ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.is_none()
);
}
#[test]
fn histogram_bins_exposed_and_sum_to_total() {
let buf = rgbf32(&[[0.1; 3], [0.5; 3], [1.0; 3], [2.0; 3], [10.0; 3]], 5, 1);
let h = <ContentLightLevel as CllMeasure>::measure_histogram(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let bin_total: u64 = h.bins().iter().map(|&c| c as u64).sum();
assert_eq!(bin_total, h.total_pixels());
assert_eq!(h.total_pixels(), 5);
assert_eq!(h.method(), LightLevelMethod::MaxRgb);
}
fn psychtoolbox_oracle_max_rgb(pixels: &[[f32; 3]], white_nits: f32) -> (f64, f64) {
let mut max_nits = 0.0_f64;
let mut sum_nits = 0.0_f64;
for px in pixels {
let r = (px[0] as f64).max(0.0);
let g = (px[1] as f64).max(0.0);
let b = (px[2] as f64).max(0.0);
let m_rel = r.max(g).max(b);
let m_nits = m_rel * (white_nits as f64);
if m_nits > max_nits {
max_nits = m_nits;
}
sum_nits += m_nits;
}
let mean_nits = sum_nits / (pixels.len() as f64);
(max_nits, mean_nits)
}
fn psychtoolbox_oracle_luma_bt2020(pixels: &[[f32; 3]], white_nits: f32) -> (f64, f64) {
let mut max_nits = 0.0_f64;
let mut sum_nits = 0.0_f64;
for px in pixels {
let r = (px[0] as f64).max(0.0);
let g = (px[1] as f64).max(0.0);
let b = (px[2] as f64).max(0.0);
let y = 0.2627 * r + 0.6780 * g + 0.0593 * b;
let y_nits = y * (white_nits as f64);
if y_nits > max_nits {
max_nits = y_nits;
}
sum_nits += y_nits;
}
let mean_nits = sum_nits / (pixels.len() as f64);
(max_nits, mean_nits)
}
#[test]
fn measure_max_matches_psychtoolbox_oracle_small_image() {
let pixels: Vec<[f32; 3]> = alloc::vec![
[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.5, 0.5, 0.5], [0.0; 3], [3.0, 2.5, 4.0], [0.18; 3], [0.95, 0.85, 0.05] ];
let buf = rgbf32(&pixels, pixels.len() as u32, 1);
let cll = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let (oracle_max, oracle_mean) =
psychtoolbox_oracle_max_rgb(&pixels, DiffuseWhite::BT2408.nits());
let want_max = nits_to_u16(oracle_max);
let want_fall = nits_to_u16(oracle_mean);
assert_eq!(cll.max_content_light_level, want_max);
assert_eq!(cll.max_frame_average_light_level, want_fall);
}
#[test]
fn measure_max_luma_bt2020_matches_oracle() {
let pixels: Vec<[f32; 3]> = alloc::vec![[1.0, 0.0, 0.0], [0.5, 0.5, 0.5], [2.0, 2.0, 2.0],];
let buf = rgbf32(&pixels, pixels.len() as u32, 1);
let cll = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::LuminanceBt2020,
)
.unwrap();
let (oracle_max, oracle_mean) =
psychtoolbox_oracle_luma_bt2020(&pixels, DiffuseWhite::BT2408.nits());
assert_eq!(cll.max_content_light_level, nits_to_u16(oracle_max));
assert_eq!(cll.max_frame_average_light_level, nits_to_u16(oracle_mean));
}
#[test]
fn measure_max_matches_oracle_at_strided_4mp_with_high_dr_outlier() {
const W: u32 = 2048;
const H: u32 = 2048;
let total = (W as usize) * (H as usize);
let mut pixels: Vec<[f32; 3]> = Vec::with_capacity(total);
for i in 0..total {
let t = (i as f32) / (total as f32);
pixels.push([t * 1.5, (1.0 - t) * 1.5, 0.5 + 0.25 * t]);
}
pixels[(W as usize) * (H as usize) / 2] = [25.0; 3];
let buf = rgbf32(&pixels, W, H);
let cll = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let (oracle_max, oracle_mean) =
psychtoolbox_oracle_max_rgb(&pixels, DiffuseWhite::BT2408.nits());
assert_eq!(cll.max_content_light_level, nits_to_u16(oracle_max));
let want_fall = nits_to_u16(oracle_mean);
let diff = (cll.max_frame_average_light_level as i32 - want_fall as i32).abs();
assert!(
diff <= 1,
"MaxFALL u16 diverged: got {} want {} (oracle f64={:.4})",
cll.max_frame_average_light_level,
want_fall,
oracle_mean
);
}
#[test]
fn measure_max_and_measure_histogram_max_agree_bit_exact() {
let pixels: Vec<[f32; 3]> = alloc::vec![
[0.1, 0.2, 0.3],
[1.5, 0.5, 0.25],
[0.0, 3.0, 0.5],
[0.7, 0.7, 0.7],
];
let buf = rgbf32(&pixels, pixels.len() as u32, 1);
let via_max = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let via_hist = measure_max_via_histogram_for_test(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(
via_max.max_content_light_level,
via_hist.max_content_light_level
);
assert_eq!(
via_max.max_frame_average_light_level,
via_hist.max_frame_average_light_level
);
}
#[test]
fn measure_max_smoothed_suppresses_single_pixel_defect() {
let mut pixels = alloc::vec![[0.0_f32; 3]; 10];
pixels[5] = [50.0; 3];
let buf = rgbf32(&pixels, 10, 1);
let lit = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
lit.max_content_light_level >= 9000,
"control: spec-literal keeps the spike, got {}",
lit.max_content_light_level
);
let sm = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let expected = (50.0_f64 / 3.0) * 203.0; let got = f64::from(sm.max_content_light_level);
assert!(
(got - expected).abs() < 2.0,
"3×1 mean of [0, 50, 0] = 50/3 → {expected:.1} nits, got {got}"
);
}
#[test]
fn measure_max_smoothed_preserves_three_pixel_cluster() {
let mut pixels = alloc::vec![[0.0_f32; 3]; 10];
pixels[4] = [5.0; 3];
pixels[5] = [5.0; 3];
pixels[6] = [5.0; 3];
let buf = rgbf32(&pixels, 10, 1);
let sm = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
sm.max_content_light_level >= 1010 && sm.max_content_light_level <= 1020,
"3-pixel cluster should preserve peak: got {}",
sm.max_content_light_level
);
}
#[test]
fn measure_max_smoothed_two_pixel_cluster_drops_to_two_thirds() {
let mut pixels = alloc::vec![[0.0_f32; 3]; 10];
pixels[4] = [9.0; 3];
pixels[5] = [9.0; 3];
let buf = rgbf32(&pixels, 10, 1);
let sm = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let expected = (2.0_f64 * 9.0 / 3.0) * 203.0; let got = f64::from(sm.max_content_light_level);
assert!(
(got - expected).abs() < 2.0,
"2-pixel cluster: expected {expected:.0}, got {got}"
);
}
#[test]
fn measure_max_smoothed_mean_matches_measure_max_mean() {
let pixels: Vec<[f32; 3]> = alloc::vec![
[0.1, 0.2, 0.3],
[1.5, 0.5, 0.25],
[0.0, 3.0, 0.5],
[0.7, 0.7, 0.7],
[50.0, 0.0, 0.0], [0.1, 0.2, 0.3],
];
let buf = rgbf32(&pixels, pixels.len() as u32, 1);
let strict = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let smooth = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(
strict.max_frame_average_light_level, smooth.max_frame_average_light_level,
"MaxFALL must match the spec-literal arithmetic mean"
);
assert!(
smooth.max_content_light_level < strict.max_content_light_level,
"smoothed must suppress the defect: strict={}, smooth={}",
strict.max_content_light_level,
smooth.max_content_light_level
);
}
#[test]
fn measure_max_smoothed_mirror_pad_handles_edge_defect() {
let mut pixels = alloc::vec![[0.0_f32; 3]; 10];
pixels[0] = [30.0; 3];
let buf = rgbf32(&pixels, 10, 1);
let sm = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let expected = (2.0_f64 * 30.0 / 3.0) * 203.0; let got = f64::from(sm.max_content_light_level);
assert!(
(got - expected).abs() < 2.0,
"edge defect with mirror pad: expected {expected:.0}, got {got}"
);
}
#[test]
fn measure_max_smoothed_degenerate_widths() {
let buf1 = rgbf32(&[[2.0; 3]], 1, 1);
let sm1 = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf1.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(sm1.max_content_light_level, 406);
let buf2 = rgbf32(&[[2.0; 3], [1.0; 3]], 2, 1);
let sm2 = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf2.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let expected2 = (5.0_f64 / 3.0) * 203.0;
let got2 = f64::from(sm2.max_content_light_level);
assert!(
(got2 - expected2).abs() < 1.0,
"width=2: expected {expected2:.0}, got {got2}"
);
}
#[test]
fn measure_max_smoothed_luma_bt2020_method() {
let mut pixels = alloc::vec![[0.0_f32; 3]; 10];
pixels[5] = [5.0, 0.0, 0.0];
let buf = rgbf32(&pixels, 10, 1);
let sm = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::LuminanceBt2020,
)
.unwrap();
let expected = (0.262_7_f64 * 5.0 / 3.0) * 203.0; let got = f64::from(sm.max_content_light_level);
assert!(
(got - expected).abs() < 2.0,
"luma method smoothed: expected {expected:.1}, got {got}"
);
}
#[test]
fn measure_max_smoothed_zero_image_returns_zero() {
let buf = rgbf32(&[[0.0; 3]; 4], 4, 1);
let sm = <ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(sm.max_content_light_level, 0);
assert_eq!(sm.max_frame_average_light_level, 0);
}
#[test]
fn measure_max_smoothed_rejects_non_linear_or_non_rgb_f32() {
let desc = PixelDescriptor::RGBF32_LINEAR.with_transfer(TransferFunction::Srgb);
let mut data = Vec::new();
for c in [0.5_f32; 3] {
data.extend_from_slice(&c.to_ne_bytes());
}
let buf = PixelBuffer::from_vec(data, 1, 1, desc).unwrap();
assert!(
<ContentLightLevel as CllMeasure>::measure_max_smoothed(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.is_none()
);
}
#[test]
fn default_percentile_constant_is_tail_tightest() {
assert_eq!(ContentLightLevel::DEFAULT_PERCENTILE, 0.99999);
}
#[test]
fn measure_robust_equals_measure_percentile_at_default() {
let pixels: Vec<[f32; 3]> = alloc::vec![
[0.1, 0.2, 0.3],
[1.5, 0.5, 0.25],
[0.0, 3.0, 0.5],
[0.7, 0.7, 0.7],
[50.0, 0.0, 0.0],
[0.1, 0.2, 0.3],
];
let buf = rgbf32(&pixels, pixels.len() as u32, 1);
for method in [LightLevelMethod::MaxRgb, LightLevelMethod::LuminanceBt2020] {
let robust = <ContentLightLevel as CllMeasure>::measure_robust(
buf.as_slice(),
DiffuseWhite::BT2408,
method,
)
.unwrap();
let pct = <ContentLightLevel as CllMeasure>::measure_percentile(
buf.as_slice(),
DiffuseWhite::BT2408,
ContentLightLevel::DEFAULT_PERCENTILE,
method,
)
.unwrap();
assert_eq!(robust.max_content_light_level, pct.max_content_light_level);
assert_eq!(
robust.max_frame_average_light_level,
pct.max_frame_average_light_level
);
}
}
#[test]
fn measure_robust_drops_dominant_defect_vs_measure_max() {
let mut pixels = alloc::vec![[0.5_f32; 3]; 100_000];
pixels[0] = [50.0; 3]; let buf = rgbf32(&pixels, 1000, 100);
let strict = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
let robust = <ContentLightLevel as CllMeasure>::measure_robust(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(strict.max_content_light_level >= 9000);
assert!(
robust.max_content_light_level < 200,
"measure_robust must drop the single defect: got {}",
robust.max_content_light_level
);
assert_eq!(
strict.max_frame_average_light_level,
robust.max_frame_average_light_level
);
}
#[test]
fn measure_robust_preserves_dense_bright_content() {
let mut pixels: Vec<[f32; 3]> = alloc::vec![[0.005_f32; 3]; 1100];
for star in pixels.iter_mut().take(100) {
*star = [5.0; 3];
}
let buf = rgbf32(&pixels, 100, 11);
let robust = <ContentLightLevel as CllMeasure>::measure_robust(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
robust.max_content_light_level >= 1010 && robust.max_content_light_level <= 1020,
"dense bright content: robust must preserve the peak: got {}",
robust.max_content_light_level
);
}
#[test]
fn measure_robust_sparse_bright_cliff() {
let mut pixels = alloc::vec![[0.005_f32; 3]; 100];
pixels[0] = [5.0; 3]; let buf = rgbf32(&pixels, 10, 10);
let robust = <ContentLightLevel as CllMeasure>::measure_robust(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(
robust.max_content_light_level < 50,
"sparse-bright cliff: 1-in-100 bright pixel must be dropped: got {}",
robust.max_content_light_level
);
let strict = <ContentLightLevel as CllMeasure>::measure_max(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert!(strict.max_content_light_level > 900);
}
#[test]
fn measure_robust_rejects_non_linear_or_non_rgb_f32() {
let desc = PixelDescriptor::RGBF32_LINEAR.with_transfer(TransferFunction::Srgb);
let mut data = Vec::new();
for c in [0.5_f32; 3] {
data.extend_from_slice(&c.to_ne_bytes());
}
let buf = PixelBuffer::from_vec(data, 1, 1, desc).unwrap();
assert!(
<ContentLightLevel as CllMeasure>::measure_robust(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.is_none()
);
}
#[test]
fn measure_robust_zero_image_returns_zero() {
let buf = rgbf32(&[[0.0; 3]; 4], 4, 1);
let robust = <ContentLightLevel as CllMeasure>::measure_robust(
buf.as_slice(),
DiffuseWhite::BT2408,
LightLevelMethod::MaxRgb,
)
.unwrap();
assert_eq!(robust.max_content_light_level, 0);
assert_eq!(robust.max_frame_average_light_level, 0);
}
}