perf: stop tessellating the whole model to measure it

`view-detail` put 7826.9 of the first paper frame's 7832.8 ms in the grid
overlay. The path is `grid_views` → `active_viewports` →
`camera_for_viewport` → `model_space_extents`, whose fast path reads the
already-built wire AABBs but is gated on `current_layout == "Model"` —
correctly, because `wire_cache` holds the *current layout's* wires, which
in a Layout are the paper sheet's. So a paper frame takes the fallback,
which tessellates every model-space entity to read its key vertices.

That fallback called `tessellate_one` per entity, and `tessellate_one`
resolves the background, annotation scale and block cache from `&self`
every time — including a `RefCell` borrow and a cache lookup for a block
cache that is identical on all 186 455 calls. The walk was also serial,
while the wire builder's own tessellation pass has been parallel for a
while.

Those inputs are now resolved once, by a `tessellation_context` that
`tessellate_one` also uses so the two cannot drift, and the walk runs
through rayon. The reduction is min/max/or, so parallel order does not
change the result; the arguments to `tessellate_entity` are unchanged.

This makes the fallback cheaper, not absent. Tessellating a million
entities to obtain a bounding box is the wrong shape, and the structural
fix — a model-space extent that does not depend on which layout is being
viewed — needs the next measurement to size. Recorded in DEBUG_NOTES.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-05 18:53:01 +03:00
1 parent 91b2344db1
commit 0b4551b6ec
1 file changed
+94 -24
+94 -24
View File
@@ -10014,20 +10014,31 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
}
/// Full tessellation pipeline for one entity.
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
let bg = if self.current_layout == "Model" {
/// The document-wide inputs `tessellate_entity` takes, resolved from the
/// current layout and viewport.
///
/// Every entity in a walk gets the same four, and resolving them per entity
/// is not free — `block_cache_arc_for` takes a `RefCell` borrow and a cache
/// lookup. Callers that tessellate more than one entity resolve them once
/// and pass them down; sharing the resolution keeps those callers from
/// drifting away from `tessellate_one`.
fn tessellation_context(
&self,
) -> ([f32; 4], f32, Option<Handle>, Arc<cache::block_cache::BlockCache>, bool) {
let is_model = self.current_layout == "Model";
let bg = if is_model {
self.bg_color
} else {
self.paper_bg_color
};
let anno = if self.current_layout == "Model" {
let anno = if is_model {
self.annotation_scale
} else if let Some(viewport) = self.active_viewport {
self.viewport_annotation_multiplier(viewport)
} else {
1.0
};
let annotation_scale_handle = if self.current_layout == "Model" {
let annotation_scale_handle = if is_model {
crate::scene::annotative::scale_handle_by_name(
&self.document,
&self.document.header.current_annotation_scale,
@@ -10042,6 +10053,12 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
self.annotation_all_visible(),
self.active_viewport,
);
(bg, anno, annotation_scale_handle, blk_cache, !is_model)
}
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
let (bg, anno, annotation_scale_handle, blk_cache, paper) =
self.tessellation_context();
// tessellate_one is used for one-off lookups (hit test, properties).
// Skip culling here so the caller always gets the full geometry.
tessellate_entity(
@@ -10055,7 +10072,7 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
Some(&blk_cache),
None,
None,
self.current_layout != "Model",
paper,
)
}
@@ -10240,27 +10257,80 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates
// offset-rel coords there silently double-subtracts world_offset
// inside `camera_for_viewport` and points the viewport at the
// wrong location on UTM-scale drawings.
for entity in self.document.entities() {
let c = entity.common();
if c.owner_handle != model_block
|| c.invisible
|| self.entity_temporarily_hidden(c.handle)
{
continue;
}
for wire in self.tessellate_one(entity) {
for &[x, y, z] in &wire.key_vertices {
let v = glam::Vec3::new(x as f32, y as f32, z as f32);
// Check finiteness *after* the f32 cast: a ray/xline endpoint
// is a huge-but-finite f64 that overflows to inf in f32, which
// the f64 `is_finite` test would have let through.
if v.is_finite() {
min = min.min(v);
max = max.max(v);
any = true;
//
// `tessellate_one` resolves the background, annotation scale and block
// cache from `&self` on every call, and this walk calls it once per
// model entity — 186 k times on a large drawing, each one taking a
// `RefCell` borrow for a block cache that is the same every time. Those
// inputs do not vary across the loop, so they are resolved once and the
// shared tessellator is called directly, which also lets the walk run
// in parallel the way the wire builder's own tessellation pass does.
// Same arguments, same output: only who computes them changed.
let (bg_for_tess, anno_for_tess, anno_handle_for_tess, blk_for_tess, paper_for_tess) =
self.tessellation_context();
let subjects: Vec<&EntityType> = self
.document
.entities()
.filter(|entity| {
let c = entity.common();
c.owner_handle == model_block
&& !c.invisible
&& !self.entity_temporarily_hidden(c.handle)
})
.collect();
let doc = &self.document;
let sel = &self.selected;
let avp = self.active_viewport;
let blk_ref: &cache::block_cache::BlockCache = &blk_for_tess;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
let reduced = subjects
.into_par_iter()
.map(|entity| {
let mut lo = glam::Vec3::splat(f32::INFINITY);
let mut hi = glam::Vec3::splat(f32::NEG_INFINITY);
let mut hit = false;
for wire in tessellate_entity(
doc,
sel,
avp,
bg_for_tess,
anno_for_tess,
anno_handle_for_tess,
entity,
Some(blk_ref),
None,
None,
paper_for_tess,
) {
for &[x, y, z] in &wire.key_vertices {
let v = glam::Vec3::new(x as f32, y as f32, z as f32);
// Check finiteness *after* the f32 cast: a ray/xline
// endpoint is a huge-but-finite f64 that overflows to
// inf in f32, which the f64 `is_finite` test would have
// let through.
if v.is_finite() {
lo = lo.min(v);
hi = hi.max(v);
hit = true;
}
}
}
}
(lo, hi, hit)
})
.reduce(
|| {
(
glam::Vec3::splat(f32::INFINITY),
glam::Vec3::splat(f32::NEG_INFINITY),
false,
)
},
|a, b| (a.0.min(b.0), a.1.max(b.1), a.2 || b.2),
);
if reduced.2 {
min = min.min(reduced.0);
max = max.max(reduced.1);
any = true;
}
// Same mesh inclusion for the tessellate fallback path.
for (&handle, set) in &self.meshes {