1use std::collections::HashMap;
15
16pub const QUALITY_BLOCK: usize = 16;
18
19#[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 pub fn path(&self) -> String {
40 format!("{}/{}/{}_{}", self.canvas, self.level, self.x, self.y)
41 }
42}
43
44#[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 pub dirty: bool,
59 pub existed: bool,
61 pub touched: Option<f64>,
63 id: u32,
64 last_use: u64,
65}
66
67impl Tile {
68 pub fn id(&self) -> u32 {
70 self.id
71 }
72
73 pub fn observed(&self) -> u64 {
75 count_covered(&self.coverage)
76 }
77}
78
79#[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 pub encoded: u64,
97 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 BadSize,
107 BadPixels,
109 Resident,
111}
112
113pub fn count_covered(coverage: &[u8]) -> u64 {
115 coverage.iter().map(|b| b.count_ones() as u64).sum()
116}
117
118const BYTES_PER_PIXEL: f64 = 4.3;
120
121#[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 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 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 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 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 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 pub fn is_resident(&self, key: &TileKey) -> bool {
210 self.by_key.contains_key(key)
211 }
212
213 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 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 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 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 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 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 pub fn encoded(&mut self) {
318 self.stats.encoded += 1;
319 }
320
321 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 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 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 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 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 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 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 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 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}