1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5fn builtin_image_bytes(_path: &Path) -> Option<&'static [u8]> {
9 None
10}
11
12#[derive(Clone, Debug, Eq, PartialEq, Hash)]
13struct CacheKey {
14 path: PathBuf,
15}
16
17#[derive(Clone)]
18#[allow(dead_code)]
19enum CacheEntry {
20 Loading,
21 Ready {
22 tex: Arc<wgpu::Texture>,
23 width: u32,
24 height: u32,
25 last_tick: u64,
26 bytes: usize,
27 },
28 Failed,
29}
30
31pub struct ImageCache {
33 device: Arc<wgpu::Device>,
34 map: HashMap<CacheKey, CacheEntry>,
36 lru: VecDeque<CacheKey>,
37 current_tick: u64,
38 max_bytes: usize,
40 total_bytes: usize,
41 max_tex_size: u32,
42}
43
44impl ImageCache {
45 pub fn new(device: Arc<wgpu::Device>) -> Self {
46 let max_bytes = 256 * 1024 * 1024;
48 let limits = device.limits();
49 let max_tex_size = limits.max_texture_dimension_2d;
50 Self {
51 device,
52 map: HashMap::new(),
53 lru: VecDeque::new(),
54 current_tick: 0,
55 max_bytes,
56 total_bytes: 0,
57 max_tex_size,
58 }
59 }
60
61 pub fn set_max_bytes(&mut self, bytes: usize) {
62 self.max_bytes = bytes;
63 self.evict_if_needed();
64 }
65
66 fn touch(&mut self, key: &CacheKey) {
67 self.current_tick = self.current_tick.wrapping_add(1);
68 if let Some(entry) = self.map.get_mut(key) {
69 if let CacheEntry::Ready { last_tick, .. } = entry {
70 *last_tick = self.current_tick;
71 }
72 }
73 if let Some(pos) = self.lru.iter().position(|k| k == key) {
75 let k = self.lru.remove(pos).unwrap();
76 self.lru.push_back(k);
77 }
78 }
79
80 fn insert(&mut self, key: CacheKey, entry: CacheEntry) {
81 self.current_tick = self.current_tick.wrapping_add(1);
82 if let CacheEntry::Ready { bytes, .. } = &entry {
83 self.total_bytes += bytes;
84 }
85 self.map.insert(key.clone(), entry);
86 self.lru.push_back(key);
87 self.evict_if_needed();
88 }
89
90 fn evict_if_needed(&mut self) {
91 while self.total_bytes > self.max_bytes {
92 if let Some(old_key) = self.lru.pop_front() {
93 if let Some(entry) = self.map.remove(&old_key) {
94 if let CacheEntry::Ready { bytes, .. } = entry {
95 self.total_bytes = self.total_bytes.saturating_sub(bytes);
96 }
97 } else {
98 break;
99 }
100 } else {
101 break;
102 }
103 }
104 }
105
106 pub fn get(&mut self, path: &Path) -> Option<(Arc<wgpu::Texture>, u32, u32)> {
109 let key = CacheKey {
110 path: path.to_path_buf(),
111 };
112
113 let result = if let Some(entry) = self.map.get(&key) {
115 match entry {
116 CacheEntry::Ready {
117 tex, width, height, ..
118 } => Some((tex.clone(), *width, *height)),
119 CacheEntry::Loading | CacheEntry::Failed => None,
120 }
121 } else {
122 None
123 };
124
125 if result.is_some() {
126 self.touch(&key);
127 }
128
129 result
130 }
131
132 pub fn start_load(&mut self, path: &Path) {
135 let key = CacheKey {
136 path: path.to_path_buf(),
137 };
138
139 if self.map.contains_key(&key) {
141 return;
142 }
143
144 self.map.insert(key, CacheEntry::Loading);
146 }
147
148 pub fn get_or_load(
151 &mut self,
152 path: &Path,
153 queue: &wgpu::Queue,
154 ) -> Option<(Arc<wgpu::Texture>, u32, u32)> {
155 let key = CacheKey {
156 path: path.to_path_buf(),
157 };
158
159 let cached_result = if let Some(entry) = self.map.get(&key) {
161 match entry {
162 CacheEntry::Ready {
163 tex, width, height, ..
164 } => Some((tex.clone(), *width, *height)),
165 CacheEntry::Loading => {
166 None
168 }
169 CacheEntry::Failed => return None,
170 }
171 } else {
172 None
173 };
174
175 if let Some(result) = cached_result {
176 self.touch(&key);
177 return Some(result);
178 }
179
180 let img = if let Some(bytes) = builtin_image_bytes(path) {
182 match image::load_from_memory(bytes) {
183 Ok(img) => img,
184 Err(_) => return None,
185 }
186 } else {
187 match image::open(path) {
188 Ok(img) => img,
189 Err(_) => return None,
190 }
191 };
192
193 let rgba = img.to_rgba8();
194 let (width, height) = rgba.dimensions();
195
196 if width > self.max_tex_size || height > self.max_tex_size {
198 return None;
199 }
200
201 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
203 label: Some(&format!("image:{}", path.display())),
204 size: wgpu::Extent3d {
205 width: width.max(1),
206 height: height.max(1),
207 depth_or_array_layers: 1,
208 },
209 mip_level_count: 1,
210 sample_count: 1,
211 dimension: wgpu::TextureDimension::D2,
212 format: wgpu::TextureFormat::Rgba8UnormSrgb,
213 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
214 view_formats: &[],
215 });
216
217 queue.write_texture(
219 wgpu::ImageCopyTexture {
220 texture: &tex,
221 mip_level: 0,
222 origin: wgpu::Origin3d::ZERO,
223 aspect: wgpu::TextureAspect::All,
224 },
225 &rgba,
226 wgpu::ImageDataLayout {
227 offset: 0,
228 bytes_per_row: Some(width * 4),
229 rows_per_image: Some(height),
230 },
231 wgpu::Extent3d {
232 width,
233 height,
234 depth_or_array_layers: 1,
235 },
236 );
237
238 let bytes = (width * height * 4) as usize;
239 let tex_arc = Arc::new(tex);
240 let entry = CacheEntry::Ready {
241 tex: tex_arc.clone(),
242 width,
243 height,
244 last_tick: self.current_tick,
245 bytes,
246 };
247
248 self.insert(key, entry);
249 Some((tex_arc, width, height))
250 }
251
252 pub fn is_loading(&self, path: &Path) -> bool {
254 let key = CacheKey {
255 path: path.to_path_buf(),
256 };
257 matches!(self.map.get(&key), Some(CacheEntry::Loading))
258 }
259
260 pub fn is_ready(&self, path: &Path) -> bool {
262 let key = CacheKey {
263 path: path.to_path_buf(),
264 };
265 matches!(self.map.get(&key), Some(CacheEntry::Ready { .. }))
266 }
267
268 pub fn store_ready(&mut self, path: &Path, tex: Arc<wgpu::Texture>, width: u32, height: u32) {
270 let key = CacheKey {
271 path: path.to_path_buf(),
272 };
273 let bytes = (width * height * 4) as usize;
274
275 let entry = CacheEntry::Ready {
276 tex,
277 width,
278 height,
279 last_tick: self.current_tick,
280 bytes,
281 };
282
283 self.insert(key, entry);
284 }
285}