Skip to main content

pax_runtime/engine/
layer_surface.rs

1use kurbo::Rect;
2use std::collections::{HashMap, HashSet, VecDeque};
3
4/// Logical and backing-pixel dimensions for one physical surface.
5pub struct LayerSurfaceSize {
6    pub logical_width: f32,
7    pub logical_height: f32,
8    pub surface_width: u32,
9    pub surface_height: u32,
10    pub dpr: [f32; 2],
11}
12
13/// Desired surface geometry for one tile in a layer layout.
14pub struct LayerSurfaceEntry {
15    pub key: String,
16    pub host_signature: String,
17    pub origin_x: f32,
18    pub origin_y: f32,
19    pub replay_priority: i32,
20    pub surface: LayerSurfaceSize,
21}
22
23/// Desired set of physical surfaces for a logical layer.
24pub struct LayerSurfaceLayout {
25    pub surfaces: Vec<LayerSurfaceEntry>,
26    pub active: bool,
27}
28
29#[derive(Clone, Copy, Debug)]
30pub struct VisibleSurfaceEscape {
31    pub previous_bounds: Rect,
32    pub visible_bounds: Rect,
33    pub visible_surfaces: usize,
34    pub escaped_visible_surfaces: usize,
35    pub max_gap_x: f64,
36    pub max_gap_y: f64,
37}
38
39#[derive(Clone, Copy, Debug, Default)]
40pub enum LayoutChangeKind {
41    #[default]
42    Unchanged,
43    OriginOnly,
44    Resized,
45}
46
47pub fn surface_intersects_coverage_bounds(
48    bounds: &Rect,
49    origin_x: f64,
50    origin_y: f64,
51    width: f64,
52    height: f64,
53) -> bool {
54    if !bounds.x0.is_finite()
55        || !bounds.y0.is_finite()
56        || !bounds.x1.is_finite()
57        || !bounds.y1.is_finite()
58    {
59        return true;
60    }
61
62    let x1 = origin_x + width;
63    let y1 = origin_y + height;
64    bounds.x1 >= origin_x && bounds.x0 <= x1 && bounds.y1 >= origin_y && bounds.y0 <= y1
65}
66
67pub fn visible_surface_escape(
68    previous_surface_bounds: &[Rect],
69    current_surfaces: &[LayerSurfaceEntry],
70) -> Option<VisibleSurfaceEscape> {
71    let previous_bounds = union_bounds(previous_surface_bounds.iter().copied())?;
72    let mut visible_bounds: Option<Rect> = None;
73    let mut visible_surfaces = 0;
74    let mut escaped_visible_surfaces = 0;
75    let mut max_gap_x: f64 = 0.0;
76    let mut max_gap_y: f64 = 0.0;
77
78    for surface in current_surfaces
79        .iter()
80        .filter(|surface| surface.replay_priority <= 0)
81    {
82        visible_surfaces += 1;
83        let bounds = surface_bounds(surface);
84        visible_bounds = Some(match visible_bounds {
85            Some(acc) => acc.union(bounds),
86            None => bounds,
87        });
88
89        if !rect_contains(&previous_bounds, &bounds) {
90            escaped_visible_surfaces += 1;
91            let (gap_x, gap_y) = rect_escape_gap(&previous_bounds, &bounds);
92            max_gap_x = max_gap_x.max(gap_x);
93            max_gap_y = max_gap_y.max(gap_y);
94        }
95    }
96
97    let visible_bounds = visible_bounds?;
98    (escaped_visible_surfaces > 0).then_some(VisibleSurfaceEscape {
99        previous_bounds,
100        visible_bounds,
101        visible_surfaces,
102        escaped_visible_surfaces,
103        max_gap_x,
104        max_gap_y,
105    })
106}
107
108pub fn replay_batches_by_priority(mut entries: Vec<(usize, i32)>) -> Vec<Vec<usize>> {
109    if entries.is_empty() {
110        return Vec::new();
111    }
112    entries.sort_by_key(|(index, priority)| (*priority, *index));
113
114    // Keep visible tile replay isolated from warm pre-render work. Replaying a warm ring in the
115    // same frame smooths fast scroll exposure, but on native it can also turn an otherwise small
116    // scroll tick into a visible frame spike.
117    let mut batches = Vec::new();
118    let mut current_priority = entries[0].1;
119    let mut current_batch = Vec::new();
120    for (index, priority) in entries {
121        if priority != current_priority {
122            batches.push(current_batch);
123            current_batch = Vec::new();
124            current_priority = priority;
125        }
126        current_batch.push(index);
127    }
128    batches.push(current_batch);
129    batches
130}
131
132/// Retargeted surface metadata used to choose replay order without involving backend resources.
133#[derive(Clone, Copy, Debug)]
134pub struct ReplayPriorityEntry {
135    pub index: usize,
136    pub priority: i32,
137    previous_bounds: Rect,
138    current_bounds: Rect,
139}
140
141impl ReplayPriorityEntry {
142    pub fn new(index: usize, priority: i32, previous_bounds: Rect, current_bounds: Rect) -> Self {
143        Self {
144            index,
145            priority,
146            previous_bounds,
147            current_bounds,
148        }
149    }
150}
151
152#[derive(Clone, Copy)]
153enum ReplayDirectionAxis {
154    X,
155    Y,
156}
157
158const REPLAY_DIRECTION_EPSILON: f64 = 0.5;
159
160/// Batch retargeted surfaces by planner priority, then by the leading row/column of travel.
161pub fn replay_batches_by_directional_priority(entries: &[ReplayPriorityEntry]) -> Vec<Vec<usize>> {
162    if entries.is_empty() {
163        return Vec::new();
164    }
165
166    let Some((axis, direction_sign)) = replay_direction(&entries) else {
167        return replay_batches_by_priority(
168            entries
169                .iter()
170                .map(|entry| (entry.index, entry.priority))
171                .collect(),
172        );
173    };
174
175    let mut keyed_entries: Vec<_> = entries
176        .iter()
177        .map(|entry| {
178            (
179                entry.index,
180                entry.priority,
181                directional_lane_key(entry.current_bounds, axis, direction_sign),
182            )
183        })
184        .collect();
185    keyed_entries.sort_by_key(|(index, priority, lane)| (*priority, *lane, *index));
186
187    let mut batches = Vec::new();
188    let mut current_priority = keyed_entries[0].1;
189    let mut current_lane = keyed_entries[0].2;
190    let mut current_batch = Vec::new();
191    for (index, priority, lane) in keyed_entries {
192        if priority != current_priority || lane != current_lane {
193            batches.push(current_batch);
194            current_batch = Vec::new();
195            current_priority = priority;
196            current_lane = lane;
197        }
198        current_batch.push(index);
199    }
200    batches.push(current_batch);
201    batches
202}
203
204fn replay_direction(entries: &[ReplayPriorityEntry]) -> Option<(ReplayDirectionAxis, f64)> {
205    let (dx, dy) = entries.iter().fold((0.0, 0.0), |(dx, dy), entry| {
206        (
207            dx + rect_center_x(&entry.current_bounds) - rect_center_x(&entry.previous_bounds),
208            dy + rect_center_y(&entry.current_bounds) - rect_center_y(&entry.previous_bounds),
209        )
210    });
211
212    if dx.abs() < REPLAY_DIRECTION_EPSILON && dy.abs() < REPLAY_DIRECTION_EPSILON {
213        return None;
214    }
215
216    if dx.abs() >= dy.abs() {
217        Some((ReplayDirectionAxis::X, dx.signum()))
218    } else {
219        Some((ReplayDirectionAxis::Y, dy.signum()))
220    }
221}
222
223fn directional_lane_key(bounds: Rect, axis: ReplayDirectionAxis, direction_sign: f64) -> i64 {
224    let coord = match axis {
225        ReplayDirectionAxis::X => rect_center_x(&bounds),
226        ReplayDirectionAxis::Y => rect_center_y(&bounds),
227    }
228    .round() as i64;
229
230    if direction_sign.is_sign_positive() {
231        -coord
232    } else {
233        coord
234    }
235}
236
237fn rect_center_x(bounds: &Rect) -> f64 {
238    (bounds.x0 + bounds.x1) * 0.5
239}
240
241fn rect_center_y(bounds: &Rect) -> f64 {
242    (bounds.y0 + bounds.y1) * 0.5
243}
244
245fn union_bounds(bounds: impl IntoIterator<Item = Rect>) -> Option<Rect> {
246    bounds
247        .into_iter()
248        .filter(|bounds| rect_is_finite(bounds))
249        .fold(None, |acc, bounds| {
250            Some(match acc {
251                Some(acc) => acc.union(bounds),
252                None => bounds,
253            })
254        })
255}
256
257fn surface_bounds(surface: &LayerSurfaceEntry) -> Rect {
258    let x0 = surface.origin_x as f64;
259    let y0 = surface.origin_y as f64;
260    Rect::new(
261        x0,
262        y0,
263        x0 + surface.surface.logical_width as f64,
264        y0 + surface.surface.logical_height as f64,
265    )
266}
267
268fn rect_contains(outer: &Rect, inner: &Rect) -> bool {
269    const EPSILON: f64 = 0.5;
270    inner.x0 >= outer.x0 - EPSILON
271        && inner.y0 >= outer.y0 - EPSILON
272        && inner.x1 <= outer.x1 + EPSILON
273        && inner.y1 <= outer.y1 + EPSILON
274}
275
276fn rect_escape_gap(outer: &Rect, inner: &Rect) -> (f64, f64) {
277    let gap_x = (outer.x0 - inner.x0)
278        .max(0.0)
279        .max((inner.x1 - outer.x1).max(0.0));
280    let gap_y = (outer.y0 - inner.y0)
281        .max(0.0)
282        .max((inner.y1 - outer.y1).max(0.0));
283    (gap_x, gap_y)
284}
285
286fn rect_is_finite(bounds: &Rect) -> bool {
287    bounds.x0.is_finite() && bounds.y0.is_finite() && bounds.x1.is_finite() && bounds.y1.is_finite()
288}
289
290/// Shared coordinator for physical-surface replay after a layer layout retarget.
291///
292/// This intentionally tracks renderer-agnostic surface indices and coverage bounds only. Backends
293/// remain responsible for applying the selected indices to their own renderer objects.
294#[derive(Default)]
295pub struct SurfaceReplayCoordinator {
296    targeted_replay_queues: Vec<VecDeque<Vec<usize>>>,
297    targeted_replay_bounds: Vec<HashMap<usize, Vec<Rect>>>,
298    canvas_node_coverage: Vec<HashMap<u32, Rect>>,
299}
300
301impl SurfaceReplayCoordinator {
302    pub fn targeted_replay_scope(&self, layer: usize) -> Option<Vec<usize>> {
303        self.targeted_replay_queues
304            .get(layer)
305            .and_then(|queue| queue.front().cloned())
306    }
307
308    pub fn targeted_or_all_indices(&self, layer: usize, renderer_count: usize) -> Vec<usize> {
309        self.targeted_replay_scope(layer)
310            .unwrap_or_else(|| (0..renderer_count).collect())
311    }
312
313    pub fn clear_targeted_replay_scope(&mut self, layer: usize) {
314        if let Some(queue) = self.targeted_replay_queues.get_mut(layer) {
315            queue.clear();
316        }
317        if let Some(bounds_by_surface) = self.targeted_replay_bounds.get_mut(layer) {
318            bounds_by_surface.clear();
319        }
320    }
321
322    pub fn set_targeted_replay_batches(
323        &mut self,
324        layer: usize,
325        batches: Vec<Vec<usize>>,
326        bounds_by_surface: HashMap<usize, Vec<Rect>>,
327    ) {
328        let mut queue = VecDeque::new();
329        let mut scheduled_indices = HashSet::new();
330        for mut batch in batches {
331            batch.sort_unstable();
332            batch.dedup();
333            if !batch.is_empty() {
334                scheduled_indices.extend(batch.iter().copied());
335                queue.push_back(batch);
336            }
337        }
338        if queue.is_empty() {
339            return;
340        }
341
342        let mut preserved_indices = HashSet::new();
343        if let Some(existing_queue) = self.targeted_replay_queues.get(layer) {
344            for batch in existing_queue {
345                let mut preserved_batch = Vec::new();
346                for index in batch.iter().copied() {
347                    if scheduled_indices.insert(index) {
348                        preserved_indices.insert(index);
349                        preserved_batch.push(index);
350                    }
351                }
352                if !preserved_batch.is_empty() {
353                    queue.push_back(preserved_batch);
354                }
355            }
356        }
357
358        let mut bounds_by_surface = bounds_by_surface;
359        if let Some(existing_bounds) = self.targeted_replay_bounds.get(layer) {
360            for index in preserved_indices {
361                if let Some(bounds) = existing_bounds.get(&index) {
362                    bounds_by_surface
363                        .entry(index)
364                        .or_insert_with(|| bounds.clone());
365                }
366            }
367        }
368
369        if self.targeted_replay_queues.len() <= layer {
370            self.targeted_replay_queues
371                .resize_with(layer + 1, VecDeque::new);
372        }
373        self.targeted_replay_queues[layer] = queue;
374
375        if self.targeted_replay_bounds.len() <= layer {
376            self.targeted_replay_bounds
377                .resize_with(layer + 1, HashMap::new);
378        }
379        self.targeted_replay_bounds[layer] = bounds_by_surface;
380    }
381
382    pub fn advance_targeted_replay_queue(&mut self, layer: usize) -> bool {
383        let (popped_batch, has_more) = {
384            let Some(queue) = self.targeted_replay_queues.get_mut(layer) else {
385                return false;
386            };
387            let popped_batch = queue.pop_front();
388            (popped_batch, !queue.is_empty())
389        };
390
391        if let Some(popped_batch) = popped_batch {
392            if let Some(bounds_by_surface) = self.targeted_replay_bounds.get_mut(layer) {
393                for index in popped_batch {
394                    bounds_by_surface.remove(&index);
395                }
396                if !has_more {
397                    bounds_by_surface.clear();
398                }
399            }
400        }
401
402        has_more
403    }
404
405    pub fn remember_canvas_node_coverage(
406        &mut self,
407        layer: usize,
408        node_id: u32,
409        coverage_bounds: Rect,
410    ) {
411        if self.canvas_node_coverage.len() <= layer {
412            self.canvas_node_coverage
413                .resize_with(layer + 1, HashMap::new);
414        }
415        self.canvas_node_coverage[layer].insert(node_id, coverage_bounds);
416    }
417
418    pub fn forget_canvas_node_coverage(&mut self, layer: usize, node_id: u32) {
419        if let Some(coverage) = self.canvas_node_coverage.get_mut(layer) {
420            coverage.remove(&node_id);
421        }
422    }
423
424    pub fn truncate_layers(&mut self, layer_count: usize) {
425        self.targeted_replay_queues.truncate(layer_count);
426        self.targeted_replay_bounds.truncate(layer_count);
427        self.canvas_node_coverage.truncate(layer_count);
428    }
429
430    pub fn targeted_replay_surface_bounds(
431        &self,
432        layer: usize,
433        mut fallback_bounds: impl FnMut(usize) -> Option<Rect>,
434    ) -> Option<Vec<Rect>> {
435        let indices = self.targeted_replay_scope(layer)?;
436        let mut bounds = Vec::with_capacity(indices.len());
437        let layer_replay_bounds = self.targeted_replay_bounds.get(layer);
438        for index in indices {
439            if let Some(surface_bounds) =
440                layer_replay_bounds.and_then(|bounds_by_surface| bounds_by_surface.get(&index))
441            {
442                bounds.extend(surface_bounds.iter().copied());
443                continue;
444            }
445            if let Some(surface_bounds) = fallback_bounds(index) {
446                bounds.push(surface_bounds);
447            }
448        }
449        (!bounds.is_empty()).then_some(bounds)
450    }
451
452    pub fn spatial_replay_node_ids(
453        &self,
454        layer: usize,
455        fallback_bounds: impl FnMut(usize) -> Option<Rect>,
456    ) -> Option<Vec<u32>> {
457        let surface_bounds = self.targeted_replay_surface_bounds(layer, fallback_bounds)?;
458        self.spatial_replay_node_ids_for_surface_bounds(layer, &surface_bounds)
459    }
460
461    pub fn spatial_replay_node_ids_for_surface_bounds(
462        &self,
463        layer: usize,
464        surface_bounds: &[Rect],
465    ) -> Option<Vec<u32>> {
466        let layer_coverage = self.canvas_node_coverage.get(layer)?;
467        if layer_coverage.is_empty() {
468            return None;
469        }
470
471        let mut node_ids: Vec<_> = layer_coverage
472            .iter()
473            .filter_map(|(node_id, bounds)| {
474                surface_bounds
475                    .iter()
476                    .any(|surface| {
477                        surface_intersects_coverage_bounds(
478                            bounds,
479                            surface.x0,
480                            surface.y0,
481                            surface.width(),
482                            surface.height(),
483                        )
484                    })
485                    .then_some(*node_id)
486            })
487            .collect();
488        node_ids.sort_unstable();
489        if node_ids.is_empty() {
490            return None;
491        }
492        Some(node_ids)
493    }
494}
495
496#[cfg(test)]
497mod replay_priority_tests {
498    use super::{
499        replay_batches_by_directional_priority, replay_batches_by_priority, visible_surface_escape,
500        LayerSurfaceEntry, LayerSurfaceSize, ReplayPriorityEntry, SurfaceReplayCoordinator,
501    };
502    use kurbo::Rect;
503    use std::collections::HashMap;
504
505    #[test]
506    fn visible_batch_excludes_warm_rings() {
507        let batches = replay_batches_by_priority(vec![(3, 4), (0, 0), (2, 2), (1, 0)]);
508
509        assert_eq!(batches[0], vec![0, 1]);
510        assert_eq!(batches[1], vec![2]);
511        assert_eq!(batches[2], vec![3]);
512    }
513
514    #[test]
515    fn starts_with_nearest_warm_batch_when_no_visible_tiles() {
516        let batches = replay_batches_by_priority(vec![(4, 5), (2, 3), (3, 3)]);
517
518        assert_eq!(batches[0], vec![2, 3]);
519        assert_eq!(batches[1], vec![4]);
520    }
521
522    #[test]
523    fn visible_escape_detects_visible_tiles_outside_previous_warm_bounds() {
524        let previous = vec![Rect::new(0.0, 0.0, 100.0, 300.0)];
525        let current = vec![
526            test_surface(0.0, 100.0, 100.0, 100.0, 0),
527            test_surface(0.0, 300.0, 100.0, 100.0, 0),
528            test_surface(0.0, 400.0, 100.0, 100.0, 1),
529        ];
530
531        let escape = visible_surface_escape(&previous, &current).expect("one visible tile escaped");
532
533        assert_eq!(escape.visible_surfaces, 2);
534        assert_eq!(escape.escaped_visible_surfaces, 1);
535        assert_eq!(escape.max_gap_y, 100.0);
536        assert_eq!(escape.max_gap_x, 0.0);
537        assert_eq!(escape.visible_bounds, Rect::new(0.0, 100.0, 100.0, 400.0));
538    }
539
540    #[test]
541    fn visible_escape_ignores_covered_visible_tiles() {
542        let previous = vec![Rect::new(0.0, 0.0, 100.0, 300.0)];
543        let current = vec![
544            test_surface(0.0, 0.0, 100.0, 100.0, 0),
545            test_surface(0.0, 100.0, 100.0, 100.0, 0),
546        ];
547
548        assert!(visible_surface_escape(&previous, &current).is_none());
549    }
550
551    #[test]
552    fn directional_batches_start_with_leading_vertical_row() {
553        let batches = replay_batches_by_directional_priority(&[
554            ReplayPriorityEntry::new(
555                0,
556                0,
557                Rect::new(0.0, 0.0, 100.0, 100.0),
558                Rect::new(0.0, 100.0, 100.0, 200.0),
559            ),
560            ReplayPriorityEntry::new(
561                1,
562                0,
563                Rect::new(100.0, 100.0, 200.0, 200.0),
564                Rect::new(100.0, 200.0, 200.0, 300.0),
565            ),
566            ReplayPriorityEntry::new(
567                2,
568                0,
569                Rect::new(200.0, -100.0, 300.0, 0.0),
570                Rect::new(200.0, 0.0, 300.0, 100.0),
571            ),
572        ]);
573
574        assert_eq!(batches, vec![vec![1], vec![0], vec![2]]);
575    }
576
577    #[test]
578    fn directional_batches_keep_same_leading_row_together() {
579        let batches = replay_batches_by_directional_priority(&[
580            ReplayPriorityEntry::new(
581                2,
582                0,
583                Rect::new(200.0, 100.0, 300.0, 200.0),
584                Rect::new(200.0, 200.0, 300.0, 300.0),
585            ),
586            ReplayPriorityEntry::new(
587                0,
588                0,
589                Rect::new(0.0, 100.0, 100.0, 200.0),
590                Rect::new(0.0, 200.0, 100.0, 300.0),
591            ),
592            ReplayPriorityEntry::new(
593                1,
594                0,
595                Rect::new(100.0, 0.0, 200.0, 100.0),
596                Rect::new(100.0, 100.0, 200.0, 200.0),
597            ),
598        ]);
599
600        assert_eq!(batches, vec![vec![0, 2], vec![1]]);
601    }
602
603    #[test]
604    fn coordinator_advances_targeted_replay_batches() {
605        let mut coordinator = SurfaceReplayCoordinator::default();
606        coordinator.set_targeted_replay_batches(
607            0,
608            vec![vec![3, 1, 1], vec![2]],
609            HashMap::from([
610                (1, vec![Rect::new(0.0, 0.0, 10.0, 10.0)]),
611                (2, vec![Rect::new(10.0, 0.0, 20.0, 10.0)]),
612                (3, vec![Rect::new(20.0, 0.0, 30.0, 10.0)]),
613            ]),
614        );
615
616        assert_eq!(coordinator.targeted_replay_scope(0), Some(vec![1, 3]));
617        assert!(coordinator.advance_targeted_replay_queue(0));
618        assert_eq!(coordinator.targeted_replay_scope(0), Some(vec![2]));
619        let bounds = coordinator
620            .targeted_replay_surface_bounds(0, |_| None)
621            .expect("second replay bounds remain");
622        assert_eq!(bounds, vec![Rect::new(10.0, 0.0, 20.0, 10.0)]);
623        assert!(!coordinator.advance_targeted_replay_queue(0));
624        assert_eq!(coordinator.targeted_replay_scope(0), None);
625    }
626
627    #[test]
628    fn coordinator_preserves_unflushed_replay_batches_when_rescheduled() {
629        let mut coordinator = SurfaceReplayCoordinator::default();
630        coordinator.set_targeted_replay_batches(
631            0,
632            vec![vec![0], vec![1]],
633            HashMap::from([
634                (0, vec![Rect::new(0.0, 0.0, 10.0, 10.0)]),
635                (1, vec![Rect::new(10.0, 0.0, 20.0, 10.0)]),
636            ]),
637        );
638        coordinator.set_targeted_replay_batches(
639            0,
640            vec![vec![2]],
641            HashMap::from([(2, vec![Rect::new(20.0, 0.0, 30.0, 10.0)])]),
642        );
643
644        assert_eq!(coordinator.targeted_replay_scope(0), Some(vec![2]));
645        assert!(coordinator.advance_targeted_replay_queue(0));
646        assert_eq!(coordinator.targeted_replay_scope(0), Some(vec![0]));
647        let bounds = coordinator
648            .targeted_replay_surface_bounds(0, |_| None)
649            .expect("preserved replay bounds remain");
650        assert_eq!(bounds, vec![Rect::new(0.0, 0.0, 10.0, 10.0)]);
651        assert!(coordinator.advance_targeted_replay_queue(0));
652        assert_eq!(coordinator.targeted_replay_scope(0), Some(vec![1]));
653    }
654
655    #[test]
656    fn coordinator_selects_spatial_replay_nodes_by_surface_bounds() {
657        let mut coordinator = SurfaceReplayCoordinator::default();
658        coordinator.remember_canvas_node_coverage(0, 10, Rect::new(0.0, 0.0, 5.0, 5.0));
659        coordinator.remember_canvas_node_coverage(0, 20, Rect::new(50.0, 50.0, 60.0, 60.0));
660        coordinator.set_targeted_replay_batches(0, vec![vec![0]], HashMap::new());
661
662        let node_ids = coordinator
663            .spatial_replay_node_ids(0, |_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)))
664            .expect("spatial replay nodes");
665
666        assert_eq!(node_ids, vec![10]);
667    }
668
669    #[test]
670    fn coordinator_falls_back_when_targeted_replay_has_no_cached_coverage() {
671        let mut coordinator = SurfaceReplayCoordinator::default();
672        coordinator.remember_canvas_node_coverage(0, 10, Rect::new(0.0, 0.0, 5.0, 5.0));
673        coordinator.set_targeted_replay_batches(0, vec![vec![0]], HashMap::new());
674
675        let node_ids =
676            coordinator.spatial_replay_node_ids(0, |_| Some(Rect::new(50.0, 50.0, 60.0, 60.0)));
677
678        assert!(node_ids.is_none());
679    }
680
681    fn test_surface(
682        origin_x: f32,
683        origin_y: f32,
684        logical_width: f32,
685        logical_height: f32,
686        replay_priority: i32,
687    ) -> LayerSurfaceEntry {
688        LayerSurfaceEntry {
689            key: String::new(),
690            host_signature: String::new(),
691            origin_x,
692            origin_y,
693            replay_priority,
694            surface: LayerSurfaceSize {
695                logical_width,
696                logical_height,
697                surface_width: logical_width as u32,
698                surface_height: logical_height as u32,
699                dpr: [1.0, 1.0],
700            },
701        }
702    }
703}