Skip to main content

frus_widgets/
paintcache.rs

1//! The **repaint boundary** cache: holds, between frames, the painted output
2//! (primitives + interaction maps) of a subtree marked
3//! [`crate::Widget::repaint_boundary`], and **reuses it as is** for as long as
4//! its geometry and its descendants' interaction state have not moved.
5//!
6//! It is the *paint* counterpart of the relayout cache (`relayout.rs`, milestone
7//! 55): where the layout cache reuses the **rectangles** when the *style and
8//! structure* are stable, this one reuses the **primitives** when the *state read
9//! at paint time* (hover, focus, animated value, opacity, cursor) and the
10//! **geometry** are stable. A widget animating elsewhere on screen no longer
11//! forces a static subtree to repaint.
12//!
13//! ## Correctness
14//! - Any rebuild of the `view` (a change of state, theme or size) **bumps the
15//!   generation**; an entry from a stale generation is ignored. Since the widgets'
16//!   configuration is then identical from one frame to the next (the tree is the
17//!   **same** retained object for as long as `build` does not run), an entry from
18//!   the current generation corresponds to an identical configuration.
19//! - The **fingerprint** covers the rest: each descendant's interaction state
20//!   (`Status`) and the subtree's absolute rectangles. Equal fingerprint and
21//!   generation ⇒ the painting would be **bit-for-bit identical** → replay the cache.
22//!
23//! The cache does not know the application's `Msg` type (the `Runtime` is
24//! generic-agnostic): the data is stored **erased** behind a `Box<dyn Any>`, and
25//! `ui.rs` downcasts it back to its concrete `BoundaryData<Msg>` (a single `Msg`
26//! per app → the `downcast` always succeeds).
27
28use std::any::Any;
29use std::collections::{HashMap, HashSet};
30
31use crate::interaction::WidgetId;
32
33/// One entry: the generation and fingerprint the output was captured under, the
34/// number of rectangles the subtree consumes (to advance the walk index on a
35/// *hit*), and the erased painted data.
36struct Slot {
37    generation: u64,
38    fingerprint: u64,
39    rect_count: usize,
40    data: Box<dyn Any>,
41}
42
43/// The paint cache, retained in the [`crate::Runtime`] from one frame to the next.
44#[derive(Default)]
45pub struct PaintCache {
46    entries: HashMap<WidgetId, Slot>,
47    /// Boundaries touched during the current frame (to evict the vanished ones).
48    touched: HashSet<WidgetId>,
49    /// The current generation: bumped on every rebuild of the `view`.
50    generation: u64,
51    hits: u32,
52    misses: u32,
53    last_hits: u32,
54    last_misses: u32,
55}
56
57impl PaintCache {
58    /// Invalidates the whole cache **logically**: the `view` has been rebuilt, so
59    /// the widgets' configuration may have changed. Entries from the old
60    /// generation will never be *hits* again.
61    pub fn bump_generation(&mut self) {
62        self.generation = self.generation.wrapping_add(1);
63    }
64
65    /// A boundary's erased data if its generation **and** its fingerprint match (a
66    /// *hit*), along with the number of rectangles it covers. Marks the boundary
67    /// as touched, so it survives the end of the frame.
68    pub(crate) fn get(&mut self, key: WidgetId, fingerprint: u64) -> Option<(usize, &dyn Any)> {
69        self.touched.insert(key);
70        let slot = self.entries.get(&key)?;
71        if slot.generation == self.generation && slot.fingerprint == fingerprint {
72            return Some((slot.rect_count, slot.data.as_ref()));
73        }
74        None
75    }
76
77    /// Records (or replaces) a boundary's painted output under the current
78    /// generation.
79    pub(crate) fn put(
80        &mut self,
81        key: WidgetId,
82        fingerprint: u64,
83        rect_count: usize,
84        data: Box<dyn Any>,
85    ) {
86        self.entries.insert(
87            key,
88            Slot {
89                generation: self.generation,
90                fingerprint,
91                rect_count,
92                data,
93            },
94        );
95    }
96
97    /// A diagnostic counter: one boundary reused this frame.
98    pub(crate) fn note_hit(&mut self) {
99        self.hits += 1;
100    }
101
102    /// A diagnostic counter: one boundary repainted this frame (a miss, or
103    /// non-cacheable).
104    pub(crate) fn note_miss(&mut self) {
105        self.misses += 1;
106    }
107
108    /// To be called at the end of a frame: forgets untouched boundaries (vanished
109    /// widgets) and freezes the frame's diagnostic counters.
110    pub(crate) fn end_frame(&mut self) {
111        let touched = std::mem::take(&mut self.touched);
112        self.entries.retain(|id, _| touched.contains(id));
113        self.last_hits = self.hits;
114        self.last_misses = self.misses;
115        self.hits = 0;
116        self.misses = 0;
117    }
118
119    /// Reuses and repaints of the last completed frame (diagnostic).
120    pub fn last_frame_stats(&self) -> (u32, u32) {
121        (self.last_hits, self.last_misses)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    const A: WidgetId = WidgetId::ROOT;
130
131    #[test]
132    fn stored_entry_is_a_hit_until_generation_bumps() {
133        let mut c = PaintCache::default();
134        c.put(A, 42, 3, Box::new(7u32));
135        // Current generation + equal fingerprint → a hit, with the rect_count.
136        let (rc, any) = c.get(A, 42).expect("hit");
137        assert_eq!(rc, 3);
138        assert_eq!(*any.downcast_ref::<u32>().unwrap(), 7);
139        // A different fingerprint → no hit.
140        assert!(c.get(A, 99).is_none());
141        // A stale generation → the entry is no longer a hit.
142        c.bump_generation();
143        assert!(c.get(A, 42).is_none());
144    }
145
146    #[test]
147    fn end_frame_evicts_untouched_boundaries() {
148        let mut c = PaintCache::default();
149        c.put(A, 1, 1, Box::new(()));
150        c.put(A.child(0), 1, 1, Box::new(()));
151        // `put` alone does not mark "touched"; simulate a frame where only A is seen.
152        c.get(A, 1);
153        c.end_frame();
154        assert!(c.entries.contains_key(&A));
155        assert!(
156            !c.entries.contains_key(&A.child(0)),
157            "vanished boundary evicted"
158        );
159    }
160
161    #[test]
162    fn frame_stats_freeze_at_end_frame() {
163        let mut c = PaintCache::default();
164        c.note_hit();
165        c.note_hit();
166        c.note_miss();
167        c.end_frame();
168        assert_eq!(c.last_frame_stats(), (2, 1));
169        // The counters start again from zero for the next frame.
170        c.end_frame();
171        assert_eq!(c.last_frame_stats(), (0, 0));
172    }
173}