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) -> [u16; 2] {
36 self.offset
37 }
38
39 pub fn size(&self) -> [u16; 2] {
41 [self.width, self.height]
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 #[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 let index = self.slots.len();
126 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 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 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 pub fn atlas_manager(&self) -> &MultiAtlasManager {
172 &self.atlas_manager
173 }
174
175 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 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 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 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 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 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 cache.deallocate(id2);
318 assert!(cache.get(id2).is_none());
319
320 let id4 = cache.allocate(80, 80, 0).unwrap();
322 assert_eq!(id4.as_u32(), 1);
324
325 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 let ids: Vec<_> = (0..5)
341 .map(|i| cache.allocate(100 + i * 10, 100 + i * 10, 0).unwrap())
342 .collect();
343
344 cache.deallocate(ids[1]);
346 cache.deallocate(ids[3]);
347
348 let new_id1 = cache.allocate(200, 200, 0).unwrap();
350 let new_id2 = cache.allocate(300, 300, 0).unwrap();
351
352 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}