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