Skip to main content

jag_draw/
image_cache.rs

1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5/// Return embedded bytes for built-in raster images that ship with the
6/// Jag binary. Currently unused for application assets; all images are
7/// loaded from the filesystem.
8fn 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
31/// Simple raster image cache for PNG/JPEG/GIF/WebP with LRU eviction.
32pub struct ImageCache {
33    device: Arc<wgpu::Device>,
34    // LRU state
35    map: HashMap<CacheKey, CacheEntry>,
36    lru: VecDeque<CacheKey>,
37    current_tick: u64,
38    // guardrails
39    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        // Conservative default budget: 256 MiB for cached images
47        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        // update LRU order: move key to back
74        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    /// Check if an image is in the cache and return it if ready.
107    /// Returns None if loading or failed, Some if ready.
108    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        // Clone the data we need before touching
114        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    /// Start loading an image if not already in cache.
133    /// Marks it as Loading immediately, actual load happens synchronously.
134    pub fn start_load(&mut self, path: &Path) {
135        let key = CacheKey {
136            path: path.to_path_buf(),
137        };
138
139        // If already in cache (any state), don't restart
140        if self.map.contains_key(&key) {
141            return;
142        }
143
144        // Mark as loading
145        self.map.insert(key, CacheEntry::Loading);
146    }
147
148    /// Load an image from disk and cache it as a GPU texture.
149    /// Returns a reference to the texture and its dimensions on success.
150    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        // Check cache first - clone data before touching
160        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                    // Still loading, proceed to load now
167                    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        // Load image from embedded bytes (for built-in assets) or from disk.
181        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        // Clamp to max texture size
197        if width > self.max_tex_size || height > self.max_tex_size {
198            return None;
199        }
200
201        // Create GPU texture
202        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        // Upload image data
218        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    /// Check if an image is currently loading
253    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    /// Check if an image is ready
261    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    /// Store a pre-loaded texture in the cache (used for async loading)
269    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}