1use alloc::vec::Vec;
14pub use guillotiere::AllocId;
15use guillotiere::AtlasAllocator;
16use thiserror::Error;
17
18#[derive(Debug)]
20pub struct Allocation {
21 pub id: AllocId,
23 pub x: u32,
25 pub y: u32,
27}
28
29pub struct Atlas {
35 pub id: AtlasId,
37 allocator: AtlasAllocator,
39 stats: AtlasUsageStats,
41 allocation_counter: u32,
43}
44
45impl Atlas {
46 pub fn new(id: AtlasId, width: u32, height: u32) -> Self {
48 Self {
49 id,
50 allocator: AtlasAllocator::new(guillotiere::size2(width as i32, height as i32)),
51 stats: AtlasUsageStats {
52 allocated_area: 0,
53 total_area: width * height,
54 allocated_count: 0,
55 },
56 allocation_counter: 0,
57 }
58 }
59
60 #[expect(
62 clippy::cast_sign_loss,
63 reason = "coordinates are always non-negative for valid allocations"
64 )]
65 pub fn allocate(&mut self, width: u32, height: u32) -> Option<Allocation> {
66 let alloc = self
67 .allocator
68 .allocate(guillotiere::size2(width as i32, height as i32))?;
69 self.stats.allocated_area += width * height;
70 self.stats.allocated_count += 1;
71 self.allocation_counter += 1;
72 Some(Allocation {
73 id: alloc.id,
74 x: alloc.rectangle.min.x as u32,
75 y: alloc.rectangle.min.y as u32,
76 })
77 }
78
79 pub fn deallocate(&mut self, alloc_id: AllocId, width: u32, height: u32) {
81 self.allocator.deallocate(alloc_id);
82 self.stats.allocated_area = self.stats.allocated_area.saturating_sub(width * height);
83 self.stats.allocated_count = self.stats.allocated_count.saturating_sub(1);
84 }
85
86 pub fn stats(&self) -> &AtlasUsageStats {
88 &self.stats
89 }
90}
91
92impl core::fmt::Debug for Atlas {
93 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94 f.debug_struct("Atlas")
95 .field("id", &self.id)
96 .field("stats", &self.stats)
97 .field("allocation_counter", &self.allocation_counter)
98 .finish_non_exhaustive()
99 }
100}
101
102pub struct MultiAtlasManager {
108 atlases: Vec<Atlas>,
110 config: AtlasConfig,
112 round_robin_counter: usize,
114}
115
116impl MultiAtlasManager {
117 pub fn new(config: AtlasConfig) -> Self {
119 let mut manager = Self {
120 atlases: Vec::new(),
121 config,
122 round_robin_counter: 0,
123 };
124
125 for _ in 0..config.initial_atlas_count {
126 manager
127 .create_atlas()
128 .expect("Failed to create initial atlas");
129 }
130
131 manager
132 }
133
134 pub fn config(&self) -> &AtlasConfig {
136 &self.config
137 }
138
139 pub fn create_atlas(&mut self) -> Result<AtlasId, AtlasError> {
141 if self.atlases.len() >= self.config.max_atlases {
142 return Err(AtlasError::AtlasLimitReached);
143 }
144
145 let atlas_id = AtlasId::new(self.next_atlas_id());
146
147 let atlas = Atlas::new(atlas_id, self.config.atlas_size.0, self.config.atlas_size.1);
148 self.atlases.push(atlas);
149
150 Ok(atlas_id)
151 }
152
153 pub fn next_atlas_id(&self) -> u32 {
155 u32::try_from(self.atlases.len()).unwrap()
156 }
157
158 pub fn try_allocate(&mut self, width: u32, height: u32) -> Result<AtlasAllocation, AtlasError> {
160 self.try_allocate_excluding(width, height, None)
161 }
162
163 pub fn try_allocate_excluding(
166 &mut self,
167 width: u32,
168 height: u32,
169 exclude_atlas_id: Option<AtlasId>,
170 ) -> Result<AtlasAllocation, AtlasError> {
171 if width > self.config.atlas_size.0 || height > self.config.atlas_size.1 {
173 return Err(AtlasError::TextureTooLarge { width, height });
174 }
175
176 match self.config.allocation_strategy {
178 AllocationStrategy::FirstFit => {
179 self.allocate_first_fit(width, height, exclude_atlas_id)
180 }
181 AllocationStrategy::BestFit => self.allocate_best_fit(width, height, exclude_atlas_id),
182 AllocationStrategy::LeastUsed => {
183 self.allocate_least_used(width, height, exclude_atlas_id)
184 }
185 AllocationStrategy::RoundRobin => {
186 self.allocate_round_robin(width, height, exclude_atlas_id)
187 }
188 }
189 }
190
191 fn allocate_first_fit(
193 &mut self,
194 width: u32,
195 height: u32,
196 exclude_atlas_id: Option<AtlasId>,
197 ) -> Result<AtlasAllocation, AtlasError> {
198 for atlas in &mut self.atlases {
199 if Some(atlas.id) == exclude_atlas_id {
200 continue;
201 }
202
203 if let Some(allocation) = atlas.allocate(width, height) {
204 return Ok(AtlasAllocation {
205 atlas_id: atlas.id,
206 allocation,
207 });
208 }
209 }
210
211 if self.config.auto_grow {
213 let atlas_id = self.create_atlas()?;
214 let atlas = self.atlases.last_mut().unwrap();
215 if let Some(allocation) = atlas.allocate(width, height) {
216 return Ok(AtlasAllocation {
217 atlas_id,
218 allocation,
219 });
220 }
221 }
222
223 Err(AtlasError::NoSpaceAvailable)
224 }
225
226 fn allocate_best_fit(
229 &mut self,
230 width: u32,
231 height: u32,
232 exclude_atlas_id: Option<AtlasId>,
233 ) -> Result<AtlasAllocation, AtlasError> {
234 let mut best_atlas_idx = None;
235 let mut best_remaining_space = u32::MAX;
236
237 for (idx, atlas) in self.atlases.iter().enumerate() {
239 if Some(atlas.id) == exclude_atlas_id {
240 continue;
241 }
242
243 let stats = atlas.stats();
244 let remaining_space = stats.total_area - stats.allocated_area;
245
246 if remaining_space >= width * height && remaining_space < best_remaining_space {
247 best_remaining_space = remaining_space;
248 best_atlas_idx = Some(idx);
249 }
250 }
251
252 if let Some(idx) = best_atlas_idx {
253 let atlas = &mut self.atlases[idx];
254 if let Some(allocation) = atlas.allocate(width, height) {
255 return Ok(AtlasAllocation {
256 atlas_id: atlas.id,
257 allocation,
258 });
259 }
260 }
261
262 self.allocate_first_fit(width, height, exclude_atlas_id)
264 }
265
266 fn allocate_least_used(
268 &mut self,
269 width: u32,
270 height: u32,
271 exclude_atlas_id: Option<AtlasId>,
272 ) -> Result<AtlasAllocation, AtlasError> {
273 let mut best_atlas_idx = None;
274 let mut lowest_usage = f32::MAX;
275
276 for (idx, atlas) in self.atlases.iter().enumerate() {
278 if Some(atlas.id) == exclude_atlas_id {
279 continue;
280 }
281
282 let usage = atlas.stats().usage_percentage();
283 if usage < lowest_usage {
284 lowest_usage = usage;
285 best_atlas_idx = Some(idx);
286 }
287 }
288
289 if let Some(idx) = best_atlas_idx
290 && let Some(allocation) = self.atlases[idx].allocate(width, height)
291 {
292 let atlas_id = self.atlases[idx].id;
293 return Ok(AtlasAllocation {
294 atlas_id,
295 allocation,
296 });
297 }
298
299 self.allocate_first_fit(width, height, exclude_atlas_id)
301 }
302
303 fn allocate_round_robin(
305 &mut self,
306 width: u32,
307 height: u32,
308 exclude_atlas_id: Option<AtlasId>,
309 ) -> Result<AtlasAllocation, AtlasError> {
310 if self.atlases.is_empty() {
311 return self.allocate_first_fit(width, height, exclude_atlas_id);
312 }
313
314 let start_idx = self.round_robin_counter % self.atlases.len();
315
316 for i in 0..self.atlases.len() {
318 let idx = (start_idx + i) % self.atlases.len();
319
320 if Some(self.atlases[idx].id) == exclude_atlas_id {
321 continue;
322 }
323
324 if let Some(allocation) = self.atlases[idx].allocate(width, height) {
325 let atlas_id = self.atlases[idx].id;
326 self.round_robin_counter = (idx + 1) % self.atlases.len();
327 return Ok(AtlasAllocation {
328 atlas_id,
329 allocation,
330 });
331 }
332 }
333
334 if self.config.auto_grow {
336 let atlas_id = self.create_atlas()?;
337 let atlas = self.atlases.last_mut().unwrap();
338 if let Some(allocation) = atlas.allocate(width, height) {
339 self.round_robin_counter = self.atlases.len() - 1;
340 return Ok(AtlasAllocation {
341 atlas_id,
342 allocation,
343 });
344 }
345 }
346
347 Err(AtlasError::NoSpaceAvailable)
348 }
349
350 pub fn deallocate(
352 &mut self,
353 atlas_id: AtlasId,
354 alloc_id: AllocId,
355 width: u32,
356 height: u32,
357 ) -> Result<(), AtlasError> {
358 let atlas = self
361 .atlases
362 .get_mut(atlas_id.0 as usize)
363 .ok_or(AtlasError::AtlasNotFound(atlas_id))?;
364 atlas.deallocate(alloc_id, width, height);
365 Ok(())
366 }
367
368 pub fn atlas_stats(&self) -> Vec<(AtlasId, &AtlasUsageStats)> {
370 self.atlases
371 .iter()
372 .map(|atlas| (atlas.id, atlas.stats()))
373 .collect()
374 }
375
376 pub fn atlas_count(&self) -> usize {
378 self.atlases.len()
379 }
380}
381
382impl core::fmt::Debug for MultiAtlasManager {
383 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
384 f.debug_struct("MultiAtlasManager")
385 .field("atlas_count", &self.atlases.len())
386 .field("config", &self.config)
387 .field("next_atlas_id", &self.next_atlas_id())
388 .field("round_robin_counter", &self.round_robin_counter)
389 .field("atlases", &self.atlases)
390 .finish()
391 }
392}
393
394#[derive(Debug, Clone, Error)]
396pub enum AtlasError {
397 #[error("No space available in any atlas")]
399 NoSpaceAvailable,
400 #[error("Maximum number of atlases reached")]
402 AtlasLimitReached,
403 #[error("Texture too large ({width}x{height}) for atlas")]
405 TextureTooLarge {
406 width: u32,
408 height: u32,
410 },
411 #[error("Atlas with Id {0:?} not found")]
413 AtlasNotFound(AtlasId),
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
418pub struct AtlasId(pub u32);
419
420impl AtlasId {
421 pub fn new(id: u32) -> Self {
423 Self(id)
424 }
425
426 pub fn as_u32(self) -> u32 {
428 self.0
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct AtlasUsageStats {
435 pub allocated_area: u32,
437 pub total_area: u32,
439 pub allocated_count: u32,
441}
442
443impl AtlasUsageStats {
444 pub fn usage_percentage(&self) -> f32 {
446 if self.total_area == 0 {
447 0.0
448 } else {
449 self.allocated_area as f32 / self.total_area as f32
450 }
451 }
452}
453
454#[derive(Debug)]
456pub struct AtlasAllocation {
457 pub atlas_id: AtlasId,
459 pub allocation: Allocation,
461}
462
463#[derive(Debug, Clone, Copy)]
470pub struct AtlasConfig {
471 pub initial_atlas_count: usize,
473 pub max_atlases: usize,
475 pub atlas_size: (u32, u32),
478 pub auto_grow: bool,
480 pub allocation_strategy: AllocationStrategy,
482}
483
484impl Default for AtlasConfig {
485 fn default() -> Self {
486 Self {
487 initial_atlas_count: 1,
488 max_atlases: 8,
489 atlas_size: (4096, 4096),
490 auto_grow: true,
491 allocation_strategy: AllocationStrategy::FirstFit,
492 }
493 }
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
498pub enum AllocationStrategy {
499 #[default]
501 FirstFit,
502 BestFit,
504 LeastUsed,
506 RoundRobin,
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn test_atlas_creation() {
516 let mut manager = MultiAtlasManager::new(AtlasConfig {
517 initial_atlas_count: 0,
518 ..Default::default()
519 });
520
521 let atlas_id = manager.create_atlas().unwrap();
522 assert_eq!(atlas_id.as_u32(), 0);
523 assert_eq!(manager.atlas_count(), 1);
524 }
525
526 #[test]
527 fn test_allocation_strategies() {
528 let mut manager = MultiAtlasManager::new(AtlasConfig {
529 initial_atlas_count: 1,
530 max_atlases: 3,
531 atlas_size: (256, 256),
532 allocation_strategy: AllocationStrategy::FirstFit,
533 auto_grow: true,
534 });
535
536 let allocation = manager.try_allocate(100, 100).unwrap();
538 assert_eq!(allocation.atlas_id.as_u32(), 0);
539 }
540
541 #[test]
542 fn test_atlas_limit() {
543 let mut manager = MultiAtlasManager::new(AtlasConfig {
544 initial_atlas_count: 1,
545 max_atlases: 1,
546 atlas_size: (256, 256),
547 allocation_strategy: AllocationStrategy::FirstFit,
548 auto_grow: false,
549 });
550
551 assert!(manager.create_atlas().is_err());
552 }
553
554 #[test]
555 fn test_texture_too_large() {
556 let mut manager = MultiAtlasManager::new(AtlasConfig {
557 atlas_size: (256, 256),
558 ..Default::default()
559 });
560
561 let result = manager.try_allocate(300, 300);
562 assert!(matches!(result, Err(AtlasError::TextureTooLarge { .. })));
563 }
564
565 #[test]
566 fn test_first_fit_allocation_strategy() {
567 let mut manager = MultiAtlasManager::new(AtlasConfig {
568 initial_atlas_count: 3,
569 max_atlases: 3,
570 atlas_size: (256, 256),
571 allocation_strategy: AllocationStrategy::FirstFit,
572 auto_grow: false,
573 });
574
575 let allocation0 = manager.try_allocate(100, 100).unwrap();
577 assert_eq!(allocation0.atlas_id.as_u32(), 0);
578
579 let allocation1 = manager.try_allocate(50, 50).unwrap();
581 assert_eq!(allocation1.atlas_id.as_u32(), 0);
582
583 let allocation2 = manager.try_allocate(80, 80).unwrap();
585 assert_eq!(allocation2.atlas_id.as_u32(), 0);
586
587 let allocation3 = manager.try_allocate(200, 200).unwrap();
590 assert_eq!(allocation3.atlas_id.as_u32(), 1);
591
592 let allocation4 = manager.try_allocate(20, 20).unwrap();
594 assert_eq!(allocation4.atlas_id.as_u32(), 0);
595 }
596
597 #[test]
598 fn test_best_fit_allocation_strategy() {
599 let mut manager = MultiAtlasManager::new(AtlasConfig {
600 initial_atlas_count: 3,
601 max_atlases: 3,
602 atlas_size: (256, 256),
603 allocation_strategy: AllocationStrategy::BestFit,
604 auto_grow: false,
605 });
606
607 let allocation0 = manager.try_allocate(150, 150).unwrap();
609 assert_eq!(allocation0.atlas_id.as_u32(), 0);
610
611 let allocation1 = manager.try_allocate(100, 100).unwrap();
614 assert_eq!(allocation1.atlas_id.as_u32(), 0);
615
616 let allocation2 = manager.try_allocate(100, 100).unwrap();
619 assert_eq!(allocation2.atlas_id.as_u32(), 0);
620
621 let allocation3 = manager.try_allocate(200, 200).unwrap();
624 assert_eq!(allocation3.atlas_id.as_u32(), 1);
625
626 let allocation4 = manager.try_allocate(80, 80).unwrap();
629 assert_eq!(allocation4.atlas_id.as_u32(), 0);
630
631 let allocation5 = manager.try_allocate(80, 80).unwrap();
634 assert_eq!(allocation5.atlas_id.as_u32(), 2);
635 }
636
637 #[test]
638 fn test_least_used_allocation_strategy() {
639 let mut manager = MultiAtlasManager::new(AtlasConfig {
640 initial_atlas_count: 3,
641 max_atlases: 3,
642 atlas_size: (256, 256),
643 allocation_strategy: AllocationStrategy::LeastUsed,
644 auto_grow: false,
645 });
646
647 let allocation0 = manager.try_allocate(100, 100).unwrap();
649 assert_eq!(allocation0.atlas_id.as_u32(), 0);
650
651 let allocation1 = manager.try_allocate(50, 50).unwrap();
653 assert_eq!(allocation1.atlas_id.as_u32(), 1);
654
655 let allocation2 = manager.try_allocate(30, 30).unwrap();
657 assert_eq!(allocation2.atlas_id.as_u32(), 2);
658
659 let allocation3 = manager.try_allocate(30, 30).unwrap();
661 assert_eq!(allocation3.atlas_id.as_u32(), 2);
662 }
663
664 #[test]
665 fn test_round_robin_allocation_strategy() {
666 let mut manager = MultiAtlasManager::new(AtlasConfig {
667 initial_atlas_count: 3,
668 max_atlases: 3,
669 atlas_size: (256, 256),
670 allocation_strategy: AllocationStrategy::RoundRobin,
671 auto_grow: false,
672 });
673
674 let allocation0 = manager.try_allocate(50, 50).unwrap();
676 assert_eq!(allocation0.atlas_id.as_u32(), 0);
677
678 let allocation1 = manager.try_allocate(50, 50).unwrap();
679 assert_eq!(allocation1.atlas_id.as_u32(), 1);
680
681 let allocation2 = manager.try_allocate(50, 50).unwrap();
682 assert_eq!(allocation2.atlas_id.as_u32(), 2);
683
684 let allocation3 = manager.try_allocate(50, 50).unwrap();
686 assert_eq!(allocation3.atlas_id.as_u32(), 0);
687
688 let allocation4 = manager.try_allocate(50, 50).unwrap();
690 assert_eq!(allocation4.atlas_id.as_u32(), 1);
691 }
692
693 #[test]
694 fn test_auto_grow() {
695 let mut manager = MultiAtlasManager::new(AtlasConfig {
696 initial_atlas_count: 1,
697 max_atlases: 3,
698 atlas_size: (256, 256),
699 allocation_strategy: AllocationStrategy::FirstFit,
700 auto_grow: true,
701 });
702
703 let allocation0 = manager.try_allocate(256, 256).unwrap();
704 assert_eq!(allocation0.atlas_id.as_u32(), 0);
705
706 let allocation1 = manager.try_allocate(256, 256).unwrap();
707 assert_eq!(allocation1.atlas_id.as_u32(), 1);
708
709 let allocation2 = manager.try_allocate(256, 256).unwrap();
710 assert_eq!(allocation2.atlas_id.as_u32(), 2);
711 }
712
713 fn test_allocate_excluding_with_strategy(strategy: AllocationStrategy) {
714 let mut manager = MultiAtlasManager::new(AtlasConfig {
715 initial_atlas_count: 3,
716 max_atlases: 3,
717 atlas_size: (256, 256),
718 allocation_strategy: strategy,
719 auto_grow: false,
720 });
721
722 let allocation0 = manager.try_allocate(100, 100).unwrap();
723 let first_atlas = allocation0.atlas_id;
724 let allocation1 = manager
725 .try_allocate_excluding(256, 256, Some(first_atlas))
726 .unwrap();
727 assert_ne!(allocation1.atlas_id, first_atlas);
728
729 let second_atlas = allocation1.atlas_id;
730 let allocation2 = manager
731 .try_allocate_excluding(100, 100, Some(second_atlas))
732 .unwrap();
733 assert_ne!(allocation2.atlas_id, second_atlas);
734
735 let allocation3 = manager
736 .try_allocate_excluding(100, 100, Some(first_atlas))
737 .unwrap();
738 assert_ne!(allocation3.atlas_id, first_atlas);
739 }
740
741 #[test]
742 fn test_allocate_excluding_first_fit() {
743 test_allocate_excluding_with_strategy(AllocationStrategy::FirstFit);
744 }
745
746 #[test]
747 fn test_allocate_excluding_best_fit() {
748 test_allocate_excluding_with_strategy(AllocationStrategy::BestFit);
749 }
750
751 #[test]
752 fn test_allocate_excluding_least_used() {
753 test_allocate_excluding_with_strategy(AllocationStrategy::LeastUsed);
754 }
755
756 #[test]
757 fn test_allocate_excluding_round_robin() {
758 test_allocate_excluding_with_strategy(AllocationStrategy::RoundRobin);
759 }
760}