Skip to main content

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;