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, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
91pub struct ThumbnailFrameIndex(u64);
92
93impl ThumbnailFrameIndex {
94 #[cfg(test)]
96 #[inline]
97 const fn new(value: u64) -> Self {
98 Self(value)
99 }
100
101 #[inline]
102 const fn zero() -> Self {
103 Self(0)
104 }
105
106 #[inline]
107 fn next_wrapping(self) -> Self {
108 Self(self.0.wrapping_add(1))
109 }
110}
111
112#[derive(Clone, Debug)]
122pub struct ThumbnailCache {
123 pub config: ThumbnailCacheConfig,
125
126 frame_index: ThumbnailFrameIndex,
127 issued_this_frame: usize,
128 next_stamp: u64,
129
130 entries: HashMap<PathBuf, ThumbnailEntry>,
131 lru: VecDeque<(PathBuf, u64)>,
132 requests: VecDeque<ThumbnailRequest>,
133 pending_destroys: Vec<TextureId>,
134}
135
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
138pub struct ThumbnailStats {
139 pub total: usize,
141 pub queued: usize,
143 pub in_flight: usize,
145 pub ready: usize,
147 pub failed: usize,
149 pub pending_requests: usize,
151 pub issued_this_frame: usize,
153 pub max_new_requests_per_frame: usize,
155}
156
157impl Default for ThumbnailCache {
158 fn default() -> Self {
159 Self::new(ThumbnailCacheConfig::default())
160 }
161}
162
163impl ThumbnailCache {
164 pub fn new(config: ThumbnailCacheConfig) -> Self {
166 Self {
167 config,
168 frame_index: ThumbnailFrameIndex::zero(),
169 issued_this_frame: 0,
170 next_stamp: 1,
171 entries: HashMap::new(),
172 lru: VecDeque::new(),
173 requests: VecDeque::new(),
174 pending_destroys: Vec::new(),
175 }
176 }
177
178 pub fn advance_frame(&mut self) {
182 self.frame_index = self.frame_index.next_wrapping();
183 self.issued_this_frame = 0;
184 }
185
186 pub fn frame_index(&self) -> ThumbnailFrameIndex {
188 self.frame_index
189 }
190
191 pub fn request_visible(&mut self, path: &Path, max_size: [u32; 2]) {
196 let key = path.to_path_buf();
197
198 if let Some(e) = self.entries.get(&key) {
199 self.touch_existing(&key, e.lru_stamp);
201 return;
202 }
203
204 if self.issued_this_frame >= self.config.max_new_requests_per_frame {
205 return;
206 }
207 self.issued_this_frame += 1;
208
209 let stamp = self.alloc_stamp();
210 self.entries.insert(
211 key.clone(),
212 ThumbnailEntry {
213 state: ThumbnailState::Queued,
214 lru_stamp: stamp,
215 },
216 );
217 self.lru.push_back((key.clone(), stamp));
218 self.requests.push_back(ThumbnailRequest {
219 path: key,
220 max_size,
221 });
222 self.evict_to_fit();
223 }
224
225 pub fn texture_id(&self, path: &Path) -> Option<TextureId> {
227 self.entries.get(path).and_then(|e| match &e.state {
228 ThumbnailState::Ready { texture_id } => Some(*texture_id),
229 _ => None,
230 })
231 }
232
233 pub fn take_requests(&mut self) -> Vec<ThumbnailRequest> {
237 let mut out = Vec::new();
238 while let Some(req) = self.requests.pop_front() {
239 if let Some(entry) = self.entries.get_mut(&req.path) {
240 if let ThumbnailState::Queued = entry.state {
241 entry.state = ThumbnailState::InFlight;
242 }
243 }
244 out.push(req);
245 }
246 out
247 }
248
249 pub fn fulfill(&mut self, path: &Path, result: Result<TextureId, String>, _max_size: [u32; 2]) {
253 let key = path.to_path_buf();
254 let stamp = self.alloc_stamp();
255 let state = match result {
256 Ok(texture_id) => ThumbnailState::Ready { texture_id },
257 Err(_message) => ThumbnailState::Failed,
258 };
259
260 if let Some(old) = self.entries.insert(
261 key.clone(),
262 ThumbnailEntry {
263 state,
264 lru_stamp: stamp,
265 },
266 ) {
267 if let ThumbnailState::Ready { texture_id } = old.state {
268 self.pending_destroys.push(texture_id);
269 }
270 }
271 self.lru.push_back((key, stamp));
272 self.evict_to_fit();
273 }
274
275 pub fn fulfill_request(&mut self, req: &ThumbnailRequest, result: Result<TextureId, String>) {
277 self.fulfill(&req.path, result, req.max_size);
278 }
279
280 pub fn maintain(&mut self, backend: &mut ThumbnailBackend<'_>) {
293 let requests = self.take_requests();
294 for req in &requests {
295 let decoded = backend.provider.decode(req);
296 let uploaded = match decoded {
297 Ok(img) => backend.renderer.upload_rgba8(&img),
298 Err(e) => Err(e),
299 };
300 self.fulfill_request(req, uploaded);
301 }
302
303 let destroys = self.take_pending_destroys();
304 for tex in destroys {
305 backend.renderer.destroy(tex);
306 }
307 }
308
309 pub fn take_pending_destroys(&mut self) -> Vec<TextureId> {
311 std::mem::take(&mut self.pending_destroys)
312 }
313
314 pub fn stats(&self) -> ThumbnailStats {
316 let mut stats = ThumbnailStats {
317 total: self.entries.len(),
318 pending_requests: self.requests.len(),
319 issued_this_frame: self.issued_this_frame,
320 max_new_requests_per_frame: self.config.max_new_requests_per_frame,
321 ..ThumbnailStats::default()
322 };
323
324 for entry in self.entries.values() {
325 match entry.state {
326 ThumbnailState::Queued => stats.queued += 1,
327 ThumbnailState::InFlight => stats.in_flight += 1,
328 ThumbnailState::Ready { .. } => stats.ready += 1,
329 ThumbnailState::Failed => stats.failed += 1,
330 }
331 }
332
333 stats
334 }
335
336 fn alloc_stamp(&mut self) -> u64 {
337 let s = self.next_stamp;
338 self.next_stamp = self.next_stamp.wrapping_add(1);
339 s
340 }
341
342 fn touch_existing(&mut self, key: &PathBuf, old_stamp: u64) {
343 let stamp = self.alloc_stamp();
344 if let Some(e) = self.entries.get_mut(key) {
345 e.lru_stamp = stamp;
346 }
347 self.lru.push_back((key.clone(), stamp));
348
349 if self.lru.len() > self.config.max_entries.saturating_mul(8).max(64) {
352 self.compact_lru(old_stamp);
353 }
354 }
355
356 fn compact_lru(&mut self, _hint_stamp: u64) {
357 let target = self.config.max_entries.saturating_mul(4).max(32);
359 while self.lru.len() > target {
360 let Some((k, s)) = self.lru.pop_front() else {
361 break;
362 };
363 let keep = self.entries.get(&k).is_some_and(|e| e.lru_stamp == s);
364 if keep {
365 self.lru.push_front((k, s));
366 break;
367 }
368 }
369 }
370
371 fn evict_to_fit(&mut self) {
372 while self.entries.len() > self.config.max_entries {
373 let Some((key, stamp)) = self.lru.pop_front() else {
374 break;
375 };
376 let Some(entry) = self.entries.get(&key) else {
377 continue;
378 };
379 if entry.lru_stamp != stamp {
380 continue;
381 }
382 let removed = self.entries.remove(&key);
383 if let Some(removed) = removed {
384 if let ThumbnailState::Ready { texture_id } = removed.state {
385 self.pending_destroys.push(texture_id);
386 }
387 }
388 }
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[derive(Default)]
397 struct DummyProvider;
398
399 impl ThumbnailProvider for DummyProvider {
400 fn decode(&mut self, _req: &ThumbnailRequest) -> Result<DecodedRgbaImage, String> {
401 Ok(DecodedRgbaImage {
402 width: 1,
403 height: 1,
404 rgba: vec![255, 0, 0, 255],
405 })
406 }
407 }
408
409 #[derive(Default)]
410 struct DummyRenderer {
411 next: u64,
412 destroyed: Vec<TextureId>,
413 }
414
415 impl ThumbnailRenderer for DummyRenderer {
416 fn upload_rgba8(&mut self, _image: &DecodedRgbaImage) -> Result<TextureId, String> {
417 self.next += 1;
418 Ok(TextureId::new(self.next))
419 }
420
421 fn destroy(&mut self, texture_id: TextureId) {
422 self.destroyed.push(texture_id);
423 }
424 }
425
426 #[test]
427 fn respects_request_budget_per_frame() {
428 let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
429 max_entries: 16,
430 max_new_requests_per_frame: 2,
431 });
432 c.advance_frame();
433 c.request_visible(Path::new("/a.png"), [64, 64]);
434 c.request_visible(Path::new("/b.png"), [64, 64]);
435 c.request_visible(Path::new("/c.png"), [64, 64]);
436 let reqs = c.take_requests();
437 assert_eq!(reqs.len(), 2);
438 }
439
440 #[test]
441 fn thumbnail_frame_index_is_typed_and_advances() {
442 let mut c = ThumbnailCache::new(ThumbnailCacheConfig::default());
443
444 assert_eq!(c.frame_index(), ThumbnailFrameIndex::new(0));
445
446 c.advance_frame();
447
448 assert_eq!(c.frame_index(), ThumbnailFrameIndex::new(1));
449 }
450
451 #[test]
452 fn evicts_lru_and_collects_pending_destroys() {
453 let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
454 max_entries: 1,
455 max_new_requests_per_frame: 10,
456 });
457 c.advance_frame();
458 c.request_visible(Path::new("/a.png"), [64, 64]);
459 c.take_requests();
460 c.fulfill(Path::new("/a.png"), Ok(TextureId::new(1)), [64, 64]);
461
462 c.advance_frame();
463 c.request_visible(Path::new("/b.png"), [64, 64]);
464 c.take_requests();
465 c.fulfill(Path::new("/b.png"), Ok(TextureId::new(2)), [64, 64]);
466
467 let destroyed = c.take_pending_destroys();
468 assert!(destroyed.contains(&TextureId::new(1)));
469 assert!(c.texture_id(Path::new("/a.png")).is_none());
470 assert_eq!(c.texture_id(Path::new("/b.png")), Some(TextureId::new(2)));
471 }
472
473 #[test]
474 fn maintain_decodes_uploads_and_destroys() {
475 let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
476 max_entries: 1,
477 max_new_requests_per_frame: 10,
478 });
479 let mut provider = DummyProvider::default();
480 let mut renderer = DummyRenderer::default();
481 let mut backend = ThumbnailBackend {
482 provider: &mut provider,
483 renderer: &mut renderer,
484 };
485
486 c.advance_frame();
487 c.request_visible(Path::new("/a.png"), [64, 64]);
488 c.maintain(&mut backend);
489 assert!(c.texture_id(Path::new("/a.png")).is_some());
490
491 c.advance_frame();
492 c.request_visible(Path::new("/b.png"), [64, 64]);
493 c.maintain(&mut backend);
494 assert!(renderer.destroyed.iter().any(|t| t == &TextureId::new(1)));
495 }
496}