Files
solidcraft-upstream/crates/render/src/lighting/hdr_tests.rs
T
selune e99b3bd4e8 render: bounded cooperative HDR input foundation (#837)
A cancellable, bounded Radiance HDR decoder for render lighting input; reference images still accept PNG and JPEG only.
2026-10-13 16:33:51 -04:00

173 lines
6.6 KiB
Rust

use super::*;
use crate::lighting::{HdrDecodeTask, HdrTaskPoll};
#[test]
fn cooperative_task_matches_codec_and_is_consumed_once() {
for bytes in [uncompressed(""), rle(), literal_rle(), old_rle()] {
let expected = image::load_from_memory_with_format(&bytes, image::ImageFormat::Hdr).unwrap().into_rgb32f();
let mut task = HdrDecodeTask::new(HdrSource::new(bytes).unwrap()).unwrap();
let mut calls = 0;
loop {
calls += 1;
assert!(calls < 1000);
match task.step(1, || false).unwrap() {
HdrTaskPoll::Pending(progress) => assert!(progress.decoded_pixels < progress.total_pixels),
HdrTaskPoll::Complete(result) => {
assert_eq!(result.pixels().len(), expected.pixels().len());
for (actual, expected) in result.pixels().iter().zip(expected.pixels()) {
assert_eq!(actual, &expected.0);
}
break;
}
}
}
assert!(calls > 8);
assert_eq!(task.step(1, || false).err().unwrap().code, ErrorCode::Consumed);
}
}
#[test]
fn cancellation_or_bad_input_cannot_publish_partial_pixels_later() {
let mut task = HdrDecodeTask::new(HdrSource::new(rle()).unwrap()).unwrap();
assert!(matches!(task.step(7, || false).unwrap(), HdrTaskPoll::Pending(_)));
assert_eq!(task.step(1, || true).err().unwrap().code, ErrorCode::Cancelled);
assert_eq!(task.step(16384, || false).err().unwrap().code, ErrorCode::Consumed);
let mut bytes = rle();
bytes.pop();
let mut task = HdrDecodeTask::new(HdrSource::new(bytes).unwrap()).unwrap();
assert_eq!(task.step(16384, || false).err().unwrap().code, ErrorCode::InvalidInput);
assert_eq!(task.step(1, || false).err().unwrap().code, ErrorCode::Consumed);
let checks = std::cell::Cell::new(0);
let mut task = HdrDecodeTask::new(HdrSource::new(rle()).unwrap()).unwrap();
let result = task.step(16384, || {
checks.set(checks.get() + 1);
checks.get() == 10
});
assert_eq!(result.err().unwrap().code, ErrorCode::Cancelled);
assert_eq!(checks.get(), 10);
assert_eq!(task.step(1, || false).err().unwrap().code, ErrorCode::Consumed);
}
#[test]
fn source_primaries_are_preserved_without_assuming_srgb() {
let source = HdrSource::new(uncompressed("PRIMARIES=0.64 0.33 0.3 0.6 0.15 0.06 0.3127 0.329\n")).unwrap();
assert_eq!(source.info().primaries, Some([0.64, 0.33, 0.3, 0.6, 0.15, 0.06, 0.3127, 0.329]));
assert!(HdrSource::new(uncompressed("PRIMARIES=NaN 0 0 0 0 0 0 0\n")).is_err());
let mut excessive_capacity = Vec::with_capacity(MAX_HDR_BYTES + 1);
excessive_capacity.extend_from_slice(&rle());
assert_eq!(HdrSource::new(excessive_capacity).err().unwrap().code, ErrorCode::LimitExceeded);
}
fn header(width: u32, height: u32, extra: &str) -> Vec<u8> {
format!("#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n{extra}\n-Y {height} +X {width}\n").into_bytes()
}
fn uncompressed(extra: &str) -> Vec<u8> {
let mut bytes = header(4, 2, extra);
for _ in 0..8 {
bytes.extend_from_slice(&[128, 64, 32, 131]);
}
bytes
}
fn rle() -> Vec<u8> {
let mut bytes = header(8, 4, "");
for _ in 0..4 {
bytes.extend_from_slice(&[2, 2, 0, 8]);
for value in [128, 64, 32, 129] {
bytes.extend_from_slice(&[136, value]);
}
}
bytes
}
fn literal_rle() -> Vec<u8> {
let mut bytes = header(8, 4, "");
for _ in 0..4 {
bytes.extend_from_slice(&[2, 2, 0, 8]);
for value in [128, 64, 32, 129] {
bytes.push(8);
bytes.extend_from_slice(&[value; 8]);
}
}
bytes
}
fn old_rle() -> Vec<u8> {
let mut bytes = header(8, 4, "");
for _ in 0..4 {
bytes.extend_from_slice(&[128, 64, 32, 129, 1, 1, 1, 7]);
}
bytes
}
#[test]
fn incremental_rows_match_the_original_codec_without_clipping_hdr_values() {
for bytes in [uncompressed(""), rle(), literal_rle(), old_rle()] {
let expected = image::load_from_memory_with_format(&bytes, image::ImageFormat::Hdr).unwrap().into_rgb32f();
let source = HdrSource::new(bytes).unwrap();
source.validate(|| false).unwrap();
let mut rows = Rows::new(source).unwrap();
while !rows.done() {
if rows.ready() {
for x in 0..rows.info().width as usize {
assert_eq!(rows.linear(x).unwrap(), expected.get_pixel(x as u32, rows.row()).0);
}
rows.next_row().unwrap();
} else {
rows.step().unwrap();
}
}
}
}
#[test]
fn header_exposure_and_colour_correction_restore_linear_radiance() {
let source = HdrSource::new(uncompressed("EXPOSURE=2\nCOLORCORR=1 2 4\n")).unwrap();
let mut rows = Rows::new(source).unwrap();
while !rows.ready() {
rows.step().unwrap();
}
assert_eq!(rows.linear(0).unwrap(), [2.0, 0.5, 0.125]);
}
#[test]
fn malformed_runs_truncation_nonfinite_metadata_and_hostile_sizes_are_refused() {
let mut truncated = rle();
truncated.pop();
let mut wrong_width = rle();
let offset = HdrSource::new(wrong_width.clone()).unwrap().info.offset;
wrong_width[offset + 3] = 9;
let mut zero_run = rle();
zero_run[offset + 4] = 0;
let mut overflow = header(8, 4, "");
overflow.extend_from_slice(&[128, 64, 32, 129]);
for _ in 0..6 {
overflow.extend_from_slice(&[1, 1, 1, 0]);
}
for bytes in [truncated, wrong_width, zero_run, overflow] {
assert!(HdrSource::new(bytes).unwrap().validate(|| false).is_err());
}
for bytes in [header(u32::MAX, 1, ""), uncompressed("EXPOSURE=NaN\n"), uncompressed("PIXASPECT=0\n")] {
assert!(HdrSource::new(bytes).is_err());
}
let mut too_large = rle();
too_large.resize(MAX_HDR_BYTES + 1, 0);
assert_eq!(HdrSource::new(too_large).err().unwrap().code, ErrorCode::LimitExceeded);
let mut too_bright = uncompressed("");
let offset = HdrSource::new(too_bright.clone()).unwrap().info.offset;
too_bright[offset + 3] = 255;
assert_eq!(HdrSource::new(too_bright).unwrap().validate(|| false).unwrap_err().code, ErrorCode::LimitExceeded);
let source = HdrSource::new(rle()).unwrap();
assert_eq!(source.validate(|| true).unwrap_err().code, ErrorCode::Cancelled);
}
#[test]
fn header_work_is_bounded_before_accepting_metadata() {
let mut bytes = b"#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n".to_vec();
bytes.extend(std::iter::repeat_n(b'x', MAX_HEADER_BYTES + 1));
bytes.extend_from_slice(b"\n\n-Y 2 +X 4\n");
bytes.extend_from_slice(&[128, 64, 32, 129]);
assert!(HdrSource::new(bytes).is_err());
}