Skip to main content

pax_runtime/
scene_geometry.rs

1//! Runtime-owned geometry preparation and spatial lookup, independent of canvas residency.
2
3use std::collections::{BTreeMap, HashMap, HashSet};
4use std::rc::Rc;
5
6use kurbo::{Affine, BezPath, Rect, Shape};
7
8/// Primitive-local geometry prepared before drawing or spatial queries.
9///
10/// An absent bound is conservatively unbounded. Primitives that draw outside their
11/// layout rectangle must supply their paint bounds. Paths can retain their prepared
12/// command geometry here so selection and drawing do not construct it twice.
13#[derive(Clone, Default)]
14pub struct CanvasGeometry {
15    /// Conservative local paint bounds; `None` requires inclusion in every region query.
16    pub local_bounds: Option<Rect>,
17    /// Optional prepared path reused by the primitive during drawing.
18    pub path: Option<Rc<BezPath>>,
19}
20
21impl CanvasGeometry {
22    /// Use the layout rectangle for a bounded primitive.
23    pub fn for_layout(bounds: (f64, f64)) -> Self {
24        Self {
25            local_bounds: Some(Rect::new(0.0, 0.0, bounds.0, bounds.1)),
26            path: None,
27        }
28    }
29}
30
31/// Geometry shared by render selection, primitive drawing, and viewport observation.
32/// Native surfaces use a separate bounds lookup in the same geometry service.
33/// Observed layout-only targets and their ancestors share these records without
34/// entering either rendering index.
35///
36/// Coverage is in the owning canvas's content coordinates, so native scrolling
37/// changes the queried region without invalidating every descendant record.
38pub struct PreparedCanvasGeometry {
39    /// Layout transform in unscrolled scene coordinates, shared with viewport observation.
40    pub world_transform: Affine,
41    pub(crate) is_canvas: bool,
42    pub(crate) is_native: bool,
43    /// Owning logical canvas layer.
44    pub layer: usize,
45    /// Layout dimensions, kept separate from overflowing paint coverage.
46    pub bounds: (f64, f64),
47    /// Transform from primitive-local space to canvas content space.
48    pub surface_transform: Affine,
49    /// Transformed paint coverage including the renderer's conservative culling pad.
50    pub coverage_bounds: Option<Rect>,
51    /// Primitive-local geometry backing this prepared record.
52    pub local: CanvasGeometry,
53}
54
55impl PreparedCanvasGeometry {
56    pub(crate) fn new(
57        layer: usize,
58        bounds: (f64, f64),
59        surface_transform: Affine,
60        local: CanvasGeometry,
61    ) -> Self {
62        // Preserve the renderer's existing conservative coverage policy. This is
63        // paint coverage, not the target's layout or viewport-intersection bounds.
64        const TILE_CULL_BOUNDS_PAD: f64 = 64.0;
65        let coverage_bounds = local.local_bounds.map(|bounds| {
66            (surface_transform * bounds.to_path(0.1))
67                .bounding_box()
68                .inflate(TILE_CULL_BOUNDS_PAD, TILE_CULL_BOUNDS_PAD)
69        });
70        Self {
71            world_transform: surface_transform,
72            is_canvas: true,
73            is_native: false,
74            layer,
75            bounds,
76            surface_transform,
77            coverage_bounds,
78            local,
79        }
80    }
81}
82
83/// Cumulative work counters for verifying geometry reuse and spatial-query scaling.
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
85pub struct SceneGeometryStats {
86    /// Primitive geometry preparations, excluding cache hits.
87    pub preparations: u64,
88    /// Insertions or changes to indexed coverage/layer ownership.
89    pub index_updates: u64,
90    /// Region-query requests.
91    pub queries: u64,
92    /// Distinct candidates reaching final rectangle tests, accumulated across queries.
93    pub candidate_tests: u64,
94}
95
96#[derive(Default)]
97pub(crate) struct SceneGeometry {
98    records: HashMap<u32, Rc<PreparedCanvasGeometry>>,
99    dirty: HashSet<u32>,
100    pending: Vec<u32>,
101    layers: HashMap<usize, SpatialIndex>,
102    native_layers: HashMap<usize, SpatialIndex>,
103    pub stats: SceneGeometryStats,
104}
105
106impl SceneGeometry {
107    pub fn invalidate(&mut self, id: u32) {
108        if self.dirty.insert(id) {
109            self.pending.push(id);
110        }
111    }
112
113    pub(crate) fn invalidate_existing(&mut self, id: u32) {
114        if self.records.contains_key(&id) {
115            self.invalidate(id);
116        }
117    }
118
119    pub fn take_dirty(&mut self) -> Vec<u32> {
120        // Walk queued IDs, not HashSet capacity left over from a large initial mount.
121        let dirty = &self.dirty;
122        self.pending
123            .drain(..)
124            .filter(|id| dirty.contains(id))
125            .collect()
126    }
127
128    pub fn get(&self, id: u32) -> Option<Rc<PreparedCanvasGeometry>> {
129        (!self.dirty.contains(&id))
130            .then(|| self.records.get(&id).cloned())
131            .flatten()
132    }
133
134    pub fn insert(
135        &mut self,
136        id: u32,
137        geometry: PreparedCanvasGeometry,
138    ) -> Rc<PreparedCanvasGeometry> {
139        self.dirty.remove(&id);
140        self.stats.preparations += 1;
141        let changed = self.records.get(&id).is_none_or(|old| {
142            old.layer != geometry.layer
143                || old.coverage_bounds != geometry.coverage_bounds
144                || old.is_canvas != geometry.is_canvas
145                || old.is_native != geometry.is_native
146                || (geometry.is_native
147                    && (old.bounds != geometry.bounds
148                        || old.surface_transform != geometry.surface_transform))
149        });
150        if changed {
151            if let Some(old) = self.records.get(&id) {
152                if let Some(index) = self.native_layers.get_mut(&old.layer) {
153                    index.remove(id);
154                    if index.is_empty() {
155                        self.native_layers.remove(&old.layer);
156                    }
157                }
158                if let Some(index) = self.layers.get_mut(&old.layer) {
159                    index.remove(id);
160                    if index.is_empty() {
161                        self.layers.remove(&old.layer);
162                    }
163                }
164            }
165            if geometry.is_canvas {
166                self.layers
167                    .entry(geometry.layer)
168                    .or_default()
169                    .insert(id, geometry.coverage_bounds);
170            }
171            if geometry.is_native {
172                self.native_layers
173                    .entry(geometry.layer)
174                    .or_default()
175                    .insert(
176                        id,
177                        Some(geometry.surface_transform.transform_rect_bbox(Rect::new(
178                            0.0,
179                            0.0,
180                            geometry.bounds.0,
181                            geometry.bounds.1,
182                        ))),
183                    );
184            }
185            self.stats.index_updates += 1;
186        }
187        let geometry = Rc::new(geometry);
188        self.records.insert(id, geometry.clone());
189        geometry
190    }
191
192    pub fn remove(&mut self, id: u32) {
193        self.dirty.remove(&id);
194        if let Some(old) = self.records.remove(&id) {
195            if let Some(index) = self.native_layers.get_mut(&old.layer) {
196                index.remove(id);
197                if index.is_empty() {
198                    self.native_layers.remove(&old.layer);
199                }
200            }
201            if let Some(index) = self.layers.get_mut(&old.layer) {
202                index.remove(id);
203                if index.is_empty() {
204                    self.layers.remove(&old.layer);
205                }
206            }
207        }
208    }
209
210    pub fn query(&mut self, layer: usize, regions: &[Rect]) -> Option<Vec<u32>> {
211        let ids = self.query_exact(layer, regions, false);
212        // Canvas replay retains its historical full-replay fallback. Compositing
213        // uses the exact query so an empty result means no affected elements.
214        (!ids.is_empty()).then_some(ids)
215    }
216
217    pub(crate) fn query_exact(&mut self, layer: usize, regions: &[Rect], native: bool) -> Vec<u32> {
218        self.stats.queries += 1;
219        let indices = if native {
220            &self.native_layers
221        } else {
222            &self.layers
223        };
224        let Some(index) = indices.get(&layer) else {
225            return Vec::new();
226        };
227        let mut candidates = HashSet::new();
228        for region in regions {
229            index.candidates(*region, &mut candidates);
230        }
231        self.stats.candidate_tests += candidates.len() as u64;
232        let mut ids: Vec<_> = candidates
233            .into_iter()
234            .filter(|id| {
235                regions
236                    .iter()
237                    .any(|region| intersects(index.bounds[id], *region))
238            })
239            .collect();
240        ids.sort_unstable();
241        ids
242    }
243}
244
245fn finite(rect: Rect) -> bool {
246    [rect.x0, rect.y0, rect.x1, rect.y1]
247        .iter()
248        .all(|x| x.is_finite())
249}
250
251fn intersects(bounds: Option<Rect>, region: Rect) -> bool {
252    let Some(bounds) = bounds.filter(|bounds| finite(*bounds)) else {
253        return true;
254    };
255    !finite(region)
256        || (bounds.x1 >= region.x0
257            && bounds.x0 <= region.x1
258            && bounds.y1 >= region.y0
259            && bounds.y0 <= region.y1)
260}
261
262// A size-tiered sparse grid bounds insertion to at most four cells per rectangle.
263// Ordered columns/rows query only occupied coordinates; neither long lists nor
264// huge sparse scroll regions require enumerating every intervening cell.
265type Columns = BTreeMap<i64, BTreeMap<i64, HashSet<u32>>>;
266
267#[derive(Clone, Copy)]
268struct CellRange {
269    level: u32,
270    x0: i64,
271    x1: i64,
272    y0: i64,
273    y1: i64,
274}
275
276impl CellRange {
277    fn for_bounds(bounds: Rect) -> Option<Self> {
278        if !finite(bounds) || bounds.width() < 0.0 || bounds.height() < 0.0 {
279            return None;
280        }
281        let span = bounds.width().max(bounds.height()).max(256.0);
282        if !span.is_finite() {
283            return None;
284        }
285        let level = (span / 256.0).log2().ceil().max(0.0) as u32;
286        let range = Self::at_level(bounds, level);
287        // Extreme coordinates can saturate integer conversion; conservative
288        // unbounded fallback also protects against floating-point tier rounding.
289        ((range.x1 as i128 - range.x0 as i128) <= 1 && (range.y1 as i128 - range.y0 as i128) <= 1)
290            .then_some(range)
291    }
292
293    fn at_level(bounds: Rect, level: u32) -> Self {
294        let size = 256.0 * 2.0f64.powi(level as i32);
295        Self {
296            level,
297            x0: (bounds.x0 / size).floor() as i64,
298            x1: (bounds.x1 / size).floor() as i64,
299            y0: (bounds.y0 / size).floor() as i64,
300            y1: (bounds.y1 / size).floor() as i64,
301        }
302    }
303}
304
305#[derive(Default)]
306pub(crate) struct SpatialIndex {
307    bounds: HashMap<u32, Option<Rect>>,
308    levels: BTreeMap<u32, Columns>,
309    unbounded: HashSet<u32>,
310}
311
312impl SpatialIndex {
313    fn is_empty(&self) -> bool {
314        self.bounds.is_empty()
315    }
316
317    pub(crate) fn insert(&mut self, id: u32, bounds: Option<Rect>) {
318        self.bounds.insert(id, bounds);
319        let Some(range) = bounds.and_then(CellRange::for_bounds) else {
320            self.unbounded.insert(id);
321            return;
322        };
323        let columns = self.levels.entry(range.level).or_default();
324        for x in range.x0..=range.x1 {
325            let rows = columns.entry(x).or_default();
326            for y in range.y0..=range.y1 {
327                rows.entry(y).or_default().insert(id);
328            }
329        }
330    }
331
332    pub(crate) fn remove(&mut self, id: u32) {
333        let Some(bounds) = self.bounds.remove(&id) else {
334            return;
335        };
336        self.unbounded.remove(&id);
337        let Some(range) = bounds.and_then(CellRange::for_bounds) else {
338            return;
339        };
340        let Some(columns) = self.levels.get_mut(&range.level) else {
341            return;
342        };
343        for x in range.x0..=range.x1 {
344            if let Some(rows) = columns.get_mut(&x) {
345                for y in range.y0..=range.y1 {
346                    if let Some(ids) = rows.get_mut(&y) {
347                        ids.remove(&id);
348                        if ids.is_empty() {
349                            rows.remove(&y);
350                        }
351                    }
352                }
353                if rows.is_empty() {
354                    columns.remove(&x);
355                }
356            }
357        }
358        if columns.is_empty() {
359            self.levels.remove(&range.level);
360        }
361    }
362
363    pub(crate) fn candidates(&self, region: Rect, result: &mut HashSet<u32>) {
364        if !finite(region) {
365            result.extend(self.bounds.keys().copied());
366            return;
367        }
368        if region.width() < 0.0 || region.height() < 0.0 {
369            return;
370        }
371        result.extend(self.unbounded.iter().copied());
372        for (&level, columns) in &self.levels {
373            let range = CellRange::at_level(region, level);
374            for (_, rows) in columns.range(range.x0..=range.x1) {
375                for (_, ids) in rows.range(range.y0..=range.y1) {
376                    result.extend(ids);
377                }
378            }
379        }
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    fn geometry(layer: usize, bounds: Option<Rect>) -> PreparedCanvasGeometry {
388        PreparedCanvasGeometry {
389            world_transform: Affine::IDENTITY,
390            is_canvas: true,
391            is_native: false,
392            layer,
393            bounds: (10.0, 10.0),
394            surface_transform: Affine::IDENTITY,
395            coverage_bounds: bounds,
396            local: CanvasGeometry::default(),
397        }
398    }
399
400    #[test]
401    fn updates_moves_layer_changes_and_removal_replace_old_coverage() {
402        let mut scene = SceneGeometry::default();
403        let a = Rect::new(0.0, 0.0, 100.0, 100.0);
404        let b = Rect::new(4000.0, 0.0, 4100.0, 100.0);
405        scene.insert(1, geometry(0, Some(a)));
406        scene.insert(2, geometry(0, Some(b)));
407        assert_eq!(scene.query(0, &[a]), Some(vec![1]));
408        scene.insert(1, geometry(0, Some(a)));
409        assert_eq!(
410            scene.stats.index_updates, 2,
411            "unchanged coverage keeps its index entries"
412        );
413        scene.insert(1, geometry(0, Some(b)));
414        assert_eq!(scene.query(0, &[a]), None);
415        assert_eq!(scene.query(0, &[b, b]), Some(vec![1, 2]));
416        scene.insert(1, geometry(5, Some(a)));
417        assert_eq!(scene.query(0, &[b]), Some(vec![2]));
418        assert_eq!(scene.query(5, &[a]), Some(vec![1]));
419        scene.invalidate(1);
420        assert!(scene.get(1).is_none());
421        scene.remove(1);
422        assert!(scene.take_dirty().is_empty());
423        assert!(!scene.layers.contains_key(&5));
424        scene.remove(2);
425        assert!(scene.layers.is_empty());
426    }
427
428    #[test]
429    fn spatial_queries_match_linear_oracle_across_sizes_and_mutations() {
430        let mut scene = SceneGeometry::default();
431        let mut seed = 13891u64;
432        let mut next = || {
433            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
434            (seed >> 32) as u32
435        };
436        for id in 0..2000 {
437            let x = next() as f64 % 40000.0 - 20000.0;
438            let y = next() as f64 % 40000.0 - 20000.0;
439            let size = 2.0f64.powi((next() % 18) as i32);
440            scene.insert(
441                id,
442                geometry(
443                    id as usize % 3,
444                    Some(Rect::new(x, y, x + size, y + size / 3.0)),
445                ),
446            );
447        }
448        scene.insert(2000, geometry(0, None));
449        scene.insert(
450            2001,
451            geometry(0, Some(Rect::new(f64::NAN, 0.0, 10.0, 10.0))),
452        );
453        scene.insert(
454            2002,
455            geometry(0, Some(Rect::new(-f64::MAX, -f64::MAX, f64::MAX, f64::MAX))),
456        );
457        for phase in 0..2 {
458            for _ in 0..150 {
459                let x = next() as f64 % 50000.0 - 25000.0;
460                let y = next() as f64 % 50000.0 - 25000.0;
461                let regions = [
462                    Rect::new(x, y, x + 800.0, y + 900.0),
463                    Rect::new(-50.0, -50.0, 0.0, 0.0),
464                ];
465                for layer in 0..3 {
466                    let mut expected: Vec<_> = scene
467                        .records
468                        .iter()
469                        .filter_map(|(&id, g)| {
470                            (g.layer == layer
471                                && regions.iter().any(|r| intersects(g.coverage_bounds, *r)))
472                            .then_some(id)
473                        })
474                        .collect();
475                    expected.sort_unstable();
476                    assert_eq!(scene.query(layer, &regions).unwrap_or_default(), expected);
477                }
478            }
479            if phase == 0 {
480                for id in (0..2000).step_by(3) {
481                    scene.remove(id);
482                }
483                for id in (1..2000).step_by(3) {
484                    scene.insert(id, geometry(2, Some(Rect::new(-256.0, -256.0, 0.0, 0.0))));
485                }
486            }
487        }
488    }
489
490    #[test]
491    fn edge_contact_unknown_bounds_and_extreme_queries_are_conservative() {
492        let mut scene = SceneGeometry::default();
493        scene.insert(1, geometry(0, Some(Rect::new(-256.0, -256.0, 0.0, 0.0))));
494        scene.insert(2, geometry(0, None));
495        scene.insert(
496            3,
497            geometry(0, Some(Rect::new(1.0e200, 1.0e200, 1.0e200, 1.0e200))),
498        );
499        assert_eq!(
500            scene.query(0, &[Rect::new(0.0, 0.0, 1.0, 1.0)]),
501            Some(vec![1, 2])
502        );
503        assert_eq!(
504            scene.query(0, &[Rect::new(-f64::MAX, -f64::MAX, f64::MAX, f64::MAX)]),
505            Some(vec![1, 2, 3])
506        );
507        assert_eq!(
508            scene.query(0, &[Rect::new(f64::NEG_INFINITY, 0.0, f64::INFINITY, 1.0)]),
509            Some(vec![1, 2, 3])
510        );
511    }
512
513    #[test]
514    fn fixed_density_queries_do_not_scan_the_collection() {
515        for horizontal in [false, true] {
516            let mut costs = Vec::new();
517            for count in [1000, 10000] {
518                let mut scene = SceneGeometry::default();
519                for id in 0..count {
520                    let offset = id as f64 * 200.0;
521                    let bounds = if horizontal {
522                        Rect::new(offset, 0.0, offset + 100.0, 100.0)
523                    } else {
524                        Rect::new(0.0, offset, 100.0, offset + 100.0)
525                    };
526                    scene.insert(id, geometry(0, Some(bounds)));
527                }
528                let region = if horizontal {
529                    Rect::new(100000.0, 0.0, 101000.0, 100.0)
530                } else {
531                    Rect::new(0.0, 100000.0, 100.0, 101000.0)
532                };
533                assert_eq!(scene.query(0, &[region]).unwrap().len(), 6);
534                costs.push(scene.stats.candidate_tests);
535                assert_eq!(scene.stats.preparations, count as u64);
536            }
537            assert_eq!(costs[0], costs[1]);
538            assert!(
539                costs[1] < 12,
540                "only nearby buckets should reach detailed tests: {costs:?}"
541            );
542        }
543    }
544
545    #[test]
546    fn preparation_preserves_canvas_coverage_padding_and_overflow() {
547        let local = CanvasGeometry {
548            local_bounds: Some(Rect::new(-25.0, -10.0, 125.0, 110.0)),
549            path: None,
550        };
551        let prepared =
552            PreparedCanvasGeometry::new(2, (100.0, 100.0), Affine::translate((10.0, 20.0)), local);
553        assert_eq!(prepared.bounds, (100.0, 100.0));
554        assert_eq!(
555            prepared.coverage_bounds,
556            Some(Rect::new(-79.0, -54.0, 199.0, 194.0))
557        );
558    }
559}