From 237d3afd2fb9b1517dc2c0a23975fd71849d626a Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Fri, 9 Oct 2026 14:09:16 +0100 Subject: [PATCH] fix(scene): normalize inverted AABBs in spatial index and add warm cache timers --- src/scene/mod.rs | 32 +++++++++++++++++++++++++++++ src/scene/pick/quadtree.rs | 41 +++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bb92874c9..1272673c4 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -9948,8 +9948,25 @@ impl Scene { /// Prepare hover indexes after derived geometry is installed during open. pub(crate) fn warm_interaction_caches(&self) { + let perf = crate::perf::enabled(); + let t_start = perf.then(iced::time::Instant::now); + let t_ei = perf.then(iced::time::Instant::now); let _ = self.entity_index(); + let ei_ms = crate::perf::elapsed_ms(t_ei); + + let t_ihi = perf.then(iced::time::Instant::now); let _ = self.interaction_handle_index(); + let ihi_ms = crate::perf::elapsed_ms(t_ihi); + let total_ms = crate::perf::elapsed_ms(t_start); + + if perf { + crate::perf_record!( + "[perf] warm_interaction_caches total={:.1}ms entity_index={:.1}ms interaction_handle_index={:.1}ms", + total_ms, + ei_ms, + ihi_ms, + ); + } } fn interaction_handle_index( @@ -12221,6 +12238,8 @@ vis_index={:.1} visible_probe={:.1}", } } + let perf = crate::perf::enabled(); + let t_aabbs_start = perf.then(iced::time::Instant::now); let mut items: Vec<(Handle, [f64; 4])> = Vec::new(); let mut unbounded: Vec = Vec::new(); let mut union: Option<[f64; 4]> = None; @@ -12244,6 +12263,7 @@ vis_index={:.1} visible_probe={:.1}", None => unbounded.push(e.common().handle), } } + let aabbs_ms = crate::perf::elapsed_ms(t_aabbs_start); let root = match union { Some(u) => { let w = (u[2] - u[0]).max(1.0); @@ -12254,10 +12274,22 @@ vis_index={:.1} visible_probe={:.1}", } None => [-1.0, -1.0, 1.0, 1.0], }; + let t_tree_start = perf.then(iced::time::Instant::now); let mut tree = pick::quadtree::QuadTree::new(root); for (h, ab) in items { tree.insert(h, ab); } + let tree_ms = crate::perf::elapsed_ms(t_tree_start); + + if perf { + crate::perf_record!( + "[perf] entity_index-rebuild aabbs={:.1}ms tree={:.1}ms entities={} root={:?}", + aabbs_ms, + tree_ms, + tree.len(), + root, + ); + } *self.entity_index_cache.borrow_mut() = Some(( self.geometry_epoch, diff --git a/src/scene/pick/quadtree.rs b/src/scene/pick/quadtree.rs index 52d3042f7..eec19a8ec 100644 --- a/src/scene/pick/quadtree.rs +++ b/src/scene/pick/quadtree.rs @@ -97,7 +97,8 @@ impl QuadTree { } pub fn insert(&mut self, handle: Handle, aabb: Aabb) { - if !aabb_contained(self.nodes[0].bounds, aabb) { + let aabb = normalize_aabb(aabb); + if !aabb.iter().all(|x| x.is_finite()) || !aabb_contained(self.nodes[0].bounds, aabb) { self.overflow.push(Item { handle, aabb }); return; } @@ -256,6 +257,16 @@ impl QuadTree { // ── AABB helpers ──────────────────────────────────────────────────── +pub fn normalize_aabb(mut aabb: Aabb) -> Aabb { + if aabb[0] > aabb[2] { + aabb.swap(0, 2); + } + if aabb[1] > aabb[3] { + aabb.swap(1, 3); + } + aabb +} + fn aabb_contained(outer: Aabb, inner: Aabb) -> bool { inner[0] >= outer[0] && inner[1] >= outer[1] @@ -391,4 +402,32 @@ mod tests { let se_hits = t.query_rect([50.0, 0.0, 100.0, 50.0]); assert!(se_hits.contains(&h(1))); } + + #[test] + fn insert_handles_nan_and_infinite_bounds_safely() { + let root = [0.0, 0.0, 100.0, 100.0]; + let mut tree = QuadTree::new(root); + tree.insert(h(1), [10.0, 10.0, 20.0, 20.0]); + tree.insert(h(2), [f64::NAN, 10.0, 20.0, 20.0]); + tree.insert(h(3), [10.0, f64::INFINITY, 20.0, 20.0]); + tree.insert(h(4), [20.0, 20.0, 10.0, 10.0]); // inverted bounds: normalized + + assert_eq!(tree.len(), 4); + // Handles 2 & 3 have non-finite coords so they land in overflow + // Handle 4 has inverted coords which normalize and fit inside root + assert_eq!(tree.overflow.len(), 2); + + let normal_hits = tree.query_rect([5.0, 5.0, 25.0, 25.0]); + assert!(normal_hits.contains(&h(1))); + assert!(normal_hits.contains(&h(4))); // normalized [10.0, 10.0, 20.0, 20.0] + } + + #[test] + fn normalize_aabb_flips_inverted_extents() { + let inverted = [50.0, 60.0, 10.0, 20.0]; + let normalized = normalize_aabb(inverted); + assert_eq!(normalized, [10.0, 20.0, 50.0, 60.0]); + } } + +