3D model viewer: STL and OBJ
git clone https://git.lucas.co/cce-model.git
Traced view: build the BVH on the worker, upload only in stage_3d
want_trace now builds a cce-ui PreparedRtScene (packing + BVH) beside the
scene's triangles, and stage_3d hands it to set_rt_scene_prepared. The
BVH is built only when the stage's rt_needs_bvh() (asked in init_3d) says
the tracer traverses one; the ray-query tier builds its own on the GPU.
torus5m.stl (5,001,600 triangles), scale-2 shadow, Intel iGPU: the UI
thread's share went from 2141 ms to 61 ms (worker 2032 ms). RTX 4080,
ray-query tier: worker 321 ms, upload with the BLAS build 146 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 14 +++++++++++---
src/main.rs | 43 +++++++++++++++++++++++--------------------
2 files changed, 34 insertions(+), 23 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 879673d..ac40ef4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -73,9 +73,17 @@ view (`r`).
with the traced image left in the backdrop. One frame past each edge is
deliberate: the HUD paints BEFORE `stage_3d`, so the cap and the
"preparing" notice each need a frame of their own to be seen.
-- **`set_rt_scene` builds the BVH on the UI thread** (the stage is only
- lent there): 2.3 s for 5M triangles on the shadow's iGPU. The window says
- "traced: preparing…" first; moving it off-thread needs cce-ui.
+- **The tracer's BVH is built on the worker**, with the rest of the scene
+ (`PreparedRtScene::new` in `want_trace`, cce-ui ≥ e5fbe9a); `stage_3d`
+ only uploads it (`set_rt_scene_prepared`). Whether to build one is the
+ stage's `rt_needs_bvh()`, asked in `init_3d`: the hardware ray-query tier
+ builds its own structure on the GPU and skips it. Measured 2026-10-07,
+ torus5m.stl (5,001,600 triangles), scale-2 shadow: Intel iGPU (compute
+ tier) worker 2032 ms, UI thread 61 ms (it was 2141 ms on the UI thread);
+ RTX 4080 (ray-query, `CCE_VK_DEVICE=discrete` plus the live `DISPLAY=:0`
+ in the shadow) worker 321 ms, upload with the BLAS build 146 ms. The
+ prepared scene stays in memory for a reconnect's re-upload (~300 MB at
+ 5M with its BVH, on top of the baked vertices).
- **The tracer has no smooth normals**: a coarse sphere shows its facets
when traced. Milestone 5's territory (cce-ui's materials and vertices).
- No root plate, on purpose (`style-audit: opt-out` in `display_list`): the
diff --git a/src/main.rs b/src/main.rs
index ff3ccaa..aeb2101 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -33,7 +33,7 @@ use std::time::{Duration, Instant};
use cce_mesh_io::{Mesh, Unit, UpAxis};
use cce_ui::engine::{
- AppSender, Application, LogicalPosition, LogicalSize, MeshId, RtCamera, RtMaterial, RtTriangle, SceneDraw, Stage3D,
+ AppSender, Application, LogicalPosition, LogicalSize, MeshId, PreparedRtScene, RtCamera, SceneDraw, Stage3D,
Vertex3D, WindowSettings,
};
use cce_ui::scene::layout::Rect;
@@ -73,7 +73,7 @@ enum Message {
/// The wireframe and the normals of the model of `generation`.
Overlays { generation: u64, edges: Arc<Vec<Vertex3D>>, normals: Arc<Vec<Vertex3D>> },
/// The traced view's scene for the model of `generation`.
- TraceScene { generation: u64, scene: Arc<TraceScene> },
+ TraceScene { generation: u64, scene: Arc<PreparedRtScene> },
Quit,
}
@@ -96,13 +96,6 @@ struct Model {
floor_y: f32,
}
-/// The tracer's triangles and materials, built on a worker.
-#[derive(Debug)]
-struct TraceScene {
- triangles: Vec<RtTriangle>,
- materials: Vec<RtMaterial>,
-}
-
impl Model {
fn read(path: &Path) -> Result<Model, String> {
let scene = cce_mesh_io::load(path)?;
@@ -191,12 +184,18 @@ struct ModelApp {
/// The path-traced view: asked for, its scene once built, whether the
/// current renderer holds it, and how far it has refined.
traced: bool,
- trace_scene: Option<Arc<TraceScene>>,
+ /// Built on a worker — packed, and its BVH built when the renderer's
+ /// tracer wants one — so the UI thread only uploads it.
+ trace_scene: Option<Arc<PreparedRtScene>>,
+ /// Whether the renderer's tracer traverses a CPU-built BVH (asked in
+ /// `init_3d`): the worker builds one only then.
+ rt_needs_bvh: bool,
building_trace: bool,
trace_uploaded: bool,
- /// A frame saying "preparing" has gone out ahead of the upload, which
- /// freezes the window while cce-ui builds the tracer's BVH (2.4 s for
- /// 5M triangles), so the freeze is explained before it happens.
+ /// A frame saying "preparing" has gone out ahead of the upload. The BVH
+ /// is the worker's now, so the upload is short (61 ms for 5M triangles
+ /// on the iGPU, against 2.1 s when it built the BVH too), but the first
+ /// one also makes the tracer's pipelines.
trace_announced: bool,
samples: u32,
sample_cap: u32,
@@ -307,17 +306,20 @@ impl ModelApp {
}
let Some(model) = self.model.clone() else { return };
self.building_trace = true;
- let (generation, sender) = (self.generation, self.sender.clone());
+ let (generation, sender, with_bvh) = (self.generation, self.sender.clone(), self.rt_needs_bvh);
std::thread::spawn(move || {
let started = Instant::now();
let (triangles, materials) = trace::scene(&model.mesh, model.floor_y);
+ let n_materials = materials.len();
+ let scene = PreparedRtScene::new(triangles, &materials, None, with_bvh);
log::info!(
- "[model] trace scene: {} triangles, {} materials in {:.0} ms",
- triangles.len(),
- materials.len(),
+ "[model] trace scene: {} triangles, {} materials{} in {:.0} ms",
+ scene.triangle_count(),
+ n_materials,
+ if with_bvh { ", BVH" } else { "" },
started.elapsed().as_secs_f64() * 1e3
);
- let _ = sender.send(Message::TraceScene { generation, scene: Arc::new(TraceScene { triangles, materials }) });
+ let _ = sender.send(Message::TraceScene { generation, scene: Arc::new(scene) });
});
}
@@ -451,6 +453,7 @@ impl Application for ModelApp {
show_normals: false,
traced: false,
trace_scene: None,
+ rt_needs_bvh: true,
building_trace: false,
trace_uploaded: false,
trace_announced: false,
@@ -575,6 +578,7 @@ impl Application for ModelApp {
bg(-1.0, 1.0, SKY_TOP),
]);
stage.set_scene_light(light::KEY.normalize().to_array());
+ self.rt_needs_bvh = stage.rt_needs_bvh();
self.gpu = Some(Gpu { background, model: None, grid: None, edges: None, normals: None });
let has = self.model.is_some();
let built = self.overlays.is_some();
@@ -636,9 +640,8 @@ impl Application for ModelApp {
if !std::mem::replace(&mut self.trace_announced, true) {
return true;
}
- let scene = self.trace_scene.as_ref().unwrap();
let started = Instant::now();
- stage.set_rt_scene(&scene.triangles, &scene.materials);
+ stage.set_rt_scene_prepared(self.trace_scene.as_ref().unwrap());
stage.set_rt_environment(trace::environment(light::KEY.normalize().to_array()));
stage.set_rt_background(Some(trace::BACKGROUND));
log::info!("[model] trace scene uploaded in {:.0} ms", started.elapsed().as_secs_f64() * 1e3);