use crate::{
geometry::{GpuFacetPlane, cuts::StandardGemCuts},
optics::raytracer::{Ray, intersect_polyhedron},
renderer::{
buffers::{GpuHitRecord, GpuRay},
gpu::{compute, ulp::ulp_distance},
},
};
use glam::Vec3;
const SHADER_SRC: &str = include_str!("../shaders/intersect_polyhedron.wgsl");
type HitSummary = Option<(f32, i64, Vec3)>;
const TIE_EPS: f32 = 1e-4;
pub const HIT_ULP_BUDGET: u32 = 512;
#[derive(Clone, Copy, Debug)]
pub struct PolyhedronCase {
pub label: &'static str,
pub origin: Vec3,
pub dir: Vec3,
}
fn fibonacci_sphere(n: usize) -> Vec<Vec3> {
let golden_angle = std::f32::consts::PI * (3.0 - 5.0_f32.sqrt());
(0..n)
.map(|i| {
let y = 1.0 - 2.0 * (i as f32) / ((n - 1).max(1) as f32);
let radius = y.mul_add(-y, 1.0).max(0.0).sqrt();
let theta = golden_angle * i as f32;
Vec3::new(theta.cos() * radius, y, theta.sin() * radius)
})
.collect()
}
#[must_use]
pub fn build_cases() -> Vec<PolyhedronCase> {
let mut cases = Vec::new();
let directions = fibonacci_sphere(1024);
for &d in &directions {
cases.push(PolyhedronCase {
label: "outside_aimed_at_center",
origin: d * 5.0,
dir: -d,
});
}
for &d in &directions {
let perturb = Vec3::new(d.z, d.x, -d.y) * 0.15;
let dir = (-d + perturb).normalize();
cases.push(PolyhedronCase {
label: "outside_grazing_silhouette",
origin: d * 5.0,
dir,
});
}
for &d in &directions {
cases.push(PolyhedronCase {
label: "inside_exit_sweep",
origin: Vec3::ZERO,
dir: d,
});
}
cases.push(PolyhedronCase {
label: "adversarial_straight_up",
origin: Vec3::ZERO,
dir: Vec3::Y,
});
cases.push(PolyhedronCase {
label: "adversarial_straight_down",
origin: Vec3::ZERO,
dir: -Vec3::Y,
});
cases.push(PolyhedronCase {
label: "adversarial_on_table_plane_downward",
origin: Vec3::new(0.0, 0.32, 0.0),
dir: -Vec3::Y,
});
cases.push(PolyhedronCase {
label: "adversarial_on_table_plane_upward",
origin: Vec3::new(0.0, 0.32, 0.0),
dir: Vec3::Y,
});
cases.push(PolyhedronCase {
label: "adversarial_on_girdle_tangential",
origin: Vec3::new(1.0, 0.0, 0.0),
dir: Vec3::new(0.0, 0.0, 1.0),
});
for &eps in &[5e-7f32, 5e-8, -5e-7, -5e-8] {
let dir = Vec3::new(1.0, eps, 0.0).normalize();
cases.push(PolyhedronCase {
label: "adversarial_near_denom_epsilon",
origin: Vec3::new(-5.0, 0.32, 0.0),
dir,
});
}
cases.push(PolyhedronCase {
label: "adversarial_total_miss",
origin: Vec3::new(5.0, 5.0, 5.0),
dir: Vec3::new(1.0, 1.0, 1.0).normalize(),
});
cases
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaseOutcome {
Agree,
WhitelistedTie,
Mismatch,
}
#[derive(Debug, Clone)]
pub struct PolyhedronMismatch {
pub case_index: usize,
pub case: PolyhedronCase,
pub outcome: CaseOutcome,
pub cpu_hit: HitSummary,
pub gpu_hit: HitSummary,
pub detail: String,
}
#[derive(Debug, Clone)]
pub struct PolyhedronCheckResult {
pub total_cases: usize,
pub whitelisted_ties: usize,
pub total_mismatches: usize,
pub mismatches: Vec<PolyhedronMismatch>,
}
impl PolyhedronCheckResult {
#[must_use]
pub const fn passed(&self) -> bool {
self.mismatches.is_empty()
}
}
#[must_use]
pub fn run(ctx: &crate::renderer::gpu::GpuContext) -> PolyhedronCheckResult {
let planes: Vec<GpuFacetPlane> = StandardGemCuts::standard_round_brilliant();
let cases = build_cases();
let total = cases.len();
let gpu_rays: Vec<GpuRay> = cases
.iter()
.map(|c| GpuRay::new(c.origin.to_array(), c.dir.to_array()))
.collect();
let planes_buf = compute::upload(
&ctx.device,
"intersect_polyhedron planes",
&planes,
wgpu::BufferUsages::STORAGE,
);
let rays_buf = compute::upload(
&ctx.device,
"intersect_polyhedron rays",
&gpu_rays,
wgpu::BufferUsages::STORAGE,
);
let out_buf = compute::zeroed_buffer::<GpuHitRecord>(
&ctx.device,
"intersect_polyhedron output",
total,
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
);
let pipeline =
compute::create_compute_pipeline(&ctx.device, "intersect_polyhedron", SHADER_SRC, "main");
let bind_group = compute::bind_buffers(
&ctx.device,
"intersect_polyhedron bind group",
&pipeline,
&[(0, &planes_buf), (1, &rays_buf), (2, &out_buf)],
);
let workgroups = (total as u32).div_ceil(64);
compute::dispatch_and_wait(
&ctx.device,
&ctx.queue,
&pipeline,
&bind_group,
(workgroups, 1, 1),
);
let gpu_hits: Vec<GpuHitRecord> = compute::readback(&ctx.device, &ctx.queue, &out_buf, total);
let mut whitelisted_ties = 0usize;
let mut total_mismatches = 0usize;
let mut mismatches = Vec::new();
for (idx, (case, gpu_hit)) in cases.iter().zip(gpu_hits.iter()).enumerate() {
let ray = Ray {
origin: case.origin,
dir: case.dir,
};
let cpu_hit = intersect_polyhedron(ray, &planes);
let (outcome, cpu_summary, gpu_summary) = classify_case(cpu_hit, gpu_hit);
match outcome {
CaseOutcome::Agree => {}
CaseOutcome::WhitelistedTie => whitelisted_ties += 1,
CaseOutcome::Mismatch => {
total_mismatches += 1;
if mismatches.len() < 64 {
let detail = format!(
"cpu={cpu_summary:?} gpu={gpu_summary:?} origin={:?} dir={:?}",
case.origin, case.dir
);
mismatches.push(PolyhedronMismatch {
case_index: idx,
case: *case,
outcome,
cpu_hit: cpu_summary,
gpu_hit: gpu_summary,
detail,
});
}
}
}
}
PolyhedronCheckResult {
total_cases: total,
whitelisted_ties,
total_mismatches,
mismatches,
}
}
fn classify_case(
cpu_hit: Option<crate::optics::raytracer::HitRecord>,
gpu_hit: &GpuHitRecord,
) -> (CaseOutcome, HitSummary, HitSummary) {
let cpu_summary = cpu_hit.map(|h| (h.t, h.facet_idx as i64, h.normal));
let gpu_summary = (gpu_hit.hit != 0).then(|| {
(
gpu_hit.t,
i64::from(gpu_hit.facet_idx),
Vec3::from_array(gpu_hit.normal),
)
});
let outcome = match (cpu_hit, gpu_hit.hit != 0) {
(None, false) => CaseOutcome::Agree,
(None, true) | (Some(_), false) => CaseOutcome::Mismatch,
(Some(cpu), true) => {
if cpu.facet_idx as i64 == i64::from(gpu_hit.facet_idx) {
let t_ulp = ulp_distance(cpu.t, gpu_hit.t);
let n_ulp = ulp_distance(cpu.normal.x, gpu_hit.normal[0])
.max(ulp_distance(cpu.normal.y, gpu_hit.normal[1]))
.max(ulp_distance(cpu.normal.z, gpu_hit.normal[2]));
if t_ulp <= HIT_ULP_BUDGET && n_ulp <= HIT_ULP_BUDGET {
CaseOutcome::Agree
} else {
CaseOutcome::Mismatch
}
} else if (cpu.t - gpu_hit.t).abs() < TIE_EPS {
CaseOutcome::WhitelistedTie
} else {
CaseOutcome::Mismatch
}
}
};
(outcome, cpu_summary, gpu_summary)
}