forked from 1b-agent/opencadstudio-upstream
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:
1 parent
e37dfa5890
commit
3b2f8431a3
3 files changed
+51
-6
No files matched your search
+12
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user