From 3b2f8431a3fe998cf16b3c2c01041271e6933908 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 5 Jun 2026 15:24:02 +0300 Subject: [PATCH] perf(scene): keep block cache across non-block edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ROADMAP.md | 12 ++++++++++++ src/app/update.rs | 8 +++++--- src/scene/mod.rs | 37 ++++++++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e638ac6c6..1d940bb34 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/src/app/update.rs b/src/app/update.rs index 50abeeaba..7ae887ee4 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 743203236..1110fd929 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 }