1use crate::MapError;
17use serde::Serialize;
18use std::collections::{HashMap, HashSet};
19use std::panic::{AssertUnwindSafe, catch_unwind};
20use std::sync::{Arc, Condvar, Mutex};
21
22pub type Key = (u32, u32);
23
24enum Slot {
25 Loading,
26 Ready {
27 data: Arc<[f32]>,
28 bytes: u64,
29 last: u64,
30 },
31}
32
33#[derive(Default, Clone, Serialize, Debug, PartialEq)]
34pub struct CacheStats {
35 pub budget_bytes: u64,
36 pub tile_reads: u64,
38 pub hits: u64,
39 pub single_flight_waits: u64,
41 pub evictions: u64,
42 pub peak_bytes: u64,
43 pub resident_bytes: u64,
44 pub decoded_bytes_total: u64,
45 pub distinct_tiles_loaded: u64,
46 pub reloads_after_eviction: u64,
48 pub load_errors: u64,
50 pub loader_panics: u64,
52 pub peak_concurrent_loads: u64,
54}
55
56struct State {
57 map: HashMap<Key, Slot>,
58 bytes: u64,
59 tick: u64,
60 seen: HashSet<Key>,
61 loading: u64,
62 stats: CacheStats,
63}
64
65struct LoadGuard<'a> {
67 cache: &'a TileCache,
68 key: Key,
69 bytes: u64,
70 armed: bool,
71}
72
73impl Drop for LoadGuard<'_> {
74 fn drop(&mut self) {
75 if !self.armed {
76 return;
77 }
78 let mut st = self.cache.lock();
79 if matches!(st.map.get(&self.key), Some(Slot::Loading)) {
80 st.map.remove(&self.key);
81 st.bytes -= self.bytes;
82 }
83 st.loading -= 1;
84 st.stats.load_errors += 1;
85 drop(st);
86 self.cache.cv.notify_all();
87 }
88}
89
90fn panic_message(p: &(dyn std::any::Any + Send)) -> String {
91 p.downcast_ref::<&str>()
92 .map(|s| s.to_string())
93 .or_else(|| p.downcast_ref::<String>().cloned())
94 .unwrap_or_else(|| "non-string panic payload".into())
95}
96
97pub struct TileCache {
98 state: Mutex<State>,
99 cv: Condvar,
100 budget: u64,
101}
102
103impl TileCache {
104 pub fn new(budget: u64) -> Self {
105 Self {
106 state: Mutex::new(State {
107 map: HashMap::new(),
108 bytes: 0,
109 tick: 0,
110 seen: HashSet::new(),
111 loading: 0,
112 stats: CacheStats {
113 budget_bytes: budget,
114 ..Default::default()
115 },
116 }),
117 cv: Condvar::new(),
118 budget,
119 }
120 }
121
122 pub fn budget(&self) -> u64 {
123 self.budget
124 }
125
126 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
127 self.state.lock().unwrap_or_else(|p| p.into_inner())
131 }
132
133 pub fn get_or_load(
135 &self,
136 key: Key,
137 bytes: u64,
138 load: impl FnOnce() -> Result<Vec<f32>, MapError>,
139 ) -> Result<Arc<[f32]>, MapError> {
140 if bytes > self.budget {
141 return Err(MapError::Budget(format!(
142 "one tile of {bytes} bytes exceeds the cache budget {}",
143 self.budget
144 )));
145 }
146 let mut st = self.lock();
147 let mut waited = false;
148 loop {
149 st.tick += 1;
150 let tick = st.tick;
151 match st.map.get_mut(&key) {
152 Some(Slot::Ready { data, last, .. }) => {
153 *last = tick;
154 let data = data.clone();
155 st.stats.hits += 1;
156 return Ok(data);
157 }
158 Some(Slot::Loading) => {
159 if !waited {
160 st.stats.single_flight_waits += 1;
161 waited = true;
162 }
163 st = self.cv.wait(st).unwrap_or_else(|p| p.into_inner());
164 }
165 None => break,
166 }
167 }
168 self.make_room(&mut st, bytes)?;
169 st.map.insert(key, Slot::Loading);
170 st.bytes += bytes;
171 st.stats.peak_bytes = st.stats.peak_bytes.max(st.bytes);
172 st.loading += 1;
173 st.stats.peak_concurrent_loads = st.stats.peak_concurrent_loads.max(st.loading);
174 drop(st);
175 let mut guard = LoadGuard {
177 cache: self,
178 key,
179 bytes,
180 armed: true,
181 };
182
183 let result = match catch_unwind(AssertUnwindSafe(load)) {
184 Ok(r) => r,
185 Err(p) => {
186 self.lock().stats.loader_panics += 1;
187 Err(MapError::Terrain(format!(
188 "tile {key:?} loader panicked: {}",
189 panic_message(p.as_ref())
190 )))
191 }
192 };
193 let data = result.and_then(|v| {
194 if (v.len() as u64).checked_mul(4) != Some(bytes) {
195 Err(MapError::Terrain(format!(
196 "decoded tile has {} values, expected {} bytes",
197 v.len(),
198 bytes
199 )))
200 } else {
201 Ok(Arc::<[f32]>::from(v))
202 }
203 })?;
204 let mut st = self.lock();
205 guard.armed = false;
206 st.loading -= 1;
207 st.tick += 1;
208 let last = st.tick;
209 st.map.insert(
210 key,
211 Slot::Ready {
212 data: data.clone(),
213 bytes,
214 last,
215 },
216 );
217 st.stats.tile_reads += 1;
218 st.stats.decoded_bytes_total += bytes;
219 if st.seen.insert(key) {
220 st.stats.distinct_tiles_loaded += 1;
221 }
222 drop(st);
223 self.cv.notify_all();
224 Ok(data)
225 }
226
227 fn make_room(&self, st: &mut State, need: u64) -> Result<(), MapError> {
228 while st.bytes + need > self.budget {
229 let victim = st
230 .map
231 .iter()
232 .filter_map(|(k, s)| match s {
233 Slot::Ready { data, last, .. } if Arc::strong_count(data) == 1 => {
234 Some((*last, *k))
235 }
236 _ => None,
237 })
238 .min();
239 let Some((_, k)) = victim else {
240 return Err(MapError::Budget(format!(
241 "terrain cache budget {} exhausted by pinned/loading tiles ({} resident, {} needed)",
242 self.budget, st.bytes, need
243 )));
244 };
245 if let Some(Slot::Ready { bytes, .. }) = st.map.remove(&k) {
246 st.bytes -= bytes;
247 st.stats.evictions += 1;
248 }
249 }
250 Ok(())
251 }
252
253 pub fn stats(&self) -> CacheStats {
254 let st = self.lock();
255 let mut s = st.stats.clone();
256 s.resident_bytes = st.bytes;
257 s.reloads_after_eviction = s.tile_reads - s.distinct_tiles_loaded;
258 s
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use std::sync::atomic::{AtomicUsize, Ordering};
266
267 #[test]
268 fn hits_evictions_and_budget() {
269 let c = TileCache::new(32);
270 let a = c.get_or_load((0, 0), 16, || Ok(vec![1.0; 4])).unwrap();
271 drop(a);
272 let _ = c.get_or_load((0, 0), 16, || panic!("must hit")).unwrap();
273 c.get_or_load((0, 1), 16, || Ok(vec![2.0; 4])).unwrap();
274 c.get_or_load((0, 2), 16, || Ok(vec![3.0; 4])).unwrap();
275 let s = c.stats();
276 assert_eq!((s.tile_reads, s.hits, s.evictions), (3, 1, 1));
277 assert!(s.peak_bytes <= 32);
278 c.get_or_load((0, 0), 16, || Ok(vec![1.0; 4])).unwrap();
280 assert_eq!(c.stats().reloads_after_eviction, 1);
281 }
282
283 #[test]
284 fn pinned_tiles_are_not_evicted() {
285 let c = TileCache::new(32);
286 let _a = c.get_or_load((0, 0), 16, || Ok(vec![1.0; 4])).unwrap();
287 let _b = c.get_or_load((0, 1), 16, || Ok(vec![1.0; 4])).unwrap();
288 let e = c.get_or_load((0, 2), 16, || Ok(vec![1.0; 4])).unwrap_err();
289 assert_eq!(e.kind(), "budget");
290 assert!(c.stats().peak_bytes <= 32);
291 }
292
293 #[test]
294 fn single_flight_and_errors() {
295 let c = Arc::new(TileCache::new(1 << 20));
296 let loads = Arc::new(AtomicUsize::new(0));
297 std::thread::scope(|s| {
298 for _ in 0..8 {
299 let (c, loads) = (c.clone(), loads.clone());
300 s.spawn(move || {
301 c.get_or_load((5, 5), 4, || {
302 loads.fetch_add(1, Ordering::SeqCst);
303 std::thread::sleep(std::time::Duration::from_millis(20));
304 Ok(vec![7.0])
305 })
306 .unwrap();
307 });
308 }
309 });
310 assert_eq!(loads.load(Ordering::SeqCst), 1);
311 assert!(c.get_or_load((1, 1), 8, || Ok(vec![1.0])).is_err());
312 assert_eq!(c.stats().resident_bytes, 4);
313 }
314
315 #[test]
318 fn loader_panic_releases_slot_and_wakes_waiters() {
319 use std::sync::mpsc;
320 use std::time::Duration;
321 let c = Arc::new(TileCache::new(1 << 20));
322 let (started_tx, started_rx) = mpsc::channel();
323 let (loader_tx, loader_rx) = mpsc::channel();
324 let c1 = c.clone();
325 std::thread::spawn(move || {
326 let r = c1.get_or_load((3, 4), 1024, || {
327 started_tx.send(()).unwrap();
328 std::thread::sleep(Duration::from_millis(100));
329 panic!("simulated decoder panic");
330 });
331 let _ = loader_tx.send(r);
332 });
333 started_rx.recv_timeout(Duration::from_secs(5)).unwrap();
334 let (waiter_tx, waiter_rx) = mpsc::channel();
336 let c2 = c.clone();
337 std::thread::spawn(move || {
338 let _ = waiter_tx.send(c2.get_or_load((3, 4), 1024, || Ok(vec![1.0f32; 256])));
339 });
340 let loader = loader_rx
341 .recv_timeout(Duration::from_secs(5))
342 .expect("loader returned");
343 let e = loader.expect_err("panic must become an error");
344 assert_eq!(e.kind(), "terrain");
345 assert!(e.to_string().contains("simulated decoder panic"), "{e}");
346 let waiter = waiter_rx
347 .recv_timeout(Duration::from_secs(5))
348 .expect("waiter must not hang");
349 assert_eq!(waiter.unwrap().len(), 256);
350 let later = c.get_or_load((3, 4), 1024, || panic!("must hit")).unwrap();
352 assert_eq!(later[0], 1.0);
353 let s = c.stats();
354 assert_eq!((s.load_errors, s.loader_panics, s.tile_reads), (1, 1, 1));
355 assert_eq!(s.resident_bytes, 1024);
356 assert!(s.peak_bytes <= 2048);
357 }
358
359 #[test]
360 fn loader_panic_without_waiters_leaves_no_bytes() {
361 let c = TileCache::new(64);
362 let e = c
363 .get_or_load((0, 0), 64, || panic!("boom"))
364 .expect_err("typed error");
365 assert_eq!(e.kind(), "terrain");
366 let s = c.stats();
367 assert_eq!(
368 (s.resident_bytes, s.load_errors, s.loader_panics),
369 (0, 1, 1)
370 );
371 c.get_or_load((0, 1), 64, || Ok(vec![0.0; 16])).unwrap();
373 assert_eq!(c.stats().peak_concurrent_loads, 1);
374 }
375}