Skip to main content

vello_common/
multi_atlas.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Multi-atlas management for texture atlases.
5//!
6//! This module provides support for managing multiple texture atlases, allowing for handling of
7//! large numbers of images.
8//!
9//! The allocator backend is [guillotiere](https://github.com/nical/guillotiere)'s tree-based
10//! guillotine algorithm, providing O(1) neighbor lookup during deallocation and automatic
11//! free-rect coalescing.
12
13use alloc::vec::Vec;
14pub use guillotiere::AllocId;
15use guillotiere::AtlasAllocator;
16use thiserror::Error;
17
18/// The result of a successful rectangle allocation within a single atlas.
19#[derive(Debug)]
20pub struct Allocation {
21    /// Opaque handle used for deallocation.
22    pub id: AllocId,
23    /// X coordinate of the top-left corner of the allocated rectangle.
24    pub x: u32,
25    /// Y coordinate of the top-left corner of the allocated rectangle.
26    pub y: u32,
27}
28
29// ---------------------------------------------------------------------------
30// Unified Atlas type
31// ---------------------------------------------------------------------------
32
33/// Represents a single atlas in the multi-atlas system.
34pub struct Atlas {
35    /// Unique identifier for this atlas.
36    pub id: AtlasId,
37    /// Rectangle allocator backend.
38    allocator: AtlasAllocator,
39    /// Current usage statistics.
40    stats: AtlasUsageStats,
41    /// Allocation counter.
42    allocation_counter: u32,
43}
44
45impl Atlas {
46    /// Create a new atlas with the given ID and size.
47    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    /// Try to allocate an image in this atlas.
61    #[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    /// Deallocate an image from this atlas.
80    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    /// Get current usage statistics.
87    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
102// ---------------------------------------------------------------------------
103// MultiAtlasManager
104// ---------------------------------------------------------------------------
105
106/// Manages multiple texture atlases.
107pub struct MultiAtlasManager {
108    /// All atlases managed by this instance.
109    atlases: Vec<Atlas>,
110    /// Configuration for atlas management.
111    config: AtlasConfig,
112    /// Round-robin counter for allocation strategy.
113    round_robin_counter: usize,
114}
115
116impl MultiAtlasManager {
117    /// Create a new multi-atlas manager with the given configuration.
118    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    /// Get the current configuration.
135    pub fn config(&self) -> &AtlasConfig {
136        &self.config
137    }
138
139    /// Create a new atlas and return its ID.
140    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    /// Get the next available atlas ID.
154    pub fn next_atlas_id(&self) -> u32 {
155        u32::try_from(self.atlases.len()).unwrap()
156    }
157
158    /// Try to allocate space for an image with the given dimensions.
159    pub fn try_allocate(&mut self, width: u32, height: u32) -> Result<AtlasAllocation, AtlasError> {
160        self.try_allocate_excluding(width, height, None)
161    }
162
163    /// Try to allocate space for an image with the given dimensions,
164    /// optionally excluding a specific atlas.
165    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        // Check if the image is too large for any atlas
172        if width > self.config.atlas_size.0 || height > self.config.atlas_size.1 {
173            return Err(AtlasError::TextureTooLarge { width, height });
174        }
175
176        // Try allocation based on strategy
177        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    /// Allocate using first-fit strategy: try atlases in order until one has space.
192    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        // Try creating a new atlas if auto-grow is enabled
212        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    /// Allocate using best-fit strategy: choose the atlas with the smallest remaining space that
227    /// can fit the image.
228    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        // Find the atlas with the least remaining space that can fit the image
238        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        // Fallback to first-fit if best-fit didn't work
263        self.allocate_first_fit(width, height, exclude_atlas_id)
264    }
265
266    /// Allocate using least-used strategy: prefer the atlas with the lowest usage percentage.
267    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        // Find the atlas with the lowest usage percentage
277        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        // Fallback to first-fit if least-used didn't work
300        self.allocate_first_fit(width, height, exclude_atlas_id)
301    }
302
303    /// Allocate using round-robin strategy: cycle through atlases using a round-robin counter.
304    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        // Try starting from the round-robin position
317        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        // Try creating a new atlas if auto-grow is enabled
335        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    /// Deallocate space in the specified atlas.
351    pub fn deallocate(
352        &mut self,
353        atlas_id: AtlasId,
354        alloc_id: AllocId,
355        width: u32,
356        height: u32,
357    ) -> Result<(), AtlasError> {
358        // Since atlases only grow (never deallocate) and id is the index into the atlases vec,
359        // we can do a lookup instead of a linear search
360        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    /// Get statistics for all atlases.
369    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    /// Get the number of atlases.
377    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/// Errors that can occur during atlas operations.
395#[derive(Debug, Clone, Error)]
396pub enum AtlasError {
397    /// No space available in any atlas.
398    #[error("No space available in any atlas")]
399    NoSpaceAvailable,
400    /// Maximum number of atlases reached.
401    #[error("Maximum number of atlases reached")]
402    AtlasLimitReached,
403    /// The requested texture size is too large for any atlas.
404    #[error("Texture too large ({width}x{height}) for atlas")]
405    TextureTooLarge {
406        /// The width of the requested texture.
407        width: u32,
408        /// The height of the requested texture.
409        height: u32,
410    },
411    /// The specified atlas was not found.
412    #[error("Atlas with Id {0:?} not found")]
413    AtlasNotFound(AtlasId),
414}
415
416/// Unique identifier for an atlas.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
418pub struct AtlasId(pub u32);
419
420impl AtlasId {
421    /// Create a new atlas ID.
422    pub fn new(id: u32) -> Self {
423        Self(id)
424    }
425
426    /// Get the raw ID value.
427    pub fn as_u32(self) -> u32 {
428        self.0
429    }
430}
431
432/// Usage statistics for an atlas.
433#[derive(Debug, Clone)]
434pub struct AtlasUsageStats {
435    /// Total allocated area in pixels.
436    pub allocated_area: u32,
437    /// Total available area in pixels.
438    pub total_area: u32,
439    /// Number of allocated images.
440    pub allocated_count: u32,
441}
442
443impl AtlasUsageStats {
444    /// Calculate usage percentage (0.0 to 1.0).
445    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/// Result of an atlas allocation attempt.
455#[derive(Debug)]
456pub struct AtlasAllocation {
457    /// The atlas where the allocation was made.
458    pub atlas_id: AtlasId,
459    /// The allocation details.
460    pub allocation: Allocation,
461}
462
463/// Configuration for multiple atlas support.
464///
465/// Note that any values provided here are recommendations and might not be fully
466/// honored depending on the capabilities of the backend. For example, if you define
467/// the atlas size to be 8192x8192 but the device only supports texture sizes up to 4096x4096,
468/// the backend will likely decide to instead use the value that is compatible with the device.
469#[derive(Debug, Clone, Copy)]
470pub struct AtlasConfig {
471    /// Initial number of atlases to create.
472    pub initial_atlas_count: usize,
473    /// Maximum number of atlases to create.
474    pub max_atlases: usize,
475    // TODO: Make those u16 instead?
476    /// Size of each atlas texture.
477    pub atlas_size: (u32, u32),
478    /// Whether to automatically create new atlases when needed.
479    pub auto_grow: bool,
480    /// Strategy for allocating images across atlases.
481    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/// Strategy for allocating images across multiple atlases.
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
498pub enum AllocationStrategy {
499    /// Try atlases in order until one has space.
500    #[default]
501    FirstFit,
502    /// Choose the atlas with the smallest remaining space that can fit the image.
503    BestFit,
504    /// Prefer the atlas with the lowest usage percentage.
505    LeastUsed,
506    /// Cycle through atlases in round-robin fashion.
507    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        // Should create atlas automatically
537        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        // First allocation should go to atlas 0
576        let allocation0 = manager.try_allocate(100, 100).unwrap();
577        assert_eq!(allocation0.atlas_id.as_u32(), 0);
578
579        // Second allocation should also go to atlas 0 (first fit)
580        let allocation1 = manager.try_allocate(50, 50).unwrap();
581        assert_eq!(allocation1.atlas_id.as_u32(), 0);
582
583        // Third allocation should still go to atlas 0 (first fit continues to use same atlas)
584        let allocation2 = manager.try_allocate(80, 80).unwrap();
585        assert_eq!(allocation2.atlas_id.as_u32(), 0);
586
587        // Try to allocate something very large that definitely won't fit in atlas 0's remaining space
588        // This should force it to go to atlas 1
589        let allocation3 = manager.try_allocate(200, 200).unwrap();
590        assert_eq!(allocation3.atlas_id.as_u32(), 1);
591
592        // Next small allocation should go back to atlas 0 (first fit tries atlas 0 first)
593        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        // All atlases start empty, so first allocation goes to atlas 0 (first available)
608        let allocation0 = manager.try_allocate(150, 150).unwrap();
609        assert_eq!(allocation0.atlas_id.as_u32(), 0);
610
611        // Second allocation should also go to atlas 0 since it still has the least remaining space
612        // that can fit the image (all atlases have same remaining space, so it picks the first)
613        let allocation1 = manager.try_allocate(100, 100).unwrap();
614        assert_eq!(allocation1.atlas_id.as_u32(), 0);
615
616        // Now atlas 0 has less remaining space than atlases 1 and 2
617        // For a small allocation, it should still go to atlas 0 (best fit - least remaining space)
618        let allocation2 = manager.try_allocate(100, 100).unwrap();
619        assert_eq!(allocation2.atlas_id.as_u32(), 0);
620
621        // Now try to allocate something very large that won't fit in atlas 0's remaining space
622        // This should force it to go to atlas 1 (which has the most remaining space)
623        let allocation3 = manager.try_allocate(200, 200).unwrap();
624        assert_eq!(allocation3.atlas_id.as_u32(), 1);
625
626        // Now atlas 1 has less remaining space
627        // A small allocation should go to atlas 0 as it can
628        let allocation4 = manager.try_allocate(80, 80).unwrap();
629        assert_eq!(allocation4.atlas_id.as_u32(), 0);
630
631        // Now atlas 1 has less remaining space but it can't fit the allocation
632        // It should go to atlas 2 (best fit - least remaining space)
633        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        // First allocation goes to atlas 0 (all atlases have 0% usage, picks first)
648        let allocation0 = manager.try_allocate(100, 100).unwrap();
649        assert_eq!(allocation0.atlas_id.as_u32(), 0);
650
651        // Second allocation should go to atlas 1 (least used among remaining)
652        let allocation1 = manager.try_allocate(50, 50).unwrap();
653        assert_eq!(allocation1.atlas_id.as_u32(), 1);
654
655        // Third allocation should go to atlas 2 (least used)
656        let allocation2 = manager.try_allocate(30, 30).unwrap();
657        assert_eq!(allocation2.atlas_id.as_u32(), 2);
658
659        // Fourth allocation should go to atlas 2 again (still least used)
660        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        // Allocations should cycle through atlases in order
675        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        // Should wrap back to atlas 0
685        let allocation3 = manager.try_allocate(50, 50).unwrap();
686        assert_eq!(allocation3.atlas_id.as_u32(), 0);
687
688        // Continue the cycle
689        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}