| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! Multi-atlas management for texture atlases. |
| //! |
| //! This module provides support for managing multiple texture atlases, allowing for handling of |
| //! large numbers of images. |
| //! |
| //! The allocator backend is [guillotiere](https://github.com/nical/guillotiere)'s tree-based |
| //! guillotine algorithm, providing O(1) neighbor lookup during deallocation and automatic |
| //! free-rect coalescing. |
| |
| use alloc::vec::Vec; |
| pub use guillotiere::AllocId; |
| use guillotiere::AtlasAllocator; |
| use thiserror::Error; |
| |
| /// The result of a successful rectangle allocation within a single atlas. |
| #[derive(Debug)] |
| pub struct Allocation { |
| /// Opaque handle used for deallocation. |
| pub id: AllocId, |
| /// X coordinate of the top-left corner of the allocated rectangle. |
| pub x: u32, |
| /// Y coordinate of the top-left corner of the allocated rectangle. |
| pub y: u32, |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Unified Atlas type |
| // --------------------------------------------------------------------------- |
| |
| /// Represents a single atlas in the multi-atlas system. |
| pub struct Atlas { |
| /// Unique identifier for this atlas. |
| pub id: AtlasId, |
| /// Rectangle allocator backend. |
| allocator: AtlasAllocator, |
| /// Current usage statistics. |
| stats: AtlasUsageStats, |
| /// Allocation counter. |
| allocation_counter: u32, |
| } |
| |
| impl Atlas { |
| /// Create a new atlas with the given ID and size. |
| pub fn new(id: AtlasId, width: u32, height: u32) -> Self { |
| Self { |
| id, |
| allocator: AtlasAllocator::new(guillotiere::size2(width as i32, height as i32)), |
| stats: AtlasUsageStats { |
| allocated_area: 0, |
| total_area: width * height, |
| allocated_count: 0, |
| }, |
| allocation_counter: 0, |
| } |
| } |
| |
| /// Try to allocate an image in this atlas. |
| #[expect( |
| clippy::cast_sign_loss, |
| reason = "coordinates are always non-negative for valid allocations" |
| )] |
| pub fn allocate(&mut self, width: u32, height: u32) -> Option<Allocation> { |
| let alloc = self |
| .allocator |
| .allocate(guillotiere::size2(width as i32, height as i32))?; |
| self.stats.allocated_area += width * height; |
| self.stats.allocated_count += 1; |
| self.allocation_counter += 1; |
| Some(Allocation { |
| id: alloc.id, |
| x: alloc.rectangle.min.x as u32, |
| y: alloc.rectangle.min.y as u32, |
| }) |
| } |
| |
| /// Deallocate an image from this atlas. |
| pub fn deallocate(&mut self, alloc_id: AllocId, width: u32, height: u32) { |
| self.allocator.deallocate(alloc_id); |
| self.stats.allocated_area = self.stats.allocated_area.saturating_sub(width * height); |
| self.stats.allocated_count = self.stats.allocated_count.saturating_sub(1); |
| } |
| |
| /// Get current usage statistics. |
| pub fn stats(&self) -> &AtlasUsageStats { |
| &self.stats |
| } |
| } |
| |
| impl core::fmt::Debug for Atlas { |
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| f.debug_struct("Atlas") |
| .field("id", &self.id) |
| .field("stats", &self.stats) |
| .field("allocation_counter", &self.allocation_counter) |
| .finish_non_exhaustive() |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // MultiAtlasManager |
| // --------------------------------------------------------------------------- |
| |
| /// Manages multiple texture atlases. |
| pub struct MultiAtlasManager { |
| /// All atlases managed by this instance. |
| atlases: Vec<Atlas>, |
| /// Configuration for atlas management. |
| config: AtlasConfig, |
| /// Round-robin counter for allocation strategy. |
| round_robin_counter: usize, |
| } |
| |
| impl MultiAtlasManager { |
| /// Create a new multi-atlas manager with the given configuration. |
| pub fn new(config: AtlasConfig) -> Self { |
| let mut manager = Self { |
| atlases: Vec::new(), |
| config, |
| round_robin_counter: 0, |
| }; |
| |
| for _ in 0..config.initial_atlas_count { |
| manager |
| .create_atlas() |
| .expect("Failed to create initial atlas"); |
| } |
| |
| manager |
| } |
| |
| /// Get the current configuration. |
| pub fn config(&self) -> &AtlasConfig { |
| &self.config |
| } |
| |
| /// Create a new atlas and return its ID. |
| pub fn create_atlas(&mut self) -> Result<AtlasId, AtlasError> { |
| if self.atlases.len() >= self.config.max_atlases { |
| return Err(AtlasError::AtlasLimitReached); |
| } |
| |
| let atlas_id = AtlasId::new(self.next_atlas_id()); |
| |
| let atlas = Atlas::new(atlas_id, self.config.atlas_size.0, self.config.atlas_size.1); |
| self.atlases.push(atlas); |
| |
| Ok(atlas_id) |
| } |
| |
| /// Get the next available atlas ID. |
| pub fn next_atlas_id(&self) -> u32 { |
| u32::try_from(self.atlases.len()).unwrap() |
| } |
| |
| /// Try to allocate space for an image with the given dimensions. |
| pub fn try_allocate(&mut self, width: u32, height: u32) -> Result<AtlasAllocation, AtlasError> { |
| self.try_allocate_excluding(width, height, None) |
| } |
| |
| /// Try to allocate space for an image with the given dimensions, |
| /// optionally excluding a specific atlas. |
| pub fn try_allocate_excluding( |
| &mut self, |
| width: u32, |
| height: u32, |
| exclude_atlas_id: Option<AtlasId>, |
| ) -> Result<AtlasAllocation, AtlasError> { |
| // Check if the image is too large for any atlas |
| if width > self.config.atlas_size.0 || height > self.config.atlas_size.1 { |
| return Err(AtlasError::TextureTooLarge { width, height }); |
| } |
| |
| // Try allocation based on strategy |
| match self.config.allocation_strategy { |
| AllocationStrategy::FirstFit => { |
| self.allocate_first_fit(width, height, exclude_atlas_id) |
| } |
| AllocationStrategy::BestFit => self.allocate_best_fit(width, height, exclude_atlas_id), |
| AllocationStrategy::LeastUsed => { |
| self.allocate_least_used(width, height, exclude_atlas_id) |
| } |
| AllocationStrategy::RoundRobin => { |
| self.allocate_round_robin(width, height, exclude_atlas_id) |
| } |
| } |
| } |
| |
| /// Allocate using first-fit strategy: try atlases in order until one has space. |
| fn allocate_first_fit( |
| &mut self, |
| width: u32, |
| height: u32, |
| exclude_atlas_id: Option<AtlasId>, |
| ) -> Result<AtlasAllocation, AtlasError> { |
| for atlas in &mut self.atlases { |
| if Some(atlas.id) == exclude_atlas_id { |
| continue; |
| } |
| |
| if let Some(allocation) = atlas.allocate(width, height) { |
| return Ok(AtlasAllocation { |
| atlas_id: atlas.id, |
| allocation, |
| }); |
| } |
| } |
| |
| // Try creating a new atlas if auto-grow is enabled |
| if self.config.auto_grow { |
| let atlas_id = self.create_atlas()?; |
| let atlas = self.atlases.last_mut().unwrap(); |
| if let Some(allocation) = atlas.allocate(width, height) { |
| return Ok(AtlasAllocation { |
| atlas_id, |
| allocation, |
| }); |
| } |
| } |
| |
| Err(AtlasError::NoSpaceAvailable) |
| } |
| |
| /// Allocate using best-fit strategy: choose the atlas with the smallest remaining space that |
| /// can fit the image. |
| fn allocate_best_fit( |
| &mut self, |
| width: u32, |
| height: u32, |
| exclude_atlas_id: Option<AtlasId>, |
| ) -> Result<AtlasAllocation, AtlasError> { |
| let mut best_atlas_idx = None; |
| let mut best_remaining_space = u32::MAX; |
| |
| // Find the atlas with the least remaining space that can fit the image |
| for (idx, atlas) in self.atlases.iter().enumerate() { |
| if Some(atlas.id) == exclude_atlas_id { |
| continue; |
| } |
| |
| let stats = atlas.stats(); |
| let remaining_space = stats.total_area - stats.allocated_area; |
| |
| if remaining_space >= width * height && remaining_space < best_remaining_space { |
| best_remaining_space = remaining_space; |
| best_atlas_idx = Some(idx); |
| } |
| } |
| |
| if let Some(idx) = best_atlas_idx { |
| let atlas = &mut self.atlases[idx]; |
| if let Some(allocation) = atlas.allocate(width, height) { |
| return Ok(AtlasAllocation { |
| atlas_id: atlas.id, |
| allocation, |
| }); |
| } |
| } |
| |
| // Fallback to first-fit if best-fit didn't work |
| self.allocate_first_fit(width, height, exclude_atlas_id) |
| } |
| |
| /// Allocate using least-used strategy: prefer the atlas with the lowest usage percentage. |
| fn allocate_least_used( |
| &mut self, |
| width: u32, |
| height: u32, |
| exclude_atlas_id: Option<AtlasId>, |
| ) -> Result<AtlasAllocation, AtlasError> { |
| let mut best_atlas_idx = None; |
| let mut lowest_usage = f32::MAX; |
| |
| // Find the atlas with the lowest usage percentage |
| for (idx, atlas) in self.atlases.iter().enumerate() { |
| if Some(atlas.id) == exclude_atlas_id { |
| continue; |
| } |
| |
| let usage = atlas.stats().usage_percentage(); |
| if usage < lowest_usage { |
| lowest_usage = usage; |
| best_atlas_idx = Some(idx); |
| } |
| } |
| |
| if let Some(idx) = best_atlas_idx |
| && let Some(allocation) = self.atlases[idx].allocate(width, height) |
| { |
| let atlas_id = self.atlases[idx].id; |
| return Ok(AtlasAllocation { |
| atlas_id, |
| allocation, |
| }); |
| } |
| |
| // Fallback to first-fit if least-used didn't work |
| self.allocate_first_fit(width, height, exclude_atlas_id) |
| } |
| |
| /// Allocate using round-robin strategy: cycle through atlases using a round-robin counter. |
| fn allocate_round_robin( |
| &mut self, |
| width: u32, |
| height: u32, |
| exclude_atlas_id: Option<AtlasId>, |
| ) -> Result<AtlasAllocation, AtlasError> { |
| if self.atlases.is_empty() { |
| return self.allocate_first_fit(width, height, exclude_atlas_id); |
| } |
| |
| let start_idx = self.round_robin_counter % self.atlases.len(); |
| |
| // Try starting from the round-robin position |
| for i in 0..self.atlases.len() { |
| let idx = (start_idx + i) % self.atlases.len(); |
| |
| if Some(self.atlases[idx].id) == exclude_atlas_id { |
| continue; |
| } |
| |
| if let Some(allocation) = self.atlases[idx].allocate(width, height) { |
| let atlas_id = self.atlases[idx].id; |
| self.round_robin_counter = (idx + 1) % self.atlases.len(); |
| return Ok(AtlasAllocation { |
| atlas_id, |
| allocation, |
| }); |
| } |
| } |
| |
| // Try creating a new atlas if auto-grow is enabled |
| if self.config.auto_grow { |
| let atlas_id = self.create_atlas()?; |
| let atlas = self.atlases.last_mut().unwrap(); |
| if let Some(allocation) = atlas.allocate(width, height) { |
| self.round_robin_counter = self.atlases.len() - 1; |
| return Ok(AtlasAllocation { |
| atlas_id, |
| allocation, |
| }); |
| } |
| } |
| |
| Err(AtlasError::NoSpaceAvailable) |
| } |
| |
| /// Deallocate space in the specified atlas. |
| pub fn deallocate( |
| &mut self, |
| atlas_id: AtlasId, |
| alloc_id: AllocId, |
| width: u32, |
| height: u32, |
| ) -> Result<(), AtlasError> { |
| // Since atlases only grow (never deallocate) and id is the index into the atlases vec, |
| // we can do a lookup instead of a linear search |
| let atlas = self |
| .atlases |
| .get_mut(atlas_id.0 as usize) |
| .ok_or(AtlasError::AtlasNotFound(atlas_id))?; |
| atlas.deallocate(alloc_id, width, height); |
| Ok(()) |
| } |
| |
| /// Get statistics for all atlases. |
| pub fn atlas_stats(&self) -> Vec<(AtlasId, &AtlasUsageStats)> { |
| self.atlases |
| .iter() |
| .map(|atlas| (atlas.id, atlas.stats())) |
| .collect() |
| } |
| |
| /// Get the number of atlases. |
| pub fn atlas_count(&self) -> usize { |
| self.atlases.len() |
| } |
| } |
| |
| impl core::fmt::Debug for MultiAtlasManager { |
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| f.debug_struct("MultiAtlasManager") |
| .field("atlas_count", &self.atlases.len()) |
| .field("config", &self.config) |
| .field("next_atlas_id", &self.next_atlas_id()) |
| .field("round_robin_counter", &self.round_robin_counter) |
| .field("atlases", &self.atlases) |
| .finish() |
| } |
| } |
| |
| /// Errors that can occur during atlas operations. |
| #[derive(Debug, Clone, Error)] |
| pub enum AtlasError { |
| /// No space available in any atlas. |
| #[error("No space available in any atlas")] |
| NoSpaceAvailable, |
| /// Maximum number of atlases reached. |
| #[error("Maximum number of atlases reached")] |
| AtlasLimitReached, |
| /// The requested texture size is too large for any atlas. |
| #[error("Texture too large ({width}x{height}) for atlas")] |
| TextureTooLarge { |
| /// The width of the requested texture. |
| width: u32, |
| /// The height of the requested texture. |
| height: u32, |
| }, |
| /// The specified atlas was not found. |
| #[error("Atlas with Id {0:?} not found")] |
| AtlasNotFound(AtlasId), |
| } |
| |
| /// Unique identifier for an atlas. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| pub struct AtlasId(pub u32); |
| |
| impl AtlasId { |
| /// Create a new atlas ID. |
| pub fn new(id: u32) -> Self { |
| Self(id) |
| } |
| |
| /// Get the raw ID value. |
| pub fn as_u32(self) -> u32 { |
| self.0 |
| } |
| } |
| |
| /// Usage statistics for an atlas. |
| #[derive(Debug, Clone)] |
| pub struct AtlasUsageStats { |
| /// Total allocated area in pixels. |
| pub allocated_area: u32, |
| /// Total available area in pixels. |
| pub total_area: u32, |
| /// Number of allocated images. |
| pub allocated_count: u32, |
| } |
| |
| impl AtlasUsageStats { |
| /// Calculate usage percentage (0.0 to 1.0). |
| pub fn usage_percentage(&self) -> f32 { |
| if self.total_area == 0 { |
| 0.0 |
| } else { |
| self.allocated_area as f32 / self.total_area as f32 |
| } |
| } |
| } |
| |
| /// Result of an atlas allocation attempt. |
| #[derive(Debug)] |
| pub struct AtlasAllocation { |
| /// The atlas where the allocation was made. |
| pub atlas_id: AtlasId, |
| /// The allocation details. |
| pub allocation: Allocation, |
| } |
| |
| /// Configuration for multiple atlas support. |
| /// |
| /// Note that any values provided here are recommendations and might not be fully |
| /// honored depending on the capabilities of the backend. For example, if you define |
| /// the atlas size to be 8192x8192 but the device only supports texture sizes up to 4096x4096, |
| /// the backend will likely decide to instead use the value that is compatible with the device. |
| #[derive(Debug, Clone, Copy)] |
| pub struct AtlasConfig { |
| /// Initial number of atlases to create. |
| /// |
| /// Set this to zero to allocate the first atlas lazily. |
| pub initial_atlas_count: usize, |
| /// Maximum number of atlases to create. |
| pub max_atlases: usize, |
| // TODO: Make those u16 instead? |
| /// Size of each atlas texture. |
| pub atlas_size: (u32, u32), |
| /// Whether to automatically create new atlases when needed. |
| pub auto_grow: bool, |
| /// Strategy for allocating images across atlases. |
| pub allocation_strategy: AllocationStrategy, |
| } |
| |
| impl Default for AtlasConfig { |
| fn default() -> Self { |
| Self { |
| initial_atlas_count: 0, |
| max_atlases: 8, |
| atlas_size: (4096, 4096), |
| auto_grow: true, |
| allocation_strategy: AllocationStrategy::FirstFit, |
| } |
| } |
| } |
| |
| /// Strategy for allocating images across multiple atlases. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| pub enum AllocationStrategy { |
| /// Try atlases in order until one has space. |
| #[default] |
| FirstFit, |
| /// Choose the atlas with the smallest remaining space that can fit the image. |
| BestFit, |
| /// Prefer the atlas with the lowest usage percentage. |
| LeastUsed, |
| /// Cycle through atlases in round-robin fashion. |
| RoundRobin, |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_atlas_creation() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 0, |
| ..Default::default() |
| }); |
| |
| let atlas_id = manager.create_atlas().unwrap(); |
| assert_eq!(atlas_id.as_u32(), 0); |
| assert_eq!(manager.atlas_count(), 1); |
| } |
| |
| #[test] |
| fn test_default_lazily_creates_first_atlas() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig::default()); |
| assert_eq!(manager.atlas_count(), 0); |
| |
| let allocation = manager.try_allocate(100, 100).unwrap(); |
| assert_eq!(allocation.atlas_id.as_u32(), 0); |
| assert_eq!(manager.atlas_count(), 1); |
| } |
| |
| #[test] |
| fn test_allocation_strategies() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 1, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::FirstFit, |
| auto_grow: true, |
| }); |
| |
| // Should create atlas automatically |
| let allocation = manager.try_allocate(100, 100).unwrap(); |
| assert_eq!(allocation.atlas_id.as_u32(), 0); |
| } |
| |
| #[test] |
| fn test_atlas_limit() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 1, |
| max_atlases: 1, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::FirstFit, |
| auto_grow: false, |
| }); |
| |
| assert!(manager.create_atlas().is_err()); |
| } |
| |
| #[test] |
| fn test_texture_too_large() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| atlas_size: (256, 256), |
| ..Default::default() |
| }); |
| |
| let result = manager.try_allocate(300, 300); |
| assert!(matches!(result, Err(AtlasError::TextureTooLarge { .. }))); |
| } |
| |
| #[test] |
| fn test_first_fit_allocation_strategy() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 3, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::FirstFit, |
| auto_grow: false, |
| }); |
| |
| // First allocation should go to atlas 0 |
| let allocation0 = manager.try_allocate(100, 100).unwrap(); |
| assert_eq!(allocation0.atlas_id.as_u32(), 0); |
| |
| // Second allocation should also go to atlas 0 (first fit) |
| let allocation1 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation1.atlas_id.as_u32(), 0); |
| |
| // Third allocation should still go to atlas 0 (first fit continues to use same atlas) |
| let allocation2 = manager.try_allocate(80, 80).unwrap(); |
| assert_eq!(allocation2.atlas_id.as_u32(), 0); |
| |
| // Try to allocate something very large that definitely won't fit in atlas 0's remaining space |
| // This should force it to go to atlas 1 |
| let allocation3 = manager.try_allocate(200, 200).unwrap(); |
| assert_eq!(allocation3.atlas_id.as_u32(), 1); |
| |
| // Next small allocation should go back to atlas 0 (first fit tries atlas 0 first) |
| let allocation4 = manager.try_allocate(20, 20).unwrap(); |
| assert_eq!(allocation4.atlas_id.as_u32(), 0); |
| } |
| |
| #[test] |
| fn test_best_fit_allocation_strategy() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 3, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::BestFit, |
| auto_grow: false, |
| }); |
| |
| // All atlases start empty, so first allocation goes to atlas 0 (first available) |
| let allocation0 = manager.try_allocate(150, 150).unwrap(); |
| assert_eq!(allocation0.atlas_id.as_u32(), 0); |
| |
| // Second allocation should also go to atlas 0 since it still has the least remaining space |
| // that can fit the image (all atlases have same remaining space, so it picks the first) |
| let allocation1 = manager.try_allocate(100, 100).unwrap(); |
| assert_eq!(allocation1.atlas_id.as_u32(), 0); |
| |
| // Now atlas 0 has less remaining space than atlases 1 and 2 |
| // For a small allocation, it should still go to atlas 0 (best fit - least remaining space) |
| let allocation2 = manager.try_allocate(100, 100).unwrap(); |
| assert_eq!(allocation2.atlas_id.as_u32(), 0); |
| |
| // Now try to allocate something very large that won't fit in atlas 0's remaining space |
| // This should force it to go to atlas 1 (which has the most remaining space) |
| let allocation3 = manager.try_allocate(200, 200).unwrap(); |
| assert_eq!(allocation3.atlas_id.as_u32(), 1); |
| |
| // Now atlas 1 has less remaining space |
| // A small allocation should go to atlas 0 as it can |
| let allocation4 = manager.try_allocate(80, 80).unwrap(); |
| assert_eq!(allocation4.atlas_id.as_u32(), 0); |
| |
| // Now atlas 1 has less remaining space but it can't fit the allocation |
| // It should go to atlas 2 (best fit - least remaining space) |
| let allocation5 = manager.try_allocate(80, 80).unwrap(); |
| assert_eq!(allocation5.atlas_id.as_u32(), 2); |
| } |
| |
| #[test] |
| fn test_least_used_allocation_strategy() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 3, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::LeastUsed, |
| auto_grow: false, |
| }); |
| |
| // First allocation goes to atlas 0 (all atlases have 0% usage, picks first) |
| let allocation0 = manager.try_allocate(100, 100).unwrap(); |
| assert_eq!(allocation0.atlas_id.as_u32(), 0); |
| |
| // Second allocation should go to atlas 1 (least used among remaining) |
| let allocation1 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation1.atlas_id.as_u32(), 1); |
| |
| // Third allocation should go to atlas 2 (least used) |
| let allocation2 = manager.try_allocate(30, 30).unwrap(); |
| assert_eq!(allocation2.atlas_id.as_u32(), 2); |
| |
| // Fourth allocation should go to atlas 2 again (still least used) |
| let allocation3 = manager.try_allocate(30, 30).unwrap(); |
| assert_eq!(allocation3.atlas_id.as_u32(), 2); |
| } |
| |
| #[test] |
| fn test_round_robin_allocation_strategy() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 3, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::RoundRobin, |
| auto_grow: false, |
| }); |
| |
| // Allocations should cycle through atlases in order |
| let allocation0 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation0.atlas_id.as_u32(), 0); |
| |
| let allocation1 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation1.atlas_id.as_u32(), 1); |
| |
| let allocation2 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation2.atlas_id.as_u32(), 2); |
| |
| // Should wrap back to atlas 0 |
| let allocation3 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation3.atlas_id.as_u32(), 0); |
| |
| // Continue the cycle |
| let allocation4 = manager.try_allocate(50, 50).unwrap(); |
| assert_eq!(allocation4.atlas_id.as_u32(), 1); |
| } |
| |
| #[test] |
| fn test_auto_grow() { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 1, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: AllocationStrategy::FirstFit, |
| auto_grow: true, |
| }); |
| |
| let allocation0 = manager.try_allocate(256, 256).unwrap(); |
| assert_eq!(allocation0.atlas_id.as_u32(), 0); |
| |
| let allocation1 = manager.try_allocate(256, 256).unwrap(); |
| assert_eq!(allocation1.atlas_id.as_u32(), 1); |
| |
| let allocation2 = manager.try_allocate(256, 256).unwrap(); |
| assert_eq!(allocation2.atlas_id.as_u32(), 2); |
| } |
| |
| fn test_allocate_excluding_with_strategy(strategy: AllocationStrategy) { |
| let mut manager = MultiAtlasManager::new(AtlasConfig { |
| initial_atlas_count: 3, |
| max_atlases: 3, |
| atlas_size: (256, 256), |
| allocation_strategy: strategy, |
| auto_grow: false, |
| }); |
| |
| let allocation0 = manager.try_allocate(100, 100).unwrap(); |
| let first_atlas = allocation0.atlas_id; |
| let allocation1 = manager |
| .try_allocate_excluding(256, 256, Some(first_atlas)) |
| .unwrap(); |
| assert_ne!(allocation1.atlas_id, first_atlas); |
| |
| let second_atlas = allocation1.atlas_id; |
| let allocation2 = manager |
| .try_allocate_excluding(100, 100, Some(second_atlas)) |
| .unwrap(); |
| assert_ne!(allocation2.atlas_id, second_atlas); |
| |
| let allocation3 = manager |
| .try_allocate_excluding(100, 100, Some(first_atlas)) |
| .unwrap(); |
| assert_ne!(allocation3.atlas_id, first_atlas); |
| } |
| |
| #[test] |
| fn test_allocate_excluding_first_fit() { |
| test_allocate_excluding_with_strategy(AllocationStrategy::FirstFit); |
| } |
| |
| #[test] |
| fn test_allocate_excluding_best_fit() { |
| test_allocate_excluding_with_strategy(AllocationStrategy::BestFit); |
| } |
| |
| #[test] |
| fn test_allocate_excluding_least_used() { |
| test_allocate_excluding_with_strategy(AllocationStrategy::LeastUsed); |
| } |
| |
| #[test] |
| fn test_allocate_excluding_round_robin() { |
| test_allocate_excluding_with_strategy(AllocationStrategy::RoundRobin); |
| } |
| } |