Skip to main content

alpha2_geometry/raster/
cache.rs

1//! Coverage cached between frames so unchanged paths are not rasterized again.
2
3use super::TileCoverage;
4use crate::{IRect, StableHash};
5use alpha2_model::FillRule;
6use kurbo::BezPath;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10/// The key of `path`'s coverage under `rule` in `clip`, hashed from exact bits.
11#[must_use]
12pub fn coverage_key(path: &BezPath, rule: FillRule, clip: IRect) -> u128 {
13    let mut hash = StableHash::new();
14    hash.bytes(b"coverage");
15    hash.path(path);
16    hash.u32(match rule {
17        FillRule::Nonzero => 0,
18        FillRule::Evenodd => 1,
19    });
20    for edge in [clip.x0, clip.y0, clip.x1, clip.y1] {
21        hash.u32(edge.cast_unsigned());
22    }
23    hash.finish()
24}
25
26/// An LRU coverage cache with a byte budget.
27#[derive(Debug)]
28pub struct CoverageCache {
29    budget: usize,
30    bytes: usize,
31    clock: u64,
32    entries: HashMap<u128, (Arc<TileCoverage>, u64)>,
33    hits: u64,
34    misses: u64,
35}
36
37/// The default budget: 64 MiB of tiles.
38pub const DEFAULT_COVERAGE_BUDGET: usize = 64 * 1024 * 1024;
39
40impl Default for CoverageCache {
41    fn default() -> Self {
42        Self::new(DEFAULT_COVERAGE_BUDGET)
43    }
44}
45
46impl CoverageCache {
47    /// An empty cache holding at most `budget` bytes.
48    #[must_use]
49    pub fn new(budget: usize) -> Self {
50        Self {
51            budget,
52            bytes: 0,
53            clock: 0,
54            entries: HashMap::new(),
55            hits: 0,
56            misses: 0,
57        }
58    }
59
60    /// The coverage kept under `key`, now the most recently used.
61    pub fn get(&mut self, key: u128) -> Option<Arc<TileCoverage>> {
62        self.clock += 1;
63        let clock = self.clock;
64        if let Some((coverage, used)) = self.entries.get_mut(&key) {
65            *used = clock;
66            self.hits += 1;
67            Some(Arc::clone(coverage))
68        } else {
69            self.misses += 1;
70            None
71        }
72    }
73
74    /// Stores `coverage` under `key`, evicting least recently used entries until it fits.
75    /// Coverage larger than the whole budget is not stored.
76    pub fn insert(&mut self, key: u128, coverage: Arc<TileCoverage>) {
77        let size = coverage.heap_bytes() + size_of::<TileCoverage>();
78        if size > self.budget {
79            return;
80        }
81        self.clock += 1;
82        if let Some((old, _)) = self.entries.insert(key, (coverage, self.clock)) {
83            self.bytes -= old.heap_bytes() + size_of::<TileCoverage>();
84        }
85        self.bytes += size;
86        while self.bytes > self.budget {
87            let Some(oldest) = self
88                .entries
89                .iter()
90                .min_by_key(|(_, (_, used))| *used)
91                .map(|(key, _)| *key)
92            else {
93                break;
94            };
95            if let Some((evicted, _)) = self.entries.remove(&oldest) {
96                self.bytes -= evicted.heap_bytes() + size_of::<TileCoverage>();
97            }
98        }
99    }
100
101    /// Drops everything kept.
102    pub fn clear(&mut self) {
103        self.entries.clear();
104        self.bytes = 0;
105    }
106
107    /// Bytes kept.
108    #[must_use]
109    pub const fn bytes(&self) -> usize {
110        self.bytes
111    }
112
113    /// Entries kept.
114    #[must_use]
115    pub fn len(&self) -> usize {
116        self.entries.len()
117    }
118
119    /// Whether nothing is kept.
120    #[must_use]
121    pub fn is_empty(&self) -> bool {
122        self.entries.is_empty()
123    }
124
125    /// Hit and miss counts.
126    #[must_use]
127    pub const fn hit_counts(&self) -> (u64, u64) {
128        (self.hits, self.misses)
129    }
130}