use glam::Vec3;
use crate::optics::{
materials::GemMaterial,
raytracer::{Camera, LightingPreset},
};
use super::{
CHUNK_BUDGET_BYTES, FLOATS_PER_TUPLE, GpuFrameScene, GpuPipelineKind, classify_material,
dispatch::chunk_pixels_for, material_class, renderer::GpuFrameRenderer,
};
pub struct ChunkEquivalenceResult {
pub chunks_forced: usize,
pub differing_pixels: usize,
pub total_pixels: usize,
pub max_abs_diff: f32,
}
impl ChunkEquivalenceResult {
#[must_use]
pub const fn passed(&self) -> bool {
self.chunks_forced > 1 && self.differing_pixels == 0
}
}
#[must_use]
pub fn run_chunk_equivalence(renderer: &mut GpuFrameRenderer) -> ChunkEquivalenceResult {
use crate::geometry::cuts::StandardGemCuts;
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 (width, height) = (64u32, 64u32);
let spp = 2u32;
let num_pixels = (width * height) as usize;
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 whole = vec![Vec3::ZERO; num_pixels];
renderer.set_chunk_budget_bytes(CHUNK_BUDGET_BYTES);
renderer
.accumulate(&scene, 0, spp, &mut whole)
.expect("a cubic material under the studio rig is GPU-supported");
let budget = 700 * FLOATS_PER_TUPLE * size_of::<f32>();
let chunk_pixels = chunk_pixels_for(budget, spp, num_pixels);
let chunks_forced = num_pixels.div_ceil(chunk_pixels);
let mut chunked = vec![Vec3::ZERO; num_pixels];
renderer.set_chunk_budget_bytes(budget);
renderer
.accumulate(&scene, 0, spp, &mut chunked)
.expect("a cubic material under the studio rig is GPU-supported");
renderer.set_chunk_budget_bytes(CHUNK_BUDGET_BYTES);
let mut differing_pixels = 0usize;
let mut max_abs_diff = 0.0f32;
for (a, b) in whole.iter().zip(&chunked) {
if a.to_array()
.iter()
.zip(b.to_array().iter())
.any(|(x, y)| x.to_bits() != y.to_bits())
{
differing_pixels += 1;
max_abs_diff = max_abs_diff.max((*a - *b).abs().max_element());
}
}
ChunkEquivalenceResult {
chunks_forced,
differing_pixels,
total_pixels: num_pixels,
max_abs_diff,
}
}
pub struct PipelineEquivalenceResult {
pub differing_pixels: usize,
pub total_pixels: usize,
pub max_abs_diff: f32,
pub tol_failing_pixels: usize,
pub tol_passed: bool,
}
impl PipelineEquivalenceResult {
#[must_use]
pub fn passed(&self) -> bool {
self.differing_pixels == 0 || self.max_abs_diff <= 5e-6 || self.tol_passed
}
}
#[must_use]
pub fn run_pipeline_equivalence(renderer: &mut GpuFrameRenderer) -> PipelineEquivalenceResult {
use crate::geometry::cuts::StandardGemCuts;
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 (width, height) = (32u32, 32u32);
let spp = 2u32;
let num_pixels = (width * height) as usize;
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),
};
renderer.set_pipeline_kind(GpuPipelineKind::Megakernel);
let mut megakernel = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate(&scene, 0, spp, &mut megakernel)
.expect("a cubic material under the studio rig is GPU-supported");
renderer.set_pipeline_kind(GpuPipelineKind::Wavefront);
let mut wavefront = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate(&scene, 0, spp, &mut wavefront)
.expect("a cubic material under the studio rig is GPU-supported");
renderer.set_pipeline_kind(GpuPipelineKind::Megakernel);
let mut differing_pixels = 0usize;
let mut max_abs_diff = 0.0f32;
for (a, b) in megakernel.iter().zip(&wavefront) {
if a.to_array()
.iter()
.zip(b.to_array().iter())
.any(|(x, y)| x.to_bits() != y.to_bits())
{
differing_pixels += 1;
max_abs_diff = max_abs_diff.max((*a - *b).abs().max_element());
}
}
let tol_passed = differing_pixels == 0 || max_abs_diff <= 5e-6;
let tol_failing_pixels = if tol_passed { 0 } else { differing_pixels };
PipelineEquivalenceResult {
differing_pixels,
total_pixels: num_pixels,
max_abs_diff,
tol_failing_pixels,
tol_passed,
}
}
#[must_use]
pub fn run_wavefront_determinism(renderer: &mut GpuFrameRenderer) -> PipelineEquivalenceResult {
use crate::geometry::cuts::StandardGemCuts;
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 (width, height) = (32u32, 32u32);
let spp = 2u32;
let num_pixels = (width * height) as usize;
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),
};
renderer.set_pipeline_kind(GpuPipelineKind::Wavefront);
let mut first = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate(&scene, 0, spp, &mut first)
.expect("a cubic material under the studio rig is GPU-supported");
let mut second = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate(&scene, 0, spp, &mut second)
.expect("a cubic material under the studio rig is GPU-supported");
renderer.set_pipeline_kind(GpuPipelineKind::Megakernel);
let mut differing_pixels = 0usize;
let mut max_abs_diff = 0.0f32;
for (a, b) in first.iter().zip(&second) {
if a.to_array()
.iter()
.zip(b.to_array().iter())
.any(|(x, y)| x.to_bits() != y.to_bits())
{
differing_pixels += 1;
max_abs_diff = max_abs_diff.max((*a - *b).abs().max_element());
}
}
let tol_passed = differing_pixels == 0 || max_abs_diff <= 5e-6;
let tol_failing_pixels = if tol_passed { 0 } else { differing_pixels };
PipelineEquivalenceResult {
differing_pixels,
total_pixels: num_pixels,
max_abs_diff,
tol_failing_pixels,
tol_passed,
}
}
#[derive(Debug, Clone)]
pub struct SpecialisationCaseResult {
pub material_name: String,
pub material_class: u32,
pub self_determinism_differing_pixels: usize,
pub generic_vs_specialised_differing_pixels: usize,
pub total_pixels: usize,
pub generic_vs_specialised_max_abs_diff: f32,
}
impl SpecialisationCaseResult {
#[must_use]
pub const fn passed(&self) -> bool {
self.self_determinism_differing_pixels == 0
}
}
pub struct SpecialisationEquivalenceResult {
pub cases: Vec<SpecialisationCaseResult>,
}
impl SpecialisationEquivalenceResult {
#[must_use]
pub fn passed(&self) -> bool {
!self.cases.is_empty() && self.cases.iter().all(SpecialisationCaseResult::passed)
}
}
#[must_use]
pub fn run_specialisation_equivalence(
renderer: &mut GpuFrameRenderer,
) -> SpecialisationEquivalenceResult {
use crate::geometry::cuts::StandardGemCuts;
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (32u32, 32u32);
let spp = 3u32;
let num_pixels = (width * height) as usize;
let environment = LightingPreset::Daylight.studio(1.0, 0.4, 0.35);
let representative_materials = ["Diamond", "Zircon", "Alexandrite"];
let mut cases = Vec::with_capacity(representative_materials.len());
for name in representative_materials {
let material = GemMaterial::by_name(name).unwrap_or_else(|| {
panic!("{name:?} is a built-in material in GemMaterial::all_materials()")
});
let scene = GpuFrameScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: &[],
material: &material,
max_bounces: 8,
environment,
};
let class = classify_material(&material);
let mut run1 = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate_via_pipeline(&scene, class, 0, spp, &mut run1, None)
.expect("every representative material here is GPU-supported under the studio rig");
let mut run2 = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate_via_pipeline(&scene, class, 0, spp, &mut run2, None)
.expect("every representative material here is GPU-supported under the studio rig");
let self_determinism_differing_pixels = run1
.iter()
.zip(&run2)
.filter(|(a, b)| {
a.to_array()
.iter()
.zip(b.to_array().iter())
.any(|(x, y)| x.to_bits() != y.to_bits())
})
.count();
let mut generic = vec![Vec3::ZERO; num_pixels];
renderer
.accumulate_via_pipeline(&scene, material_class::GENERIC, 0, spp, &mut generic, None)
.expect("every representative material here is GPU-supported under the studio rig");
let mut generic_vs_specialised_differing_pixels = 0usize;
let mut generic_vs_specialised_max_abs_diff = 0.0f32;
for (a, b) in generic.iter().zip(&run1) {
if a.to_array()
.iter()
.zip(b.to_array().iter())
.any(|(x, y)| x.to_bits() != y.to_bits())
{
generic_vs_specialised_differing_pixels += 1;
generic_vs_specialised_max_abs_diff =
generic_vs_specialised_max_abs_diff.max((*a - *b).abs().max_element());
}
}
cases.push(SpecialisationCaseResult {
material_name: name.to_string(),
material_class: class,
self_determinism_differing_pixels,
generic_vs_specialised_differing_pixels,
total_pixels: num_pixels,
generic_vs_specialised_max_abs_diff,
});
}
SpecialisationEquivalenceResult { cases }
}
#[must_use]
pub fn run_event_wavefront_equivalence(
ctx: &crate::renderer::gpu::GpuContext,
class: u32,
) -> PipelineEquivalenceResult {
use crate::{
geometry::cuts::{STANDARD_ROUND_BRILLIANT_GIRDLE_FACETS, StandardGemCuts},
optics::raytracer::FacetFinish,
renderer::gpu::event_wavefront::{EventWavefrontRenderer, compare::compare_frames},
};
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (32u32, 32u32);
let spp = 2u32;
let num_pixels = (width * height) as usize;
let studio_env = LightingPreset::Daylight.studio(1.0, 0.4, 0.35);
let synthetic_hdr = ew_cases::synthetic_hdr_map();
let mut frosted_finishes = vec![FacetFinish::Polished; planes.len()];
for idx in STANDARD_ROUND_BRILLIANT_GIRDLE_FACETS {
frosted_finishes[idx] = FacetFinish::Frosted;
}
let cases = ew_cases::equivalence_cases(class, studio_env, &frosted_finishes, &synthetic_hdr);
let mut mega_renderer = GpuFrameRenderer::new().expect("acquire renderer for megakernel");
let mut ew_renderer = EventWavefrontRenderer::new(ctx.clone());
let mut total_differing = 0usize;
let mut total_compared = 0usize;
let mut max_abs_diff = 0.0f32;
let mut total_tol_failing = 0usize;
let mut all_tol_passed = true;
for case in cases {
let scene = GpuFrameScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes: case.finishes,
material: &case.material,
max_bounces: case.max_bounces,
environment: case.env,
};
let mut mega_out = vec![Vec3::ZERO; num_pixels];
mega_renderer
.accumulate_via_pipeline(&scene, class, 0, spp, &mut mega_out, None)
.expect("megakernel accumulation");
let mut ew_out = vec![Vec3::ZERO; num_pixels];
pollster::block_on(ew_renderer.accumulate_via_class(&scene, class, 0, spp, &mut ew_out))
.expect("event wavefront accumulation");
let diff = compare_frames(&mega_out, &ew_out, spp);
if !diff.tol_passed(num_pixels) {
all_tol_passed = false;
}
total_differing += diff.differing_pixels;
total_compared += num_pixels;
max_abs_diff = max_abs_diff.max(diff.max_abs_diff);
total_tol_failing += diff.tol_failing_pixels;
}
PipelineEquivalenceResult {
differing_pixels: total_differing,
total_pixels: total_compared,
max_abs_diff,
tol_failing_pixels: total_tol_failing,
tol_passed: all_tol_passed,
}
}
mod ew_cases {
use crate::{
optics::{
materials::GemMaterial,
raytracer::{EnvironmentSource, FacetFinish},
},
renderer::env_map::EnvironmentMap,
};
pub struct TestCase<'a> {
_name: &'static str,
pub material: GemMaterial,
pub finishes: &'a [FacetFinish],
pub env: EnvironmentSource<'a>,
pub max_bounces: u32,
}
const fn case<'a>(
name: &'static str,
material: GemMaterial,
finishes: &'a [FacetFinish],
env: EnvironmentSource<'a>,
) -> TestCase<'a> {
TestCase {
_name: name,
material,
finishes,
env,
max_bounces: 8,
}
}
#[must_use]
pub fn synthetic_hdr_map() -> EnvironmentMap {
let cols = 16;
let rows = 8;
let mut pixels = Vec::with_capacity(cols * rows);
for row in 0..rows {
for col in 0..cols {
let red = (((col * 3 + row * 5) % 7) as f32).mul_add(0.4, 0.1);
let green = (((col * 2 + row * 3) % 5) as f32).mul_add(0.3, 0.1);
let blue = (((col * 5 + row * 2) % 6) as f32).mul_add(0.5, 0.1);
pixels.push([red, green, blue]);
}
}
EnvironmentMap::from_rgb(cols, rows, pixels).expect("valid synthetic env map")
}
#[must_use]
pub fn equivalence_cases<'a>(
class: u32,
studio_env: EnvironmentSource<'a>,
frosted_finishes: &'a [FacetFinish],
synthetic_hdr: &'a EnvironmentMap,
) -> Vec<TestCase<'a>> {
let diamond = GemMaterial::by_name("Diamond").expect("Diamond is built-in");
let spinel = GemMaterial::by_name("Spinel").expect("Spinel is built-in");
let zircon = GemMaterial::by_name("Zircon").expect("Zircon is built-in");
let tanzanite = GemMaterial::by_name("Tanzanite").expect("Tanzanite is built-in");
let ruby = GemMaterial::by_name("Ruby").expect("Ruby is built-in");
let spinel_scattering = spinel.clone().with_scattering(1.0, 0.3);
let ruby_scattering = ruby.with_scattering(1.0, 0.3);
let hdr = EnvironmentSource::HdrMap(synthetic_hdr);
match class {
1 => vec![
case("Diamond, daylight", diamond.clone(), &[], studio_env),
case("Spinel, daylight", spinel, &[], studio_env),
case(
"Diamond, frosted girdle",
diamond.clone(),
frosted_finishes,
studio_env,
),
case("Spinel, scattering", spinel_scattering, &[], studio_env),
case("Diamond, synthetic HDR", diamond.clone(), &[], hdr),
case("Ruby, scattering, synthetic HDR", ruby_scattering, &[], hdr),
TestCase {
max_bounces: 3,
..case(
"Diamond, max bounces 3 (survivors)",
diamond,
&[],
studio_env,
)
},
],
2 => vec![
case("Zircon, daylight", zircon.clone(), &[], studio_env),
case(
"Zircon, frosted girdle",
zircon.clone(),
frosted_finishes,
studio_env,
),
case("Zircon, synthetic HDR", zircon, &[], hdr),
],
3 => vec![
case("Tanzanite, daylight", tanzanite.clone(), &[], studio_env),
case("Tanzanite, synthetic HDR", tanzanite, &[], hdr),
],
_ => vec![case("Diamond, daylight", diamond, &[], studio_env)],
}
}
}