Display tessellation plans (3/4): display meshes from plans, GPU evaluation in the viewport (#805)

Display meshes are built from tessellation plans, with GPU evaluation in the viewport.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-13 16:27:16 -04:00
1 parent 534393ed5a
commit ff7a347013
19 files changed
+1474 -77

No files matched your search

+57 -2
View File
@@ -338,7 +338,24 @@ fn build_scene(app: &SolveApp) -> GpuScene {
let opacity = s.doc.model_body_opacity(b);
let face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look> =
if shaded { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded) {
// Opaque bodies are drawn from their display plans on the GPU where the device evaluates
// them; faded and see-through ones (sorted and blended per triangle) are built here.
let opaque = !faded && opacity >= 0.999 && face_looks.values().all(|l| l.opacity >= 0.999);
let planned = (shaded && opaque && app.viewport.gpu.is_some_and(|g| g.tess))
.then(|| b.display_plan())
.flatten()
.filter(|plan| plan.vertex_count() <= solidcraft_gpu::MAX_BATCH);
let on_gpu = planned.is_some();
if let Some(plan) = planned {
let face_colors = (0..b.body.face_count())
.map(|f| match face_looks.get(&f) {
Some(l) => c4(Color32::from_rgb(l.color[0], l.color[1], l.color[2])),
None => col,
})
.collect();
sc.planned.push(crate::gpu::PlannedBody { plan, face_colors });
}
for (ti, t) in m.triangles.iter().enumerate().filter(|_| shaded && !on_gpu) {
let (mut col, a) = match m.tri_face.get(ti).and_then(|f| face_looks.get(&(*f as usize))) {
Some(l) => {
let c = Color32::from_rgb(l.color[0], l.color[1], l.color[2]);
@@ -3189,6 +3206,44 @@ fn view_menu(app: &mut SolveApp, ui: &mut egui::Ui, button: Rect) -> bool {
mod tests {
use super::*;
/// Bodies drawn from their display plans, evaluated on the GPU, look like the same bodies
/// built into triangles on the CPU: the sample design rendered both ways through the
/// viewport's real pipelines, offscreen, on this machine's GPU (skipped without one).
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn bodies_evaluated_on_the_gpu_look_like_bodies_built_on_the_cpu() {
let mut s = solidcraft_engine::Session::default();
s.run_script(&solidcraft_engine::sample::script()).unwrap();
let mut app = SolveApp::new(s, crate::Services::default());
app.viewport.no_pixels = true;
app.fit_view();
const SIZE: u32 = 256;
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
let mut render = |tess: bool| {
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
let scene = build_scene(&app);
let proj = projection(&app, rect);
let e = proj.cam.eye().to_f32();
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
let planned = scene.planned.len();
(planned, scene.tris.len(), crate::gpu::render_offscreen(scene, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess))
};
let (planned, gpu_tris, gpu) = render(true);
let (none, cpu_tris, cpu) = render(false);
let (Some(gpu), Some(cpu)) = (gpu, cpu) else {
eprintln!("skipping: no GPU adapter");
return;
};
assert!(planned > 0 && none == 0, "{planned} bodies planned");
assert!(gpu_tris < cpu_tris, "the planned bodies' triangles are not built on the CPU ({gpu_tris} vs {cpu_tris} bytes)");
let px = |img: &[u8], i: usize| [img[4 * i], img[4 * i + 1], img[4 * i + 2]];
let n = (SIZE * SIZE) as usize;
let drawn = (0..n).filter(|&i| px(&cpu, i) != [255, 255, 255]).count();
assert!(drawn > n / 20, "the model covers the view: {drawn} pixels");
let differ = (0..n).filter(|&i| px(&gpu, i).iter().zip(px(&cpu, i)).any(|(a, b)| a.abs_diff(b) > 24)).count();
assert!(differ * 100 < drawn, "{differ} of {drawn} model pixels differ");
}
#[test]
fn construction_drawing_and_hover_overlays_preserve_visible_gaps() {
fn length(shape: &Shape) -> f32 {
@@ -3383,7 +3438,7 @@ mod tests {
app.viewport.no_pixels = true;
if gpu {
// The callback is queued by egui; no device is needed to inspect scene updates.
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1 });
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
}
app
}