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