use std::{
sync::{Mutex, atomic::AtomicBool},
thread,
};
use glam::Vec3;
use super::{GpuAccumulate, GpuBackend, GpuSceneRef, turnstile::Turnstile};
use crate::{
geometry::cuts::StandardGemCuts,
optics::{
materials::GemMaterial,
raytracer::{Camera, LightingPreset},
},
};
#[test]
fn turnstile_serves_tickets_in_order_even_when_threads_queue_out_of_order() {
let turnstile = Turnstile::new();
let served_order: Mutex<Vec<u64>> = Mutex::new(Vec::new());
let t0 = turnstile.take_ticket();
let t1 = turnstile.take_ticket();
let t2 = turnstile.take_ticket();
thread::scope(|scope| {
scope.spawn(|| {
let _turn = turnstile.wait_for_turn(t2);
served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(2);
});
scope.spawn(|| {
let _turn = turnstile.wait_for_turn(t1);
served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(1);
});
{
let _turn0 = turnstile.wait_for_turn(t0);
served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(0);
}
});
assert_eq!(
*served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![0, 1, 2],
"tickets must be served in the order they were taken, not the order threads \
happened to start waiting"
);
}
#[test]
fn turnstile_releases_on_an_early_return_out_of_the_turn_scope() {
fn takes_a_turn_then_bails_early(turnstile: &Turnstile, ticket: u64) -> Option<()> {
let _turn = turnstile.wait_for_turn(ticket);
None }
let turnstile = Turnstile::new();
let t0 = turnstile.take_ticket();
let t1 = turnstile.take_ticket();
assert_eq!(takes_a_turn_then_bails_early(&turnstile, t0), None);
let _turn1 = turnstile.wait_for_turn(t1);
}
#[test]
fn poisoned_renderer_mutex_declines_and_sets_lost() {
let backend = GpuBackend::acquire();
let Some(mutex) = &backend.renderer else {
println!("skipping poisoned_renderer_mutex_declines_and_sets_lost: no GPU adapter");
return;
};
let join_result = thread::scope(|scope| {
scope
.spawn(|| {
let _guard = mutex
.lock()
.expect("first lock of a fresh mutex cannot fail");
panic!("deliberate poison for poisoned_renderer_mutex_declines_and_sets_lost");
})
.join()
});
assert!(
join_result.is_err(),
"the scoped thread was expected to panic (that's what poisons the mutex)"
);
assert!(mutex.is_poisoned(), "the mutex should now be poisoned");
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 cubic material");
let scene = GpuSceneRef {
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 never_cancel = AtomicBool::new(false);
let outcome = backend.try_accumulate_cancellable(&scene, 0, 1, &mut accum, &never_cancel);
assert_eq!(
outcome,
GpuAccumulate::Declined,
"a poisoned renderer mutex must decline, not dispatch into possibly-corrupt state"
);
assert!(
backend.lost.load(std::sync::atomic::Ordering::Relaxed),
"a poisoned renderer mutex must permanently set `lost`, like DeviceLost does"
);
let outcome2 = backend.try_accumulate_cancellable(&scene, 0, 1, &mut accum, &never_cancel);
assert_eq!(outcome2, GpuAccumulate::Declined);
}
#[test]
fn viewport_frames_with_camera_changes_never_poison_the_backend() {
let backend = GpuBackend::acquire();
if backend.renderer.is_none() {
println!(
"skipping viewport_frames_with_camera_changes_never_poison_the_backend: no \
GPU adapter"
);
return;
}
let planes = StandardGemCuts::standard_round_brilliant();
let material = GemMaterial::by_name("Spinel").expect("Spinel is a built-in cubic material");
let environment = LightingPreset::Daylight.studio(1.0, 0.4, 0.35);
let never_cancel = AtomicBool::new(false);
let (width, height) = (480u32, 360u32);
let spp = 2u32;
let mut accum = vec![Vec3::ZERO; (width * height) as usize];
for frame in 0..12u32 {
#[allow(
clippy::cast_precision_loss,
reason = "frame index is tiny; precision loss is not a concern in this test"
)]
let yaw = (frame as f32).mul_add(0.29, 0.1);
#[allow(
clippy::cast_precision_loss,
reason = "frame index is tiny; precision loss is not a concern in this test"
)]
let pitch = (frame as f32).mul_add(0.13, 0.2).clamp(-1.4, 1.4);
let camera = Camera::new(yaw, pitch, 5.0, 18.0);
let scene = GpuSceneRef {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 4,
environment,
};
let outcome =
backend.try_accumulate_cancellable(&scene, frame * spp, spp, &mut accum, &never_cancel);
assert_eq!(
outcome,
GpuAccumulate::Done,
"frame {frame} (yaw={yaw}, pitch={pitch}) did not complete -- got {outcome:?} \
instead of Done; `backend.lost` = {}",
backend.lost.load(std::sync::atomic::Ordering::Relaxed)
);
}
assert!(
accum.iter().any(|v| v.length_squared() > 0.0),
"a lit studio-rig scene traced over 12 frames must leave SOME nonzero radiance"
);
}