Skip to main content

felis_render_core/
atlas.rs

1//! First-fit shelf packer for the glyph atlas. Shelves are never
2//! defragmented: when the atlas fills, the caller resets and rebuilds.
3
4use std::num::NonZeroU32;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Allocation {
8    pub x: u32,
9    pub y: u32,
10    pub width: u32,
11    pub height: u32,
12}
13
14#[derive(Debug)]
15pub struct ShelfAtlas {
16    width: u32,
17    height: u32,
18    shelves: Vec<Shelf>,
19    next_shelf_y: u32,
20}
21
22#[derive(Debug)]
23struct Shelf {
24    y_origin: u32,
25    height: u32,
26    x_cursor: u32,
27}
28
29impl ShelfAtlas {
30    #[must_use]
31    pub const fn new(width: NonZeroU32, height: NonZeroU32) -> Self {
32        Self {
33            width: width.get(),
34            height: height.get(),
35            shelves: Vec::new(),
36            next_shelf_y: 0,
37        }
38    }
39
40    #[must_use]
41    pub const fn width(&self) -> u32 {
42        self.width
43    }
44
45    #[must_use]
46    pub const fn height(&self) -> u32 {
47        self.height
48    }
49
50    pub fn alloc(&mut self, width: u32, height: u32) -> Option<Allocation> {
51        if width == 0 || height == 0 {
52            return Some(Allocation {
53                x: 0,
54                y: 0,
55                width,
56                height,
57            });
58        }
59        if width > self.width || height > self.height {
60            return None;
61        }
62        for shelf in &mut self.shelves {
63            if height <= shelf.height && shelf.x_cursor + width <= self.width {
64                let alloc = Allocation {
65                    x: shelf.x_cursor,
66                    y: shelf.y_origin,
67                    width,
68                    height,
69                };
70                shelf.x_cursor += width;
71                return Some(alloc);
72            }
73        }
74        if self.next_shelf_y.saturating_add(height) > self.height {
75            return None;
76        }
77        let y = self.next_shelf_y;
78        self.shelves.push(Shelf {
79            y_origin: y,
80            height,
81            x_cursor: width,
82        });
83        self.next_shelf_y += height;
84        Some(Allocation {
85            x: 0,
86            y,
87            width,
88            height,
89        })
90    }
91
92    pub fn reset(&mut self) {
93        self.shelves.clear();
94        self.next_shelf_y = 0;
95    }
96
97    #[must_use]
98    pub const fn is_empty(&self) -> bool {
99        self.shelves.is_empty()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    fn nz(v: u32) -> NonZeroU32 {
108        NonZeroU32::new(v).unwrap()
109    }
110
111    fn overlap(a: Allocation, b: Allocation) -> bool {
112        let ax2 = a.x + a.width;
113        let ay2 = a.y + a.height;
114        let bx2 = b.x + b.width;
115        let by2 = b.y + b.height;
116        a.x < bx2 && b.x < ax2 && a.y < by2 && b.y < ay2
117    }
118
119    #[test]
120    fn first_alloc_lands_at_origin() {
121        let mut atlas = ShelfAtlas::new(nz(64), nz(64));
122        let a = atlas.alloc(10, 12).unwrap();
123        assert_eq!(
124            a,
125            Allocation {
126                x: 0,
127                y: 0,
128                width: 10,
129                height: 12
130            }
131        );
132    }
133
134    #[test]
135    fn fills_first_shelf_then_opens_a_second() {
136        let mut atlas = ShelfAtlas::new(nz(20), nz(20));
137        let a = atlas.alloc(10, 8).unwrap();
138        let b = atlas.alloc(10, 8).unwrap();
139        assert_eq!(a.y, b.y, "two items of equal height share a shelf");
140        assert_eq!(b.x, 10);
141
142        let c = atlas.alloc(10, 5).unwrap();
143        assert_eq!(c.y, 8);
144        assert_eq!(c.x, 0);
145    }
146
147    #[test]
148    fn returns_none_when_atlas_is_full() {
149        let mut atlas = ShelfAtlas::new(nz(8), nz(8));
150        assert!(atlas.alloc(8, 8).is_some());
151        assert!(atlas.alloc(1, 1).is_none(), "no shelves left to open");
152    }
153
154    #[test]
155    fn rejects_request_larger_than_atlas() {
156        let mut atlas = ShelfAtlas::new(nz(16), nz(16));
157        assert!(atlas.alloc(17, 1).is_none());
158        assert!(atlas.alloc(1, 17).is_none());
159    }
160
161    #[test]
162    fn zero_sized_alloc_succeeds_without_consuming_space() {
163        let mut atlas = ShelfAtlas::new(nz(8), nz(8));
164        let a = atlas.alloc(0, 0).unwrap();
165        assert_eq!(
166            a,
167            Allocation {
168                x: 0,
169                y: 0,
170                width: 0,
171                height: 0
172            }
173        );
174        assert!(atlas.is_empty());
175    }
176
177    #[test]
178    fn reset_makes_the_atlas_reusable() {
179        let mut atlas = ShelfAtlas::new(nz(8), nz(8));
180        atlas.alloc(8, 8).unwrap();
181        assert!(atlas.alloc(1, 1).is_none());
182        atlas.reset();
183        assert!(atlas.alloc(8, 8).is_some());
184    }
185
186    #[test]
187    fn shelf_height_is_locked_by_first_item() {
188        let mut atlas = ShelfAtlas::new(nz(20), nz(30));
189        let _tall = atlas.alloc(5, 10).unwrap();
190        let next = atlas.alloc(5, 12).unwrap();
191        assert_eq!(next.y, 10, "new shelf opens immediately above the first");
192    }
193
194    #[test]
195    fn allocations_never_overlap_for_a_realistic_glyph_mix() {
196        let mut atlas = ShelfAtlas::new(nz(128), nz(128));
197        let sizes = [
198            (8, 12),
199            (8, 12),
200            (8, 12),
201            (12, 12),
202            (8, 14),
203            (8, 14),
204            (10, 16),
205            (10, 16),
206            (8, 12),
207            (8, 12),
208            (16, 20),
209            (8, 12),
210        ];
211        let mut allocs = Vec::new();
212        for (w, h) in sizes {
213            let a = atlas.alloc(w, h).expect("realistic mix should fit");
214            for prev in &allocs {
215                assert!(!overlap(a, *prev), "{a:?} overlaps {prev:?}");
216            }
217            assert!(a.x + a.width <= atlas.width());
218            assert!(a.y + a.height <= atlas.height());
219            allocs.push(a);
220        }
221    }
222}