perf(scene): keep block cache across non-block edits

The block-definition tessellation cache (BlockCache::build — every block
defn) was keyed on geometry_epoch, so every edit rebuilt all block defns.
On a block-heavy drawing that turned the ~30ms visible-wire re-tess into a
~400ms spike at line-commit, grip first-move and grip release.

Key the block cache on a separate block_epoch. bump_geometry bumps both
geometry_epoch and block_epoch (safe default — xref, block create,
explode, REFCLOSE, undo, open all keep rebuilding blocks). Operations that
provably can't change a block definition now call bump_geometry_no_blocks,
which bumps only geometry_epoch so the visible wires re-tessellate against
the reused block cache:

- add_entity for a non-Insert/Block entity (line, arc, text, hatch, …) —
  scene.add_entity only ever routes to model/paper space, never block
  content;
- grip drag hide / commit / Esc-cancel — moving a top-level entity or an
  insert never edits a block defn.

Line-commit and grip start/end drop from ~400ms to the ~30ms baseline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-05 15:24:02 +03:00
1 parent e37dfa5890
commit 3b2f8431a3
3 files changed
+51 -6

No files matched your search

+12
View File
@@ -132,6 +132,18 @@ handles; the render path re-tessellates only those, reusing the rest.
Also useful on open: any partial cache (e.g. from block defns) can be
re-used.
**Partial (landed): block cache survives non-block edits.** The
block-definition tessellation cache (`BlockCache::build`, every block defn)
was keyed on `geometry_epoch`, so *any* edit rebuilt all block defns — a
~30 ms baseline wire re-tess turned into a ~400 ms spike on block-heavy
drawings at line-commit / grip-start / grip-release. The cache now keys on a
separate `block_epoch`. `bump_geometry` bumps both (safe default); the
operations that provably can't change a block defn — adding a top-level
entity (`add_entity` for non-Insert/Block) and grip-moving an entity/insert —
call `bump_geometry_no_blocks`, which re-tessellates only the visible wires
(block cache reused). Block-content edits (REFCLOSE, block create, xref,
explode) still go through full `bump_geometry`.
**Partial (landed): grip drag.** Dragging an entity's grip called
`scene.apply_grip` every move, which `bump_geometry`'d → a full model
re-tessellation per move (plus an O(N) clone of all wires for snapping). Now
+5 -3
View File
@@ -1325,7 +1325,7 @@ impl OpenCADStudio {
}
self.tabs[i].scene.hidden.remove(&h);
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.bump_geometry_no_blocks();
self.refresh_selected_grips();
}
self.tabs[self.active_tab].snap_result = None;
@@ -2127,7 +2127,9 @@ impl OpenCADStudio {
self.grip_original =
self.tabs[i].scene.document.get_entity(grip.handle).cloned();
self.tabs[i].scene.hidden.insert(grip.handle);
self.tabs[i].scene.bump_geometry();
// Grip drag never changes a block definition — keep the
// block cache so the hide doesn't re-tessellate blocks.
self.tabs[i].scene.bump_geometry_no_blocks();
self.grip_preview_handle = Some(grip.handle);
}
@@ -2650,7 +2652,7 @@ impl OpenCADStudio {
self.grip_original = None;
self.tabs[i].scene.hidden.remove(&h);
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.bump_geometry_no_blocks();
}
// Placement confirmed — keep the just-added leader.
self.grip_add_provisional = None;
+34 -3
View File
@@ -616,6 +616,13 @@ pub struct Scene {
/// preview wires, layer visibility, layout). The GPU pipeline uses this to
/// skip re-uploading unchanged geometry buffers every frame.
pub geometry_epoch: u64,
/// Separate epoch for the (expensive) block-definition tessellation cache.
/// Bumped together with `geometry_epoch` by `bump_geometry`, but NOT by
/// `bump_geometry_no_blocks` — so edits that provably can't change any
/// block definition (drawing a top-level entity, grip-moving an
/// entity/insert) re-tessellate only the visible wires (~baseline cost)
/// instead of rebuilding every block defn (the edit-time spike).
pub block_epoch: u64,
/// Incremented when the selection / hover-highlight set changes WITHOUT a
/// geometry change. The wire tessellation is selection-independent, so a
/// pick only refreshes the GPU xray overlay (cheap) instead of bumping
@@ -796,6 +803,7 @@ impl Scene {
interim_wire: None,
camera_generation: 0,
geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
block_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
selection_generation: 0,
wire_cache: RefCell::new(None),
model_tile_wire_cache: RefCell::new(HashMap::default()),
@@ -920,7 +928,7 @@ impl Scene {
{
let cache = self.block_defn_cache.borrow();
if let Some((epoch, ref arc)) = *cache {
if epoch == self.geometry_epoch {
if epoch == self.block_epoch {
return Arc::clone(arc);
}
}
@@ -937,12 +945,23 @@ impl Scene {
};
let built = block_cache::BlockCache::build(&self.document, anno, bg);
let arc = Arc::new(built);
*self.block_defn_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
*self.block_defn_cache.borrow_mut() = Some((self.block_epoch, Arc::clone(&arc)));
arc
}
pub fn bump_geometry(&mut self) {
self.geometry_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
// Default: also invalidate block definitions. Safe for every caller;
// operations that know blocks are untouched use `bump_geometry_no_blocks`.
self.block_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
}
/// Invalidate the visible-wire tessellation but KEEP the cached block
/// definitions. Use only when the edit provably can't change any block
/// defn (top-level entity create/edit, grip-moving an entity or insert) —
/// it skips the all-blocks re-tessellation that otherwise spikes edit time.
pub fn bump_geometry_no_blocks(&mut self) {
self.geometry_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
}
/// Mark the selection / hover-highlight set dirty without invalidating the
@@ -3251,6 +3270,14 @@ impl Scene {
// ── Entity management ─────────────────────────────────────────────────
pub fn add_entity(&mut self, mut entity: EntityType) -> Handle {
// Only Insert / Block entities can introduce or reference a block
// definition that the block cache must (re)build. Adding a plain
// top-level entity (line, arc, text, …) leaves every block defn intact,
// so it keeps the cache and skips the all-blocks re-tessellation.
let affects_blocks = matches!(
&entity,
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
);
let hatch_offset = if self.current_layout == "Model" {
self.world_offset
} else {
@@ -3325,7 +3352,11 @@ impl Scene {
if let Some(model) = mesh_seed {
self.meshes.insert(handle, model);
}
self.bump_geometry();
if affects_blocks {
self.bump_geometry();
} else {
self.bump_geometry_no_blocks();
}
}
handle
}