use std::sync::atomic::Ordering;
use glam::Vec3;
use crate::{
geometry::cuts::StandardGemCuts,
optics::{
materials::GemMaterial,
raytracer::{Camera, EnvironmentSource, LightingPreset, add_finite_sample},
},
renderer::{
env_map::EnvironmentMap,
gpu::{compute, merge_tests::renderer_or_skip},
},
};
use super::{GpuFrameError, GpuFrameScene, GpuPipelineKind};
fn tiny_scene_material() -> GemMaterial {
GemMaterial::by_name("Spinel").expect("Spinel is a built-in cubic material")
}
#[test]
fn oversized_hdr_environment_is_declined_not_panicked() {
let Some(mut renderer) = renderer_or_skip("oversized_hdr_environment_is_declined_not_panicked")
else {
return;
};
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = tiny_scene_material();
let map = EnvironmentMap::from_rgb(4100, 2050, vec![[0.0, 0.0, 0.0]; 4100 * 2050])
.expect("4100x2050 black texels is a self-consistent buffer");
let scene = GpuFrameScene {
camera: &camera,
width: 8,
height: 8,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 4,
environment: EnvironmentSource::HdrMap(&map),
};
let mut accum = vec![Vec3::ZERO; 64];
let result = renderer.accumulate(&scene, 0, 1, &mut accum);
assert!(
matches!(result, Err(GpuFrameError::UnsupportedEnvironment)),
"expected UnsupportedEnvironment, got {result:?}"
);
let small_map = EnvironmentMap::uniform(1024, 512, [1.0, 1.0, 1.0]);
let scene2 = GpuFrameScene {
environment: EnvironmentSource::HdrMap(&small_map),
..scene
};
renderer
.accumulate(&scene2, 0, 1, &mut accum)
.expect("a 1024x512 HDR map is well within the device's binding limit");
}
#[test]
fn in_bounds_hdr_environment_renders() {
let Some(mut renderer) = renderer_or_skip("in_bounds_hdr_environment_renders") else {
return;
};
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = tiny_scene_material();
let map = EnvironmentMap::uniform(1024, 512, [1.0, 1.0, 1.0]);
let scene = GpuFrameScene {
camera: &camera,
width: 8,
height: 8,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 4,
environment: EnvironmentSource::HdrMap(&map),
};
let mut accum = vec![Vec3::ZERO; 64];
renderer
.accumulate(&scene, 0, 1, &mut accum)
.expect("a 1024x512 HDR map is well within the device's binding limit");
assert!(
accum.iter().any(|v| v.length_squared() > 0.0),
"a lit uniform environment must leave SOME nonzero radiance in accum"
);
}
#[test]
fn wavefront_1080p_dispatch_does_not_error_as_the_ema_grows() {
let Some(mut renderer) =
renderer_or_skip("wavefront_1080p_dispatch_does_not_error_as_the_ema_grows")
else {
return;
};
renderer.set_pipeline_kind(GpuPipelineKind::Wavefront);
if renderer.pipeline_kind() != GpuPipelineKind::Wavefront {
println!(
"skipping wavefront_1080p_dispatch_does_not_error_as_the_ema_grows: this \
device cannot bind enough buffers for the wavefront pipeline \
(set_pipeline_kind fell back to Megakernel)"
);
return;
}
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = tiny_scene_material();
let (width, height) = (1920u32, 1080u32);
let scene = GpuFrameScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 8,
environment: LightingPreset::Daylight.studio(1.0, 0.4, 0.35),
};
let mut accum = vec![Vec3::ZERO; (width * height) as usize];
for sample in 0..4u32 {
renderer
.accumulate(&scene, sample, 1, &mut accum)
.unwrap_or_else(|e| panic!("wavefront 1920x1080 dispatch #{sample} errored: {e}"));
}
assert!(
accum.iter().any(|v| v.length_squared() > 0.0),
"a lit studio-rig scene must leave SOME nonzero radiance in accum"
);
}
#[test]
fn abandon_in_flight_poisons_the_renderer_permanently() {
let Some(mut renderer) = renderer_or_skip("abandon_in_flight_poisons_the_renderer_permanently")
else {
return;
};
renderer.abandon_in_flight();
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = tiny_scene_material();
let scene = GpuFrameScene {
camera: &camera,
width: 4,
height: 4,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 2,
environment: LightingPreset::Daylight.studio(1.0, 0.4, 0.35),
};
let mut accum = vec![Vec3::ZERO; 16];
let first = renderer.accumulate(&scene, 0, 1, &mut accum);
assert!(
matches!(first, Err(GpuFrameError::DeviceLost(_))),
"expected DeviceLost right after abandon_in_flight, got {first:?}"
);
let second = renderer.accumulate(&scene, 0, 1, &mut accum);
assert!(
matches!(second, Err(GpuFrameError::DeviceLost(_))),
"expected DeviceLost again on a second call, got {second:?}"
);
}
#[test]
fn validation_error_seen_flag_turns_the_next_turn_into_device_lost() {
let Some(mut renderer) =
renderer_or_skip("validation_error_seen_flag_turns_the_next_turn_into_device_lost")
else {
return;
};
renderer
.ctx
.validation_error_seen
.store(true, Ordering::Relaxed);
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let material = tiny_scene_material();
let scene = GpuFrameScene {
camera: &camera,
width: 4,
height: 4,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 2,
environment: LightingPreset::Daylight.studio(1.0, 0.4, 0.35),
};
let mut accum = vec![Vec3::ZERO; 16];
let result = renderer.accumulate(&scene, 0, 1, &mut accum);
assert!(
matches!(result, Err(GpuFrameError::DeviceLost(_))),
"expected DeviceLost, got {result:?}"
);
}
#[test]
fn reduce_drops_non_finite_samples_like_its_cpu_twin() {
let Some(renderer) = renderer_or_skip("reduce_drops_non_finite_samples_like_its_cpu_twin")
else {
return;
};
let ctx = &renderer.ctx;
let nan = f32::NAN;
let inf = f32::INFINITY;
let samples: [[f32; 3]; 12] = [
[1.0, 2.0, 3.0],
[nan, 1.0, 1.0],
[4.0, 5.0, 6.0],
[1.0, inf, 1.0],
[0.5, 0.25, 0.125],
[0.5, 0.25, 0.125],
[0.5, 0.25, 0.125],
[0.5, 0.25, 0.125],
[nan, nan, nan],
[-inf, 0.0, 0.0],
[0.0, 0.0, inf],
[f32::from_bits(0x7fc0_1234), 1.0, 1.0],
];
let (num_pixels, num_samples) = (3u32, 4u32);
let params = super::GpuReduceParams {
num_pixels,
num_samples,
_pad0: 0,
_pad1: 0,
};
let pipeline = compute::create_compute_pipeline(
&ctx.device,
"reduce non-finite test",
super::REDUCE_SHADER_SRC,
"reduce_xyz_main",
);
let params_buf = compute::upload(
&ctx.device,
"reduce non-finite test params",
std::slice::from_ref(¶ms),
wgpu::BufferUsages::UNIFORM,
);
let input_buf = compute::upload(
&ctx.device,
"reduce non-finite test input",
samples.as_flattened(),
wgpu::BufferUsages::STORAGE,
);
let output_buf = compute::zeroed_buffer::<f32>(
&ctx.device,
"reduce non-finite test output",
(num_pixels * 3) as usize,
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
);
let bind_group = compute::bind_buffers(
&ctx.device,
"reduce non-finite test bind group",
&pipeline,
&[(0, ¶ms_buf), (1, &input_buf), (2, &output_buf)],
);
compute::dispatch_and_wait(&ctx.device, &ctx.queue, &pipeline, &bind_group, (1, 1, 1));
let gpu: Vec<f32> = compute::readback(
&ctx.device,
&ctx.queue,
&output_buf,
(num_pixels * 3) as usize,
);
for (pixel, tuples) in samples.chunks(num_samples as usize).enumerate() {
let mut cpu = Vec3::ZERO;
for t in tuples {
add_finite_sample(&mut cpu, Vec3::from_array(*t));
}
let gpu_sum = Vec3::new(gpu[pixel * 3], gpu[pixel * 3 + 1], gpu[pixel * 3 + 2]);
assert!(
gpu_sum.is_finite(),
"pixel {pixel}: GPU sum {gpu_sum} is not finite"
);
assert_eq!(
gpu_sum.to_array().map(f32::to_bits),
cpu.to_array().map(f32::to_bits),
"pixel {pixel}: GPU reduction {gpu_sum} differs from the CPU rule's {cpu}"
);
}
assert_eq!(
Vec3::from_slice(&gpu[0..3]),
Vec3::new(5.0, 7.0, 9.0),
"pixel 0 keeps only its finite samples"
);
assert_eq!(
Vec3::from_slice(&gpu[6..9]),
Vec3::ZERO,
"an all-non-finite pixel sums to zero"
);
}
#[test]
fn repeated_turns_with_camera_changes_never_poison_the_renderer() {
let Some(mut renderer) =
renderer_or_skip("repeated_turns_with_camera_changes_never_poison_the_renderer")
else {
return;
};
let planes = StandardGemCuts::standard_round_brilliant();
let material = tiny_scene_material();
let environment = LightingPreset::Daylight.studio(1.0, 0.4, 0.35);
let full = (480u32, 360u32);
let preview = (160u32, 120u32);
let sizes = [full, full, preview, preview, full, full, full];
let mut sample_offset = 0u32;
for (turn, &(width, height)) in sizes.iter().enumerate() {
let yaw = (turn as f32).mul_add(0.37, 0.1);
let pitch = (turn as f32).mul_add(0.11, 0.2).clamp(-1.4, 1.4);
let camera = Camera::new(yaw, pitch, 5.0, 18.0);
let scene = GpuFrameScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 4,
environment,
};
let spp = 2u32;
let mut accum = vec![Vec3::ZERO; (width * height) as usize];
let result = renderer.accumulate(&scene, sample_offset, spp, &mut accum);
if let Err(GpuFrameError::DeviceLost(ref why)) = result {
panic!(
"turn {turn} ({width}x{height}, yaw={yaw}, pitch={pitch}) was declared \
DeviceLost -- this is the exact poisoning the bug report describes: \
{why}"
);
}
result.unwrap_or_else(|e| panic!("turn {turn} failed: {e}"));
sample_offset += spp;
}
}