use glam::Vec3;
use super::{
BYTES_PER_RAY, CHUNK_BUDGET_BYTES, EwError, chunk_pixels_for, compare::compare_frames,
next_chunk_pixels,
};
use crate::{
geometry::cuts::StandardGemCuts,
optics::{
materials::GemMaterial,
raytracer::{Camera, LightingPreset},
},
renderer::{
frame_scene::FrameScene,
gpu::{
GpuContext, event_wavefront::EventWavefrontRenderer,
frame::run_event_wavefront_equivalence,
},
},
};
use super::accumulate::FIRST_DISPATCH_MAX_TUPLES;
const REQUIRE_GPU_ENV: &str = "INDICATRIX_REQUIRE_GPU";
fn gpu_required() -> bool {
std::env::var(REQUIRE_GPU_ENV).is_ok_and(|v| v == "1")
}
fn ew_renderer_or_skip(test_name: &str) -> Option<EventWavefrontRenderer> {
match GpuContext::acquire() {
Ok(ctx) => Some(EventWavefrontRenderer::new(ctx)),
Err(err) => {
assert!(
!gpu_required(),
"{test_name}: {REQUIRE_GPU_ENV}=1 was set, but GPU acquisition failed: {err}"
);
println!("skipping {test_name}: no GPU adapter ({err})");
None
}
}
}
#[test]
fn ew_renderer_initializes_cleanly() {
let Some(renderer) = ew_renderer_or_skip("ew_renderer_initializes_cleanly") else {
return;
};
assert!(renderer.ns_per_tuple_ema().is_none());
assert_eq!(
renderer.chunk_budget_bytes(),
super::accumulate::CHUNK_BUDGET_BYTES
);
}
#[test]
fn ew_renders_spinel_tiny() {
let Some(mut renderer) = ew_renderer_or_skip("ew_renders_spinel_tiny") else {
return;
};
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::by_name("Spinel").expect("Spinel is a built-in material");
let (width, height) = (16u32, 16u32);
let spp = 2u32;
let num_pixels = (width * height) as usize;
let scene = FrameScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 6,
environment: LightingPreset::Daylight.studio(1.0, 0.4, 0.35),
};
let mut accum = vec![Vec3::ZERO; num_pixels];
pollster::block_on(renderer.accumulate(&scene, 0, spp, &mut accum))
.expect("accumulate should succeed for Spinel");
assert!(
accum.iter().all(|v| v.is_finite()),
"all accumulated XYZ values must be finite"
);
assert!(
accum.iter().any(|v| v.y > 0.0),
"at least some pixels must receive light"
);
}
fn render_spinel(
renderer: &mut EventWavefrontRenderer,
sample_offset: u32,
spp: u32,
) -> Result<Vec<Vec3>, EwError> {
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::by_name("Spinel").expect("Spinel is a built-in material");
let (width, height) = (32u32, 32u32);
let scene = FrameScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 6,
environment: LightingPreset::Daylight.studio(1.0, 0.4, 0.35),
};
let mut accum = vec![Vec3::ZERO; (width * height) as usize];
pollster::block_on(renderer.accumulate(&scene, sample_offset, spp, &mut accum))?;
Ok(accum)
}
#[test]
fn ew_sample_offset_ranges_add_up_to_one_larger_pass() {
let Some(mut renderer) = ew_renderer_or_skip("ew_sample_offset_ranges_add_up") else {
return;
};
let first = render_spinel(&mut renderer, 0, 4).expect("samples 0..4");
let second = render_spinel(&mut renderer, 4, 4).expect("samples 4..8");
let whole = render_spinel(&mut renderer, 0, 8).expect("samples 0..8");
let summed: Vec<Vec3> = first.iter().zip(&second).map(|(a, b)| *a + *b).collect();
let diff = compare_frames(&summed, &whole, 8);
assert_eq!(diff.non_finite_pixels, 0);
assert!(
diff.tol_passed(whole.len()),
"sample_offset ranges do not add up: {diff:?}"
);
assert!(compare_frames(&first, &second, 4).differing_pixels > 0);
}
#[test]
fn ew_multi_chunk_accumulation_equals_single_chunk() {
let Some(mut single) = ew_renderer_or_skip("ew_multi_chunk_accumulation") else {
return;
};
let reference = render_spinel(&mut single, 0, 2).expect("single-chunk render");
let Some(mut chunked) = ew_renderer_or_skip("ew_multi_chunk_accumulation") else {
return;
};
chunked.set_chunk_budget_bytes(BYTES_PER_RAY * 64);
let pieces = render_spinel(&mut chunked, 0, 2).expect("multi-chunk render");
let diff = compare_frames(&reference, &pieces, 2);
assert_eq!(
diff.differing_pixels, 0,
"chunking changed the image: {diff:?}"
);
}
#[test]
fn ew_renderer_recovers_after_a_failed_call() {
let Some(mut renderer) = ew_renderer_or_skip("ew_renderer_recovers_after_a_failed_call") else {
return;
};
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::by_name("Spinel").expect("Spinel is a built-in material");
let scene = FrameScene {
camera: &camera,
width: 16,
height: 16,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 4,
environment: LightingPreset::Daylight.studio(1.0, 0.4, 0.35),
};
let mut wrong = vec![Vec3::ZERO; 3];
let err = pollster::block_on(renderer.accumulate(&scene, 0, 1, &mut wrong))
.expect_err("a wrong-sized accumulator must be refused");
assert!(matches!(err, EwError::InvalidBufferSize { .. }));
render_spinel(&mut renderer, 0, 1).expect("renderer must work after an error");
}
#[test]
fn ew_uncaptured_validation_error_becomes_device_lost() {
use std::sync::atomic::Ordering;
let Some(renderer) = ew_renderer_or_skip("ew_uncaptured_validation_error") else {
return;
};
assert!(renderer.check_uncaptured_error().is_ok());
renderer
.context()
.validation_error_seen
.store(true, Ordering::Relaxed);
let err = renderer
.check_uncaptured_error()
.expect_err("a seen validation error must not pass silently");
assert!(matches!(err, EwError::DeviceLost(_)), "got {err:?}");
assert!(renderer.check_uncaptured_error().is_ok());
}
#[test]
fn ew_matches_megakernel_per_material_class() {
let Some(renderer) = ew_renderer_or_skip("ew_matches_megakernel_per_material_class") else {
return;
};
for class in [1u32, 2, 3] {
let result = run_event_wavefront_equivalence(renderer.context(), class);
assert!(
result.passed(),
"class {class}: {} / {} pixels differ, {} beyond tolerance, max |delta| {:e}",
result.differing_pixels,
result.total_pixels,
result.tol_failing_pixels,
result.max_abs_diff
);
}
}
#[test]
fn chunk_pixels_for_respects_budget_binding_and_workgroup_limits() {
let big_binding = u64::MAX / 2;
let rays = CHUNK_BUDGET_BYTES / BYTES_PER_RAY;
assert_eq!(
chunk_pixels_for(CHUNK_BUDGET_BYTES, 4, usize::MAX, big_binding),
(rays.min(65535 * 64)) / 4
);
assert_eq!(chunk_pixels_for(CHUNK_BUDGET_BYTES, 1, 10, big_binding), 10);
assert_eq!(
chunk_pixels_for(usize::MAX / 2, 1, usize::MAX, 288 * 100),
100
);
assert_eq!(chunk_pixels_for(1, 1, 50, big_binding), 1);
assert_eq!(chunk_pixels_for(CHUNK_BUDGET_BYTES, 0, 50, big_binding), 50);
}
#[test]
fn next_chunk_pixels_uses_first_dispatch_cap_then_the_ema() {
assert_eq!(
next_chunk_pixels(None, usize::MAX, 4, usize::MAX),
FIRST_DISPATCH_MAX_TUPLES / 4
);
assert_eq!(next_chunk_pixels(None, 10, 4, 1000), 10);
assert_eq!(next_chunk_pixels(None, 1000, 4, 7), 7);
let ns = 1000.0;
let target_tuples = super::TARGET_CHUNK_MS * 1.0e6 / ns;
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "target_tuples is a positive, finite count far below usize::MAX"
)]
let expected = (target_tuples / 2.0).floor() as usize;
assert_eq!(
next_chunk_pixels(Some(ns), usize::MAX, 2, usize::MAX),
expected
);
assert_eq!(next_chunk_pixels(Some(ns), 5, 2, usize::MAX), 5);
assert_eq!(next_chunk_pixels(Some(ns), usize::MAX, 2, 3), 3);
assert!(next_chunk_pixels(Some(1.0e12), 100, 1, 100) >= 1);
assert_eq!(
next_chunk_pixels(Some(0.0), usize::MAX, 4, usize::MAX),
FIRST_DISPATCH_MAX_TUPLES / 4
);
}
#[test]
fn rust_tag_constants_match_the_wgsl_common_unit() {
use super::{
ARM_FROSTED, ARM_GENERIC, ARM_TIR, ARM_UNI_ENTRY, ARM_UNI_INTERNAL, TAG_ARM_BASE, TAG_DONE,
TAG_EXTEND, TAG_FINALIZE, TAG_NEE_HG, TAG_SCATTER, TAG_SPLIT, TAG_SURFACE, TAG_TAIL,
};
const WGSL: &str = include_str!("../../shaders/event_wavefront/00_common.wgsl");
let wgsl_value = |name: &str| -> u32 {
let needle = format!("const {name}: u32 = ");
let start = WGSL
.find(&needle)
.unwrap_or_else(|| panic!("{name} is missing from 00_common.wgsl"))
+ needle.len();
let digits: String = WGSL[start..]
.chars()
.take_while(char::is_ascii_digit)
.collect();
digits.parse().expect("numeric WGSL constant")
};
for (name, rust) in [
("TAG_DONE", TAG_DONE),
("TAG_EXTEND", TAG_EXTEND),
("TAG_SCATTER", TAG_SCATTER),
("TAG_SURFACE", TAG_SURFACE),
("TAG_ARM_BASE", TAG_ARM_BASE),
("TAG_SPLIT", TAG_SPLIT),
("TAG_TAIL", TAG_TAIL),
("TAG_FINALIZE", TAG_FINALIZE),
("TAG_NEE_HG", TAG_NEE_HG),
] {
assert_eq!(wgsl_value(name), rust, "{name} drifted from the WGSL unit");
}
for (name, rust) in [
("ARM_FROSTED", ARM_FROSTED),
("ARM_TIR", ARM_TIR),
("ARM_UNI_ENTRY", ARM_UNI_ENTRY),
("ARM_UNI_INTERNAL", ARM_UNI_INTERNAL),
("ARM_GENERIC", ARM_GENERIC),
] {
if WGSL.contains(&format!("const {name}: u32 = ")) {
assert_eq!(wgsl_value(name), rust, "{name} drifted from the WGSL unit");
}
}
}