use super::{Direction, ToneMapCurve, ToneMapper};
const fn px(r: u16, g: u16, b: u16, a: u16) -> u64 {
(r as u64) | ((g as u64) << 16) | ((b as u64) << 32) | ((a as u64) << 48)
}
fn mapper_for(name: &str) -> ToneMapper {
match name {
"up" => ToneMapper::bt709_to_bt2020_pq(100.0),
"up203" => ToneMapper::bt709_to_bt2020_pq(203.0),
"down" => ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::Reinhard),
"down_hable" => ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::Hable),
"down_none" => ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::None),
"hlg" => ToneMapper::hlg_to_bt709(100.0),
other => panic!("unknown mapper {other}"),
}
}
const CASES: &[(&str, &str, u64, u64)] = &[
("up_black", "up", px(0, 0, 0, 0xFFFF), 0xFFFF_0000_0000_0000),
(
"up_white",
"up",
px(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF),
0xFFFF_820C_820C_820C,
),
(
"up_mid_grey",
"up",
px(0x8000, 0x8000, 0x8000, 0xFFFF),
0xFFFF_5CA0_5CA0_5CA0,
),
(
"up_red",
"up",
px(0xFFFF, 0, 0, 0xFFFF),
0xFFFF_0CCE_35F2_7639,
),
(
"up_alpha_kept",
"up",
px(0x4000, 0x4000, 0x4000, 0x1234),
0x1234_27B9_27B9_27B9,
),
(
"up203_mid_grey",
"up203",
px(0x8000, 0x8000, 0x8000, 0xFFFF),
0xFFFF_6D2E_6D2E_6D2E,
),
(
"down_black",
"down",
px(0, 0, 0, 0xFFFF),
0xFFFF_0000_0000_0000,
),
(
"down_white",
"down",
px(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF),
0xFFFF_FEF0_FEF0_FEF0,
),
(
"down_mid",
"down",
px(0x8000, 0x8000, 0x8000, 0xFFFF),
0xFFFF_BC84_BC85_BC86,
),
(
"down_green",
"down",
px(0, 0xC000, 0, 0xFFFF),
0xFFFF_0000_F6FD_0000,
),
(
"down_hable_mid",
"down_hable",
px(0x8000, 0x8000, 0x8000, 0xFFFF),
0xFFFF_97EC_97ED_97EF,
),
(
"down_none_mid",
"down_none",
px(0x8000, 0x8000, 0x8000, 0xFFFF),
0xFFFF_F785_F788_F78A,
),
(
"hlg_black",
"hlg",
px(0, 0, 0, 0xFFFF),
0xFFFF_0000_0000_0000,
),
(
"hlg_half",
"hlg",
px(0x8000, 0x8000, 0x8000, 0xFFFF),
0xFFFF_AABA_AABB_AABC,
),
(
"hlg_white",
"hlg",
px(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF),
0xFFFF_F799_F799_F799,
),
];
#[test]
fn the_tone_mapper_matches_drm_cxx() {
let mut wrong = Vec::new();
for (name, which, input, want) in CASES {
let got = mapper_for(which).map(*input);
if got != *want {
wrong.push(format!("{name}: got {got:016X}, drm-cxx gives {want:016X}"));
}
}
assert!(
wrong.is_empty(),
"the tone mapper diverges from the reference:\n{}",
wrong.join("\n")
);
}
#[test]
fn alpha_passes_through_unchanged() {
let mapper = ToneMapper::bt709_to_bt2020_pq(100.0);
for alpha in [0u16, 0x1234, 0x8000, 0xFFFF] {
let out = mapper.map(px(0x4000, 0x8000, 0xC000, alpha));
assert_eq!(
(out >> 48) as u16,
alpha,
"alpha {alpha:#06x} was altered by a colour transform"
);
}
}
#[test]
fn black_is_preserved_in_every_direction() {
let mappers = [
ToneMapper::bt709_to_bt2020_pq(100.0),
ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::Reinhard),
ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::Hable),
ToneMapper::hlg_to_bt709(100.0),
];
for mapper in &mappers {
let out = mapper.map(px(0, 0, 0, 0xFFFF));
assert_eq!(
out,
px(0, 0, 0, 0xFFFF),
"{:?} moved black",
mapper.direction()
);
}
}
#[test]
fn a_ramp_stays_monotonic() {
for mapper in [
ToneMapper::bt709_to_bt2020_pq(100.0),
ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::Reinhard),
ToneMapper::hlg_to_bt709(100.0),
] {
let mut previous = 0u16;
for step in 0..=255u32 {
let v = u16::try_from(step * 257).expect("a 0..=255 ramp expands inside u16");
let out = (mapper.map(px(v, v, v, 0xFFFF)) & 0xFFFF) as u16;
assert!(
out >= previous,
"{:?} dipped at input {v:#06x}: {out:#06x} after {previous:#06x}",
mapper.direction()
);
previous = out;
}
}
}
#[test]
fn a_higher_sdr_white_encodes_brighter() {
let dim = ToneMapper::bt709_to_bt2020_pq(100.0);
let bright = ToneMapper::bt709_to_bt2020_pq(203.0);
let input = px(0x8000, 0x8000, 0x8000, 0xFFFF);
assert!(
(bright.map(input) & 0xFFFF) > (dim.map(input) & 0xFFFF),
"203-nit SDR white must encode above 100-nit"
);
}
#[test]
fn apply_matches_the_single_pixel_path() {
let mapper = ToneMapper::bt2020_pq_to_bt709(100.0, ToneMapCurve::Reinhard);
let src: Vec<u64> = (0..64u16)
.map(|i| px(i * 1024, 0x4000, 0x8000, 0xFFFF))
.collect();
let mut dst = vec![0u64; src.len()];
mapper.apply(&src, &mut dst);
for (i, pixel) in src.iter().enumerate() {
assert_eq!(dst[i], mapper.map(*pixel), "pixel {i} differs");
}
}
#[test]
fn a_mapper_reports_its_configuration() {
let m = ToneMapper::bt2020_pq_to_bt709(203.0, ToneMapCurve::Hable);
assert_eq!(m.direction(), Direction::Bt2020PqToBt709);
assert_eq!(m.curve(), ToneMapCurve::Hable);
assert!((m.target_max_nits() - 203.0).abs() < f32::EPSILON);
}
#[cfg(feature = "edid")]
mod edid {
use crate::edid::{EdidError, parse_edid};
const DELL: [u8; 128] = [
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x10, 0xAC, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11, 0x01, 0x03, 0x80, 0x34, 0x20, 0x78, 0xEA, 0xEE, 0x95, 0xA3, 0x54, 0x4C, 0x99, 0x26, 0x0F, 0x50, 0x54, 0xA5, 0x4B, 0x00, 0x71, 0x4F, 0x81, 0x80, 0xA9, 0xC0, 0xD1, 0xC0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3A, 0x80, 0x18, 0x71, 0x38, 0x2D, 0x40, 0x58, 0x2C, 0x45, 0x00, 0x09, 0x25, 0x21, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x46, 0x4F, 0x4F, 0x42, 0x41, 0x52, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x44, 0x45, 0x4C, 0x4C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x38, 0x4C, 0x1E, 0x51, 0x11, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x3F, ];
#[test]
fn the_fixture_checksums() {
let sum: u32 = DELL.iter().map(|b| u32::from(*b)).sum();
assert_eq!(
sum % 256,
0,
"EDID requires all 128 bytes to sum to zero mod 256"
);
}
#[test]
fn the_fixture_states_a_refresh_range() {
let info = parse_edid(&DELL).expect("parse");
let range = info.vrefresh_range.expect("the 0xFD descriptor is there");
assert_eq!((range.min_hz, range.max_hz), (56, 76));
assert!(range.is_variable(), "a range, not a single rate");
}
#[test]
fn a_blob_without_the_descriptor_states_no_range() {
let mut blob = DELL;
blob[0x6F] = 0xFE;
let sum: u32 = blob[..127].iter().map(|b| u32::from(*b)).sum();
blob[127] = u8::try_from((256 - (sum % 256)) % 256).expect("a checksum byte");
let info = parse_edid(&blob).expect("still a valid EDID");
assert_eq!(info.vrefresh_range, None);
}
#[test]
fn a_real_blob_matches_the_reference() {
let info = parse_edid(&DELL).expect("a checksummed EDID must parse");
assert_eq!(info.make, "Dell Inc.");
assert_eq!(info.model, "DELL");
assert_eq!(info.name, "Dell Inc. DELL");
assert_eq!(info.serial.as_deref(), Some("FOOBAR"));
assert_eq!((info.width_mm, info.height_mm), (520, 320));
let c = info
.colorimetry
.expect("this blob carries chromaticity bytes");
assert!(c.has_primaries && c.has_default_white);
let close = |a: f32, b: f32| (a - b).abs() < 1e-5;
assert!(
close(c.red.x, 0.639_648) && close(c.red.y, 0.330_078),
"red {:?}",
c.red
);
assert!(
close(c.green.x, 0.299_805) && close(c.green.y, 0.599_609),
"green {:?}",
c.green
);
assert!(
close(c.blue.x, 0.150_391) && close(c.blue.y, 0.059_570),
"blue {:?}",
c.blue
);
assert!(
close(c.white.x, 0.313_477) && close(c.white.y, 0.329_102),
"white {:?}",
c.white
);
}
#[test]
fn a_display_advertising_no_hdr_reports_none() {
let info = parse_edid(&DELL).expect("parse");
assert_eq!(
info.hdr, None,
"a pre-CTA EDID 1.3 blob carries no HDR static metadata block"
);
assert_eq!(info.wide_gamut, None, "nor any extended colorimetry block");
}
#[test]
fn an_empty_blob_is_refused() {
assert!(matches!(parse_edid(&[]), Err(EdidError::Unparseable)));
}
#[test]
fn garbage_is_refused() {
assert!(matches!(
parse_edid(&[0xDE, 0xAD, 0xBE, 0xEF]),
Err(EdidError::Unparseable)
));
}
#[test]
fn a_bad_checksum_is_refused() {
let mut broken = DELL;
broken[127] = 0x21; assert!(
matches!(parse_edid(&broken), Err(EdidError::Unparseable)),
"a blob that does not checksum must not parse"
);
}
}
#[cfg(feature = "edid")]
#[test]
fn edid_fuzz_invariants_hold_on_random_blobs() {
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut parsed = 0usize;
for round in 0..20_000u32 {
let len = match round % 4 {
0 => 128,
1 => 256,
2 => usize::try_from(next() % 300).unwrap_or(0),
_ => usize::try_from(next() % 32).unwrap_or(0),
};
let mut blob: Vec<u8> = (0..len)
.map(|_| u8::try_from(next() & 0xFF).expect("masked to a byte"))
.collect();
if round % 2 == 0 && blob.len() >= 8 {
blob[..8].copy_from_slice(&[0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00]);
if blob.len() >= 128 {
let sum: u32 = blob[..127].iter().map(|b| u32::from(*b)).sum();
blob[127] = u8::try_from((256 - (sum % 256)) % 256).expect("a byte");
}
}
let Ok(info) = crate::edid::parse_edid(&blob) else {
continue;
};
parsed += 1;
if !info.make.is_empty() {
assert!(
info.name.contains(&info.make),
"name {:?} omits make {:?}",
info.name,
info.make
);
}
if !info.model.is_empty() {
assert!(
info.name.contains(&info.model),
"name {:?} omits model {:?}",
info.name,
info.model
);
}
assert!(
info.width_mm >= 0 && info.height_mm >= 0,
"negative size {} x {}",
info.width_mm,
info.height_mm
);
if let Some(hdr) = info.hdr {
assert!(
hdr.type1 || hdr.traditional_hdr || hdr.pq || hdr.hlg,
"empty HDR {hdr:?}"
);
}
if let Some(g) = info.wide_gamut {
assert!(
g.bt2020_cycc || g.bt2020_ycc || g.bt2020_rgb || g.st2113_rgb || g.ictcp,
"empty gamut {g:?}"
);
}
}
eprintln!("note: {parsed} of 20000 random blobs parsed");
assert!(
parsed > 0,
"no blob parsed at all; the smoke proved nothing"
);
}
#[cfg(feature = "edid")]
#[test]
fn every_fuzz_seed_parses() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../fuzz/seeds/edid_blob");
let mut checked = 0;
for entry in std::fs::read_dir(dir).expect("the seed corpus must exist") {
let path = entry.expect("entry").path();
let blob = std::fs::read(&path).expect("read seed");
assert!(
crate::edid::parse_edid(&blob).is_ok(),
"seed {} does not parse, so it teaches the fuzzer nothing",
path.display()
);
checked += 1;
}
assert!(checked >= 2, "expected at least two seeds, found {checked}");
}
#[cfg(feature = "edid")]
#[test]
fn the_cta_seed_reaches_the_hdr_path() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../fuzz/seeds/edid_blob/dell_with_hdr_cta.bin"
);
let blob = std::fs::read(path).expect("read the CTA seed");
let info = crate::edid::parse_edid(&blob).expect("parse");
assert!(
info.hdr.is_some(),
"this seed exists to carry the fuzzer into the HDR gate; without an \
HDR block it is just another copy of the base seed"
);
}
mod profile {
use crate::{PanelSelfRefresh, PrimeCaps, connector_type_self_refreshes, decode_prime_caps};
#[test]
fn prime_caps_decode_each_bit() {
assert_eq!(decode_prime_caps(0), PrimeCaps::default());
let import = decode_prime_caps(0x1);
assert!(import.can_import && !import.can_export);
let export = decode_prime_caps(0x2);
assert!(!export.can_import && export.can_export);
let both = decode_prime_caps(0x3);
assert!(both.can_import && both.can_export);
let future = decode_prime_caps(0xFF00);
assert!(!future.can_import && !future.can_export);
}
#[test]
fn only_internal_panels_self_refresh() {
const EDP: u32 = 14;
const DSI: u32 = 16;
const HDMI_A: u32 = 11;
const DISPLAY_PORT: u32 = 10;
const VGA: u32 = 1;
assert!(connector_type_self_refreshes(EDP));
assert!(connector_type_self_refreshes(DSI));
for external in [HDMI_A, DISPLAY_PORT, VGA, 0] {
assert!(
!connector_type_self_refreshes(external),
"connector type {external} is on a cable and has to be fed"
);
}
}
#[test]
fn an_unreported_capability_falls_back() {
use crate::profile::nonzero_or;
assert_eq!(nonzero_or(Some(256), 64), 256, "a real value is kept");
assert_eq!(nonzero_or(None, 64), 64, "an absent one falls back");
assert_eq!(nonzero_or(Some(0), 64), 64, "and so does a zero");
}
#[test]
fn self_refresh_defaults_to_unknown() {
assert_eq!(PanelSelfRefresh::default(), PanelSelfRefresh::Unknown);
}
}
#[cfg(test)]
mod profile_vkms {
use std::sync::Mutex;
use drmkit_core::Device;
use crate::DriverProfile;
static CARD_LOCK: Mutex<()> = Mutex::new(());
fn open_card_or_skip() -> Option<Device> {
let _guard = CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let path =
std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned());
match Device::open(&path) {
Ok(device) => Some(device),
Err(error) => {
assert!(
std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
"{path}: {error}, but DRMKIT_REQUIRE_MASTER is set"
);
println!("note: skipped -- no DRM device at {path} ({error})");
None
}
}
}
#[test]
#[ignore = "needs a DRM device"]
fn a_real_device_probes_coherently_vkms() {
let Some(device) = open_card_or_skip() else {
return;
};
let profile = DriverProfile::probe(&device).expect("probe a KMS node");
println!("note: {profile:?}");
assert!(!profile.name.is_empty(), "every driver reports a name");
assert!(
profile.cursor_width >= 64 && profile.cursor_height >= 64,
"the 64x64 fallback is the floor, not a value to fall below: {}x{}",
profile.cursor_width,
profile.cursor_height
);
assert!(
profile.prime_import || profile.prime_export,
"a KMS node that can share no buffer in either direction cannot \
participate in anything"
);
}
}
mod pipeline_curves {
use crate::curves::{
ColorLut, build_bt709_to_bt2020_ctm, build_bt2020_to_bt709_ctm, build_hlg_oetf_inverse_lut,
build_identity_ctm, build_identity_lut, build_pq_eotf_lut, build_pq_oetf_lut,
decode_s31_32, encode_s31_32, fill_lut, quantize_lut_value,
};
#[test]
fn a_negative_coefficient_sets_the_sign_bit_and_nothing_else() {
let encoded = encode_s31_32(-0.5);
assert_eq!(encoded, (1_u64 << 63) | (1_u64 << 31));
assert!((decode_s31_32(encoded) + 0.5).abs() < 1e-9);
}
#[test]
fn one_encodes_to_the_scale_factor() {
assert_eq!(encode_s31_32(1.0), 1 << 32);
assert_eq!(encode_s31_32(0.0), 0);
assert_eq!(encode_s31_32(-0.0), 0, "negative zero is still zero");
}
#[test]
fn every_coefficient_survives_the_round_trip() {
for value in [0.0, 0.0083, 0.5, 1.0, 1.6605, -0.0728, -1.1187, 2047.5] {
let back = decode_s31_32(encode_s31_32(value));
assert!((back - value).abs() < 1e-9, "{value} came back as {back}");
}
}
#[test]
fn nonsense_encodes_to_zero_rather_than_something_plausible() {
assert_eq!(encode_s31_32(f64::NAN), 0);
assert_eq!(encode_s31_32(f64::INFINITY), (1_u64 << 63) - 1);
assert_eq!(
encode_s31_32(f64::NEG_INFINITY),
u64::MAX,
"sign bit and full magnitude"
);
}
#[test]
fn the_lut_quantizer_clamps_at_both_ends() {
assert_eq!(quantize_lut_value(1.000_000_1), u16::MAX);
assert_eq!(quantize_lut_value(-0.000_000_1), 0);
assert_eq!(quantize_lut_value(f64::NAN), 0);
assert_eq!(quantize_lut_value(0.5), 32_768);
}
#[test]
fn an_identity_lut_walks_from_black_to_white() {
let mut lut = vec![ColorLut::default(); 256];
build_identity_lut(&mut lut);
assert_eq!(lut[0].red, 0);
assert_eq!(lut[255].red, u16::MAX);
for entry in &lut {
assert_eq!(entry.red, entry.green);
assert_eq!(entry.red, entry.blue);
assert_eq!(entry.reserved, 0, "the kernel reads this and wants zero");
}
assert!(
lut.windows(2).all(|pair| pair[0].red <= pair[1].red),
"an identity that is not monotonic is not an identity"
);
}
#[test]
fn a_single_entry_lut_does_not_divide_by_zero() {
let mut lut = vec![ColorLut::default(); 1];
build_identity_lut(&mut lut);
assert_eq!(lut[0].red, 0);
}
#[test]
fn an_empty_lut_is_left_alone() {
let mut lut: Vec<ColorLut> = Vec::new();
build_identity_lut(&mut lut);
assert!(lut.is_empty());
}
#[test]
fn the_pq_curves_are_inverses_of_each_other() {
let mut eotf = vec![ColorLut::default(); 4096];
let mut oetf = vec![ColorLut::default(); 4096];
build_pq_eotf_lut(&mut eotf);
build_pq_oetf_lut(&mut oetf);
assert_eq!(eotf[0].red, 0);
assert_eq!(eotf[4095].red, u16::MAX);
assert_eq!(oetf[0].red, 0);
assert_eq!(oetf[4095].red, u16::MAX);
assert!(
eotf[2048].red < oetf[2048].red,
"the two curves are on the same side of the diagonal"
);
for lut in [&eotf, &oetf] {
assert!(
lut.windows(2).all(|pair| pair[0].red <= pair[1].red),
"a transfer function that is not monotonic inverts somewhere"
);
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn index_for(encoded: f64, len: usize) -> usize {
((encoded * (len - 1) as f64).round() as usize).min(len - 1)
}
#[test]
fn the_pq_lut_lands_where_the_spec_says() {
let mut lut = vec![ColorLut::default(); 4096];
build_pq_eotf_lut(&mut lut);
let at = |encoded: f64| lut[index_for(encoded, lut.len())].red;
assert!(
at(0.5081).abs_diff(655) <= 30,
"100 cd/m2 came out at {}",
at(0.5081)
);
assert!(
at(0.7518).abs_diff(6553) <= 60,
"1000 cd/m2 came out at {}",
at(0.7518)
);
}
#[test]
fn the_hlg_lut_lands_where_the_spec_says() {
let mut lut = vec![ColorLut::default(); 4096];
build_hlg_oetf_inverse_lut(&mut lut);
let midpoint = lut[index_for(0.5, lut.len())].red;
assert!(midpoint.abs_diff(5461) <= 20, "half came out at {midpoint}");
}
#[test]
fn a_half_rounds_up_rather_than_truncating() {
assert_eq!(encode_s31_32(0.5), 1 << 31);
assert_eq!(encode_s31_32(2.5), 5 << 31);
}
#[test]
fn an_identity_matrix_is_one_on_the_diagonal() {
let ctm = build_identity_ctm();
for (index, entry) in ctm.matrix.iter().enumerate() {
let expected = if index % 4 == 0 { 1.0 } else { 0.0 };
assert!(
(decode_s31_32(*entry) - expected).abs() < 1e-9,
"entry {index}"
);
}
}
#[test]
fn the_gamut_matrices_undo_each_other() {
let forward = build_bt2020_to_bt709_ctm();
let back = build_bt709_to_bt2020_ctm();
let decode = |ctm: &crate::curves::ColorCtm| ctm.matrix.map(decode_s31_32);
let (a, b) = (decode(&forward), decode(&back));
for row in 0..3 {
for column in 0..3 {
let product: f64 = (0..3).map(|k| a[row * 3 + k] * b[k * 3 + column]).sum();
let expected = if row == column { 1.0 } else { 0.0 };
assert!(
(product - expected).abs() < 1e-3,
"row {row} column {column}: {product}"
);
}
}
}
#[test]
fn each_gamut_matrix_preserves_white() {
for ctm in [build_bt2020_to_bt709_ctm(), build_bt709_to_bt2020_ctm()] {
for row in 0..3 {
let sum: f64 = (0..3).map(|k| decode_s31_32(ctm.matrix[row * 3 + k])).sum();
assert!((sum - 1.0).abs() < 1e-3, "row {row} sums to {sum}");
}
}
}
#[test]
fn a_curve_is_applied_to_every_channel_alike() {
let mut lut = vec![ColorLut::default(); 16];
fill_lut(&mut lut, |value| value * value);
for entry in &lut {
assert_eq!(entry.red, entry.green);
assert_eq!(entry.green, entry.blue);
}
let midpoint = f64::from(lut[8].red) / f64::from(u16::MAX);
assert!(
(midpoint - (8.0_f64 / 15.0).powi(2)).abs() < 1e-4,
"midpoint came out at {midpoint}"
);
}
}