use alloc::vec::Vec;
pub use guillotiere::AllocId;
use guillotiere::AtlasAllocator;
use thiserror::Error;
#[derive(Debug)]
pub struct Allocation {
pub id: AllocId,
pub x: u32,
pub y: u32,
}
pub struct Atlas {
pub id: AtlasId,
allocator: AtlasAllocator,
stats: AtlasUsageStats,
allocation_counter: u32,
}
impl Atlas {
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,
}
}
#[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,
})
}
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);
}
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()
}
}
pub struct MultiAtlasManager {
atlases: Vec<Atlas>,
config: AtlasConfig,
round_robin_counter: usize,
}
impl MultiAtlasManager {
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
}
pub fn config(&self) -> &AtlasConfig {
&self.config
}
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)
}
pub fn next_atlas_id(&self) -> u32 {
u32::try_from(self.atlases.len()).unwrap()
}
pub fn try_allocate(&mut self, width: u32, height: u32) -> Result<AtlasAllocation, AtlasError> {
self.try_allocate_excluding(width, height, None)
}
pub fn try_allocate_excluding(
&mut self,
width: u32,
height: u32,
exclude_atlas_id: Option<AtlasId>,
) -> Result<AtlasAllocation, AtlasError> {
if width > self.config.atlas_size.0 || height > self.config.atlas_size.1 {
return Err(AtlasError::TextureTooLarge { width, height });
}
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)
}
}
}
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,
});
}
}
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)
}
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;
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,
});
}
}
self.allocate_first_fit(width, height, exclude_atlas_id)
}
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;
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,
});
}
self.allocate_first_fit(width, height, exclude_atlas_id)
}
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();
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,
});
}
}
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)
}
pub fn deallocate(
&mut self,
atlas_id: AtlasId,
alloc_id: AllocId,
width: u32,
height: u32,
) -> Result<(), AtlasError> {
let atlas = self
.atlases
.get_mut(atlas_id.0 as usize)
.ok_or(AtlasError::AtlasNotFound(atlas_id))?;
atlas.deallocate(alloc_id, width, height);
Ok(())
}
pub fn atlas_stats(&self) -> Vec<(AtlasId, &AtlasUsageStats)> {
self.atlases
.iter()
.map(|atlas| (atlas.id, atlas.stats()))
.collect()
}
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()
}
}
#[derive(Debug, Clone, Error)]
pub enum AtlasError {
#[error("No space available in any atlas")]
NoSpaceAvailable,
#[error("Maximum number of atlases reached")]
AtlasLimitReached,
#[error("Texture too large ({width}x{height}) for atlas")]
TextureTooLarge {
width: u32,
height: u32,
},
#[error("Atlas with Id {0:?} not found")]
AtlasNotFound(AtlasId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AtlasId(pub u32);
impl AtlasId {
pub fn new(id: u32) -> Self {
Self(id)
}
pub fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone)]
pub struct AtlasUsageStats {
pub allocated_area: u32,
pub total_area: u32,
pub allocated_count: u32,
}
impl AtlasUsageStats {
pub fn usage_percentage(&self) -> f32 {
if self.total_area == 0 {
0.0
} else {
self.allocated_area as f32 / self.total_area as f32
}
}
}
#[derive(Debug)]
pub struct AtlasAllocation {
pub atlas_id: AtlasId,
pub allocation: Allocation,
}
#[derive(Debug, Clone, Copy)]
pub struct AtlasConfig {
pub initial_atlas_count: usize,
pub max_atlases: usize,
pub atlas_size: (u32, u32),
pub auto_grow: bool,
pub allocation_strategy: AllocationStrategy,
}
impl Default for AtlasConfig {
fn default() -> Self {
Self {
initial_atlas_count: 1,
max_atlases: 8,
atlas_size: (4096, 4096),
auto_grow: true,
allocation_strategy: AllocationStrategy::FirstFit,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AllocationStrategy {
#[default]
FirstFit,
BestFit,
LeastUsed,
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_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,
});
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,
});
let allocation0 = manager.try_allocate(100, 100).unwrap();
assert_eq!(allocation0.atlas_id.as_u32(), 0);
let allocation1 = manager.try_allocate(50, 50).unwrap();
assert_eq!(allocation1.atlas_id.as_u32(), 0);
let allocation2 = manager.try_allocate(80, 80).unwrap();
assert_eq!(allocation2.atlas_id.as_u32(), 0);
let allocation3 = manager.try_allocate(200, 200).unwrap();
assert_eq!(allocation3.atlas_id.as_u32(), 1);
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,
});
let allocation0 = manager.try_allocate(150, 150).unwrap();
assert_eq!(allocation0.atlas_id.as_u32(), 0);
let allocation1 = manager.try_allocate(100, 100).unwrap();
assert_eq!(allocation1.atlas_id.as_u32(), 0);
let allocation2 = manager.try_allocate(100, 100).unwrap();
assert_eq!(allocation2.atlas_id.as_u32(), 0);
let allocation3 = manager.try_allocate(200, 200).unwrap();
assert_eq!(allocation3.atlas_id.as_u32(), 1);
let allocation4 = manager.try_allocate(80, 80).unwrap();
assert_eq!(allocation4.atlas_id.as_u32(), 0);
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,
});
let allocation0 = manager.try_allocate(100, 100).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(30, 30).unwrap();
assert_eq!(allocation2.atlas_id.as_u32(), 2);
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,
});
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);
let allocation3 = manager.try_allocate(50, 50).unwrap();
assert_eq!(allocation3.atlas_id.as_u32(), 0);
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);
}
}