Skip to main content

vello_common/
image_cache.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Image resource caching with multi-atlas allocation.
5//!
6//! This module provides an [`ImageCache`] that manages image resources across multiple texture
7//! atlases, supporting allocation, deallocation, and slot reuse.
8
9use crate::multi_atlas::{
10    AllocId, AllocationStrategy, AtlasConfig, AtlasError, AtlasId, MultiAtlasManager,
11};
12use crate::paint::ImageId;
13use alloc::vec::Vec;
14
15/// Represents an image resource for rendering.
16#[derive(Debug)]
17pub struct ImageResource {
18    /// The width of the image.
19    pub width: u16,
20    /// The height of the image.
21    pub height: u16,
22    /// The Id of the atlas containing this image.
23    pub atlas_id: AtlasId,
24    /// The offset of the image within its atlas (does not include padding, i.e. it points to the
25    /// position of the first actual top-left pixel).
26    pub offset: [u16; 2],
27    /// The number of transparent padding pixels around the image in the atlas.
28    pub padding: u16,
29    /// The atlas allocation ID for deallocation.
30    atlas_alloc_id: AllocId,
31}
32
33impl ImageResource {
34    /// Returns the offset as `[u16; 2]`.
35    pub fn offsets(&self) -> [u16; 2] {
36        self.offset
37    }
38
39    /// Returns the size as `[u16; 2]`.
40    pub fn size(&self) -> [u16; 2] {
41        [self.width, self.height]
42    }
43}
44
45/// Manages image resources for the renderer.
46pub struct ImageCache {
47    /// Multi-atlas manager for handling multiple texture atlases.
48    atlas_manager: MultiAtlasManager,
49    /// Vector of optional image resources (None = free slot).
50    slots: Vec<Option<ImageResource>>,
51    /// Stack of free indices.
52    free_idxs: Vec<usize>,
53}
54
55impl core::fmt::Debug for ImageCache {
56    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57        let atlas_stats = self.atlas_manager.atlas_stats();
58
59        f.debug_struct("ImageCache")
60            .field("slots", &self.slots)
61            .field("free_idxs", &self.free_idxs)
62            .field("atlas_count", &self.atlas_manager.atlas_count())
63            .field("atlas_stats", &atlas_stats)
64            .finish()
65    }
66}
67
68impl ImageCache {
69    /// Create a new image cache with custom atlas configuration.
70    pub fn new_with_config(config: AtlasConfig) -> Self {
71        Self {
72            atlas_manager: MultiAtlasManager::new(config),
73            slots: Vec::new(),
74            free_idxs: Vec::new(),
75        }
76    }
77
78    /// Create a new dummy image atlas that is supposed to act as a stub.
79    pub fn new_dummy() -> Self {
80        Self::new_with_config(AtlasConfig {
81            initial_atlas_count: 1,
82            max_atlases: 1,
83            atlas_size: (1, 1),
84            auto_grow: false,
85            allocation_strategy: AllocationStrategy::FirstFit,
86        })
87    }
88
89    /// Get an image resource by its Id.
90    pub fn get(&self, id: ImageId) -> Option<&ImageResource> {
91        self.slots.get(id.as_u32() as usize)?.as_ref()
92    }
93
94    /// Allocate an image in the cache, with optional transparent padding.
95    #[expect(
96        clippy::cast_possible_truncation,
97        reason = "u16 is enough for the offset and width/height"
98    )]
99    pub fn allocate(
100        &mut self,
101        width: u16,
102        height: u16,
103        padding: u16,
104    ) -> Result<ImageId, AtlasError> {
105        let doubled_padding = u32::from(padding) * 2;
106        let padded_width = u32::from(width) + doubled_padding;
107        let padded_height = u32::from(height) + doubled_padding;
108        let (max_width, max_height) = self.atlas_manager.config().atlas_size;
109        let (Ok(padded_width), Ok(padded_height)) =
110            (u16::try_from(padded_width), u16::try_from(padded_height))
111        else {
112            return Err(AtlasError::TextureTooLarge {
113                width: padded_width,
114                height: padded_height,
115                max_width,
116                max_height,
117            });
118        };
119        let atlas_alloc = self
120            .atlas_manager
121            .try_allocate(padded_width, padded_height)?;
122
123        let slot_idx = self.free_idxs.pop().unwrap_or_else(|| {
124            // No free slots, append to vector
125            let index = self.slots.len();
126            // Placeholder, will be replaced
127            self.slots.push(None);
128            index
129        });
130
131        let image_id = ImageId::new(slot_idx as u32);
132        let image_resource = ImageResource {
133            width,
134            height,
135            atlas_id: atlas_alloc.atlas_id,
136            offset: [
137                atlas_alloc.allocation.x + padding,
138                atlas_alloc.allocation.y + padding,
139            ],
140            padding,
141            atlas_alloc_id: atlas_alloc.allocation.id,
142        };
143        self.slots[slot_idx] = Some(image_resource);
144
145        Ok(image_id)
146    }
147
148    /// Deallocate an image from the cache, returning the image resource if it existed.
149    pub fn deallocate(&mut self, id: ImageId) -> Option<ImageResource> {
150        let index = id.as_u32() as usize;
151        if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) {
152            // Deallocate from the appropriate atlas
153            let padded_width = image_resource.width + image_resource.padding * 2;
154            let padded_height = image_resource.height + image_resource.padding * 2;
155            self.atlas_manager
156                .deallocate(
157                    image_resource.atlas_id,
158                    image_resource.atlas_alloc_id,
159                    padded_width,
160                    padded_height,
161                )
162                .unwrap();
163            self.free_idxs.push(index);
164            Some(image_resource)
165        } else {
166            None
167        }
168    }
169
170    /// Get access to the atlas manager.
171    pub fn atlas_manager(&self) -> &MultiAtlasManager {
172        &self.atlas_manager
173    }
174
175    /// Get the number of atlases.
176    pub fn atlas_count(&self) -> usize {
177        self.atlas_manager.atlas_count()
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    const ATLAS_SIZE: u16 = 1024;
186
187    #[test]
188    fn test_insert_single_image() {
189        let mut cache = ImageCache::new_with_config(AtlasConfig {
190            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
191            ..Default::default()
192        });
193
194        let id = cache.allocate(100, 100, 0).unwrap();
195
196        assert_eq!(id.as_u32(), 0);
197        let resource = cache.get(id).unwrap();
198        assert_eq!(resource.width, 100);
199        assert_eq!(resource.height, 100);
200        // First image should be at origin
201        assert_eq!(resource.offset, [0, 0]);
202    }
203
204    #[test]
205    fn test_insert_single_image_with_padding() {
206        let mut cache = ImageCache::new_with_config(AtlasConfig {
207            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
208            ..Default::default()
209        });
210
211        let id = cache.allocate(100, 100, 4).unwrap();
212
213        assert_eq!(id.as_u32(), 0);
214        let resource = cache.get(id).unwrap();
215        assert_eq!(resource.width, 100);
216        assert_eq!(resource.height, 100);
217        assert_eq!(resource.padding, 4);
218        // Offset should be shifted inward by padding.
219        assert_eq!(resource.offset, [4, 4]);
220    }
221
222    #[test]
223    fn test_rejects_padded_dimensions_outside_u16_atlas_domain() {
224        let mut cache = ImageCache::new_with_config(AtlasConfig {
225            atlas_size: (u16::MAX, u16::MAX),
226            ..Default::default()
227        });
228
229        assert!(matches!(
230            cache.allocate(u16::MAX, 100, 1),
231            Err(AtlasError::TextureTooLarge {
232                width: 65_537,
233                height: 102,
234                max_width: u16::MAX,
235                max_height: u16::MAX,
236            })
237        ));
238    }
239
240    #[test]
241    fn test_insert_multiple_images() {
242        let mut cache = ImageCache::new_with_config(AtlasConfig {
243            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
244            ..Default::default()
245        });
246
247        let id1 = cache.allocate(50, 50, 0).unwrap();
248        let id2 = cache.allocate(75, 75, 0).unwrap();
249
250        assert_eq!(id1.as_u32(), 0);
251        assert_eq!(id2.as_u32(), 1);
252
253        let resource1 = cache.get(id1).unwrap();
254        let resource2 = cache.get(id2).unwrap();
255
256        assert_eq!(resource1.width, 50);
257        assert_eq!(resource2.width, 75);
258
259        // Second image should be placed adjacent to first
260        assert_ne!(resource1.offset, resource2.offset);
261    }
262
263    #[test]
264    fn test_get_nonexistent_image() {
265        let cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
266            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
267            ..Default::default()
268        });
269
270        assert!(cache.get(ImageId::new(0)).is_none());
271        assert!(cache.get(ImageId::new(999)).is_none());
272    }
273
274    #[test]
275    fn test_remove_image() {
276        let mut cache = ImageCache::new_with_config(AtlasConfig {
277            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
278            ..Default::default()
279        });
280
281        let id = cache.allocate(100, 100, 0).unwrap();
282        assert!(cache.get(id).is_some());
283
284        cache.deallocate(id);
285        assert!(cache.get(id).is_none());
286    }
287
288    #[test]
289    fn test_remove_nonexistent_image() {
290        let mut cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
291            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
292            ..Default::default()
293        });
294
295        // Should not panic when unregistering non-existent image
296        cache.deallocate(ImageId::new(0));
297        cache.deallocate(ImageId::new(999));
298    }
299
300    #[test]
301    fn test_slot_reuse_after_remove() {
302        let mut cache = ImageCache::new_with_config(AtlasConfig {
303            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
304            ..Default::default()
305        });
306
307        // Register three images
308        let id1 = cache.allocate(50, 50, 0).unwrap();
309        let id2 = cache.allocate(60, 60, 0).unwrap();
310        let id3 = cache.allocate(70, 70, 0).unwrap();
311
312        assert_eq!(id1.as_u32(), 0);
313        assert_eq!(id2.as_u32(), 1);
314        assert_eq!(id3.as_u32(), 2);
315
316        // Unregister the middle one
317        cache.deallocate(id2);
318        assert!(cache.get(id2).is_none());
319
320        // Register a new image - should reuse slot 1
321        let id4 = cache.allocate(80, 80, 0).unwrap();
322        // Reused slot 1
323        assert_eq!(id4.as_u32(), 1);
324
325        // Verify other images are still there
326        assert!(cache.get(id1).is_some());
327        assert!(cache.get(id3).is_some());
328        assert!(cache.get(id4).is_some());
329        assert_eq!(cache.get(id4).unwrap().width, 80);
330    }
331
332    #[test]
333    fn test_multiple_remove_and_reuse() {
334        let mut cache = ImageCache::new_with_config(AtlasConfig {
335            atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
336            ..Default::default()
337        });
338
339        // Register several images
340        let ids: Vec<_> = (0..5)
341            .map(|i| cache.allocate(100 + i * 10, 100 + i * 10, 0).unwrap())
342            .collect();
343
344        // Unregister some in the middle
345        cache.deallocate(ids[1]);
346        cache.deallocate(ids[3]);
347
348        // Register new images - should reuse the freed slots
349        let new_id1 = cache.allocate(200, 200, 0).unwrap();
350        let new_id2 = cache.allocate(300, 300, 0).unwrap();
351
352        // Should have reused slots 3 and 1 (in reverse order due to stack behavior)
353        assert_eq!(new_id1.as_u32(), 3);
354        assert_eq!(new_id2.as_u32(), 1);
355        assert_ne!(new_id1.as_u32(), new_id2.as_u32());
356    }
357}