use solidcraft_geom::Vec3; use solidcraft_kernel::{Body, BoolOp}; use super::*; /// The GPU, or `None` with a note when the machine has no adapter (CI runners without a GPU). fn gpu() -> Option { match TessGpu::headless() { Ok(g) => { eprintln!("GPU: {}", g.adapter_name()); Some(g) } Err(e) => { eprintln!("skipping the GPU comparison: {e}"); None } } } fn ok(r: std::result::Result) -> T { match r { Ok(v) => v, Err(e) => panic!("{e:?}"), } } fn bodies() -> Vec<(&'static str, Body)> { let bx = ok(solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 4.0))); let pin = ok(solidcraft_kernel::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 6.0)); let cross = ok(solidcraft_kernel::cylinder(Vec3::new(5.0, -1.0, 2.0), Vec3::Y, 1.0, 12.0)); let cut = |a: &Body, b: &Body| ok(solidcraft_kernel::boolean(a, b, BoolOp::Cut)).unwrap_or_else(|| a.clone()); let edges = [Vec3::new(5.0, 0.0, 4.0), Vec3::new(10.0, 5.0, 4.0), Vec3::new(5.0, 10.0, 4.0), Vec3::new(0.0, 5.0, 4.0)]; vec![ ("box with a hole", cut(&bx, &pin)), ("cross hole", cut(&pin, &cross)), ("torus", ok(solidcraft_kernel::torus(Vec3::ZERO, 6.0, 1.5))), ("fillets", ok(solidcraft_kernel::fillet(&bx, &edges, 1.5))), ( "mirrored", ok(solidcraft_kernel::transform_matrix(&pin, [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [3.0, 0.0, 1.0, 1.0]])), ), ] } #[test] fn gpu_evaluation_matches_the_cpu() { let Some(gpu) = gpu() else { return }; for (name, body) in bodies() { let plan = ok(body.display_plan(0.01)); let cpu = plan.evaluate(); let got = ok(gpu.evaluate(&plan)); assert_eq!(got.triangles, cpu.triangles, "{name}"); assert_eq!(got.tri_face, cpu.tri_face, "{name}"); assert_eq!(got.positions.len(), cpu.positions.len(), "{name}"); let size = 20.0; for (k, (p, q)) in got.positions.iter().zip(&cpu.positions).enumerate() { assert!(p.dist(*q) < size * 1e-5, "{name}: vertex {k} at {p:?}, CPU {q:?}"); } for (k, (n, m)) in got.normals.iter().zip(&cpu.normals).enumerate() { assert!(n.dist(*m) < 2e-3, "{name}: normal {k} {n:?}, CPU {m:?}"); } // Fixed edge nodes are exact: shared boundaries are bit-identical to the CPU mesh's. let mut k = 0; for f in &plan.faces { for (j, p) in f.fixed.iter().enumerate() { assert_eq!(got.positions.get(k + j), Some(p), "{name}"); } k += f.uv.len(); } } } /// A plan drawn straight from the GPU: `prepare` and `record` into an encoder, as the viewport /// does, give the vertices and triangles the CPU evaluator gives (read back here only to check). #[test] fn a_resident_mesh_matches_the_cpu() { let Some(gpu) = gpu() else { return }; for (name, body) in bodies() { let plan = ok(body.display_plan_angular(0.01, std::f64::consts::PI / 9.0)); let cpu = plan.evaluate(); let mesh = ok(gpu.prepare(&plan)); assert_eq!(mesh.vertex_count as usize, cpu.positions.len(), "{name}"); assert_eq!(mesh.index_count as usize, cpu.triangles.len() * 3, "{name}"); assert_eq!(mesh.vertex_face.len(), cpu.positions.len(), "{name}"); for (t, f) in cpu.triangles.iter().zip(&cpu.tri_face) { assert!(t.iter().all(|&i| mesh.vertex_face[i as usize] == *f), "{name}: a triangle's vertices belong to its face"); } let d = &gpu.device; let read = |src: &wgpu::Buffer, size: u64| { let dst = d.create_buffer(&wgpu::BufferDescriptor { label: Some("test read"), size, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let mut enc = d.create_command_encoder(&Default::default()); gpu.record(&mut enc, &mesh); enc.copy_buffer_to_buffer(src, 0, &dst, 0, size); gpu.queue.submit([enc.finish()]); let slice = dst.slice(..); slice.map_async(wgpu::MapMode::Read, |_| {}); ok(d.poll(wgpu::PollType::wait_indefinitely())); let bytes = ok(slice.get_mapped_range()); bytes.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect::>() }; let out = read(&mesh.vertices, u64::from(mesh.vertex_count) * VERTEX_STRIDE); for (k, (p, n)) in cpu.positions.iter().zip(&cpu.normals).enumerate() { let at = |i: usize| f64::from(out[8 * k + i]); assert!(Vec3::new(at(0), at(1), at(2)).dist(*p) < 20.0 * 1e-5, "{name}: vertex {k}"); assert!(Vec3::new(at(4), at(5), at(6)).dist(*n) < 2e-3, "{name}: normal {k}"); } } } /// Display meshing of a 400-hole plate (#303's benchmark): reference mesher, plan, CPU and GPU /// evaluation. `cargo test --release -p solidcraft-gpu bench_plate -- --ignored --nocapture`. #[test] #[ignore] fn bench_plate() { use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2}; let rect = |w: f64| Loop2 { segs: vec![ Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(w, 0.0) }, Seg2::Line { a: Vec2::new(w, 0.0), b: Vec2::new(w, w) }, Seg2::Line { a: Vec2::new(w, w), b: Vec2::new(0.0, w) }, Seg2::Line { a: Vec2::new(0.0, w), b: Vec2::new(0.0, 0.0) }, ], }; let holes: Vec = (0..400) .map(|k| { let c = Vec2::new(5.0 + 10.0 * (k % 20) as f64, 5.0 + 10.0 * (k / 20) as f64); Loop2 { segs: vec![Seg2::Arc { center: c, radius: 3.0, start: 0.0, sweep: -std::f64::consts::TAU }] } }) .collect(); let plate = ok(solidcraft_kernel::extrude(&Plane::XY, &[Region2 { outer: rect(200.0), holes }], 0.0, 5.0)).remove(0); let gpu = gpu(); let ms = |t: std::time::Instant| t.elapsed().as_secs_f64() * 1e3; let median = |mut v: Vec| { v.sort_by(f64::total_cmp); v[v.len() / 2] }; println!("faces {}", plate.face_count()); println!("| tol | reference | plan | CPU eval | GPU eval | triangles (ref / plan) |"); for tol in [0.3, 0.03, 0.006] { let (mut r, mut p, mut c, mut g) = (vec![], vec![], vec![], vec![]); let (mut nr, mut np) = (0, 0); for _ in 0..5 { let t = std::time::Instant::now(); let m = ok(plate.tessellate(tol)); r.push(ms(t)); nr = m.triangles.len(); let t = std::time::Instant::now(); let plan = ok(plate.display_plan(tol)); p.push(ms(t)); let t = std::time::Instant::now(); let m = plan.evaluate(); c.push(ms(t)); np = m.triangles.len(); if let Some(gpu) = &gpu { let t = std::time::Instant::now(); let _ = ok(gpu.evaluate(&plan)); g.push(ms(t)); } } let g = if g.is_empty() { "-".to_string() } else { format!("{:.1} ms", median(g)) }; println!("| {tol} | {:.1} ms | {:.1} ms | {:.1} ms | {g} | {nr} / {np} |", median(r), median(p), median(c)); } }