1use crate::multi_atlas::{
10 AllocId, AllocationStrategy, AtlasConfig, AtlasError, AtlasId, MultiAtlasManager,
11};
12use crate::paint::ImageId;
13use alloc::vec::Vec;
14
15#[derive(Debug)]
17pub struct ImageResource {
18 pub width: u16,
20 pub height: u16,
22 pub atlas_id: AtlasId,
24 pub offset: [u16; 2],
27 pub padding: u16,
29 atlas_alloc_id: AllocId,
31}
32
33impl ImageResource {
34 pub fn offsets(&self) -> [u32; 2] {
36 [self.offset[0] as u32, self.offset[1] as u32]
37 }
38
39 pub fn size(&self) -> [u32; 2] {
41 [self.width as u32, self.height as u32]
42 }
43}
44
45pub struct ImageCache {
47 atlas_manager: MultiAtlasManager,
49 slots: Vec<Option<ImageResource>>,
51 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 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 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 pub fn get(&self, id: ImageId) -> Option<&ImageResource> {
91 self.slots.get(id.as_u32() as usize)?.as_ref()
92 }
93
94 pub fn allocate(
96 &mut self,
97 width: u32,
98 height: u32,
99 padding: u16,
100 ) -> Result<ImageId, AtlasError> {
101 self.allocate_excluding(width, height, padding, None)
102 }
103
104 #[expect(
107 clippy::cast_possible_truncation,
108 reason = "u16 is enough for the offset and width/height"
109 )]
110 pub fn allocate_excluding(
111 &mut self,
112 width: u32,
113 height: u32,
114 padding: u16,
115 exclude_atlas_id: Option<AtlasId>,
116 ) -> Result<ImageId, AtlasError> {
117 let padded_width = width + u32::from(padding) * 2;
118 let padded_height = height + u32::from(padding) * 2;
119 let atlas_alloc = self.atlas_manager.try_allocate_excluding(
120 padded_width,
121 padded_height,
122 exclude_atlas_id,
123 )?;
124
125 let slot_idx = self.free_idxs.pop().unwrap_or_else(|| {
126 let index = self.slots.len();
128 self.slots.push(None);
130 index
131 });
132
133 let image_id = ImageId::new(slot_idx as u32);
134 let image_resource = ImageResource {
135 width: width as u16,
136 height: height as u16,
137 atlas_id: atlas_alloc.atlas_id,
138 offset: [
139 atlas_alloc.allocation.x as u16 + padding,
140 atlas_alloc.allocation.y as u16 + padding,
141 ],
142 padding,
143 atlas_alloc_id: atlas_alloc.allocation.id,
144 };
145 self.slots[slot_idx] = Some(image_resource);
146
147 Ok(image_id)
148 }
149
150 pub fn deallocate(&mut self, id: ImageId) -> Option<ImageResource> {
152 let index = id.as_u32() as usize;
153 if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) {
154 let padded_width = image_resource.width as u32 + u32::from(image_resource.padding) * 2;
156 let padded_height =
157 image_resource.height as u32 + u32::from(image_resource.padding) * 2;
158 self.atlas_manager
159 .deallocate(
160 image_resource.atlas_id,
161 image_resource.atlas_alloc_id,
162 padded_width,
163 padded_height,
164 )
165 .unwrap();
166 self.free_idxs.push(index);
167 Some(image_resource)
168 } else {
169 None
170 }
171 }
172
173 pub fn atlas_manager(&self) -> &MultiAtlasManager {
175 &self.atlas_manager
176 }
177
178 pub fn atlas_count(&self) -> usize {
180 self.atlas_manager.atlas_count()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 const ATLAS_SIZE: u32 = 1024;
189
190 #[test]
191 fn test_insert_single_image() {
192 let mut cache = ImageCache::new_with_config(AtlasConfig {
193 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
194 ..Default::default()
195 });
196
197 let id = cache.allocate(100, 100, 0).unwrap();
198
199 assert_eq!(id.as_u32(), 0);
200 let resource = cache.get(id).unwrap();
201 assert_eq!(resource.width, 100);
202 assert_eq!(resource.height, 100);
203 assert_eq!(resource.offset, [0, 0]);
205 }
206
207 #[test]
208 fn test_insert_single_image_with_padding() {
209 let mut cache = ImageCache::new_with_config(AtlasConfig {
210 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
211 ..Default::default()
212 });
213
214 let id = cache.allocate(100, 100, 4).unwrap();
215
216 assert_eq!(id.as_u32(), 0);
217 let resource = cache.get(id).unwrap();
218 assert_eq!(resource.width, 100);
219 assert_eq!(resource.height, 100);
220 assert_eq!(resource.padding, 4);
221 assert_eq!(resource.offset, [4, 4]);
223 }
224
225 #[test]
226 fn test_insert_multiple_images() {
227 let mut cache = ImageCache::new_with_config(AtlasConfig {
228 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
229 ..Default::default()
230 });
231
232 let id1 = cache.allocate(50, 50, 0).unwrap();
233 let id2 = cache.allocate(75, 75, 0).unwrap();
234
235 assert_eq!(id1.as_u32(), 0);
236 assert_eq!(id2.as_u32(), 1);
237
238 let resource1 = cache.get(id1).unwrap();
239 let resource2 = cache.get(id2).unwrap();
240
241 assert_eq!(resource1.width, 50);
242 assert_eq!(resource2.width, 75);
243
244 assert_ne!(resource1.offset, resource2.offset);
246 }
247
248 #[test]
249 fn test_get_nonexistent_image() {
250 let cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
251 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
252 ..Default::default()
253 });
254
255 assert!(cache.get(ImageId::new(0)).is_none());
256 assert!(cache.get(ImageId::new(999)).is_none());
257 }
258
259 #[test]
260 fn test_remove_image() {
261 let mut cache = ImageCache::new_with_config(AtlasConfig {
262 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
263 ..Default::default()
264 });
265
266 let id = cache.allocate(100, 100, 0).unwrap();
267 assert!(cache.get(id).is_some());
268
269 cache.deallocate(id);
270 assert!(cache.get(id).is_none());
271 }
272
273 #[test]
274 fn test_remove_nonexistent_image() {
275 let mut cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
276 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
277 ..Default::default()
278 });
279
280 cache.deallocate(ImageId::new(0));
282 cache.deallocate(ImageId::new(999));
283 }
284
285 #[test]
286 fn test_slot_reuse_after_remove() {
287 let mut cache = ImageCache::new_with_config(AtlasConfig {
288 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
289 ..Default::default()
290 });
291
292 let id1 = cache.allocate(50, 50, 0).unwrap();
294 let id2 = cache.allocate(60, 60, 0).unwrap();
295 let id3 = cache.allocate(70, 70, 0).unwrap();
296
297 assert_eq!(id1.as_u32(), 0);
298 assert_eq!(id2.as_u32(), 1);
299 assert_eq!(id3.as_u32(), 2);
300
301 cache.deallocate(id2);
303 assert!(cache.get(id2).is_none());
304
305 let id4 = cache.allocate(80, 80, 0).unwrap();
307 assert_eq!(id4.as_u32(), 1);
309
310 assert!(cache.get(id1).is_some());
312 assert!(cache.get(id3).is_some());
313 assert!(cache.get(id4).is_some());
314 assert_eq!(cache.get(id4).unwrap().width, 80);
315 }
316
317 #[test]
318 fn test_multiple_remove_and_reuse() {
319 let mut cache = ImageCache::new_with_config(AtlasConfig {
320 atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
321 ..Default::default()
322 });
323
324 let ids: Vec<_> = (0..5)
326 .map(|i| cache.allocate(100 + i * 10, 100 + i * 10, 0).unwrap())
327 .collect();
328
329 cache.deallocate(ids[1]);
331 cache.deallocate(ids[3]);
332
333 let new_id1 = cache.allocate(200, 200, 0).unwrap();
335 let new_id2 = cache.allocate(300, 300, 0).unwrap();
336
337 assert_eq!(new_id1.as_u32(), 3);
339 assert_eq!(new_id2.as_u32(), 1);
340 assert_ne!(new_id1.as_u32(), new_id2.as_u32());
341 }
342}