Skip to main content

long_screen/engine/
tiles.rs

1//! The tile cache: every canvas tile the render, reveal and presentation passes paint, held in engine memory under
2//! a working-memory budget with least-recently-used eviction (docs/ARCHITECTURE.md §十 "瓦片缓存").
3//!
4//! The cache never touches storage. When a tile the caller asks for is not resident, the caller makes room: while
5//! [`TileCache::victim`] names a tile, it stores that tile if it is dirty and only then lets it go with
6//! [`TileCache::evict`], so a write that fails leaves the tile cached and dirty for a later flush. It then supplies
7//! the new tile's stored planes, or none for a tile that was never written, with [`TileCache::insert`]. Which
8//! tiles a flush must write comes from [`TileCache::dirty`]; the host encodes and stores them and reports each back
9//! with [`TileCache::saved`]. Tile pixels are stored raw: encoding them (PNG) is the host's business.
10//!
11//! The clock is the host's too: a write records the host's timestamp in [`Tile::touched`], and a periodic flush
12//! passes the time of the previous one so tiles still being painted every frame are left for later.
13
14use std::collections::HashMap;
15
16/// Side of one quality/ownership block in native pixels.
17pub const QUALITY_BLOCK: usize = 16;
18
19/// Identifies one tile: canvas, pyramid level and tile coordinates. Its storage path is `canvas/level/x_y`.
20#[derive(Clone, Debug, PartialEq, Eq, Hash)]
21pub struct TileKey {
22    pub canvas: String,
23    pub level: u32,
24    pub x: i32,
25    pub y: i32,
26}
27
28impl TileKey {
29    pub fn new(canvas: &str, level: u32, x: i32, y: i32) -> Self {
30        Self {
31            canvas: canvas.to_string(),
32            level,
33            x,
34            y,
35        }
36    }
37
38    /// `canvas/level/x_y`, the suffix of the tile's `tile/` and `tile-index/` storage keys.
39    pub fn path(&self) -> String {
40        format!("{}/{}/{}_{}", self.canvas, self.level, self.x, self.y)
41    }
42}
43
44/// One resident tile: RGBA pixels and the side planes kept per pixel (coverage, provisional) and per 16×16 block
45/// (quality, conflicts, owner frame + 1, score, frozen).
46#[derive(Clone, Debug, PartialEq)]
47pub struct Tile {
48    pub key: TileKey,
49    pub pixels: Vec<u8>,
50    pub coverage: Vec<u8>,
51    pub provisional: Vec<u8>,
52    pub quality: Vec<u8>,
53    pub conflicts: Vec<u8>,
54    pub owner: Vec<u32>,
55    pub score: Vec<f32>,
56    pub frozen: Vec<u8>,
57    /// Written since it was last stored.
58    pub dirty: bool,
59    /// Has a stored copy (it was loaded from one, or stored since).
60    pub existed: bool,
61    /// The host's timestamp of the last write that dirtied it; `None` reads as 0.
62    pub touched: Option<f64>,
63    id: u32,
64    last_use: u64,
65}
66
67impl Tile {
68    /// Stable while the tile stays resident; a reloaded tile gets a new one.
69    pub fn id(&self) -> u32 {
70        self.id
71    }
72
73    /// Covered pixels, the `observed` field of the tile's `tile-index/` row.
74    pub fn observed(&self) -> u64 {
75        count_covered(&self.coverage)
76    }
77}
78
79/// What the host read back for a tile it stored earlier. `None` planes (rows written before that plane existed)
80/// start empty, as a fresh tile's do.
81#[derive(Clone, Debug, Default, PartialEq)]
82pub struct StoredTile {
83    pub pixels: Vec<u8>,
84    pub coverage: Option<Vec<u8>>,
85    pub provisional: Option<Vec<u8>>,
86    pub quality: Option<Vec<u8>>,
87    pub conflicts: Option<Vec<u8>>,
88    pub owner: Option<Vec<u32>>,
89    pub score: Option<Vec<f32>>,
90    pub frozen: Option<Vec<u8>>,
91}
92
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct TileCacheStats {
95    /// Tiles encoded for storage (reported by the host through [`TileCache::encoded`]).
96    pub encoded: u64,
97    /// Tiles loaded from a stored copy.
98    pub decoded: u64,
99    pub evictions: u64,
100    pub peak_resident: u64,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum TileError {
105    /// The tile size is not a positive multiple of [`QUALITY_BLOCK`].
106    BadSize,
107    /// A stored tile's pixels do not hold `size × size` RGBA pixels.
108    BadPixels,
109    /// The key is already resident.
110    Resident,
111}
112
113/// Set bits in a coverage bitmap.
114pub fn count_covered(coverage: &[u8]) -> u64 {
115    coverage.iter().map(|b| b.count_ones() as u64).sum()
116}
117
118/// Bytes the budget counts per tile pixel: RGBA plus the side planes and bookkeeping.
119const BYTES_PER_PIXEL: f64 = 4.3;
120
121/// The in-memory tile cache.
122#[derive(Debug)]
123pub struct TileCache {
124    size: usize,
125    max_tiles: u64,
126    tiles: HashMap<u32, Tile>,
127    by_key: HashMap<TileKey, u32>,
128    clock: u64,
129    next_id: u32,
130    stats: TileCacheStats,
131}
132
133impl TileCache {
134    /// A cache for `size`-pixel tiles under a `memory_mb` working-memory budget, of which it reserves 30% for
135    /// tiles (the rest is for native frames, decoder surfaces, features and the page).
136    pub fn new(size: usize, memory_mb: f64) -> Result<Self, TileError> {
137        if size < QUALITY_BLOCK || !size.is_multiple_of(QUALITY_BLOCK) {
138            return Err(TileError::BadSize);
139        }
140        let per_tile = (size * size) as f64 * BYTES_PER_PIXEL;
141        let max_tiles = ((memory_mb * 1024.0 * 1024.0 * 0.30 / per_tile).floor() as u64).max(2);
142        Ok(Self {
143            size,
144            max_tiles,
145            tiles: HashMap::new(),
146            by_key: HashMap::new(),
147            clock: 0,
148            next_id: 0,
149            stats: TileCacheStats::default(),
150        })
151    }
152
153    pub fn size(&self) -> usize {
154        self.size
155    }
156
157    pub fn max_tiles(&self) -> u64 {
158        self.max_tiles
159    }
160
161    /// Overrides the limit (tests use small fixed caches).
162    pub fn set_max_tiles(&mut self, max_tiles: u64) {
163        self.max_tiles = max_tiles;
164    }
165
166    pub fn stats(&self) -> TileCacheStats {
167        self.stats
168    }
169
170    pub fn len(&self) -> usize {
171        self.tiles.len()
172    }
173
174    pub fn is_empty(&self) -> bool {
175        self.tiles.is_empty()
176    }
177
178    /// Rebalances the same budget between passes: 80% of what `reserved_bytes` leaves of `memory_mb`.
179    pub fn configure_budget(&mut self, memory_mb: f64, reserved_bytes: f64) {
180        let available = (memory_mb * 1024.0 * 1024.0 - reserved_bytes).max(0.0);
181        let per_tile = (self.size * self.size) as f64 * BYTES_PER_PIXEL;
182        self.max_tiles = ((available * 0.8 / per_tile).floor() as u64).max(2);
183    }
184
185    /// Tiles a `width`×`height` area can touch at any integer pose (a partial tile on each side).
186    pub fn footprint(&self, width: f64, height: f64) -> u64 {
187        let size = self.size as f64;
188        ((width / size).ceil() as u64 + 1) * ((height / size).ceil() as u64 + 1)
189    }
190
191    /// Raises the limit to at least what one frame paints: the footprint of each of `areas` (every region it
192    /// places, each on its own canvas), plus two. An LRU cache smaller than the set one frame touches turns every
193    /// frame into a full miss cycle, so the budget yields here. Returns the tiles added, 0 if none.
194    pub fn ensure_footprint(&mut self, areas: &[(f64, f64)]) -> u64 {
195        let needed = areas
196            .iter()
197            .map(|&(w, h)| self.footprint(w, h))
198            .sum::<u64>()
199            + 2;
200        if self.max_tiles >= needed {
201            return 0;
202        }
203        let added = needed - self.max_tiles;
204        self.max_tiles = needed;
205        added
206    }
207
208    /// Whether `key` is resident, without changing the eviction order.
209    pub fn is_resident(&self, key: &TileKey) -> bool {
210        self.by_key.contains_key(key)
211    }
212
213    /// A resident tile becomes the most recently used one; returns its id, `None` when it is not resident.
214    pub fn touch(&mut self, key: &TileKey) -> Option<u32> {
215        let id = *self.by_key.get(key)?;
216        self.clock += 1;
217        self.tiles.get_mut(&id).unwrap().last_use = self.clock;
218        Some(id)
219    }
220
221    /// The tile to let go to make room for one more: the least recently used one while the cache is full, `None`
222    /// when there is room. Changes nothing; the host stores the tile if it is dirty, then calls
223    /// [`TileCache::evict`]. Repeat until `None` before [`TileCache::insert`].
224    pub fn victim(&self) -> Option<u32> {
225        if (self.tiles.len() as u64) < self.max_tiles {
226            return None;
227        }
228        self.tiles.values().min_by_key(|t| t.last_use).map(|t| t.id)
229    }
230
231    /// Removes a resident tile and counts the eviction, handing the tile back; `None` (and nothing counted) when
232    /// `id` is not resident.
233    pub fn evict(&mut self, id: u32) -> Option<Tile> {
234        let tile = self.tiles.remove(&id)?;
235        self.by_key.remove(&tile.key);
236        self.stats.evictions += 1;
237        Some(tile)
238    }
239
240    /// Adds `key` as the most recently used tile, from its stored copy or, with `None`, empty. Returns its id.
241    pub fn insert(&mut self, key: TileKey, stored: Option<StoredTile>) -> Result<u32, TileError> {
242        if self.by_key.contains_key(&key) {
243            return Err(TileError::Resident);
244        }
245        let n = self.size * self.size;
246        let bits = n.div_ceil(8);
247        let blocks = (self.size / QUALITY_BLOCK) * (self.size / QUALITY_BLOCK);
248        let existed = stored.is_some();
249        let s = stored.unwrap_or_else(|| StoredTile {
250            pixels: vec![0; n * 4],
251            ..StoredTile::default()
252        });
253        if s.pixels.len() != n * 4 {
254            return Err(TileError::BadPixels);
255        }
256        if existed {
257            self.stats.decoded += 1;
258        }
259        self.next_id += 1;
260        self.clock += 1;
261        let id = self.next_id;
262        let tile = Tile {
263            key: key.clone(),
264            pixels: s.pixels,
265            coverage: s.coverage.unwrap_or_else(|| vec![0; bits]),
266            provisional: s.provisional.unwrap_or_else(|| vec![0; bits]),
267            quality: s.quality.unwrap_or_else(|| vec![0; blocks]),
268            conflicts: s.conflicts.unwrap_or_else(|| vec![0; blocks]),
269            owner: s.owner.unwrap_or_else(|| vec![0; blocks]),
270            score: s.score.unwrap_or_else(|| vec![0.0; blocks]),
271            frozen: s.frozen.unwrap_or_else(|| vec![0; blocks]),
272            dirty: false,
273            existed,
274            touched: None,
275            id,
276            last_use: self.clock,
277        };
278        self.tiles.insert(id, tile);
279        self.by_key.insert(key, id);
280        self.stats.peak_resident = self.stats.peak_resident.max(self.tiles.len() as u64);
281        Ok(id)
282    }
283
284    pub fn get(&self, id: u32) -> Option<&Tile> {
285        self.tiles.get(&id)
286    }
287
288    pub fn get_mut(&mut self, id: u32) -> Option<&mut Tile> {
289        self.tiles.get_mut(&id)
290    }
291
292    pub fn id_of(&self, key: &TileKey) -> Option<u32> {
293        self.by_key.get(key).copied()
294    }
295
296    /// Marks a tile written at the host's time `now`.
297    pub fn mark_dirty(&mut self, id: u32, now: f64) {
298        if let Some(t) = self.tiles.get_mut(&id) {
299            t.dirty = true;
300            t.touched = Some(now);
301        }
302    }
303
304    /// Dirty resident tiles a flush must store, least recently used first. With `settled_before`, only tiles
305    /// whose last write precedes it: tiles still being painted every frame wait for a later flush or eviction.
306    pub fn dirty(&self, settled_before: Option<f64>) -> Vec<u32> {
307        let mut out: Vec<&Tile> = self
308            .tiles
309            .values()
310            .filter(|t| t.dirty && settled_before.is_none_or(|s| t.touched.unwrap_or(0.0) < s))
311            .collect();
312        out.sort_by_key(|t| t.last_use);
313        out.into_iter().map(|t| t.id).collect()
314    }
315
316    /// The host encoded one tile for storage.
317    pub fn encoded(&mut self) {
318        self.stats.encoded += 1;
319    }
320
321    /// The host stored a resident tile: it is clean and has a stored copy.
322    pub fn saved(&mut self, id: u32) {
323        if let Some(t) = self.tiles.get_mut(&id) {
324            t.dirty = false;
325            t.existed = true;
326        }
327    }
328
329    /// Drops every resident tile (the host flushes first).
330    pub fn clear(&mut self) {
331        self.tiles.clear();
332        self.by_key.clear();
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    fn key(x: i32) -> TileKey {
341        TileKey::new("c", 0, x, 0)
342    }
343
344    #[test]
345    fn rejects_a_size_that_is_not_a_block_multiple() {
346        assert_eq!(TileCache::new(0, 128.0).unwrap_err(), TileError::BadSize);
347        assert_eq!(TileCache::new(24, 128.0).unwrap_err(), TileError::BadSize);
348        assert!(TileCache::new(16, 128.0).is_ok());
349    }
350
351    #[test]
352    fn budget_never_drops_below_two_tiles() {
353        let mut cache = TileCache::new(512, 0.0).unwrap();
354        assert_eq!(cache.max_tiles(), 2);
355        cache.configure_budget(1.0, 1e12);
356        assert_eq!(cache.max_tiles(), 2);
357    }
358
359    #[test]
360    fn budget_matches_the_page_formula() {
361        // floor(128 MiB × 0.30 / (512² × 4.3)) and floor((256 MiB − 100 MiB) × 0.8 / (512² × 4.3)).
362        let mut cache = TileCache::new(512, 128.0).unwrap();
363        assert_eq!(cache.max_tiles(), 35);
364        cache.configure_budget(256.0, 100.0 * 1024.0 * 1024.0);
365        assert_eq!(cache.max_tiles(), 116);
366    }
367
368    #[test]
369    fn footprint_floor_only_raises_the_limit() {
370        let mut cache = TileCache::new(512, 128.0).unwrap();
371        cache.set_max_tiles(10);
372        // (ceil(1000/512)+1)×(ceil(600/512)+1) = 3×3, plus 2×2 for the second area, plus 2.
373        assert_eq!(
374            cache.ensure_footprint(&[(1000.0, 600.0), (100.0, 100.0)]),
375            5
376        );
377        assert_eq!(cache.max_tiles(), 15);
378        assert_eq!(cache.ensure_footprint(&[(100.0, 100.0)]), 0);
379        assert_eq!(cache.max_tiles(), 15);
380    }
381
382    #[test]
383    fn a_stored_tile_with_the_wrong_pixel_count_is_refused() {
384        let mut cache = TileCache::new(16, 128.0).unwrap();
385        let stored = StoredTile {
386            pixels: vec![0; 10],
387            ..StoredTile::default()
388        };
389        assert_eq!(
390            cache.insert(key(0), Some(stored)),
391            Err(TileError::BadPixels)
392        );
393        assert!(cache.is_empty());
394        assert_eq!(cache.stats().decoded, 0);
395    }
396
397    #[test]
398    fn inserting_a_resident_key_is_refused() {
399        let mut cache = TileCache::new(16, 128.0).unwrap();
400        cache.insert(key(0), None).unwrap();
401        assert_eq!(cache.insert(key(0), None), Err(TileError::Resident));
402    }
403
404    #[test]
405    fn eviction_takes_the_least_recently_used_tile_and_only_when_full() {
406        let mut cache = TileCache::new(16, 128.0).unwrap();
407        cache.set_max_tiles(2);
408        assert!(cache.victim().is_none());
409        cache.insert(key(0), None).unwrap();
410        cache.insert(key(1), None).unwrap();
411        // Touching 0 makes 1 the oldest.
412        let a = cache.touch(&key(0)).unwrap();
413        let b = cache.victim().unwrap();
414        let out = cache.evict(b).unwrap();
415        assert_eq!(out.key, key(1));
416        assert!(cache.victim().is_none());
417        assert_eq!(cache.stats().evictions, 1);
418        assert!(!cache.is_resident(&key(1)));
419        assert!(cache.get(a).is_some());
420    }
421
422    #[test]
423    fn naming_the_victim_changes_nothing() {
424        // Failure cases: choosing the victim already removes it (a host whose write then fails has lost the tile),
425        // counts an eviction, or reorders the cache so a second call names another tile.
426        let mut cache = TileCache::new(16, 128.0).unwrap();
427        cache.set_max_tiles(2);
428        let a = cache.insert(key(0), None).unwrap();
429        cache.insert(key(1), None).unwrap();
430        cache.mark_dirty(a, 1.0);
431        assert_eq!(cache.victim(), Some(a));
432        assert_eq!(cache.victim(), Some(a));
433        assert!(cache.is_resident(&key(0)));
434        assert!(cache.get(a).unwrap().dirty);
435        assert_eq!(cache.len(), 2);
436        assert_eq!(cache.stats().evictions, 0);
437        // The host stored it; now it goes.
438        cache.saved(a);
439        let out = cache.evict(a).unwrap();
440        assert!(!out.dirty && out.existed);
441        assert_eq!(cache.stats().evictions, 1);
442        assert_eq!(cache.victim(), None);
443    }
444
445    #[test]
446    fn evicting_a_tile_that_is_not_resident_does_nothing() {
447        let mut cache = TileCache::new(16, 128.0).unwrap();
448        cache.set_max_tiles(1);
449        let a = cache.insert(key(0), None).unwrap();
450        assert!(cache.evict(a + 1).is_none());
451        assert!(cache.evict(a).is_some());
452        assert!(cache.evict(a).is_none());
453        assert_eq!(cache.stats().evictions, 1);
454    }
455
456    #[test]
457    fn eviction_with_an_empty_cache_and_a_zero_limit_stops() {
458        let mut cache = TileCache::new(16, 128.0).unwrap();
459        cache.set_max_tiles(0);
460        assert!(cache.victim().is_none());
461    }
462
463    #[test]
464    fn missing_planes_start_empty_and_a_stored_tile_counts_as_decoded() {
465        let mut cache = TileCache::new(32, 128.0).unwrap();
466        let id = cache
467            .insert(
468                key(0),
469                Some(StoredTile {
470                    pixels: vec![7; 32 * 32 * 4],
471                    owner: Some(vec![]),
472                    ..StoredTile::default()
473                }),
474            )
475            .unwrap();
476        let t = cache.get(id).unwrap();
477        assert!(t.existed && !t.dirty);
478        assert_eq!(t.coverage.len(), 128);
479        assert_eq!(t.quality.len(), 4);
480        // A stored empty plane is kept as stored (pyramid rows carry empty owner/score).
481        assert!(t.owner.is_empty());
482        assert_eq!(cache.stats().decoded, 1);
483        assert_eq!(cache.stats().peak_resident, 1);
484    }
485
486    #[test]
487    fn settled_flush_skips_tiles_written_since_the_previous_flush() {
488        let mut cache = TileCache::new(16, 128.0).unwrap();
489        let a = cache.insert(key(0), None).unwrap();
490        let b = cache.insert(key(1), None).unwrap();
491        let c = cache.insert(key(2), None).unwrap();
492        cache.mark_dirty(a, 10.0);
493        cache.mark_dirty(b, 30.0);
494        cache.mark_dirty(c, 20.0);
495        cache.touch(&key(0));
496        assert_eq!(cache.dirty(Some(25.0)), vec![c, a]);
497        // Equal to the previous flush is not settled.
498        assert_eq!(cache.dirty(Some(20.0)), vec![a]);
499        assert_eq!(cache.dirty(None), vec![b, c, a]);
500        cache.saved(a);
501        assert_eq!(cache.dirty(None), vec![b, c]);
502        assert!(cache.get(a).unwrap().existed);
503    }
504
505    #[test]
506    fn untouched_dirty_tile_reads_as_written_at_zero() {
507        let mut cache = TileCache::new(16, 128.0).unwrap();
508        let a = cache.insert(key(0), None).unwrap();
509        cache.get_mut(a).unwrap().dirty = true;
510        assert_eq!(cache.dirty(Some(0.0)), Vec::<u32>::new());
511        assert_eq!(cache.dirty(Some(1.0)), vec![a]);
512    }
513
514    #[test]
515    fn a_reloaded_tile_gets_a_new_id() {
516        let mut cache = TileCache::new(16, 128.0).unwrap();
517        cache.set_max_tiles(1);
518        let a = cache.insert(key(0), None).unwrap();
519        let out = cache.evict(cache.victim().unwrap()).unwrap();
520        let b = cache.insert(key(0), None).unwrap();
521        assert_ne!(a, b);
522        assert_eq!(out.id(), a);
523        assert!(cache.get(a).is_none());
524    }
525}