use std::collections::{HashMap, VecDeque};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AtlasRect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct AtlasHandle(u64);
#[derive(Debug)]
struct Shelf {
y: u32,
height: u32,
cursor_x: u32,
}
pub struct TextureAtlas {
pub width: u32,
pub height: u32,
next_y: u32,
shelves: Vec<Shelf>,
allocations: HashMap<AtlasHandle, AtlasRect>,
lru_order: VecDeque<AtlasHandle>,
free_list: Vec<AtlasRect>,
next_id: u64,
used_area: u64,
}
impl TextureAtlas {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
next_y: 0,
shelves: Vec::new(),
allocations: HashMap::new(),
lru_order: VecDeque::new(),
free_list: Vec::new(),
next_id: 0,
used_area: 0,
}
}
pub fn insert(&mut self, w: u32, h: u32) -> Option<AtlasHandle> {
if w == 0 || h == 0 || w > self.width || h > self.height {
return None;
}
if let Some(rect) = self.alloc_from_free_list(w, h) {
return Some(self.register(rect));
}
if let Some(rect) = self.alloc_from_shelves(w, h) {
return Some(self.register(rect));
}
if let Some(rect) = self.alloc_new_shelf(w, h) {
return Some(self.register(rect));
}
self.evict_lru()?;
if let Some(rect) = self.alloc_from_free_list(w, h) {
return Some(self.register(rect));
}
if let Some(rect) = self.alloc_from_shelves(w, h) {
return Some(self.register(rect));
}
if let Some(rect) = self.alloc_new_shelf(w, h) {
return Some(self.register(rect));
}
None
}
pub fn get(&self, h: AtlasHandle) -> Option<AtlasRect> {
self.allocations.get(&h).copied()
}
pub fn utilization(&self) -> f32 {
let total = u64::from(self.width) * u64::from(self.height);
if total == 0 {
return 0.0;
}
(self.used_area as f32) / (total as f32)
}
pub fn resize(&mut self, new_width: u32, new_height: u32) {
self.width = new_width;
self.height = new_height;
self.next_y = 0;
self.shelves.clear();
self.allocations.clear();
self.lru_order.clear();
self.free_list.clear();
self.next_id = 0;
self.used_area = 0;
}
fn alloc_from_free_list(&mut self, w: u32, h: u32) -> Option<AtlasRect> {
let pos = self.free_list.iter().position(|r| r.w >= w && r.h >= h)?;
let free_rect = self.free_list.remove(pos);
Some(AtlasRect {
x: free_rect.x,
y: free_rect.y,
w,
h,
})
}
fn alloc_from_shelves(&mut self, w: u32, h: u32) -> Option<AtlasRect> {
for shelf in &mut self.shelves {
if shelf.height >= h && self.width.saturating_sub(shelf.cursor_x) >= w {
let rect = AtlasRect {
x: shelf.cursor_x,
y: shelf.y,
w,
h,
};
shelf.cursor_x += w;
return Some(rect);
}
}
None
}
fn alloc_new_shelf(&mut self, w: u32, h: u32) -> Option<AtlasRect> {
if self.height.saturating_sub(self.next_y) < h || self.width < w {
return None;
}
let shelf = Shelf {
y: self.next_y,
height: h,
cursor_x: w,
};
let rect = AtlasRect {
x: 0,
y: self.next_y,
w,
h,
};
self.next_y += h;
self.shelves.push(shelf);
Some(rect)
}
fn register(&mut self, rect: AtlasRect) -> AtlasHandle {
let handle = AtlasHandle(self.next_id);
self.next_id += 1;
self.used_area += u64::from(rect.w) * u64::from(rect.h);
self.allocations.insert(handle, rect);
self.lru_order.push_back(handle);
handle
}
fn evict_lru(&mut self) -> Option<()> {
loop {
let candidate = self.lru_order.pop_front()?;
if let Some(rect) = self.allocations.remove(&candidate) {
self.used_area = self
.used_area
.saturating_sub(u64::from(rect.w) * u64::from(rect.h));
self.free_list.push(rect);
return Some(());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atlas_packs_100_rects_no_overlap() {
let mut atlas = TextureAtlas::new(128, 128);
let mut rects: Vec<(AtlasHandle, AtlasRect)> = Vec::new();
for _ in 0..100 {
if let Some(h) = atlas.insert(4, 4) {
let r = atlas.get(h).expect("handle must be valid");
rects.push((h, r));
}
}
assert!(!rects.is_empty());
for i in 0..rects.len() {
for j in (i + 1)..rects.len() {
let a = rects[i].1;
let b = rects[j].1;
let overlap =
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
assert!(!overlap, "rects {i} and {j} overlap: {a:?} vs {b:?}");
}
}
}
#[test]
fn atlas_utilization_above_threshold() {
let mut atlas = TextureAtlas::new(32, 32);
let mut count = 0u32;
while atlas.insert(1, 1).is_some() {
count += 1;
if count > 1024 * 4 {
break; }
}
assert!(
atlas.utilization() >= 0.70,
"utilization was {}",
atlas.utilization()
);
}
#[test]
fn atlas_lru_eviction_keeps_invariants() {
let mut atlas = TextureAtlas::new(4, 4);
let h0 = atlas.insert(4, 1).expect("first insert");
let h1 = atlas.insert(4, 1).expect("second insert");
let h2 = atlas.insert(4, 1).expect("third insert");
let h3 = atlas.insert(4, 1).expect("fourth insert");
let h4 = atlas.insert(4, 1).expect("insert with eviction");
assert!(
atlas.get(h0).is_none(),
"LRU handle h0 must have been evicted"
);
assert!(atlas.get(h4).is_some(), "new handle h4 must be live");
assert!(atlas.get(h1).is_some());
assert!(atlas.get(h2).is_some());
assert!(atlas.get(h3).is_some());
let live_rects: Vec<AtlasRect> = [h1, h2, h3, h4]
.iter()
.filter_map(|&h| atlas.get(h))
.collect();
for i in 0..live_rects.len() {
for j in (i + 1)..live_rects.len() {
let a = live_rects[i];
let b = live_rects[j];
let overlap =
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
assert!(!overlap, "live rects {i} and {j} overlap: {a:?} vs {b:?}");
}
}
}
#[test]
fn atlas_resize_clears() {
let mut atlas = TextureAtlas::new(16, 16);
let h = atlas.insert(4, 4).expect("insert before resize");
atlas.resize(32, 32);
assert!(
atlas.get(h).is_none(),
"handle must be invalid after resize"
);
assert!(
(atlas.utilization() - 0.0).abs() < f32::EPSILON,
"utilization must be 0 after resize"
);
assert!(
atlas.insert(4, 4).is_some(),
"insert after resize must succeed"
);
}
}