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
+11
@@ -176,6 +176,17 @@ outright — no re-tessellation, `tess ms` drops to ~0 on the PERF HUD. Zoom
|
||||
exactly as before; the cull margin is unchanged, so miss cost is identical
|
||||
(no zoom regression).
|
||||
|
||||
**Partial (landed): selection decoupled.** Picking an entity used to call
|
||||
`bump_geometry`, invalidating the wire cache and re-tessellating the WHOLE
|
||||
model just to repaint one entity (30 ms → 400 ms on a large drawing). The
|
||||
highlight is no longer baked into tessellation: wires are always base-coloured
|
||||
(`sel` is empty in `wires_for_block_culled`), and the selection highlight is
|
||||
applied in the GPU xray overlay from the live `selected ∪ hover` set,
|
||||
recoloured to `WireModel::SELECTED`. Selection / hover now bump a cheap
|
||||
`selection_generation` instead of `geometry_epoch`, so the overlay refreshes
|
||||
without any re-tessellation or main-buffer re-upload. `tess ms` stays flat
|
||||
when selecting.
|
||||
|
||||
**Partial (landed): per-frame split.** `build_primitive` ran
|
||||
`split_face3d_wires` every frame — an O(N) per-wire handle lookup + clone to
|
||||
separate Face3D wires — even on a pan that reused the tessellation. It's now
|
||||
|
||||
+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 ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -122,6 +122,11 @@ pub struct Pipeline {
|
||||
/// Model-tile tessellation) and `upload_wires` is skipped. `u64::MAX` =
|
||||
/// nothing uploaded yet.
|
||||
pub cached_wire_id: u64,
|
||||
/// `(wire_content_id, selection_generation)` the selection xray overlay
|
||||
/// (`gpu_selected_wires`) was last built for. Rebuilt when either changes —
|
||||
/// a pick bumps only `selection_generation`, refreshing the overlay without
|
||||
/// touching the main wire buffers.
|
||||
pub cached_selection: (u64, u64),
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
@@ -891,6 +896,7 @@ impl Pipeline {
|
||||
viewcube,
|
||||
cached_epoch: (u64::MAX, u64::MAX),
|
||||
cached_wire_id: u64::MAX,
|
||||
cached_selection: (u64::MAX, u64::MAX),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -925,13 +931,46 @@ impl Pipeline {
|
||||
i = j;
|
||||
}
|
||||
self.gpu_wires = batches;
|
||||
}
|
||||
|
||||
// Full-brightness copies of selected wires, drawn on top of everything
|
||||
// in the selection overlay pass so they're always visible. The xray
|
||||
// pass applies neither scissor nor mesh-edge skip, so all selected
|
||||
// wires batch into one run (None / false) — select-all over a large
|
||||
// drawing no longer costs one draw call per entity. Order preserved.
|
||||
let selected: Vec<WireModel> = wires.iter().filter(|w| w.selected).cloned().collect();
|
||||
/// Build the selection xray overlay: full-brightness copies of the wires
|
||||
/// whose entity handle is in `highlight`, drawn on top so the selection is
|
||||
/// always visible. Selection is no longer baked into the wire tessellation,
|
||||
/// so this is driven by the live highlight set and rebuilt only when the
|
||||
/// selection (or the underlying wire content) changes — picking an entity
|
||||
/// refreshes just this overlay instead of re-tessellating the model. The
|
||||
/// xray pass applies neither scissor nor mesh-edge skip, so everything
|
||||
/// merges into one order-preserving run.
|
||||
pub fn upload_selected_wires(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
wires: &[WireModel],
|
||||
highlight: &rustc_hash::FxHashSet<acadrust::Handle>,
|
||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||
) {
|
||||
if highlight.is_empty() {
|
||||
self.gpu_selected_wires = vec![];
|
||||
return;
|
||||
}
|
||||
// Recolor to the selection highlight: the xray pass uses the normal
|
||||
// wire shader (no forced colour), so the highlight now lives here
|
||||
// instead of being baked into the tessellation. Drawn on top with
|
||||
// depth-compare Always, so it overrides the base-coloured main pass.
|
||||
let selected: Vec<WireModel> = wires
|
||||
.iter()
|
||||
.filter(|w| {
|
||||
w.name
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.map(acadrust::Handle::new)
|
||||
.is_some_and(|h| highlight.contains(&h))
|
||||
})
|
||||
.map(|w| {
|
||||
let mut c = w.clone();
|
||||
c.color = WireModel::SELECTED;
|
||||
c
|
||||
})
|
||||
.collect();
|
||||
self.gpu_selected_wires = WireGpu::from_run(device, &selected, depth_map, None, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,13 @@ pub struct ViewportData {
|
||||
/// world-space wire buffer when only the camera moved. Non-tile and
|
||||
/// preview/interim frames carry a fresh id each time → always re-upload.
|
||||
pub(super) wire_content_id: u64,
|
||||
/// `selected ∪ hover` handles for the GPU xray overlay. The highlight is no
|
||||
/// longer baked into the wire tessellation, so `prepare` rebuilds the
|
||||
/// selected-wire batch by filtering `wires` against this set.
|
||||
pub(super) highlight_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
||||
/// Bumped on selection / hover change. Paired with `wire_content_id` to
|
||||
/// decide when the xray overlay batch needs rebuilding.
|
||||
pub(super) selection_generation: u64,
|
||||
/// Screen rectangle this viewport fills, **normalized** to the widget
|
||||
/// bounds (each component in 0..1). A single full-widget view is
|
||||
/// `(0, 0, 1, 1)`; tiled / floating viewports are sub-rectangles.
|
||||
@@ -251,6 +258,20 @@ impl shader::Primitive for Primitive {
|
||||
inner.upload_wires(device, &vp.wires[..], &vp.draw_depths);
|
||||
inner.cached_wire_id = vp.wire_content_id;
|
||||
}
|
||||
// Selection xray overlay — rebuilt when the selection changes or the
|
||||
// underlying wires changed. A pick bumps only selection_generation,
|
||||
// so this refreshes without re-tessellating or re-uploading the main
|
||||
// wire buffers.
|
||||
let sel_key = (vp.wire_content_id, vp.selection_generation);
|
||||
if sel_key != inner.cached_selection {
|
||||
inner.upload_selected_wires(
|
||||
device,
|
||||
&vp.wires[..],
|
||||
&vp.highlight_handles,
|
||||
&vp.draw_depths,
|
||||
);
|
||||
inner.cached_selection = sel_key;
|
||||
}
|
||||
let vproj = vp.uniforms.view_proj;
|
||||
inner.compute_wire_scissors(vproj, clip_size.width, clip_size.height);
|
||||
inner.compute_wipeout_scissors(vproj, clip_size.width, clip_size.height);
|
||||
@@ -747,6 +768,8 @@ impl Scene {
|
||||
geometry_epoch: self.geometry_epoch,
|
||||
camera_generation: self.camera_generation,
|
||||
wire_content_id,
|
||||
highlight_handles: self.highlight_handles(),
|
||||
selection_generation: self.selection_generation,
|
||||
screen_rect,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user