From 1c7e4bb850431885822da04e861eb6acd7789fac Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 25 Jul 2026 01:34:49 +0300 Subject: [PATCH] perf: reuse interaction index after edits Remap immutable base candidates and cache only changed-wire indexes so block-heavy drawings avoid rebuilding millions of primitives on pointer movement. Build 3D BVHs lazily for plan views. --- src/scene/mod.rs | 174 ++++++++++++++++----- src/scene/pick/interaction_index.rs | 232 ++++++++++++++++++++++++++-- 2 files changed, 353 insertions(+), 53 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index d90ca9670..fd5884a77 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1042,6 +1042,18 @@ pub struct Scene { Arc, )>, >, + /// Index of only the live handles changed since the immutable interaction + /// base. Reused across pointer events so a large unchanged block is never + /// re-indexed merely because one top-level entity was added or removed. + interaction_overlay_index_cache: RefCell< + Option<( + u64, + u64, + u64, + Arc>, + Arc, + )>, + >, /// Auxiliary broad phase for objects that have no wire representation: /// hatch-only and solid-only block instances. interaction_handle_index_cache: RefCell< @@ -1346,6 +1358,7 @@ impl Scene { wire_cache: RefCell::new(None), interaction_index_cache: RefCell::new(Vec::new()), interaction_base_index_cache: RefCell::new(None), + interaction_overlay_index_cache: RefCell::new(None), interaction_handle_index_cache: RefCell::new(None), interaction_aux_work_cache: std::cell::Cell::new(None), sort_cache: RefCell::new(None), @@ -4851,7 +4864,16 @@ impl Scene { return index; } } + let started = iced::time::Instant::now(); let index = Arc::new(crate::scene::pick::interaction_index::InteractionIndex::build(wires)); + let build_ms = started.elapsed().as_secs_f64() * 1000.0; + if std::env::var_os("OCS_PERF").is_some() && build_ms >= 50.0 { + eprintln!( + "[perf] interaction-index {:>7.1}ms wires={}", + build_ms, + wires.len(), + ); + } let mut cache = self.interaction_index_cache.borrow_mut(); cache.push(( self.geometry_epoch, @@ -4877,6 +4899,7 @@ impl Scene { fn interaction_overlay_base( &self, ) -> Option<( + u64, Arc, Vec<(Handle, ChangeKind)>, )> { @@ -4890,20 +4913,67 @@ impl Scene { (*epoch, Arc::clone(index)) }; let changes = self.replay_since(epoch)?; - (changes.len() <= Self::INTERACTION_OVERLAY_MAX_HANDLES).then_some((index, changes)) + (changes.len() <= Self::INTERACTION_OVERLAY_MAX_HANDLES) + .then_some((epoch, index, changes)) + } + + fn interaction_overlay_changed_index( + &self, + base_epoch: u64, + changes: &[(Handle, ChangeKind)], + ) -> ( + Arc>, + Arc, + ) { + let space_key = self.interaction_space_key(); + { + let cache = self.interaction_overlay_index_cache.borrow(); + if let Some((epoch, cached_base, cached_space, wires, index)) = cache.as_ref() { + if *epoch == self.geometry_epoch + && *cached_base == base_epoch + && *cached_space == space_key + { + return (Arc::clone(wires), Arc::clone(index)); + } + } + } + let changed_live: Vec = changes + .iter() + .filter_map(|(handle, kind)| { + (!matches!(kind, ChangeKind::Removed)).then_some(*handle) + }) + .collect(); + let wires = Arc::new(self.wire_models_for(&changed_live)); + let index = Arc::new( + crate::scene::pick::interaction_index::InteractionIndex::build(&wires), + ); + *self.interaction_overlay_index_cache.borrow_mut() = Some(( + self.geometry_epoch, + base_epoch, + space_key, + Arc::clone(&wires), + Arc::clone(&index), + )); + (wires, index) } fn interaction_overlay_wires( &self, base_keys: impl IntoIterator, changes: &[(Handle, ChangeKind)], - ) -> Arc> { + changed_wires: &[WireModel], + ) -> ( + Arc>, + HashMap<(u64, u32), u32>, + HashMap<(u64, u32), u32>, + ) { let changed: HashSet = changes.iter().map(|(handle, _)| *handle).collect(); let memo = self.resident_tess_memo.borrow(); let mut wires = Vec::new(); + let mut base_slots = HashMap::default(); let mut misses: HashMap> = HashMap::default(); - for (handle, ordinal) in base_keys { - let handle = Handle::new(handle); + for (handle_value, ordinal) in base_keys { + let handle = Handle::new(handle_value); if changed.contains(&handle) || self.document.get_entity(handle).is_none() { continue; } @@ -4911,6 +4981,7 @@ impl Scene { .get(&handle) .and_then(|entity_wires| entity_wires.get(ordinal as usize)) { + base_slots.insert((handle_value, ordinal), wires.len() as u32); wires.push(wire.clone()); } else { misses.entry(handle).or_default().push(ordinal); @@ -4925,22 +4996,22 @@ impl Scene { let entity_wires = self.wire_models_for(&[handle]); for ordinal in ordinals { if let Some(wire) = entity_wires.get(ordinal as usize) { + base_slots.insert((handle.value(), ordinal), wires.len() as u32); wires.push(wire.clone()); } } } - // Changed handles are absent or stale in the immutable base index. The - // journal is deliberately small, so append their current runs in full. - let changed_live: Vec = changes - .iter() - .filter_map(|(handle, kind)| { - (!matches!(kind, ChangeKind::Removed)).then_some(*handle) - }) - .collect(); - if !changed_live.is_empty() { - wires.extend(self.wire_models_for(&changed_live)); + let mut changed_slots = HashMap::default(); + let mut next_ordinal: HashMap = HashMap::default(); + for wire in changed_wires { + if let Ok(handle) = wire.name.parse::() { + let ordinal = next_ordinal.entry(handle).or_default(); + changed_slots.insert((handle, *ordinal), wires.len() as u32); + *ordinal += 1; + } + wires.push(wire.clone()); } - Arc::new(wires) + (Arc::new(wires), base_slots, changed_slots) } fn indexed_interaction_candidates_xy( @@ -4948,29 +5019,40 @@ impl Scene { wires: Arc>, aabb: [f64; 4], ) -> crate::scene::pick::interaction_index::InteractionCandidates { - if let Some((base, changes)) = self.interaction_overlay_base() { + if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() { let perf = std::env::var_os("OCS_PERF").is_some(); let t0 = iced::time::Instant::now(); let keys = base.query_wire_keys_xy(aabb); let key_ms = t0.elapsed().as_secs_f64() * 1000.0; + let t_changed = iced::time::Instant::now(); + let (changed_wires, changed_index) = + self.interaction_overlay_changed_index(base_epoch, &changes); + let changed_ms = t_changed.elapsed().as_secs_f64() * 1000.0; let t_local = iced::time::Instant::now(); - let local = self.interaction_overlay_wires(keys.iter().copied(), &changes); + let (local, base_slots, changed_slots) = self.interaction_overlay_wires( + keys.iter().copied(), + &changes, + &changed_wires, + ); let local_ms = t_local.elapsed().as_secs_f64() * 1000.0; - let t_index = iced::time::Instant::now(); - let local_index = - crate::scene::pick::interaction_index::InteractionIndex::build(&local); - let index_ms = t_index.elapsed().as_secs_f64() * 1000.0; - let result = local_index.query_xy(Arc::clone(&local), aabb); + let t_remap = iced::time::Instant::now(); + let mut result = + base.query_remapped_xy(Arc::clone(&local), &base_slots, aabb); + result.extend_indexed( + changed_index.query_remapped_xy(local, &changed_slots, aabb), + ); + let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0; let total_ms = t0.elapsed().as_secs_f64() * 1000.0; if perf && total_ms >= 50.0 { eprintln!( - "[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} gather={:.1} index={:.1}", + "[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} changed={:.1} gather={:.1} remap={:.1}", total_ms, keys.len(), - local.len(), + result.len(), key_ms, + changed_ms, local_ms, - index_ms, + remap_ms, ); } return result; @@ -4986,35 +5068,51 @@ impl Scene { eye: glam::DVec3, bounds: iced::Rectangle, ) -> crate::scene::pick::interaction_index::InteractionCandidates { - if let Some((base, changes)) = self.interaction_overlay_base() { + if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() { let perf = std::env::var_os("OCS_PERF").is_some(); let t0 = iced::time::Instant::now(); let keys = base.query_wire_keys_screen(screen_rect, view_rot, eye, bounds); let key_ms = t0.elapsed().as_secs_f64() * 1000.0; + let t_changed = iced::time::Instant::now(); + let (changed_wires, changed_index) = + self.interaction_overlay_changed_index(base_epoch, &changes); + let changed_ms = t_changed.elapsed().as_secs_f64() * 1000.0; let t_local = iced::time::Instant::now(); - let local = self.interaction_overlay_wires(keys.iter().copied(), &changes); + let (local, base_slots, changed_slots) = self.interaction_overlay_wires( + keys.iter().copied(), + &changes, + &changed_wires, + ); let local_ms = t_local.elapsed().as_secs_f64() * 1000.0; - let t_index = iced::time::Instant::now(); - let local_index = - crate::scene::pick::interaction_index::InteractionIndex::build(&local); - let index_ms = t_index.elapsed().as_secs_f64() * 1000.0; - let result = local_index.query_screen( + let t_remap = iced::time::Instant::now(); + let mut result = base.query_remapped_screen( Arc::clone(&local), + &base_slots, screen_rect, view_rot, eye, bounds, ); + result.extend_indexed(changed_index.query_remapped_screen( + local, + &changed_slots, + screen_rect, + view_rot, + eye, + bounds, + )); + let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0; let total_ms = t0.elapsed().as_secs_f64() * 1000.0; if perf && total_ms >= 50.0 { eprintln!( - "[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} gather={:.1} index={:.1}", + "[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} changed={:.1} gather={:.1} remap={:.1}", total_ms, keys.len(), - local.len(), + result.len(), key_ms, + changed_ms, local_ms, - index_ms, + remap_ms, ); } return result; @@ -5028,8 +5126,10 @@ impl Scene { wires: &Arc>, base_radius_px: f32, ) -> f32 { - if let Some((base, _)) = self.interaction_overlay_base() { - base.pick_radius_px(base_radius_px) + if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() { + let (_, changed) = + self.interaction_overlay_changed_index(base_epoch, &changes); + base.pick_radius_px(changed.pick_radius_px(base_radius_px)) } else { self.interaction_index(wires).pick_radius_px(base_radius_px) } diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index a396424f8..2c3ea480e 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -163,28 +163,24 @@ struct BvhNode3 { len: u32, } -struct SpatialBvh3 { - entries: Vec>, +struct SpatialBvh3 { nodes: Vec, order: Vec, } -impl SpatialBvh3 { - fn build(entries: Vec>) -> Self { +impl SpatialBvh3 { + fn build(entries: &[Entry3]) -> Self { let mut order: Vec = (0..entries.len() as u32).collect(); let mut nodes = Vec::new(); if !order.is_empty() { - build_bvh3_node(&entries, &mut order, 0, &mut nodes); - } - Self { - entries, - nodes, - order, + build_bvh3_node(entries, &mut order, 0, &mut nodes); } + Self { nodes, order } } - fn query_screen( + fn query_screen( &self, + entries: &[Entry3], screen_rect: [f32; 4], view_rot: Mat4, eye: DVec3, @@ -203,7 +199,7 @@ impl SpatialBvh3 { if node.left == u32::MAX { for &entry_idx in &self.order[node.start as usize..(node.start + node.len) as usize] { - let entry = self.entries[entry_idx as usize]; + let entry = entries[entry_idx as usize]; if aabb3_projects_into(entry.aabb, screen_rect, view_rot, eye, bounds) { entry_indices.push(entry.value); } @@ -425,21 +421,23 @@ impl SpatialGrid { } struct SpatialSet { + entries: Vec>, xy: SpatialGrid, - xyz: SpatialBvh3, + xyz: std::sync::OnceLock, } impl SpatialSet { fn build(entries: Vec>) -> Self { let xy = SpatialGrid::build(&entries); Self { + entries, xy, - xyz: SpatialBvh3::build(entries), + xyz: std::sync::OnceLock::new(), } } fn query_xy(&self, aabb: [f64; 4]) -> Vec { - self.xy.query(&self.xyz.entries, aabb) + self.xy.query(&self.entries, aabb) } fn query_screen( @@ -449,7 +447,9 @@ impl SpatialSet { eye: DVec3, bounds: Rectangle, ) -> Vec { - self.xyz.query_screen(screen_rect, view_rot, eye, bounds) + self.xyz + .get_or_init(|| SpatialBvh3::build(&self.entries)) + .query_screen(&self.entries, screen_rect, view_rot, eye, bounds) } } @@ -771,6 +771,184 @@ impl InteractionIndex { self.queried_wire_keys(self.wires.query_screen(screen_rect, view_rot, eye, bounds)) } + fn remap_wire_index( + &self, + index: u32, + slots: &rustc_hash::FxHashMap<(u64, u32), u32>, + ) -> Option { + let handle = self + .wire_handles + .get(index as usize) + .copied() + .flatten()?; + let ordinal = self + .wire_ordinals + .get(index as usize) + .copied() + .flatten()?; + slots.get(&(handle, ordinal)).copied() + } + + fn remap_wire_indices( + &self, + mut indices: Vec, + slots: &rustc_hash::FxHashMap<(u64, u32), u32>, + ) -> Vec { + indices.extend_from_slice(&self.unbounded_wires); + let mut local: Vec = indices + .into_iter() + .filter_map(|index| self.remap_wire_index(index, slots)) + .collect(); + local.sort_unstable(); + local.dedup(); + local + } + + fn remap_refs( + &self, + values: Vec, + slots: &rustc_hash::FxHashMap<(u64, u32), u32>, + wire_of: impl Fn(&T) -> u32, + set_wire: impl Fn(&mut T, u32), + ) -> Vec { + values + .into_iter() + .filter_map(|mut value| { + let wire = self.remap_wire_index(wire_of(&value), slots)?; + set_wire(&mut value, wire); + Some(value) + }) + .collect() + } + + pub(crate) fn query_remapped_xy( + &self, + wires: Arc>, + slots: &rustc_hash::FxHashMap<(u64, u32), u32>, + aabb: [f64; 4], + ) -> InteractionCandidates { + InteractionCandidates { + wires, + wire_indices: Some(self.remap_wire_indices(self.wires.query_xy(aabb), slots)), + segments: Some(self.remap_refs( + self.segments.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + snap_points: Some(self.remap_refs( + self.snap_points.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + key_vertices: Some(self.remap_refs( + self.key_vertices.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + key_segments: Some(self.remap_refs( + self.key_segments.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + fill_triangles: Some(self.remap_refs( + self.fill_triangles.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + pick_triangles: Some(self.remap_refs( + self.pick_triangles.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + glyphs: Some(self.remap_refs( + self.glyphs.query_xy(aabb), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + query_aabb: Some(aabb), + screen_rect: None, + screen_view: None, + } + } + + pub(crate) fn query_remapped_screen( + &self, + wires: Arc>, + slots: &rustc_hash::FxHashMap<(u64, u32), u32>, + screen_rect: [f32; 4], + view_rot: Mat4, + eye: DVec3, + bounds: Rectangle, + ) -> InteractionCandidates { + InteractionCandidates { + wires, + wire_indices: Some(self.remap_wire_indices( + self.wires + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + )), + segments: Some(self.remap_refs( + self.segments + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + snap_points: Some(self.remap_refs( + self.snap_points + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + key_vertices: Some(self.remap_refs( + self.key_vertices + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + key_segments: Some(self.remap_refs( + self.key_segments + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + fill_triangles: Some(self.remap_refs( + self.fill_triangles + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + pick_triangles: Some(self.remap_refs( + self.pick_triangles + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + glyphs: Some(self.remap_refs( + self.glyphs + .query_screen(screen_rect, view_rot, eye, bounds), + slots, + |value| value.wire, + |value, wire| value.wire = wire, + )), + query_aabb: None, + screen_rect: Some(screen_rect), + screen_view: Some((view_rot, eye, bounds)), + } + } + pub fn query_xy(&self, wires: Arc>, aabb: [f64; 4]) -> InteractionCandidates { let mut wire_indices = self.wires.query_xy(aabb); wire_indices.extend_from_slice(&self.unbounded_wires); @@ -970,6 +1148,28 @@ impl InteractionCandidates { .zip(self.screen_view) .map(|(rect, (view, eye, bounds))| (rect, view, eye, bounds)) } + + pub(crate) fn extend_indexed(&mut self, other: Self) { + debug_assert!(Arc::ptr_eq(&self.wires, &other.wires)); + + fn extend(target: &mut Option>, incoming: Option>) { + let (Some(target), Some(incoming)) = (target.as_mut(), incoming) else { + return; + }; + target.extend(incoming); + target.sort_unstable(); + target.dedup(); + } + + extend(&mut self.wire_indices, other.wire_indices); + extend(&mut self.segments, other.segments); + extend(&mut self.snap_points, other.snap_points); + extend(&mut self.key_vertices, other.key_vertices); + extend(&mut self.key_segments, other.key_segments); + extend(&mut self.fill_triangles, other.fill_triangles); + extend(&mut self.pick_triangles, other.pick_triangles); + extend(&mut self.glyphs, other.glyphs); + } } impl WireSource for InteractionCandidates {