perf(hover): don't rebuild/re-upload hatches on every rollover (0.5.1 regr)
Selected hatches carry a tint (#71), so 0.5.1 keyed both the hatch model cache and the static GPU buffer re-upload on `selection_generation`. But that counter also bumps on hover, so moving the cursor over a new entity rebuilt EVERY hatch model and re-uploaded the hatch + face3d buffers each time — the large-file hover stutter introduced between 0.5.0 and 0.5.1. The hatch tint depends only on the *selected set*, never on hover. Key both on an order-independent signature of `selected` instead, so a rollover keeps the caches warm; an actual select/deselect still rebuilds. Also cache the picking wire set on `geometry_epoch` alone (separate from the camera-keyed render cache) so a pan/zoom no longer re-tessellates the whole un-culled model on the next mouse move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
14e85171b3
commit
462d9b4b66
2 changed files with 55 additions and 8 deletions
|
|
@ -633,6 +633,13 @@ pub struct Scene {
|
|||
/// invalidates the cull-dependent wire list as well as a geometry change.
|
||||
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
|
||||
wire_cache: RefCell<Option<((u64, u64), Arc<Vec<WireModel>>)>>,
|
||||
/// Camera-invariant full wire set for **picking** (Model layout), keyed on
|
||||
/// `geometry_epoch` alone. The wires are world-space, so a pan/zoom must not
|
||||
/// re-tessellate the whole un-culled model just to hit-test the next mouse
|
||||
/// move (seconds on a 100k-entity drawing). Built once per geometry change
|
||||
/// and reused across every camera move; a slightly stale curve tol is
|
||||
/// harmless for an 8 px pick threshold.
|
||||
hit_wire_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>,
|
||||
/// Per-Model-tile cached tessellation. Each tile has its own camera
|
||||
/// (live for the active tile, stored snapshot for the others), so
|
||||
/// LOD / frustum culling has to run independently — the shared
|
||||
|
|
@ -820,6 +827,7 @@ impl Scene {
|
|||
block_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
|
||||
selection_generation: 0,
|
||||
wire_cache: RefCell::new(None),
|
||||
hit_wire_cache: RefCell::new(None),
|
||||
model_tile_wire_cache: RefCell::new(HashMap::default()),
|
||||
sort_cache: RefCell::new(None),
|
||||
draw_depth_cache: RefCell::new(None),
|
||||
|
|
@ -1875,22 +1883,37 @@ impl Scene {
|
|||
}
|
||||
|
||||
pub(super) fn hatch_models_arc(&self) -> Arc<Vec<HatchModel>> {
|
||||
// Hatch models bake the selection tint (issue #71), so they depend on
|
||||
// the *selected set* — but NOT on hover. Keying on `selection_generation`
|
||||
// (which also bumps on every hover) made each hover-over a new entity
|
||||
// rebuild every hatch model: an O(N-hatch) stutter on hatch-heavy
|
||||
// drawings. Key on a signature of `selected` instead, so hover (which
|
||||
// never changes `selected`) keeps the cache warm.
|
||||
let sel_sig = self.selected_set_sig();
|
||||
{
|
||||
let cache = self.hatch_cache.borrow();
|
||||
if let Some((cached_epoch, cached_sel, ref arc)) = *cache {
|
||||
if cached_epoch == self.geometry_epoch
|
||||
&& cached_sel == self.selection_generation
|
||||
{
|
||||
if cached_epoch == self.geometry_epoch && cached_sel == sel_sig {
|
||||
return Arc::clone(arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
let arc = Arc::new(self.synced_hatch_models());
|
||||
*self.hatch_cache.borrow_mut() =
|
||||
Some((self.geometry_epoch, self.selection_generation, Arc::clone(&arc)));
|
||||
*self.hatch_cache.borrow_mut() = Some((self.geometry_epoch, sel_sig, Arc::clone(&arc)));
|
||||
arc
|
||||
}
|
||||
|
||||
/// Order-independent signature of the selected set. Cheap (the set is
|
||||
/// normally a handful of entities) and unchanged by hover, so caches that
|
||||
/// only depend on what's *selected* don't thrash on rollover.
|
||||
fn selected_set_sig(&self) -> u64 {
|
||||
let mut sig: u64 = self.selected.len() as u64;
|
||||
for h in self.selected.iter() {
|
||||
sig ^= h.value().wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
}
|
||||
sig
|
||||
}
|
||||
|
||||
pub(super) fn wipeout_models_arc(&self) -> Arc<Vec<HatchModel>> {
|
||||
{
|
||||
let cache = self.wipeout_cache.borrow();
|
||||
|
|
@ -2068,7 +2091,23 @@ impl Scene {
|
|||
/// only — paper-space entities are NOT interactive.
|
||||
pub fn hit_test_wires(&self) -> Arc<Vec<WireModel>> {
|
||||
if self.current_layout == "Model" {
|
||||
return self.entity_wires_arc();
|
||||
// Camera-invariant pick cache: reuse the full wire set across pan /
|
||||
// zoom (world-space wires don't change), rebuilding only when the
|
||||
// geometry changes. `entity_wires_arc` itself keys on the camera,
|
||||
// so calling it on every mouse move after a navigate would
|
||||
// re-tessellate the whole un-culled model — the large-file hover
|
||||
// stutter. Gate on `geometry_epoch` here instead.
|
||||
{
|
||||
let c = self.hit_wire_cache.borrow();
|
||||
if let Some((g, ref arc)) = *c {
|
||||
if g == self.geometry_epoch {
|
||||
return Arc::clone(arc);
|
||||
}
|
||||
}
|
||||
}
|
||||
let arc = self.entity_wires_arc();
|
||||
*self.hit_wire_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
||||
return arc;
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
match self.active_viewport {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ pub struct ViewportData {
|
|||
/// Bumped on selection / hover change. Paired with `wire_content_id` to
|
||||
/// decide when the xray overlay batch needs rebuilding.
|
||||
pub(super) selection_generation: u64,
|
||||
/// Signature of the *selected set* only (not hover). Gates the static-buffer
|
||||
/// re-upload (hatch tint, issue #71) so a hover doesn't re-upload every
|
||||
/// hatch / face3d buffer on hatch-heavy drawings.
|
||||
pub(super) selected_sig: 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.
|
||||
|
|
@ -221,7 +225,10 @@ impl shader::Primitive for Primitive {
|
|||
let (uo_y, us_y) = uv_crop_axis(sr.y, sr.height);
|
||||
inner.upload_blit_uv(queue, [uo_x, uo_y], [us_x, us_y]);
|
||||
inner.upload_uniforms(queue, &vp.uniforms);
|
||||
let cur_key = (vp.geometry_epoch, vp.camera_generation, vp.selection_generation);
|
||||
// Third component is the *selected-set* signature (not
|
||||
// selection_generation, which also bumps on hover) so a rollover
|
||||
// doesn't re-upload the static hatch / face3d buffers.
|
||||
let cur_key = (vp.geometry_epoch, vp.camera_generation, vp.selected_sig);
|
||||
let fill_mode = vp.fill_mode;
|
||||
// 3D face fill requires *both* the doc-level FILLMODE *and* the
|
||||
// per-view Solid toggle. Hatches / wipeouts deliberately ignore
|
||||
|
|
@ -233,7 +240,7 @@ impl shader::Primitive for Primitive {
|
|||
// a selection change (issue #71); images / meshes only need a
|
||||
// geometry change.
|
||||
let geo_changed = vp.geometry_epoch != inner.cached_epoch.0;
|
||||
let sel_changed = vp.selection_generation != inner.cached_epoch.2;
|
||||
let sel_changed = vp.selected_sig != inner.cached_epoch.2;
|
||||
if geo_changed || sel_changed {
|
||||
if fill_mode {
|
||||
inner.upload_hatches(device, &vp.hatches[..]);
|
||||
|
|
@ -788,6 +795,7 @@ impl Scene {
|
|||
wire_content_id,
|
||||
highlight_handles: self.highlight_handles(),
|
||||
selection_generation: self.selection_generation,
|
||||
selected_sig: self.selected_set_sig(),
|
||||
screen_rect,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue