1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3
4use dear_imgui_rs::texture::TextureId;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct DecodedRgbaImage {
9 pub width: u32,
11 pub height: u32,
13 pub rgba: Vec<u8>,
15}
16
17pub trait ThumbnailProvider {
24 fn decode(&mut self, req: &ThumbnailRequest) -> Result<DecodedRgbaImage, String>;
26}
27
28pub trait ThumbnailRenderer {
32 fn upload_rgba8(&mut self, image: &DecodedRgbaImage) -> Result<TextureId, String>;
34 fn destroy(&mut self, texture_id: TextureId);
36}
37
38pub struct ThumbnailBackend<'a> {
40 pub provider: &'a mut dyn ThumbnailProvider,
42 pub renderer: &'a mut dyn ThumbnailRenderer,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct ThumbnailCacheConfig {
49 pub max_entries: usize,
51 pub max_new_requests_per_frame: usize,
53}
54
55impl Default for ThumbnailCacheConfig {
56 fn default() -> Self {
57 Self {
58 max_entries: 256,
59 max_new_requests_per_frame: 24,
60 }
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct ThumbnailRequest {
67 pub path: PathBuf,
69 pub max_size: [u32; 2],
71}
72
73#[derive(Clone, Debug)]
74enum ThumbnailState {
75 Queued,
76 InFlight,
77 Ready { texture_id: TextureId },
78 Failed,
79}
80
81#[derive(Clone, Debug)]
82struct ThumbnailEntry {
83 state: ThumbnailState,
84 lru_stamp: u64,
85}
86
87#[derive(Clone, Debug)]
97pub struct ThumbnailCache {
98 pub config: ThumbnailCacheConfig,
100
101 frame_index: u64,
102 issued_this_frame: usize,
103 next_stamp: u64,
104
105 entries: HashMap<PathBuf, ThumbnailEntry>,
106 lru: VecDeque<(PathBuf, u64)>,
107 requests: VecDeque<ThumbnailRequest>,
108 pending_destroys: Vec<TextureId>,
109}
110
111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub struct ThumbnailStats {
114 pub total: usize,
116 pub queued: usize,
118 pub in_flight: usize,
120 pub ready: usize,
122 pub failed: usize,
124 pub pending_requests: usize,
126 pub issued_this_frame: usize,
128 pub max_new_requests_per_frame: usize,
130}
131
132impl Default for ThumbnailCache {
133 fn default() -> Self {
134 Self::new(ThumbnailCacheConfig::default())
135 }
136}
137
138impl ThumbnailCache {
139 pub fn new(config: ThumbnailCacheConfig) -> Self {
141 Self {
142 config,
143 frame_index: 0,
144 issued_this_frame: 0,
145 next_stamp: 1,
146 entries: HashMap::new(),
147 lru: VecDeque::new(),
148 requests: VecDeque::new(),
149 pending_destroys: Vec::new(),
150 }
151 }
152
153 pub fn advance_frame(&mut self) {
157 self.frame_index = self.frame_index.wrapping_add(1);
158 self.issued_this_frame = 0;
159 }
160
161 pub fn frame_index(&self) -> u64 {
163 self.frame_index
164 }
165
166 pub fn request_visible(&mut self, path: &Path, max_size: [u32; 2]) {
171 let key = path.to_path_buf();
172
173 if let Some(e) = self.entries.get(&key) {
174 self.touch_existing(&key, e.lru_stamp);
176 return;
177 }
178
179 if self.issued_this_frame >= self.config.max_new_requests_per_frame {
180 return;
181 }
182 self.issued_this_frame += 1;
183
184 let stamp = self.alloc_stamp();
185 self.entries.insert(
186 key.clone(),
187 ThumbnailEntry {
188 state: ThumbnailState::Queued,
189 lru_stamp: stamp,
190 },
191 );
192 self.lru.push_back((key.clone(), stamp));
193 self.requests.push_back(ThumbnailRequest {
194 path: key,
195 max_size,
196 });
197 self.evict_to_fit();
198 }
199
200 pub fn texture_id(&self, path: &Path) -> Option<TextureId> {
202 self.entries.get(path).and_then(|e| match &e.state {
203 ThumbnailState::Ready { texture_id } => Some(*texture_id),
204 _ => None,
205 })
206 }
207
208 pub fn take_requests(&mut self) -> Vec<ThumbnailRequest> {
212 let mut out = Vec::new();
213 while let Some(req) = self.requests.pop_front() {
214 if let Some(entry) = self.entries.get_mut(&req.path) {
215 if let ThumbnailState::Queued = entry.state {
216 entry.state = ThumbnailState::InFlight;
217 }
218 }
219 out.push(req);
220 }
221 out
222 }
223
224 pub fn fulfill(&mut self, path: &Path, result: Result<TextureId, String>, _max_size: [u32; 2]) {
228 let key = path.to_path_buf();
229 let stamp = self.alloc_stamp();
230 let state = match result {
231 Ok(texture_id) => ThumbnailState::Ready { texture_id },
232 Err(_message) => ThumbnailState::Failed,
233 };
234
235 if let Some(old) = self.entries.insert(
236 key.clone(),
237 ThumbnailEntry {
238 state,
239 lru_stamp: stamp,
240 },
241 ) {
242 if let ThumbnailState::Ready { texture_id } = old.state {
243 self.pending_destroys.push(texture_id);
244 }
245 }
246 self.lru.push_back((key, stamp));
247 self.evict_to_fit();
248 }
249
250 pub fn fulfill_request(&mut self, req: &ThumbnailRequest, result: Result<TextureId, String>) {
252 self.fulfill(&req.path, result, req.max_size);
253 }
254
255 pub fn maintain(&mut self, backend: &mut ThumbnailBackend<'_>) {
268 let requests = self.take_requests();
269 for req in &requests {
270 let decoded = backend.provider.decode(req);
271 let uploaded = match decoded {
272 Ok(img) => backend.renderer.upload_rgba8(&img),
273 Err(e) => Err(e),
274 };
275 self.fulfill_request(req, uploaded);
276 }
277
278 let destroys = self.take_pending_destroys();
279 for tex in destroys {
280 backend.renderer.destroy(tex);
281 }
282 }
283
284 pub fn take_pending_destroys(&mut self) -> Vec<TextureId> {
286 std::mem::take(&mut self.pending_destroys)
287 }
288
289 pub fn stats(&self) -> ThumbnailStats {
291 let mut stats = ThumbnailStats {
292 total: self.entries.len(),
293 pending_requests: self.requests.len(),
294 issued_this_frame: self.issued_this_frame,
295 max_new_requests_per_frame: self.config.max_new_requests_per_frame,
296 ..ThumbnailStats::default()
297 };
298
299 for entry in self.entries.values() {
300 match entry.state {
301 ThumbnailState::Queued => stats.queued += 1,
302 ThumbnailState::InFlight => stats.in_flight += 1,
303 ThumbnailState::Ready { .. } => stats.ready += 1,
304 ThumbnailState::Failed => stats.failed += 1,
305 }
306 }
307
308 stats
309 }
310
311 fn alloc_stamp(&mut self) -> u64 {
312 let s = self.next_stamp;
313 self.next_stamp = self.next_stamp.wrapping_add(1);
314 s
315 }
316
317 fn touch_existing(&mut self, key: &PathBuf, old_stamp: u64) {
318 let stamp = self.alloc_stamp();
319 if let Some(e) = self.entries.get_mut(key) {
320 e.lru_stamp = stamp;
321 }
322 self.lru.push_back((key.clone(), stamp));
323
324 if self.lru.len() > self.config.max_entries.saturating_mul(8).max(64) {
327 self.compact_lru(old_stamp);
328 }
329 }
330
331 fn compact_lru(&mut self, _hint_stamp: u64) {
332 let target = self.config.max_entries.saturating_mul(4).max(32);
334 while self.lru.len() > target {
335 let Some((k, s)) = self.lru.pop_front() else {
336 break;
337 };
338 let keep = self.entries.get(&k).is_some_and(|e| e.lru_stamp == s);
339 if keep {
340 self.lru.push_front((k, s));
341 break;
342 }
343 }
344 }
345
346 fn evict_to_fit(&mut self) {
347 while self.entries.len() > self.config.max_entries {
348 let Some((key, stamp)) = self.lru.pop_front() else {
349 break;
350 };
351 let Some(entry) = self.entries.get(&key) else {
352 continue;
353 };
354 if entry.lru_stamp != stamp {
355 continue;
356 }
357 let removed = self.entries.remove(&key);
358 if let Some(removed) = removed {
359 if let ThumbnailState::Ready { texture_id } = removed.state {
360 self.pending_destroys.push(texture_id);
361 }
362 }
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[derive(Default)]
372 struct DummyProvider;
373
374 impl ThumbnailProvider for DummyProvider {
375 fn decode(&mut self, _req: &ThumbnailRequest) -> Result<DecodedRgbaImage, String> {
376 Ok(DecodedRgbaImage {
377 width: 1,
378 height: 1,
379 rgba: vec![255, 0, 0, 255],
380 })
381 }
382 }
383
384 #[derive(Default)]
385 struct DummyRenderer {
386 next: u64,
387 destroyed: Vec<TextureId>,
388 }
389
390 impl ThumbnailRenderer for DummyRenderer {
391 fn upload_rgba8(&mut self, _image: &DecodedRgbaImage) -> Result<TextureId, String> {
392 self.next += 1;
393 Ok(TextureId::new(self.next))
394 }
395
396 fn destroy(&mut self, texture_id: TextureId) {
397 self.destroyed.push(texture_id);
398 }
399 }
400
401 #[test]
402 fn respects_request_budget_per_frame() {
403 let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
404 max_entries: 16,
405 max_new_requests_per_frame: 2,
406 });
407 c.advance_frame();
408 c.request_visible(Path::new("/a.png"), [64, 64]);
409 c.request_visible(Path::new("/b.png"), [64, 64]);
410 c.request_visible(Path::new("/c.png"), [64, 64]);
411 let reqs = c.take_requests();
412 assert_eq!(reqs.len(), 2);
413 }
414
415 #[test]
416 fn evicts_lru_and_collects_pending_destroys() {
417 let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
418 max_entries: 1,
419 max_new_requests_per_frame: 10,
420 });
421 c.advance_frame();
422 c.request_visible(Path::new("/a.png"), [64, 64]);
423 c.take_requests();
424 c.fulfill(Path::new("/a.png"), Ok(TextureId::new(1)), [64, 64]);
425
426 c.advance_frame();
427 c.request_visible(Path::new("/b.png"), [64, 64]);
428 c.take_requests();
429 c.fulfill(Path::new("/b.png"), Ok(TextureId::new(2)), [64, 64]);
430
431 let destroyed = c.take_pending_destroys();
432 assert!(destroyed.contains(&TextureId::new(1)));
433 assert!(c.texture_id(Path::new("/a.png")).is_none());
434 assert_eq!(c.texture_id(Path::new("/b.png")), Some(TextureId::new(2)));
435 }
436
437 #[test]
438 fn maintain_decodes_uploads_and_destroys() {
439 let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
440 max_entries: 1,
441 max_new_requests_per_frame: 10,
442 });
443 let mut provider = DummyProvider::default();
444 let mut renderer = DummyRenderer::default();
445 let mut backend = ThumbnailBackend {
446 provider: &mut provider,
447 renderer: &mut renderer,
448 };
449
450 c.advance_frame();
451 c.request_visible(Path::new("/a.png"), [64, 64]);
452 c.maintain(&mut backend);
453 assert!(c.texture_id(Path::new("/a.png")).is_some());
454
455 c.advance_frame();
456 c.request_visible(Path::new("/b.png"), [64, 64]);
457 c.maintain(&mut backend);
458 assert!(renderer.destroyed.iter().any(|t| t == &TextureId::new(1)));
459 }
460}