use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use dear_imgui_rs::texture::TextureId;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DecodedRgbaImage {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
pub trait ThumbnailProvider {
fn decode(&mut self, req: &ThumbnailRequest) -> Result<DecodedRgbaImage, String>;
}
pub trait ThumbnailRenderer {
fn upload_rgba8(&mut self, image: &DecodedRgbaImage) -> Result<TextureId, String>;
fn destroy(&mut self, texture_id: TextureId);
}
pub struct ThumbnailBackend<'a> {
pub provider: &'a mut dyn ThumbnailProvider,
pub renderer: &'a mut dyn ThumbnailRenderer,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ThumbnailCacheConfig {
pub max_entries: usize,
pub max_new_requests_per_frame: usize,
}
impl Default for ThumbnailCacheConfig {
fn default() -> Self {
Self {
max_entries: 256,
max_new_requests_per_frame: 24,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ThumbnailRequest {
pub path: PathBuf,
pub max_size: [u32; 2],
}
#[derive(Clone, Debug)]
enum ThumbnailState {
Queued,
InFlight,
Ready { texture_id: TextureId },
Failed,
}
#[derive(Clone, Debug)]
struct ThumbnailEntry {
state: ThumbnailState,
lru_stamp: u64,
}
#[derive(Clone, Debug)]
pub struct ThumbnailCache {
pub config: ThumbnailCacheConfig,
frame_index: u64,
issued_this_frame: usize,
next_stamp: u64,
entries: HashMap<PathBuf, ThumbnailEntry>,
lru: VecDeque<(PathBuf, u64)>,
requests: VecDeque<ThumbnailRequest>,
pending_destroys: Vec<TextureId>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ThumbnailStats {
pub total: usize,
pub queued: usize,
pub in_flight: usize,
pub ready: usize,
pub failed: usize,
pub pending_requests: usize,
pub issued_this_frame: usize,
pub max_new_requests_per_frame: usize,
}
impl Default for ThumbnailCache {
fn default() -> Self {
Self::new(ThumbnailCacheConfig::default())
}
}
impl ThumbnailCache {
pub fn new(config: ThumbnailCacheConfig) -> Self {
Self {
config,
frame_index: 0,
issued_this_frame: 0,
next_stamp: 1,
entries: HashMap::new(),
lru: VecDeque::new(),
requests: VecDeque::new(),
pending_destroys: Vec::new(),
}
}
pub fn advance_frame(&mut self) {
self.frame_index = self.frame_index.wrapping_add(1);
self.issued_this_frame = 0;
}
pub fn frame_index(&self) -> u64 {
self.frame_index
}
pub fn request_visible(&mut self, path: &Path, max_size: [u32; 2]) {
let key = path.to_path_buf();
if let Some(e) = self.entries.get(&key) {
self.touch_existing(&key, e.lru_stamp);
return;
}
if self.issued_this_frame >= self.config.max_new_requests_per_frame {
return;
}
self.issued_this_frame += 1;
let stamp = self.alloc_stamp();
self.entries.insert(
key.clone(),
ThumbnailEntry {
state: ThumbnailState::Queued,
lru_stamp: stamp,
},
);
self.lru.push_back((key.clone(), stamp));
self.requests.push_back(ThumbnailRequest {
path: key,
max_size,
});
self.evict_to_fit();
}
pub fn texture_id(&self, path: &Path) -> Option<TextureId> {
self.entries.get(path).and_then(|e| match &e.state {
ThumbnailState::Ready { texture_id } => Some(*texture_id),
_ => None,
})
}
pub fn take_requests(&mut self) -> Vec<ThumbnailRequest> {
let mut out = Vec::new();
while let Some(req) = self.requests.pop_front() {
if let Some(entry) = self.entries.get_mut(&req.path) {
if let ThumbnailState::Queued = entry.state {
entry.state = ThumbnailState::InFlight;
}
}
out.push(req);
}
out
}
pub fn fulfill(&mut self, path: &Path, result: Result<TextureId, String>, _max_size: [u32; 2]) {
let key = path.to_path_buf();
let stamp = self.alloc_stamp();
let state = match result {
Ok(texture_id) => ThumbnailState::Ready { texture_id },
Err(_message) => ThumbnailState::Failed,
};
if let Some(old) = self.entries.insert(
key.clone(),
ThumbnailEntry {
state,
lru_stamp: stamp,
},
) {
if let ThumbnailState::Ready { texture_id } = old.state {
self.pending_destroys.push(texture_id);
}
}
self.lru.push_back((key, stamp));
self.evict_to_fit();
}
pub fn fulfill_request(&mut self, req: &ThumbnailRequest, result: Result<TextureId, String>) {
self.fulfill(&req.path, result, req.max_size);
}
pub fn maintain(&mut self, backend: &mut ThumbnailBackend<'_>) {
let requests = self.take_requests();
for req in &requests {
let decoded = backend.provider.decode(req);
let uploaded = match decoded {
Ok(img) => backend.renderer.upload_rgba8(&img),
Err(e) => Err(e),
};
self.fulfill_request(req, uploaded);
}
let destroys = self.take_pending_destroys();
for tex in destroys {
backend.renderer.destroy(tex);
}
}
pub fn take_pending_destroys(&mut self) -> Vec<TextureId> {
std::mem::take(&mut self.pending_destroys)
}
pub fn stats(&self) -> ThumbnailStats {
let mut stats = ThumbnailStats {
total: self.entries.len(),
pending_requests: self.requests.len(),
issued_this_frame: self.issued_this_frame,
max_new_requests_per_frame: self.config.max_new_requests_per_frame,
..ThumbnailStats::default()
};
for entry in self.entries.values() {
match entry.state {
ThumbnailState::Queued => stats.queued += 1,
ThumbnailState::InFlight => stats.in_flight += 1,
ThumbnailState::Ready { .. } => stats.ready += 1,
ThumbnailState::Failed => stats.failed += 1,
}
}
stats
}
fn alloc_stamp(&mut self) -> u64 {
let s = self.next_stamp;
self.next_stamp = self.next_stamp.wrapping_add(1);
s
}
fn touch_existing(&mut self, key: &PathBuf, old_stamp: u64) {
let stamp = self.alloc_stamp();
if let Some(e) = self.entries.get_mut(key) {
e.lru_stamp = stamp;
}
self.lru.push_back((key.clone(), stamp));
if self.lru.len() > self.config.max_entries.saturating_mul(8).max(64) {
self.compact_lru(old_stamp);
}
}
fn compact_lru(&mut self, _hint_stamp: u64) {
let target = self.config.max_entries.saturating_mul(4).max(32);
while self.lru.len() > target {
let Some((k, s)) = self.lru.pop_front() else {
break;
};
let keep = self.entries.get(&k).is_some_and(|e| e.lru_stamp == s);
if keep {
self.lru.push_front((k, s));
break;
}
}
}
fn evict_to_fit(&mut self) {
while self.entries.len() > self.config.max_entries {
let Some((key, stamp)) = self.lru.pop_front() else {
break;
};
let Some(entry) = self.entries.get(&key) else {
continue;
};
if entry.lru_stamp != stamp {
continue;
}
let removed = self.entries.remove(&key);
if let Some(removed) = removed {
if let ThumbnailState::Ready { texture_id } = removed.state {
self.pending_destroys.push(texture_id);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct DummyProvider;
impl ThumbnailProvider for DummyProvider {
fn decode(&mut self, _req: &ThumbnailRequest) -> Result<DecodedRgbaImage, String> {
Ok(DecodedRgbaImage {
width: 1,
height: 1,
rgba: vec![255, 0, 0, 255],
})
}
}
#[derive(Default)]
struct DummyRenderer {
next: u64,
destroyed: Vec<TextureId>,
}
impl ThumbnailRenderer for DummyRenderer {
fn upload_rgba8(&mut self, _image: &DecodedRgbaImage) -> Result<TextureId, String> {
self.next += 1;
Ok(TextureId::new(self.next))
}
fn destroy(&mut self, texture_id: TextureId) {
self.destroyed.push(texture_id);
}
}
#[test]
fn respects_request_budget_per_frame() {
let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
max_entries: 16,
max_new_requests_per_frame: 2,
});
c.advance_frame();
c.request_visible(Path::new("/a.png"), [64, 64]);
c.request_visible(Path::new("/b.png"), [64, 64]);
c.request_visible(Path::new("/c.png"), [64, 64]);
let reqs = c.take_requests();
assert_eq!(reqs.len(), 2);
}
#[test]
fn evicts_lru_and_collects_pending_destroys() {
let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
max_entries: 1,
max_new_requests_per_frame: 10,
});
c.advance_frame();
c.request_visible(Path::new("/a.png"), [64, 64]);
c.take_requests();
c.fulfill(Path::new("/a.png"), Ok(TextureId::new(1)), [64, 64]);
c.advance_frame();
c.request_visible(Path::new("/b.png"), [64, 64]);
c.take_requests();
c.fulfill(Path::new("/b.png"), Ok(TextureId::new(2)), [64, 64]);
let destroyed = c.take_pending_destroys();
assert!(destroyed.contains(&TextureId::new(1)));
assert!(c.texture_id(Path::new("/a.png")).is_none());
assert_eq!(c.texture_id(Path::new("/b.png")), Some(TextureId::new(2)));
}
#[test]
fn maintain_decodes_uploads_and_destroys() {
let mut c = ThumbnailCache::new(ThumbnailCacheConfig {
max_entries: 1,
max_new_requests_per_frame: 10,
});
let mut provider = DummyProvider::default();
let mut renderer = DummyRenderer::default();
let mut backend = ThumbnailBackend {
provider: &mut provider,
renderer: &mut renderer,
};
c.advance_frame();
c.request_visible(Path::new("/a.png"), [64, 64]);
c.maintain(&mut backend);
assert!(c.texture_id(Path::new("/a.png")).is_some());
c.advance_frame();
c.request_visible(Path::new("/b.png"), [64, 64]);
c.maintain(&mut backend);
assert!(renderer.destroyed.iter().any(|t| t == &TextureId::new(1)));
}
}