forked from 1b-agent/opencadstudio-upstream
perf(scene): decouple selection from wire tessellation
Picking an entity called bump_geometry, which invalidated the wire cache and re-tessellated the entire model just to repaint one entity — 30ms to 400ms on a large drawing (visible as a tess-ms spike on the PERF HUD). Selection highlight is no longer baked into tessellation. wires_for_block_ culled now feeds tessellate_entity an empty selection set, so wires are always base-coloured and the wire cache is selection-independent. The highlight is applied in the GPU xray overlay: prepare filters the cached wires by the live `selected ∪ hover` handle set, recolours them to WireModel::SELECTED, and batches them — rebuilt only when a new selection_generation (or the underlying wire content) changes. Pure select / deselect / hover paths now call bump_selection instead of bump_geometry, so a pick refreshes just the overlay — no re-tessellation, no main wire re-upload (geometry_epoch and wire_content_id are unchanged). Geometry edits, undo and file-open still bump_geometry and rebuild everything. Grips/properties refresh through their existing app paths. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
0d18e0d1bf
commit
ff9f49511b
4 files changed
+140
-34
No files matched your search
+61
-28
@@ -616,6 +616,11 @@ 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,
|
||||
/// 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
|
||||
/// `geometry_epoch` and re-tessellating the whole model.
|
||||
pub selection_generation: u64,
|
||||
/// Cached tessellation of all visible entity wires for the current layout.
|
||||
/// Keyed by `(geometry_epoch, camera_generation)` so a camera change
|
||||
/// invalidates the cull-dependent wire list as well as a geometry change.
|
||||
@@ -759,6 +764,10 @@ pub struct Scene {
|
||||
#[allow(clippy::type_complexity)]
|
||||
split_cache:
|
||||
RefCell<Option<(u64, Arc<Vec<WireModel>>, Arc<Vec<WireModel>>)>>,
|
||||
/// Cached `selected ∪ hover` handle set for the GPU xray overlay, keyed by
|
||||
/// `selection_generation`. Rebuilt only when the selection changes so
|
||||
/// `build_primitive` doesn't clone the set every frame.
|
||||
highlight_set_cache: RefCell<Option<(u64, Arc<HashSet<Handle>>)>>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
@@ -787,6 +796,7 @@ impl Scene {
|
||||
interim_wire: None,
|
||||
camera_generation: 0,
|
||||
geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
|
||||
selection_generation: 0,
|
||||
wire_cache: RefCell::new(None),
|
||||
model_tile_wire_cache: RefCell::new(HashMap::default()),
|
||||
sort_cache: RefCell::new(None),
|
||||
@@ -821,6 +831,7 @@ impl Scene {
|
||||
last_model_wire_gen: std::cell::Cell::new(0),
|
||||
wire_force_nonce: std::cell::Cell::new(0),
|
||||
split_cache: RefCell::new(None),
|
||||
highlight_set_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,6 +945,35 @@ impl Scene {
|
||||
self.geometry_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Mark the selection / hover-highlight set dirty without invalidating the
|
||||
/// (selection-independent) wire tessellation. Only the GPU xray overlay is
|
||||
/// rebuilt — no re-tessellation. Use this for pure select / deselect /
|
||||
/// hover changes; use [`bump_geometry`] when the geometry itself changed.
|
||||
pub fn bump_selection(&mut self) {
|
||||
self.selection_generation = self.selection_generation.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// `selected ∪ hover` handles, cached per `selection_generation`. Drives the
|
||||
/// GPU xray overlay that highlights the current selection without baking it
|
||||
/// into the wire tessellation.
|
||||
pub(super) fn highlight_handles(&self) -> Arc<HashSet<Handle>> {
|
||||
{
|
||||
let c = self.highlight_set_cache.borrow();
|
||||
if let Some((gen, arc)) = &*c {
|
||||
if *gen == self.selection_generation {
|
||||
return Arc::clone(arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut hl = self.selected.clone();
|
||||
if let Some(h) = self.hover_highlight {
|
||||
hl.insert(h);
|
||||
}
|
||||
let arc = Arc::new(hl);
|
||||
*self.highlight_set_cache.borrow_mut() = Some((self.selection_generation, Arc::clone(&arc)));
|
||||
arc
|
||||
}
|
||||
|
||||
/// Re-evaluate every cached mesh's color through `render_style` so a
|
||||
/// Register a Model-tab solid: cache its truck B-rep (for boolean ops) and
|
||||
/// tessellate it into the shaded mesh pipeline under `handle`. The solid is
|
||||
@@ -1417,23 +1457,21 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Set (or clear) the previewed entity that renders with the selection
|
||||
/// highlight without joining the real selection. Re-tessellates on change.
|
||||
/// highlight without joining the real selection. Only refreshes the GPU
|
||||
/// xray overlay (no re-tessellation).
|
||||
pub fn set_hover_highlight(&mut self, handle: Option<Handle>) {
|
||||
if self.hover_highlight == handle {
|
||||
return;
|
||||
}
|
||||
// The hover entity is folded into the tessellation highlight set
|
||||
// (selected ∪ {hover}); see `wires_for_block_culled`. A hover handle
|
||||
// that's already selected contributes nothing, so the effective set —
|
||||
// and thus every WireModel — is identical. Only re-tessellate when the
|
||||
// effective highlight contribution actually changes; hovering over (or
|
||||
// between) already-selected entities then costs nothing. The field is
|
||||
// still updated so hit-test / UI state stays current.
|
||||
// Hover is folded into the highlight set (selected ∪ {hover}) that
|
||||
// drives the xray overlay. A hover handle that's already selected
|
||||
// contributes nothing, so the effective set is unchanged — skip the
|
||||
// overlay refresh then. The field is still updated for hit-test / UI.
|
||||
let contribution = |h: Option<Handle>| h.filter(|h| !self.selected.contains(h));
|
||||
let changed = contribution(self.hover_highlight) != contribution(handle);
|
||||
self.hover_highlight = handle;
|
||||
if changed {
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2138,19 +2176,14 @@ impl Scene {
|
||||
// Tessellate in parallel across all available CPU cores.
|
||||
use rayon::prelude::*;
|
||||
let doc = &self.document;
|
||||
// Fold the hover-preview entity into the highlight set so it renders
|
||||
// selected without being part of the real selection.
|
||||
let hl_set;
|
||||
let sel: &HashSet<Handle> = if let Some(h) = self.hover_highlight {
|
||||
hl_set = {
|
||||
let mut s = self.selected.clone();
|
||||
s.insert(h);
|
||||
s
|
||||
};
|
||||
&hl_set
|
||||
} else {
|
||||
&self.selected
|
||||
};
|
||||
// Selection / hover highlight is NOT baked into tessellation. It is
|
||||
// applied per frame in the GPU xray overlay pass from the live
|
||||
// selection set (`Scene::selected` ∪ hover). Keeping `sel` empty here
|
||||
// makes the wire cache selection-independent, so picking an entity
|
||||
// bumps only `selection_generation` (cheap overlay refresh) instead of
|
||||
// `geometry_epoch` (a full model re-tessellation).
|
||||
let empty_sel: HashSet<Handle> = HashSet::default();
|
||||
let sel: &HashSet<Handle> = &empty_sel;
|
||||
let avp = self.active_viewport;
|
||||
// A paper-space content viewport renders MODEL block entities while
|
||||
// the user is sitting in a paper layout — that path expects
|
||||
@@ -4262,18 +4295,18 @@ impl Scene {
|
||||
self.selected.clear();
|
||||
}
|
||||
self.selected.insert(handle);
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
}
|
||||
|
||||
pub fn deselect_all(&mut self) {
|
||||
self.selected.clear();
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
}
|
||||
|
||||
/// Remove a single entity from the selection (Shift+click subtractive pick).
|
||||
pub fn deselect_entity(&mut self, handle: Handle) {
|
||||
if self.selected.remove(&handle) {
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4329,7 +4362,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
if added > 0 {
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
}
|
||||
added
|
||||
}
|
||||
@@ -4397,7 +4430,7 @@ impl Scene {
|
||||
matched += 1;
|
||||
}
|
||||
}
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
matched
|
||||
}
|
||||
|
||||
@@ -4658,7 +4691,7 @@ impl Scene {
|
||||
for h in to_add {
|
||||
self.selected.insert(h);
|
||||
}
|
||||
self.bump_geometry();
|
||||
self.bump_selection();
|
||||
}
|
||||
|
||||
// ── Layer helpers ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user