forked from 1b-agent/opencadstudio-upstream
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:
1 parent
91b2344db1
commit
0b4551b6ec
1 file changed
+94
-24
+94
-24
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user