use super::*;
const EPSILON_F32: f32 = 4e-6;
fn assert_close(a: f32, b: f32, tag: &str) {
let diff = (a - b).abs();
assert!(diff <= EPSILON_F32, "{tag}: {a} vs {b} (diff {diff})");
}
#[test]
fn smpte428_inverse_oetf_zero_is_zero() {
assert_eq!(smpte428_inverse_oetf(0), 0.0);
}
#[test]
fn smpte428_inverse_oetf_max_is_normalised() {
let actual = smpte428_inverse_oetf(4095);
assert!((actual - 1.0_f32 / 0.91653_f32).abs() < EPSILON_F32);
}
#[test]
#[cfg_attr(miri, ignore = "f32::powf is non-deterministic under Miri")]
fn smpte428_inverse_oetf_masks_upper_bits() {
let clean = smpte428_inverse_oetf(0x0800);
let dirty = smpte428_inverse_oetf(0xF800);
assert_eq!(clean, dirty);
}
#[test]
fn read_xyz12_sample_extracts_high_bit_packed_code_le() {
assert_eq!(read_xyz12_sample::<false>(pack12_le(0x800)), 0x800);
assert_eq!(read_xyz12_sample::<false>(pack12_le(0xFFF)), 0x0FFF);
assert_eq!(read_xyz12_sample::<false>(pack12_le(0x000)), 0x0000);
}
#[test]
fn read_xyz12_sample_extracts_high_bit_packed_code_be() {
assert_eq!(read_xyz12_sample::<true>(pack12_be(0x800)), 0x800);
}
#[test]
fn smpte428_mid_gray_high_bit_packed_is_nonzero() {
let mid_gray = read_xyz12_sample::<false>(pack12_le(0x800));
assert_eq!(mid_gray, 0x800);
let xyz_lin = smpte428_inverse_oetf(mid_gray);
assert!(xyz_lin > 0.1, "expected mid-gray > 0.1, got {xyz_lin}");
}
#[test]
fn oetf_srgb_zero_is_zero() {
assert_eq!(oetf_srgb(0.0), 0.0);
}
#[test]
fn oetf_srgb_uses_linear_below_threshold() {
let c = 0.001_f32;
let expected = 12.92_f32 * c;
assert_eq!(oetf_srgb(c), expected);
}
#[test]
fn oetf_srgb_one_is_one() {
let v = oetf_srgb(1.0);
assert!((v - 1.0).abs() < EPSILON_F32);
}
#[test]
fn oetf_srgb_continuous_at_threshold() {
let lo = oetf_srgb(0.0031307);
let hi = oetf_srgb(0.0031309);
assert!((hi - lo).abs() < 1e-5);
}
fn oetf_srgb_reference_f64(c: f32) -> f32 {
if c < 0.0031308_f32 {
12.92_f32 * c
} else {
let c64 = c as f64;
(1.055_f64 * powf64(c64, 1.0_f64 / 2.4_f64) - 0.055_f64) as f32
}
}
fn f32_to_sortable(x: f32) -> i64 {
let bits = x.to_bits() as i32;
if bits >= 0 {
bits as i64
} else {
(i32::MIN as i64) - (bits as i64)
}
}
fn f32_ulps(a: f32, b: f32) -> u64 {
let a_b = f32_to_sortable(a);
let b_b = f32_to_sortable(b);
a_b.abs_diff(b_b)
}
#[test]
fn oetf_srgb_below_threshold_uses_linear() {
let c = 0.001_f32;
assert_eq!(oetf_srgb(c), 12.92_f32 * c);
assert_eq!(oetf_srgb(c), oetf_srgb_reference_f64(c));
}
#[test]
fn oetf_srgb_polynomial_within_2_ulp_of_reference() {
const SAMPLES: usize = 65_536;
let lo = 0.003_130_8_f64;
let hi = 1.0_f64;
let mut max_ulp = 0_u64;
let mut max_x = 0.0_f32;
for i in 0..SAMPLES {
let t = (i as f64) / ((SAMPLES - 1) as f64);
let c = (lo + (hi - lo) * t) as f32;
let poly = oetf_srgb(c);
let reference = oetf_srgb_reference_f64(c);
let u = f32_ulps(poly, reference);
if u > max_ulp {
max_ulp = u;
max_x = c;
}
}
assert!(
max_ulp <= 2,
"polynomial OETF exceeded 2 ULP vs f64-narrowed reference: max = {} at x = {}",
max_ulp,
max_x,
);
}
#[test]
fn oetf_srgb_at_segment_boundary_within_2_ulp() {
let c = 0.003_131_f32;
let poly = oetf_srgb(c);
let reference = oetf_srgb_reference_f64(c);
assert!(
f32_ulps(poly, reference) <= 2,
"polynomial vs reference at boundary: {} vs {}",
poly,
reference,
);
}
#[test]
fn narrow_unit_to_u8_round_half_up() {
assert_eq!(narrow_unit_to_u8(0.0), 0);
assert_eq!(narrow_unit_to_u8(1.0), 255);
assert_eq!(narrow_unit_to_u8(0.5_f32), 128);
assert_eq!(narrow_unit_to_u8(-1.0), 0);
assert_eq!(narrow_unit_to_u8(2.0), 255);
}
#[test]
fn narrow_unit_to_u16_round_half_up() {
assert_eq!(narrow_unit_to_u16(0.0), 0);
assert_eq!(narrow_unit_to_u16(1.0), 65535);
assert_eq!(narrow_unit_to_u16(-1.0), 0);
assert_eq!(narrow_unit_to_u16(2.0), 65535);
}
#[test]
fn xyz12_to_rgb_f32_rec709_zero_input() {
let xyz = [0_u16; 3];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec709);
assert_eq!(out, [0.0; 3]);
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn pack12_le(code: u16) -> u16 {
u16::from_ne_bytes((code << 4).to_le_bytes())
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn pack12_be(code: u16) -> u16 {
u16::from_ne_bytes((code << 4).to_be_bytes())
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn pack12_le_dirty(code: u16, low_bits: u16) -> u16 {
u16::from_ne_bytes(((code << 4) | (low_bits & 0xF)).to_le_bytes())
}
#[test]
fn xyz12_to_rgb_f32_dci_p3_mid_gray() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_close(out[0], 0.228_194_8, "R");
assert_close(out[1], 0.165_165_9, "G");
assert_close(out[2], 0.189_893_85, "B");
}
#[test]
fn xyz12_to_rgb_f32_rec709_mid_gray() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec709);
assert_close(out[0], 0.216_984_87, "R");
assert_close(out[1], 0.170_760_4, "G");
assert_close(out[2], 0.163_619_68, "B");
}
#[test]
fn xyz12_to_rgb_f32_rec2020_three_quarter() {
let xyz: [u16; 3] = [pack12_le(0xC00), pack12_le(0xC00), pack12_le(0xC00)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec2020);
assert_close(out[0], 0.572_369_93, "R");
assert_close(out[1], 0.498_964_94, "G");
assert_close(out[2], 0.473_854_f32, "B");
}
#[test]
fn xyz12_to_rgb_f32_preserves_negative_after_matrix() {
let xyz: [u16; 3] = [pack12_le(0), pack12_le(0xFFF), pack12_le(0)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec709);
assert!(out[0] < 0.0, "expected negative R, got {}", out[0]);
assert!(out[2] < 0.0, "expected negative B, got {}", out[2]);
assert_close(out[0], -1.677_395_3, "R");
assert_close(out[1], 2.046_815_2, "G");
assert_close(out[2], -0.222_553_5, "B");
}
#[test]
fn xyz12_to_rgb_clamps_at_u8() {
let xyz: [u16; 3] = [pack12_le(0xFFF), pack12_le(0), pack12_le(0)];
let mut out = [0_u8; 3];
xyz12_to_rgb_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec709);
assert_eq!(out[0], 255);
assert_eq!(out[1], 0);
}
#[test]
fn xyz12_to_rgba_fills_alpha_max() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0_u8; 4];
xyz12_to_rgba_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_eq!(out[3], 0xFF);
}
#[test]
fn xyz12_to_rgba_u16_fills_alpha_max() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0_u16; 4];
xyz12_to_rgba_u16_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_eq!(out[3], 0xFFFF);
}
#[test]
fn xyz12_to_xyz_f32_lossless_round_trip() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0.0_f32; 3];
xyz12_to_xyz_f32_row::<false>(&xyz, &mut out, 1);
let expected = powf32(0x800_u16 as f32 * INV_4095, 2.6_f32) * SMPTE428_INV_NORM;
assert_close(out[0], expected, "X");
assert_close(out[1], expected, "Y");
assert_close(out[2], expected, "Z");
}
#[test]
#[cfg_attr(miri, ignore = "f32::powf is non-deterministic under Miri")]
fn xyz12_be_byte_swap_matches_le() {
let xyz_le: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let xyz_be: [u16; 3] = [pack12_be(0x800), pack12_be(0x800), pack12_be(0x800)];
let mut out_le = [0.0_f32; 3];
let mut out_be = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz_le, &mut out_le, 1, DcpTargetGamut::DciP3);
xyz12_to_rgb_f32_row::<true>(&xyz_be, &mut out_be, 1, DcpTargetGamut::DciP3);
assert_eq!(out_le, out_be);
}
#[test]
fn xyz12_to_rgb_u16_full_range_scaling() {
let xyz: [u16; 3] = [pack12_le(0xFFF), pack12_le(0xFFF), pack12_le(0xFFF)];
let mut out = [0_u16; 3];
xyz12_to_rgb_u16_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_eq!(out[0], 65535);
assert_eq!(out[1], 65535);
assert_eq!(out[2], 65535);
}
#[test]
#[cfg_attr(
miri,
ignore = "half::f16 uses inline assembly on aarch64 unsupported by Miri"
)]
fn xyz12_to_rgb_f16_clamps_to_unit_range() {
let xyz: [u16; 3] = [pack12_le(0xFFF), pack12_le(0), pack12_le(0)];
let mut out = [half::f16::from_f32(0.0); 3];
xyz12_to_rgb_f16_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec709);
assert_eq!(out[0].to_f32(), 1.0);
assert_eq!(out[1].to_f32(), 0.0);
}
#[test]
#[cfg_attr(
miri,
ignore = "half::f16 uses inline assembly on aarch64 unsupported by Miri"
)]
fn xyz12_to_rgba_f16_alpha_one() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [half::f16::from_f32(0.0); 4];
xyz12_to_rgba_f16_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_eq!(out[3].to_f32(), 1.0);
}
#[test]
fn xyz12_to_rgb_target_gamut_changes_output() {
let xyz: [u16; 3] = [pack12_le(0xC00), pack12_le(0xC00), pack12_le(0xC00)];
let mut out_p3 = [0.0_f32; 3];
let mut out_709 = [0.0_f32; 3];
let mut out_2020 = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out_p3, 1, DcpTargetGamut::DciP3);
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out_709, 1, DcpTargetGamut::Rec709);
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out_2020, 1, DcpTargetGamut::Rec2020);
assert!(
(out_p3[0] - out_709[0]).abs() > 1e-3,
"DCI-P3 vs Rec.709 R: {} vs {}",
out_p3[0],
out_709[0],
);
assert!(
(out_p3[0] - out_2020[0]).abs() > 1e-3,
"DCI-P3 vs Rec.2020 R: {} vs {}",
out_p3[0],
out_2020[0],
);
}
#[test]
fn xyz12_to_rgb_low_4_bits_ignored() {
let xyz_clean: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let xyz_dirty: [u16; 3] = [
pack12_le_dirty(0x800, 0xF),
pack12_le_dirty(0x800, 0xA),
pack12_le_dirty(0x800, 0x7),
];
let mut out_clean = [0_u8; 3];
let mut out_dirty = [0_u8; 3];
xyz12_to_rgb_row::<false>(&xyz_clean, &mut out_clean, 1, DcpTargetGamut::DciP3);
xyz12_to_rgb_row::<false>(&xyz_dirty, &mut out_dirty, 1, DcpTargetGamut::DciP3);
assert_eq!(out_clean, out_dirty);
}
#[test]
fn xyz12_to_rgb_multi_pixel_independence() {
let xyz: [u16; 6] = [
pack12_le(0x800),
pack12_le(0x800),
pack12_le(0x800), pack12_le(0xFFF),
pack12_le(0),
pack12_le(0), ];
let mut out = [0_u8; 6];
xyz12_to_rgb_row::<false>(&xyz, &mut out, 2, DcpTargetGamut::Rec709);
let mut single = [0_u8; 3];
xyz12_to_rgb_row::<false>(&xyz[..3], &mut single, 1, DcpTargetGamut::Rec709);
assert_eq!(&out[..3], &single);
let mut single1 = [0_u8; 3];
xyz12_to_rgb_row::<false>(&xyz[3..], &mut single1, 1, DcpTargetGamut::Rec709);
assert_eq!(&out[3..], &single1);
}
#[test]
fn xyz12_dci_p3_zero_input_zero_output_reference() {
let xyz: [u16; 3] = [pack12_le(0), pack12_le(0), pack12_le(0)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_eq!(out, [0.0, 0.0, 0.0]);
}
#[test]
fn xyz12_dci_p3_mid_gray_reference_dci_white() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_close(out[0], 0.228_194_8, "DciP3 mid-gray R");
assert_close(out[1], 0.165_165_9, "DciP3 mid-gray G");
assert_close(out[2], 0.189_893_85, "DciP3 mid-gray B");
assert!(
(out[0] - 0.216_984_87_f32).abs() > 1e-3,
"must differ from Rec.709 R"
);
assert!(
(out[2] - 0.163_619_68_f32).abs() > 1e-3,
"must differ from Rec.709 B"
);
}
#[test]
fn xyz12_dci_p3_peak_white_reference() {
let xyz: [u16; 3] = [pack12_le(0xFFF), pack12_le(0xFFF), pack12_le(0xFFF)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_close(out[0], 1.382_636_5, "DciP3 peak R");
assert_close(out[1], 1.000_743_3, "DciP3 peak G");
assert_close(out[2], 1.150_570_4, "DciP3 peak B");
}
#[test]
fn xyz12_dci_p3_x_only_axis_reference() {
let xyz: [u16; 3] = [pack12_le(0xFFF), pack12_le(0), pack12_le(0)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::DciP3);
assert_close(out[0], 2.973_600_4, "DciP3 X-only R");
assert_close(out[1], -0.867_585_36, "DciP3 X-only G");
assert_close(out[2], 0.044_997_863, "DciP3 X-only B");
assert!(out[1] < 0.0, "X-only must produce negative G under DciP3");
}
#[test]
fn xyz12_rec709_mid_gray_reference() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec709);
assert_close(out[0], 0.216_984_87, "Rec.709 mid-gray R");
assert_close(out[1], 0.170_760_4, "Rec.709 mid-gray G");
assert_close(out[2], 0.163_619_68, "Rec.709 mid-gray B");
}
#[test]
fn xyz12_rec2020_mid_gray_reference() {
let xyz: [u16; 3] = [pack12_le(0x800), pack12_le(0x800), pack12_le(0x800)];
let mut out = [0.0_f32; 3];
xyz12_to_rgb_f32_row::<false>(&xyz, &mut out, 1, DcpTargetGamut::Rec2020);
assert_close(out[0], 0.199_452_52, "Rec.2020 mid-gray R");
assert_close(out[1], 0.173_873_25, "Rec.2020 mid-gray G");
assert_close(out[2], 0.165_122_9, "Rec.2020 mid-gray B");
}
#[test]
fn xyz12_rgb_to_luma_dci_p3_pure_red_reference() {
let rgb: [u8; 3] = [255, 0, 0];
let mut out_p3 = [0_u8; 1];
let mut out_709 = [0_u8; 1];
let p3 = (6865_i32, 23645_i32, 2258_i32); let bt709 = (6966_i32, 23436_i32, 2366_i32); xyz12_rgb_to_luma_row(&rgb, &mut out_p3, 1, p3);
xyz12_rgb_to_luma_row(&rgb, &mut out_709, 1, bt709);
assert_eq!(out_p3[0], 53, "DciP3 luma(255,0,0) must be 53");
assert_eq!(out_709[0], 54, "Bt709 luma(255,0,0) must be 54 (control)");
assert_ne!(
out_p3[0], out_709[0],
"DciP3 and Bt709 luma weights must differ for saturated red",
);
}
#[test]
fn xyz12_rgb_to_luma_u16_dci_p3_pure_green_reference() {
let rgb: [u8; 3] = [0, 255, 0];
let mut out_p3 = [0_u16; 1];
let mut out_709 = [0_u16; 1];
let p3 = (6865_i32, 23645_i32, 2258_i32);
let bt709 = (6966_i32, 23436_i32, 2366_i32);
xyz12_rgb_to_luma_u16_row(&rgb, &mut out_p3, 1, p3);
xyz12_rgb_to_luma_u16_row(&rgb, &mut out_709, 1, bt709);
assert_eq!(out_p3[0], 184, "DciP3 luma_u16(0,255,0) must be 184");
assert_eq!(
out_709[0], 182,
"Bt709 luma_u16(0,255,0) must be 182 (control)"
);
assert_ne!(
out_p3[0], out_709[0],
"DciP3 and Bt709 luma weights must differ for saturated green",
);
}
#[test]
fn xyz12_rgb_to_luma_uniform_gray_reproduces_input_all_gamuts() {
let rgb: [u8; 3] = [128, 128, 128];
let mut out = [0_u8; 1];
for triple in [
(6865_i32, 23645, 2258), (6966_i32, 23436, 2366), (8607_i32, 22217, 1944), ] {
out[0] = 0;
xyz12_rgb_to_luma_row(&rgb, &mut out, 1, triple);
assert_eq!(
out[0], 128,
"uniform gray must reproduce input under {:?}",
triple
);
}
}