use crate::multi_atlas::{
AllocId, AllocationStrategy, AtlasConfig, AtlasError, AtlasId, MultiAtlasManager,
};
use crate::paint::ImageId;
use alloc::vec::Vec;
#[derive(Debug)]
pub struct ImageResource {
pub width: u16,
pub height: u16,
pub atlas_id: AtlasId,
pub offset: [u16; 2],
pub padding: u16,
atlas_alloc_id: AllocId,
}
impl ImageResource {
pub fn offsets(&self) -> [u32; 2] {
[self.offset[0] as u32, self.offset[1] as u32]
}
pub fn size(&self) -> [u32; 2] {
[self.width as u32, self.height as u32]
}
}
pub struct ImageCache {
atlas_manager: MultiAtlasManager,
slots: Vec<Option<ImageResource>>,
free_idxs: Vec<usize>,
}
impl core::fmt::Debug for ImageCache {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let atlas_stats = self.atlas_manager.atlas_stats();
f.debug_struct("ImageCache")
.field("slots", &self.slots)
.field("free_idxs", &self.free_idxs)
.field("atlas_count", &self.atlas_manager.atlas_count())
.field("atlas_stats", &atlas_stats)
.finish()
}
}
impl ImageCache {
pub fn new_with_config(config: AtlasConfig) -> Self {
Self {
atlas_manager: MultiAtlasManager::new(config),
slots: Vec::new(),
free_idxs: Vec::new(),
}
}
pub fn new_dummy() -> Self {
Self::new_with_config(AtlasConfig {
initial_atlas_count: 1,
max_atlases: 1,
atlas_size: (1, 1),
auto_grow: false,
allocation_strategy: AllocationStrategy::FirstFit,
})
}
pub fn get(&self, id: ImageId) -> Option<&ImageResource> {
self.slots.get(id.as_u32() as usize)?.as_ref()
}
pub fn allocate(
&mut self,
width: u32,
height: u32,
padding: u16,
) -> Result<ImageId, AtlasError> {
self.allocate_excluding(width, height, padding, None)
}
#[expect(
clippy::cast_possible_truncation,
reason = "u16 is enough for the offset and width/height"
)]
pub fn allocate_excluding(
&mut self,
width: u32,
height: u32,
padding: u16,
exclude_atlas_id: Option<AtlasId>,
) -> Result<ImageId, AtlasError> {
let padded_width = width + u32::from(padding) * 2;
let padded_height = height + u32::from(padding) * 2;
let atlas_alloc = self.atlas_manager.try_allocate_excluding(
padded_width,
padded_height,
exclude_atlas_id,
)?;
let slot_idx = self.free_idxs.pop().unwrap_or_else(|| {
let index = self.slots.len();
self.slots.push(None);
index
});
let image_id = ImageId::new(slot_idx as u32);
let image_resource = ImageResource {
width: width as u16,
height: height as u16,
atlas_id: atlas_alloc.atlas_id,
offset: [
atlas_alloc.allocation.x as u16 + padding,
atlas_alloc.allocation.y as u16 + padding,
],
padding,
atlas_alloc_id: atlas_alloc.allocation.id,
};
self.slots[slot_idx] = Some(image_resource);
Ok(image_id)
}
pub fn deallocate(&mut self, id: ImageId) -> Option<ImageResource> {
let index = id.as_u32() as usize;
if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) {
let padded_width = image_resource.width as u32 + u32::from(image_resource.padding) * 2;
let padded_height =
image_resource.height as u32 + u32::from(image_resource.padding) * 2;
self.atlas_manager
.deallocate(
image_resource.atlas_id,
image_resource.atlas_alloc_id,
padded_width,
padded_height,
)
.unwrap();
self.free_idxs.push(index);
Some(image_resource)
} else {
None
}
}
pub fn atlas_manager(&self) -> &MultiAtlasManager {
&self.atlas_manager
}
pub fn atlas_count(&self) -> usize {
self.atlas_manager.atlas_count()
}
}
#[cfg(test)]
mod tests {
use super::*;
const ATLAS_SIZE: u32 = 1024;
#[test]
fn test_insert_single_image() {
let mut cache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
let id = cache.allocate(100, 100, 0).unwrap();
assert_eq!(id.as_u32(), 0);
let resource = cache.get(id).unwrap();
assert_eq!(resource.width, 100);
assert_eq!(resource.height, 100);
assert_eq!(resource.offset, [0, 0]);
}
#[test]
fn test_insert_single_image_with_padding() {
let mut cache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
let id = cache.allocate(100, 100, 4).unwrap();
assert_eq!(id.as_u32(), 0);
let resource = cache.get(id).unwrap();
assert_eq!(resource.width, 100);
assert_eq!(resource.height, 100);
assert_eq!(resource.padding, 4);
assert_eq!(resource.offset, [4, 4]);
}
#[test]
fn test_insert_multiple_images() {
let mut cache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
let id1 = cache.allocate(50, 50, 0).unwrap();
let id2 = cache.allocate(75, 75, 0).unwrap();
assert_eq!(id1.as_u32(), 0);
assert_eq!(id2.as_u32(), 1);
let resource1 = cache.get(id1).unwrap();
let resource2 = cache.get(id2).unwrap();
assert_eq!(resource1.width, 50);
assert_eq!(resource2.width, 75);
assert_ne!(resource1.offset, resource2.offset);
}
#[test]
fn test_get_nonexistent_image() {
let cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
assert!(cache.get(ImageId::new(0)).is_none());
assert!(cache.get(ImageId::new(999)).is_none());
}
#[test]
fn test_remove_image() {
let mut cache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
let id = cache.allocate(100, 100, 0).unwrap();
assert!(cache.get(id).is_some());
cache.deallocate(id);
assert!(cache.get(id).is_none());
}
#[test]
fn test_remove_nonexistent_image() {
let mut cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
cache.deallocate(ImageId::new(0));
cache.deallocate(ImageId::new(999));
}
#[test]
fn test_slot_reuse_after_remove() {
let mut cache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
let id1 = cache.allocate(50, 50, 0).unwrap();
let id2 = cache.allocate(60, 60, 0).unwrap();
let id3 = cache.allocate(70, 70, 0).unwrap();
assert_eq!(id1.as_u32(), 0);
assert_eq!(id2.as_u32(), 1);
assert_eq!(id3.as_u32(), 2);
cache.deallocate(id2);
assert!(cache.get(id2).is_none());
let id4 = cache.allocate(80, 80, 0).unwrap();
assert_eq!(id4.as_u32(), 1);
assert!(cache.get(id1).is_some());
assert!(cache.get(id3).is_some());
assert!(cache.get(id4).is_some());
assert_eq!(cache.get(id4).unwrap().width, 80);
}
#[test]
fn test_multiple_remove_and_reuse() {
let mut cache = ImageCache::new_with_config(AtlasConfig {
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
..Default::default()
});
let ids: Vec<_> = (0..5)
.map(|i| cache.allocate(100 + i * 10, 100 + i * 10, 0).unwrap())
.collect();
cache.deallocate(ids[1]);
cache.deallocate(ids[3]);
let new_id1 = cache.allocate(200, 200, 0).unwrap();
let new_id2 = cache.allocate(300, 300, 0).unwrap();
assert_eq!(new_id1.as_u32(), 3);
assert_eq!(new_id2.as_u32(), 1);
assert_ne!(new_id1.as_u32(), new_id2.as_u32());
}
}