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}