1use std::{
8 any::TypeId,
9 collections::HashMap,
10 io,
11 sync::{Arc, OnceLock, RwLock, RwLockWriteGuard},
12};
13
14type AssetCacheKey = (TypeId, u64);
15type AssetCacheMap = HashMap<AssetCacheKey, AssetCacheEntry>;
16
17const CACHE_MAX_ENTRIES: usize = 4096;
18const CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
19
20static ASSET_BYTES_CACHE: OnceLock<RwLock<AssetLruCache>> = OnceLock::new();
21
22#[derive(Clone)]
23struct AssetCacheEntry {
24 bytes: Arc<[u8]>,
25 len: usize,
26 last_access_tick: u64,
27}
28
29#[derive(Default)]
30struct AssetLruCache {
31 entries: AssetCacheMap,
32 total_bytes: usize,
33 next_tick: u64,
34}
35
36impl AssetLruCache {
37 fn get(&mut self, key: AssetCacheKey) -> Option<Arc<[u8]>> {
38 let tick = self.bump_tick();
39 if let Some(entry) = self.entries.get_mut(&key) {
40 entry.last_access_tick = tick;
41 return Some(entry.bytes.clone());
42 }
43 None
44 }
45
46 fn insert(&mut self, key: AssetCacheKey, bytes: Arc<[u8]>) {
47 let tick = self.bump_tick();
48 let entry_len = bytes.len();
49 let entry = AssetCacheEntry {
50 len: entry_len,
51 bytes,
52 last_access_tick: tick,
53 };
54
55 if let Some(previous) = self.entries.insert(key, entry) {
56 self.total_bytes = self.total_bytes.saturating_sub(previous.len);
57 }
58 self.total_bytes = self.total_bytes.saturating_add(entry_len);
59 self.evict_if_needed();
60 }
61
62 fn bump_tick(&mut self) -> u64 {
63 self.next_tick = self.next_tick.wrapping_add(1);
64 self.next_tick
65 }
66
67 fn evict_if_needed(&mut self) {
68 while (self.total_bytes > CACHE_MAX_BYTES || self.entries.len() > CACHE_MAX_ENTRIES)
69 && self.entries.len() > 1
70 {
71 let Some(victim_key) = self
72 .entries
73 .iter()
74 .min_by_key(|(_, entry)| entry.last_access_tick)
75 .map(|(key, _)| *key)
76 else {
77 break;
78 };
79
80 if let Some(removed) = self.entries.remove(&victim_key) {
81 self.total_bytes = self.total_bytes.saturating_sub(removed.len);
82 }
83 }
84 }
85}
86
87pub trait AssetExt: Copy {
89 fn read(self) -> io::Result<Arc<[u8]>>;
91}
92
93#[doc(hidden)]
98pub fn read_with_lru_cache<T, F>(asset_id: u64, loader: F) -> io::Result<Arc<[u8]>>
99where
100 T: 'static,
101 F: FnOnce() -> io::Result<Arc<[u8]>>,
102{
103 let cache_key = (TypeId::of::<T>(), asset_id);
104
105 {
106 let mut cache = cache_write();
107 if let Some(bytes) = cache.get(cache_key) {
108 return Ok(bytes);
109 }
110 }
111
112 let loaded = loader()?;
113
114 let mut cache = cache_write();
115 if let Some(bytes) = cache.get(cache_key) {
116 return Ok(bytes);
117 }
118
119 cache.insert(cache_key, loaded.clone());
120 Ok(loaded)
121}
122
123#[doc(hidden)]
125pub fn read_with_weak_cache<T, F>(asset_id: u64, loader: F) -> io::Result<Arc<[u8]>>
126where
127 T: 'static,
128 F: FnOnce() -> io::Result<Arc<[u8]>>,
129{
130 read_with_lru_cache::<T, F>(asset_id, loader)
131}
132
133fn cache_instance() -> &'static RwLock<AssetLruCache> {
134 ASSET_BYTES_CACHE.get_or_init(|| RwLock::new(AssetLruCache::default()))
135}
136
137fn cache_write() -> RwLockWriteGuard<'static, AssetLruCache> {
138 match cache_instance().write() {
139 Ok(guard) => guard,
140 Err(poisoned) => poisoned.into_inner(),
141 }
142}