devpage_design/tiles/plan.rs
1//! The frame plan: what a browser canvas shows each frame and which tiles
2//! it paints next, for whichever host carries it out — the GPU, painting
3//! each tile into a texture as it is asked, or the page on the CPU, asking
4//! a worker and keeping its bitmaps. The plan holds all of the state that
5//! decides it: the tiles held, least recently used dropped first past a
6//! cap, which damage made stale; the tiles asked for and not yet arrived,
7//! each under a ticket, and whether damage reached it meanwhile. A host
8//! keeps only the paintings, by ticket.
9//!
10//! **One budget rule**: at most a budget of tiles are in flight — asked for
11//! and not yet arrived. The GPU paints what it is asked for at once, so its
12//! budget is the tiles of a frame; the CPU's worker answers later, so a
13//! frame asks only for as many as have arrived since.
14
15use std::collections::HashMap;
16use std::sync::atomic::{AtomicU32, Ordering};
17
18use kurbo::Rect as KRect;
19
20use crate::scene::Theme;
21
22use super::*;
23
24/// What a host knows a painting by: given when a tile is asked for, never
25/// given again by any plan, so an answer for the design a canvas showed
26/// before matches nothing.
27pub type Ticket = u32;
28
29/// The most tiles a canvas holds: more than a 4K screen at a device pixel
30/// ratio of 2 shows, about 160, so a view never evicts its own.
31pub const CAP: usize = 384;
32
33/// The most tiles a canvas has in flight at once.
34pub const BUDGET: usize = 6;
35
36/// The next ticket; 0 is never given.
37static NEXT: AtomicU32 = AtomicU32::new(1);
38
39/// A tile asked for and not yet arrived.
40struct Asked {
41 key: Key,
42 /// Damage reached it after it was asked for: what arrives is stale.
43 stale: bool,
44}
45
46/// One canvas's frame plan.
47pub struct Plan {
48 /// The tiles held, each by the ticket its painting arrived under.
49 held: Cache<Ticket>,
50 /// The tiles in flight, by ticket.
51 asked: HashMap<Ticket, Asked>,
52 budget: usize,
53 /// Paintings let go since the last frame.
54 evicted: Vec<Ticket>,
55}
56
57/// A frame's work: the paintings to draw, bottom first, each where it lands in
58/// the view, in device pixels; the tiles to paint next, each under its
59/// ticket; the paintings to free; and how many tiles of the view at its own
60/// level are missing or stale — 0 when the frame shows the view current.
61#[derive(Debug)]
62pub struct Work {
63 pub draw: Vec<(Ticket, KRect)>,
64 pub request: Vec<(Ticket, Key)>,
65 pub evict: Vec<Ticket>,
66 pub missing: usize,
67}
68
69/// What became of a painting that arrived.
70#[derive(Clone, Copy, PartialEq, Eq, Debug)]
71pub enum Arrival {
72 /// Its ticket isn't one in flight: the host frees it.
73 Dropped,
74 /// Held, current.
75 Current,
76 /// Held and shown, but damage reached its tile while it was painted: it
77 /// is asked for again.
78 Stale,
79}
80
81impl Plan {
82 /// A plan holding at most `cap` tiles, with at most `budget` in flight.
83 pub fn new(cap: usize, budget: usize) -> Plan {
84 Plan {
85 held: Cache::new(cap),
86 asked: HashMap::new(),
87 budget,
88 evicted: Vec::new(),
89 }
90 }
91
92 /// Marks stale every tile of `theme`, held or in flight, that overlaps
93 /// one of `rects` — in the design's coordinates, the drawing's top-left
94 /// at `origin` — at every level.
95 pub fn damage(&mut self, theme: Theme, origin: (f64, f64), rects: &[KRect]) {
96 self.held.damage(theme, origin, rects);
97 for asked in self.asked.values_mut() {
98 if asked.key.theme == theme && asked.key.overlaps(origin, rects) {
99 asked.stale = true;
100 }
101 }
102 }
103
104 /// Marks every tile of `theme`, held or in flight, stale.
105 pub fn damage_all(&mut self, theme: Theme) {
106 self.held.damage_all(theme);
107 for asked in self.asked.values_mut() {
108 if asked.key.theme == theme {
109 asked.stale = true;
110 }
111 }
112 }
113
114 /// The frame of `view` of the drawing `area` in `theme`, which `needed`
115 /// — the tiles the view needs at its own level, nearest its middle first
116 /// — makes current. Every held tile of the theme the view shows is
117 /// drawn, as a stand-in where it is stale or at another level; once a
118 /// zoom has `settled`, the missing and stale tiles are asked for in
119 /// `needed`'s order while fewer than the budget are in flight.
120 pub fn frame(
121 &mut self,
122 theme: Theme,
123 area: KRect,
124 view: &View,
125 needed: &[(u32, u32)],
126 settled: bool,
127 ) -> Work {
128 let level = Level::of(view.zoom);
129 let missing: Vec<Key> = needed
130 .iter()
131 .map(|&(col, row)| Key {
132 theme,
133 level,
134 col,
135 row,
136 })
137 // Each tile the view needs counts as used.
138 .filter(|k| self.held.get(k).is_none_or(|(_, stale)| stale))
139 .collect();
140 let mut draw: Vec<(Level, Ticket, KRect)> = self
141 .held
142 .iter()
143 .filter(|(k, _, _)| k.theme == theme)
144 .filter_map(|(k, &ticket, _)| Some((k.level, ticket, view.place(area, k)?)))
145 .collect();
146 draw.sort_by_key(|&(l, _, _)| (l == level, l.0));
147 let mut request = Vec::new();
148 if settled {
149 for &key in &missing {
150 if self.asked.len() >= self.budget {
151 break;
152 }
153 if self.asked.values().any(|a| a.key == key) {
154 continue;
155 }
156 let ticket = NEXT.fetch_add(1, Ordering::Relaxed);
157 self.asked.insert(ticket, Asked { key, stale: false });
158 request.push((ticket, key));
159 }
160 }
161 Work {
162 draw: draw.into_iter().map(|(_, t, r)| (t, r)).collect(),
163 request,
164 evict: std::mem::take(&mut self.evicted),
165 missing: missing.len(),
166 }
167 }
168
169 /// The painting asked for under `ticket` has arrived: held in place of
170 /// the tile's last, which is let go, as are the least recently used past
171 /// the cap — or dropped, when the ticket isn't one in flight.
172 pub fn arrived(&mut self, ticket: Ticket) -> Arrival {
173 let Some(Asked { key, stale }) = self.asked.remove(&ticket) else {
174 return Arrival::Dropped;
175 };
176 self.evicted.extend(self.held.remove(&key));
177 let dropped = self.held.insert(key, ticket, 1);
178 self.evicted.extend(dropped.into_iter().map(|(_, t)| t));
179 if stale {
180 self.held.damage_all_where(|k| *k == key);
181 Arrival::Stale
182 } else {
183 Arrival::Current
184 }
185 }
186
187 /// The tile asked for under `ticket` was painted as nothing — the scene
188 /// couldn't be drawn when it was asked of — so it is no longer in
189 /// flight, and is asked for again.
190 pub fn missed(&mut self, ticket: Ticket) {
191 self.asked.remove(&ticket);
192 }
193}
194
195#[cfg(test)]
196mod tests;