emblema_text/atlas.rs
1//! Packing rasterized glyph coverage into one texture.
2//!
3//! # What this does and does not do
4//!
5//! It takes coverage bitmaps and answers where in a texture each one sits. It
6//! does not rasterize them: turning an outline into coverage needs a font
7//! parser, and font parsing is out of scope here — bring `swash` or
8//! `ttf-parser` and hand the result over. That boundary is what lets the atlas
9//! be tested with no font anywhere in the tree, against bitmaps whose contents
10//! are known exactly.
11//!
12//! # Shelf packing
13//!
14//! Glyphs are packed into horizontal shelves: a new glyph goes on the first
15//! shelf tall enough for it with room to its right, and starts a new shelf
16//! otherwise. That wastes the space above short glyphs on a tall shelf, which
17//! a skyline or a max-rects packer would recover.
18//!
19//! It is the right trade for glyphs specifically. A run of text is a stream of
20//! boxes of very similar height, so shelves fill densely in practice, and the
21//! packer runs once per glyph per size rather than per frame. A packer with
22//! better worst-case density would cost more per insertion and more to read for
23//! a gain that the input's own shape mostly removes.
24//!
25//! # Making room
26//!
27//! Shelves cannot free a glyph in place: a hole in the middle of one is not
28//! reusable by anything but a glyph of the same height, and tracking holes is
29//! most of what makes a general packer expensive. So room is made by compacting
30//! — keeping the glyphs this frame has asked for, discarding the rest, and
31//! repacking from scratch.
32//!
33//! That is a heavier operation than freeing one entry and a much simpler one to
34//! be sure of, and its cost is bounded by how often it can happen: it runs only
35//! when an insertion would otherwise fail, and it cannot run twice in a frame
36//! without the second one failing outright, because everything left after the
37//! first is something this frame needs. Text that genuinely needs more than an
38//! atlas holds is a case for a second page, not for a cleverer packer.
39
40use std::collections::HashMap;
41
42/// Where a glyph sits in the atlas, in texels.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub struct AtlasRect {
45 pub x: u32,
46 pub y: u32,
47 pub width: u32,
48 pub height: u32,
49}
50
51impl AtlasRect {
52 /// The rectangle in texture coordinates, from zero to one.
53 ///
54 /// Returned as `[left, top, right, bottom]`, with V running downward to
55 /// match the row order the texture was uploaded in.
56 pub fn uv(&self, atlas: u32) -> [f32; 4] {
57 let scale = 1.0 / atlas as f32;
58 [
59 self.x as f32 * scale,
60 self.y as f32 * scale,
61 (self.x + self.width) as f32 * scale,
62 (self.y + self.height) as f32 * scale,
63 ]
64 }
65}
66
67/// What identifies a glyph in the atlas.
68///
69/// A font identifier alongside the glyph index, because glyph indices are
70/// per font and two fonts will disagree about what index seven means. Size is
71/// in whole pixels: a cache keyed by a float would miss on values that differ
72/// only in their last bit, and rasterizing at a rounded size is what a caller
73/// is doing anyway.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
75pub struct GlyphKey {
76 pub font: u64,
77 pub glyph: u16,
78 pub size: u16,
79}
80
81/// A rasterized glyph, as a caller supplies it.
82#[derive(Debug, Clone, PartialEq)]
83pub struct Coverage {
84 pub width: u32,
85 pub height: u32,
86 /// One byte per texel, row-major from the top, tightly packed.
87 pub texels: Vec<u8>,
88}
89
90impl Coverage {
91 /// Whether the dimensions and the buffer agree.
92 pub fn is_consistent(&self) -> bool {
93 self.texels.len() as u64 == self.width as u64 * self.height as u64
94 }
95}
96
97/// Why a glyph could not be added.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum AtlasError {
100 /// The coverage's dimensions and its buffer disagree.
101 Inconsistent,
102 /// The glyph does not fit, even in an empty atlas.
103 TooLarge,
104 /// The atlas is full.
105 ///
106 /// Distinct from `TooLarge` because the remedies differ: this one is fixed
107 /// by evicting or by growing, and that one never is.
108 Full,
109}
110
111impl std::fmt::Display for AtlasError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 Self::Inconsistent => write!(f, "coverage dimensions do not match its buffer"),
115 Self::TooLarge => write!(f, "glyph is larger than the atlas"),
116 Self::Full => write!(f, "atlas is full"),
117 }
118 }
119}
120
121impl std::error::Error for AtlasError {}
122
123/// One shelf: a horizontal band, filled left to right.
124#[derive(Debug, Clone, Copy)]
125struct Shelf {
126 top: u32,
127 height: u32,
128 /// Where the next glyph on this shelf would start.
129 used: u32,
130}
131
132/// A square atlas of glyph coverage, and where each glyph landed.
133///
134/// Holds the texels itself rather than a device texture, so that packing can be
135/// exercised and reasoned about without a GPU. Whoever owns the device uploads
136/// [`Atlas::texels`] when [`Atlas::is_dirty`] says something changed.
137#[derive(Debug)]
138pub struct Atlas {
139 size: u32,
140 texels: Vec<u8>,
141 shelves: Vec<Shelf>,
142 placed: HashMap<GlyphKey, Placed>,
143 frame: u64,
144 dirty: bool,
145 compactions: u64,
146 /// The largest this may grow to.
147 ///
148 /// A caller's to choose, because the ceiling that matters is the device's
149 /// maximum texture size and the atlas has no way to ask.
150 limit: u32,
151 growths: u64,
152}
153
154/// Where a glyph is, and when it was last asked for.
155#[derive(Debug, Clone, Copy)]
156struct Placed {
157 rect: AtlasRect,
158 /// The frame this glyph was most recently inserted in.
159 ///
160 /// Insertion rather than lookup, because insertion is what a caller does
161 /// for every glyph of every run each frame — that is the usage the atlas is
162 /// built around — and marking on lookup would need a unique borrow at the
163 /// point where a run is being recorded from a shared one.
164 used: u64,
165}
166
167/// Blank texels around each glyph.
168///
169/// One texel is enough and less is not. A linear filter samples a two-by-two
170/// neighborhood, so a glyph flush against its neighbor bleeds that neighbor's
171/// coverage into its own edge under any magnification. The padding is cleared
172/// rather than merely skipped, since the space may hold an evicted glyph.
173const PADDING: u32 = 1;
174
175impl Atlas {
176 /// An empty atlas of `size` by `size` texels.
177 pub fn new(size: u32) -> Self {
178 // Four thousand and ninety-six is the smallest maximum texture size
179 // this project's supported devices are required to offer, so it is the
180 // largest an atlas can grow to without asking. A caller that knows the
181 // device says so.
182 Self::with_limit(size, 4096)
183 }
184
185 /// An atlas that may grow up to `limit` texels on a side.
186 ///
187 /// A limit below the starting size means it never grows, which is a
188 /// reasonable thing to ask for and not an error.
189 pub fn with_limit(size: u32, limit: u32) -> Self {
190 Self {
191 size,
192 texels: vec![0; (size as usize) * (size as usize)],
193 shelves: Vec::new(),
194 placed: HashMap::new(),
195 frame: 0,
196 dirty: false,
197 compactions: 0,
198 limit,
199 growths: 0,
200 }
201 }
202
203 pub fn size(&self) -> u32 {
204 self.size
205 }
206
207 /// One byte of coverage per texel, row-major from the top.
208 pub fn texels(&self) -> &[u8] {
209 &self.texels
210 }
211
212 /// Whether anything has been added since [`Atlas::mark_clean`].
213 ///
214 /// The upload is the caller's to make, because only they know which device
215 /// the texture lives on and when in the frame it is safe to write.
216 pub fn is_dirty(&self) -> bool {
217 self.dirty
218 }
219
220 pub fn mark_clean(&mut self) {
221 self.dirty = false;
222 }
223
224 pub fn len(&self) -> usize {
225 self.placed.len()
226 }
227
228 pub fn is_empty(&self) -> bool {
229 self.placed.is_empty()
230 }
231
232 /// Where a glyph sits, if it is present.
233 pub fn get(&self, key: GlyphKey) -> Option<AtlasRect> {
234 self.placed.get(&key).map(|placed| placed.rect)
235 }
236
237 /// Begin a new frame, after which glyphs not inserted again are evictable.
238 ///
239 /// Nothing happens here but a counter moving. A caller that never calls it
240 /// keeps every glyph forever, which is correct rather than a leak: without
241 /// frame boundaries nothing is stale, and an atlas that filled would be
242 /// genuinely out of room.
243 pub fn begin_frame(&mut self) {
244 self.frame += 1;
245 }
246
247 /// How many times room has been made by repacking.
248 ///
249 /// Worth reporting because it is the number that says the atlas is too
250 /// small for the text going through it: a compaction or two as a working
251 /// set settles is ordinary, and one per frame means every frame is
252 /// repacking everything.
253 pub fn compactions(&self) -> u64 {
254 self.compactions
255 }
256
257 /// How many times the atlas has doubled.
258 ///
259 /// Every growth reallocates and repacks everything, so a count that keeps
260 /// climbing says the atlas started far too small. It settles once the
261 /// working set fits.
262 pub fn growths(&self) -> u64 {
263 self.growths
264 }
265
266 /// The largest this may grow to.
267 pub fn limit(&self) -> u32 {
268 self.limit
269 }
270
271 /// Add a glyph, or return where it already is.
272 ///
273 /// Idempotent, so a caller can insert every glyph of every run each frame
274 /// and pay only for the ones that are new. That is the usage this is for:
275 /// deciding what is already present is exactly what an atlas is.
276 pub fn insert(&mut self, key: GlyphKey, coverage: &Coverage) -> Result<AtlasRect, AtlasError> {
277 if let Some(placed) = self.placed.get_mut(&key) {
278 // Refreshed rather than merely found: a glyph asked for again this
279 // frame is one this frame needs, and that is what keeps it from
280 // being the one compaction discards.
281 placed.used = self.frame;
282 return Ok(placed.rect);
283 }
284 if !coverage.is_consistent() {
285 return Err(AtlasError::Inconsistent);
286 }
287 // A glyph with no area still has a position, which is what lets a space
288 // travel through a run like any other glyph rather than being a case
289 // every caller has to remember to skip.
290 if coverage.width == 0 || coverage.height == 0 {
291 let rect = AtlasRect {
292 x: 0,
293 y: 0,
294 width: 0,
295 height: 0,
296 };
297 self.placed.insert(
298 key,
299 Placed {
300 rect,
301 used: self.frame,
302 },
303 );
304 return Ok(rect);
305 }
306
307 let rect = match self.allocate(coverage.width, coverage.height) {
308 Ok(rect) => rect,
309 // Only fullness is worth retrying. A glyph larger than the atlas
310 // does not fit an empty one either, and compacting to discover that
311 // would throw away everything for nothing.
312 // Compaction first, then growth. Discarding what nothing has
313 // asked for is far cheaper than doubling, and an atlas that grew
314 // before compacting would keep the memory it had stopped needing.
315 Err(AtlasError::Full) => {
316 if !self.compact() && !self.grow() {
317 return Err(AtlasError::Full);
318 }
319 self.allocate(coverage.width, coverage.height)?
320 }
321 Err(e) => return Err(e),
322 };
323 for row in 0..coverage.height {
324 let from = (row * coverage.width) as usize;
325 let to = ((rect.y + row) * self.size + rect.x) as usize;
326 self.texels[to..to + coverage.width as usize]
327 .copy_from_slice(&coverage.texels[from..from + coverage.width as usize]);
328 }
329 self.placed.insert(
330 key,
331 Placed {
332 rect,
333 used: self.frame,
334 },
335 );
336 self.dirty = true;
337 Ok(rect)
338 }
339
340 /// Discard glyphs this frame has not asked for, and repack the rest.
341 ///
342 /// Returns whether anything was freed. False means every glyph present is
343 /// one this frame needs, so there is nothing to give up and the atlas is
344 /// genuinely too small for the text going through it.
345 fn compact(&mut self) -> bool {
346 let survivors: Vec<(GlyphKey, Placed)> = self
347 .placed
348 .iter()
349 .filter(|(_, placed)| placed.used == self.frame)
350 .map(|(key, placed)| (*key, *placed))
351 .collect();
352 if survivors.len() == self.placed.len() {
353 return false;
354 }
355
356 // Copied out before anything is cleared, since the atlas's own texels
357 // are the only place a glyph's coverage still exists — the bitmaps a
358 // caller supplied were borrowed and are long gone.
359 let mut coverage: Vec<(GlyphKey, Coverage)> = survivors
360 .iter()
361 .map(|(key, placed)| (*key, self.extract(placed.rect)))
362 .collect();
363 repacking_order(&mut coverage);
364
365 self.texels.fill(0);
366 self.shelves.clear();
367 self.placed.clear();
368 self.compactions += 1;
369 self.dirty = true;
370
371 for (key, coverage) in coverage {
372 // Everything here fitted a moment ago and is being packed into an
373 // atlas holding a subset of what it held then, so this cannot fail.
374 // If it somehow does, dropping the glyph is better than refusing
375 // the insertion that triggered the compaction: the caller will
376 // offer it again next frame.
377 let _ = self.insert(key, &coverage);
378 }
379 true
380 }
381
382 /// Double the atlas and repack everything into it.
383 ///
384 /// Returns whether it grew. False means it is already at its limit, which
385 /// is the point at which being full is genuinely full.
386 ///
387 /// Everything is kept, not only what this frame asked for: growing is what
388 /// happens when nothing was stale, so there is nothing to discard, and a
389 /// glyph dropped here would be re-rasterized by the caller for no reason.
390 fn grow(&mut self) -> bool {
391 if self.size >= self.limit {
392 return false;
393 }
394 let grown = (self.size.saturating_mul(2)).min(self.limit);
395 if grown <= self.size {
396 return false;
397 }
398
399 // Read back before the texels are replaced: the atlas is the only place
400 // a glyph's coverage still exists, the bitmaps a caller supplied having
401 // been borrowed.
402 let mut kept: Vec<(GlyphKey, Coverage)> = self
403 .placed
404 .iter()
405 .map(|(key, placed)| (*key, self.extract(placed.rect)))
406 .collect();
407 repacking_order(&mut kept);
408
409 self.size = grown;
410 self.texels = vec![0; (grown as usize) * (grown as usize)];
411 self.shelves.clear();
412 self.placed.clear();
413 self.growths += 1;
414 self.dirty = true;
415
416 for (key, coverage) in kept {
417 // Everything fitted the smaller atlas, so it fits this one.
418 let _ = self.insert(key, &coverage);
419 }
420 true
421 }
422
423 /// Read a glyph's coverage back out of the atlas.
424 fn extract(&self, rect: AtlasRect) -> Coverage {
425 let mut texels = Vec::with_capacity((rect.width * rect.height) as usize);
426 for row in 0..rect.height {
427 let from = ((rect.y + row) * self.size + rect.x) as usize;
428 texels.extend_from_slice(&self.texels[from..from + rect.width as usize]);
429 }
430 Coverage {
431 width: rect.width,
432 height: rect.height,
433 texels,
434 }
435 }
436
437 /// Find room for a glyph and reserve it.
438 fn allocate(&mut self, width: u32, height: u32) -> Result<AtlasRect, AtlasError> {
439 let padded_width = width + PADDING;
440 let padded_height = height + PADDING;
441 if padded_width > self.size || padded_height > self.size {
442 return Err(AtlasError::TooLarge);
443 }
444
445 // The first shelf tall enough with room to the right. Shelves are not
446 // sorted by height, so this is a scan; a run of text produces shelves
447 // of similar height and few of them, which is why that is not worth
448 // indexing.
449 for shelf in &mut self.shelves {
450 if shelf.height >= padded_height && self.size - shelf.used >= padded_width {
451 let rect = AtlasRect {
452 x: shelf.used,
453 y: shelf.top,
454 width,
455 height,
456 };
457 shelf.used += padded_width;
458 return Ok(rect);
459 }
460 }
461
462 let top = self
463 .shelves
464 .last()
465 .map_or(0, |shelf| shelf.top + shelf.height);
466 if top + padded_height > self.size {
467 return Err(AtlasError::Full);
468 }
469 self.shelves.push(Shelf {
470 top,
471 height: padded_height,
472 used: padded_width,
473 });
474 Ok(AtlasRect {
475 x: 0,
476 y: top,
477 width,
478 height,
479 })
480 }
481}
482
483/// The order glyphs are packed in when the atlas is rebuilt.
484///
485/// Tallest first, which is what shelf packing wants: a shelf is as tall as the
486/// tallest glyph on it, so a short glyph landing first opens a shelf that a
487/// tall one cannot use and every tall glyph after it opens another. Width and
488/// then the key break ties, the key because two glyphs of the same size have to
489/// land in the same order every run.
490///
491/// Determinism is the reason this exists at all, not the packing. Both rebuilds
492/// took their order from a `HashMap`, whose iteration order Rust seeds per
493/// process -- so the same text through the same atlas produced a different
494/// layout each run, and, worse, a different *number of compactions*: measured
495/// over four runs of one fixed sequence, three compacted once and one did not
496/// compact at all. Anything asserting on those counters was a coin flip, and
497/// the comment claiming a repack "cannot fail" rested on packing a subset in an
498/// order nothing guaranteed.
499fn repacking_order(glyphs: &mut [(GlyphKey, Coverage)]) {
500 glyphs.sort_by(|(left_key, left), (right_key, right)| {
501 right
502 .height
503 .cmp(&left.height)
504 .then(right.width.cmp(&left.width))
505 .then(left_key.cmp(right_key))
506 });
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 fn solid(width: u32, height: u32, value: u8) -> Coverage {
514 Coverage {
515 width,
516 height,
517 texels: vec![value; (width * height) as usize],
518 }
519 }
520
521 fn key(glyph: u16) -> GlyphKey {
522 GlyphKey {
523 font: 1,
524 glyph,
525 size: 16,
526 }
527 }
528
529 #[test]
530 fn a_compaction_lays_its_survivors_out_tallest_first() {
531 // The observable consequence of packing a rebuild in a defined order,
532 // and the reason there is one. Shelves are opened top-down, so if the
533 // tallest survivor is placed first it takes the first shelf, and every
534 // survivor after it sits on that shelf or a later one -- which makes
535 // "sorted by height descending" and "sorted by row" the same ordering.
536 //
537 // What this stands in for is a property that cannot be asserted from
538 // inside a single run. Both rebuilds took their order from a `HashMap`,
539 // seeded per process, so the same text packed differently every run;
540 // measured over five runs of one sequence, the atlas held thirty-six
541 // glyphs three times and thirty-five twice, a glyph lost to the hasher.
542 // That loss is a probabilistic consequence and a test for it would
543 // catch the fault only sometimes, which is worse than not testing it.
544 // This asks the deterministic thing that causes it instead.
545 let mut atlas = Atlas::with_limit(64, 64);
546 let sizes: Vec<(u32, u32)> = (0..40u32)
547 .map(|i| (3 + (i * 7) % 11, 3 + (i * 5) % 13))
548 .collect();
549 for (i, (w, h)) in sizes.iter().enumerate() {
550 let _ = atlas.insert(key(i as u16), &solid(*w, *h, 200));
551 }
552 atlas.begin_frame();
553 let mut survivors = Vec::new();
554 for i in (0..40usize).step_by(2) {
555 let (w, h) = sizes[i];
556 if atlas.insert(key(i as u16), &solid(w, h, 200)).is_ok() {
557 survivors.push((i as u16, h));
558 }
559 }
560 // One more, which needs the room the stale glyphs are holding.
561 let _ = atlas.insert(key(90), &solid(6, 9, 200));
562 assert_eq!(
563 atlas.compactions(),
564 1,
565 "nothing compacted, so this proves nothing"
566 );
567
568 // The tallest survivor is placed first into an empty atlas, so it
569 // opens the first shelf and sits at its top. Nothing stronger holds:
570 // shelves are scanned for the first that fits, so a shorter glyph
571 // placed later can land on an earlier shelf, and rows do go backward.
572 survivors.sort_by(|(left_key, left), (right_key, right)| {
573 right.cmp(left).then(left_key.cmp(right_key))
574 });
575 let (tallest, height) = survivors[0];
576 let rect = atlas
577 .get(key(tallest))
578 .expect("the tallest survivor should have been kept");
579 assert_eq!(
580 rect.y, 0,
581 "glyph {tallest}, the tallest survivor at {height}, landed at row \
582 {} rather than opening the first shelf -- so the rebuild placed \
583 something else before it",
584 rect.y
585 );
586 }
587
588 #[test]
589 fn a_rebuild_packs_the_tallest_glyphs_first() {
590 // A shelf is as tall as the tallest glyph on it, so a short glyph
591 // landing first opens a shelf a tall one cannot use, and every tall
592 // glyph after it opens another. Sorting by height is what makes a
593 // rebuild pack at least as well as the atlas it is rebuilding.
594 let mut glyphs = vec![
595 (key(1), solid(4, 4, 1)),
596 (key(2), solid(4, 20, 2)),
597 (key(3), solid(9, 12, 3)),
598 (key(4), solid(2, 20, 4)),
599 ];
600 repacking_order(&mut glyphs);
601 let heights: Vec<u32> = glyphs.iter().map(|(_, c)| c.height).collect();
602 assert_eq!(heights, vec![20, 20, 12, 4], "tallest first");
603 // Ties broken by width and then by the key, so two glyphs of a size
604 // land in the same order every run rather than in whichever order they
605 // arrived.
606 assert_eq!(
607 (glyphs[0].1.width, glyphs[1].1.width),
608 (4, 2),
609 "a tie in height is broken by width"
610 );
611 }
612
613 #[test]
614 fn an_inserted_glyph_can_be_found_again() {
615 let mut atlas = Atlas::new(64);
616 let rect = atlas.insert(key(1), &solid(8, 10, 200)).expect("insert");
617 assert_eq!(atlas.get(key(1)), Some(rect));
618 assert_eq!(rect.width, 8);
619 assert_eq!(rect.height, 10);
620 assert_eq!(atlas.len(), 1);
621 }
622
623 #[test]
624 fn inserting_the_same_glyph_twice_returns_the_same_place() {
625 // The usage this exists for: a caller inserts every glyph of every run
626 // each frame and pays only for the new ones. A second insertion that
627 // allocated again would fill the atlas in proportion to frames drawn
628 // rather than to distinct glyphs.
629 let mut atlas = Atlas::new(64);
630 let first = atlas.insert(key(1), &solid(8, 10, 200)).expect("first");
631 let second = atlas.insert(key(1), &solid(8, 10, 200)).expect("second");
632 assert_eq!(first, second);
633 assert_eq!(atlas.len(), 1);
634 }
635
636 #[test]
637 fn glyphs_differing_only_in_font_or_size_are_distinct() {
638 // Glyph indices are per font, and a glyph rasterized at one size is not
639 // the same picture as at another. A key that ignored either would serve
640 // one glyph's coverage for another's.
641 let mut atlas = Atlas::new(64);
642 let a = atlas.insert(key(1), &solid(8, 8, 255)).expect("a");
643 let b = atlas
644 .insert(GlyphKey { font: 2, ..key(1) }, &solid(8, 8, 255))
645 .expect("b");
646 let c = atlas
647 .insert(GlyphKey { size: 32, ..key(1) }, &solid(8, 8, 255))
648 .expect("c");
649 assert_ne!(a, b);
650 assert_ne!(a, c);
651 assert_ne!(b, c);
652 assert_eq!(atlas.len(), 3);
653 }
654
655 #[test]
656 fn coverage_lands_where_the_rectangle_says() {
657 let mut atlas = Atlas::new(16);
658 // A bitmap with a distinct value per texel, so a transposed or
659 // off-by-one copy produces a different atlas rather than the same one.
660 let coverage = Coverage {
661 width: 3,
662 height: 2,
663 texels: vec![10, 20, 30, 40, 50, 60],
664 };
665 let rect = atlas.insert(key(1), &coverage).expect("insert");
666 for row in 0..coverage.height {
667 for column in 0..coverage.width {
668 let at = ((rect.y + row) * atlas.size() + rect.x + column) as usize;
669 assert_eq!(
670 atlas.texels()[at],
671 coverage.texels[(row * coverage.width + column) as usize],
672 "texel ({column}, {row}) landed wrong"
673 );
674 }
675 }
676 }
677
678 #[test]
679 fn glyphs_do_not_touch_each_other() {
680 // A linear filter samples a neighborhood, so a glyph flush against its
681 // neighbor bleeds that neighbor's coverage into its own edge. This
682 // asserts the gap rather than the filtering, since the filtering is the
683 // device's business.
684 let mut atlas = Atlas::new(64);
685 let a = atlas.insert(key(1), &solid(8, 8, 255)).expect("a");
686 let b = atlas.insert(key(2), &solid(8, 8, 255)).expect("b");
687 assert!(
688 b.x >= a.x + a.width + PADDING || b.y >= a.y + a.height + PADDING,
689 "{a:?} and {b:?} are adjacent"
690 );
691 }
692
693 #[test]
694 fn a_second_shelf_starts_below_the_first() {
695 let mut atlas = Atlas::new(32);
696 // Three glyphs eleven wide with padding do not fit across thirty-two.
697 let first = atlas.insert(key(1), &solid(11, 6, 255)).expect("first");
698 let second = atlas.insert(key(2), &solid(11, 6, 255)).expect("second");
699 let third = atlas.insert(key(3), &solid(11, 6, 255)).expect("third");
700 assert_eq!(first.y, second.y, "the first two share a shelf");
701 assert!(third.y > first.y, "the third started a new shelf");
702 assert_eq!(third.x, 0, "a new shelf starts at the left edge");
703 }
704
705 #[test]
706 fn a_short_glyph_reuses_a_taller_shelf() {
707 // The trade shelf packing makes: the space above a short glyph on a
708 // tall shelf is lost, and in exchange placement is a scan of a few
709 // entries. A run of text is boxes of similar height, which is what
710 // keeps that loss small.
711 let mut atlas = Atlas::new(64);
712 let tall = atlas.insert(key(1), &solid(8, 20, 255)).expect("tall");
713 let short = atlas.insert(key(2), &solid(8, 4, 255)).expect("short");
714 assert_eq!(tall.y, short.y, "the short glyph opened a new shelf");
715 }
716
717 #[test]
718 fn a_glyph_larger_than_the_atlas_is_reported_as_such() {
719 // Distinct from being full, because the remedies differ: growing or
720 // evicting fixes one and never fixes the other.
721 let mut atlas = Atlas::new(16);
722 assert_eq!(
723 atlas.insert(key(1), &solid(16, 16, 255)),
724 Err(AtlasError::TooLarge),
725 "a glyph needing padding beyond the edge should not fit"
726 );
727 assert_eq!(
728 atlas.insert(key(2), &solid(64, 4, 255)),
729 Err(AtlasError::TooLarge)
730 );
731 }
732
733 #[test]
734 fn a_full_atlas_says_so_rather_than_overwriting() {
735 let mut atlas = fixed(16);
736 let mut inserted = 0;
737 for glyph in 0..64u16 {
738 match atlas.insert(key(glyph), &solid(6, 6, 255)) {
739 Ok(_) => inserted += 1,
740 Err(e) => {
741 assert_eq!(e, AtlasError::Full);
742 break;
743 }
744 }
745 }
746 assert!(inserted > 0, "nothing fit at all");
747 assert!(inserted < 64, "everything fit, so fullness went untested");
748
749 // And every glyph that did fit is still where it was put: running out
750 // of room must not disturb what is already there.
751 let mut seen = std::collections::HashSet::new();
752 for glyph in 0..inserted as u16 {
753 let rect = atlas.get(key(glyph)).expect("still present");
754 assert!(seen.insert((rect.x, rect.y)), "two glyphs share a place");
755 }
756 }
757
758 #[test]
759 fn a_glyph_with_no_area_is_placed_rather_than_refused() {
760 // A space has a position in a run like any other glyph, and making
761 // every caller remember to skip it is how one of them forgets.
762 let mut atlas = Atlas::new(16);
763 let rect = atlas.insert(key(1), &solid(0, 0, 0)).expect("insert");
764 assert_eq!(rect.width, 0);
765 assert!(!atlas.is_dirty(), "an empty glyph changed no texels");
766 }
767
768 #[test]
769 fn inconsistent_coverage_is_refused() {
770 let mut atlas = Atlas::new(16);
771 let bad = Coverage {
772 width: 4,
773 height: 4,
774 texels: vec![0; 3],
775 };
776 assert_eq!(atlas.insert(key(1), &bad), Err(AtlasError::Inconsistent));
777 }
778
779 #[test]
780 fn the_dirty_flag_tracks_whether_an_upload_is_owed() {
781 let mut atlas = Atlas::new(32);
782 assert!(!atlas.is_dirty(), "an empty atlas owes no upload");
783 atlas.insert(key(1), &solid(4, 4, 255)).expect("insert");
784 assert!(atlas.is_dirty());
785 atlas.mark_clean();
786 assert!(!atlas.is_dirty());
787 // A repeat insertion changes no texels, so it owes nothing either.
788 atlas.insert(key(1), &solid(4, 4, 255)).expect("again");
789 assert!(
790 !atlas.is_dirty(),
791 "a glyph already present dirtied the atlas"
792 );
793 }
794
795 /// An atlas that cannot grow, for the behaviors that only appear at the
796 /// limit: compaction, and being genuinely full.
797 fn fixed(size: u32) -> Atlas {
798 Atlas::with_limit(size, size)
799 }
800
801 /// Insert glyphs until the atlas grows, returning how many went in.
802 ///
803 /// The counterpart of `fill` for an atlas that can grow, where "until it
804 /// refuses" never arrives until the limit.
805 fn fill_until_growth(atlas: &mut Atlas, from: u16) -> u16 {
806 let mut glyph = from;
807 while atlas.growths() == 0 {
808 atlas
809 .insert(key(glyph), &solid(6, 6, 255))
810 .unwrap_or_else(|e| panic!("insert {glyph}: {e}"));
811 glyph += 1;
812 assert!(glyph < from + 200, "the atlas never grew");
813 }
814 glyph - from
815 }
816
817 /// Fill an atlas until it refuses, returning how many glyphs fitted.
818 fn fill(atlas: &mut Atlas, from: u16) -> u16 {
819 let mut glyph = from;
820 while atlas.insert(key(glyph), &solid(6, 6, 255)).is_ok() {
821 glyph += 1;
822 assert!(glyph < from + 200, "the atlas never filled");
823 }
824 glyph - from
825 }
826
827 #[test]
828 fn a_full_atlas_makes_room_for_what_the_new_frame_needs() {
829 let mut atlas = fixed(32);
830 let fitted = fill(&mut atlas, 0);
831 assert!(
832 fitted > 2,
833 "only {fitted} glyphs fitted; too few to evict from"
834 );
835
836 // A new frame, and nothing from the old one asked for again. Everything
837 // present is now stale, so an insertion that would have failed makes
838 // room instead.
839 atlas.begin_frame();
840 let rect = atlas
841 .insert(key(500), &solid(6, 6, 255))
842 .expect("a stale atlas should make room");
843 assert_eq!(rect.width, 6);
844 assert_eq!(atlas.compactions(), 1);
845 assert_eq!(atlas.len(), 1, "the stale glyphs were not discarded");
846 }
847
848 #[test]
849 fn compaction_keeps_the_glyphs_this_frame_asked_for() {
850 let mut atlas = fixed(32);
851 let fitted = fill(&mut atlas, 0);
852
853 // A new frame that asks for two of the old glyphs before filling up.
854 // Those two are what this frame needs; the rest are not.
855 atlas.begin_frame();
856 let kept: Vec<u16> = vec![0, 1];
857 for glyph in &kept {
858 atlas
859 .insert(key(*glyph), &solid(6, 6, 255))
860 .expect("refresh");
861 }
862 atlas
863 .insert(key(500), &solid(6, 6, 255))
864 .expect("should make room");
865
866 assert_eq!(atlas.compactions(), 1);
867 for glyph in &kept {
868 assert!(
869 atlas.get(key(*glyph)).is_some(),
870 "glyph {glyph} was asked for this frame and discarded anyway"
871 );
872 }
873 assert!(
874 atlas.get(key(fitted - 1)).is_none(),
875 "a glyph nothing asked for survived"
876 );
877 }
878
879 #[test]
880 fn a_glyph_kept_through_compaction_keeps_its_coverage() {
881 // The coverage a caller supplied was borrowed and is long gone, so
882 // repacking has to read it back out of the atlas. Getting that wrong
883 // gives a glyph that is present, addressable, and blank.
884 let mut atlas = fixed(32);
885 let distinct = Coverage {
886 width: 3,
887 height: 2,
888 texels: vec![11, 22, 33, 44, 55, 66],
889 };
890 atlas.insert(key(0), &distinct).expect("insert");
891 fill(&mut atlas, 1);
892
893 atlas.begin_frame();
894 atlas.insert(key(0), &distinct).expect("refresh");
895 atlas
896 .insert(key(500), &solid(6, 6, 255))
897 .expect("should make room");
898
899 let rect = atlas.get(key(0)).expect("survived");
900 let mut got = Vec::new();
901 for row in 0..rect.height {
902 let from = ((rect.y + row) * atlas.size() + rect.x) as usize;
903 got.extend_from_slice(&atlas.texels()[from..from + rect.width as usize]);
904 }
905 assert_eq!(got, distinct.texels, "coverage was lost in repacking");
906 }
907
908 #[test]
909 fn an_atlas_full_of_glyphs_this_frame_needs_reports_full() {
910 // Compaction can free only what nothing has asked for, so an atlas
911 // whose every glyph is in use this frame is genuinely out of room.
912 // Saying so is the honest answer; the remedy is a second page, and
913 // pretending otherwise would evict a glyph about to be drawn.
914 let mut atlas = fixed(32);
915 let fitted = fill(&mut atlas, 0);
916 atlas.begin_frame();
917 for glyph in 0..fitted {
918 atlas
919 .insert(key(glyph), &solid(6, 6, 255))
920 .expect("refresh");
921 }
922 assert_eq!(
923 atlas.insert(key(500), &solid(6, 6, 255)),
924 Err(AtlasError::Full)
925 );
926 assert_eq!(
927 atlas.compactions(),
928 0,
929 "it repacked without freeing anything"
930 );
931 }
932
933 #[test]
934 fn a_glyph_too_large_is_not_worth_compacting_for() {
935 // Nothing an empty atlas cannot hold is made to fit by emptying it, and
936 // compacting to find that out throws away every glyph for nothing.
937 let mut atlas = fixed(32);
938 fill(&mut atlas, 0);
939 let before = atlas.len();
940 atlas.begin_frame();
941 assert_eq!(
942 atlas.insert(key(500), &solid(64, 64, 255)),
943 Err(AtlasError::TooLarge)
944 );
945 assert_eq!(atlas.compactions(), 0);
946 assert_eq!(atlas.len(), before, "the atlas was emptied for nothing");
947 }
948
949 #[test]
950 fn an_atlas_that_is_never_told_about_frames_keeps_everything() {
951 // Without frame boundaries nothing is stale, so a full atlas is
952 // genuinely full. That is correct rather than a leak: a caller that
953 // never says a frame ended has never said any glyph stopped mattering.
954 let mut atlas = fixed(32);
955 let fitted = fill(&mut atlas, 0);
956 assert_eq!(
957 atlas.insert(key(500), &solid(6, 6, 255)),
958 Err(AtlasError::Full)
959 );
960 assert_eq!(atlas.len(), fitted as usize);
961 assert_eq!(atlas.compactions(), 0);
962 }
963
964 #[test]
965 fn an_atlas_with_nothing_stale_grows_rather_than_refusing() {
966 // Everything present was asked for this frame, so compaction has
967 // nothing to free. Growing is what keeps a run of any length one draw;
968 // a second page would make it two, which is the property the vertex
969 // format exists to provide.
970 // No frame boundary, so nothing is ever stale and compaction can free
971 // nothing. Growing is the only way forward, which is the ordering this
972 // pins as well as the growth.
973 let mut atlas = Atlas::with_limit(32, 128);
974 let inserted = fill_until_growth(&mut atlas, 0);
975 assert!(inserted > 2, "only {inserted} glyphs went in");
976
977 assert_eq!(atlas.size(), 64, "it did not double");
978 assert_eq!(atlas.growths(), 1);
979 assert_eq!(atlas.compactions(), 0, "it discarded something it needed");
980 assert_eq!(atlas.len(), inserted as usize, "a glyph was lost");
981 }
982
983 #[test]
984 fn growth_keeps_every_glyph_and_its_coverage() {
985 // Growing happens because nothing was stale, so nothing may be dropped
986 // — and the coverage has to be read back out of the atlas, the bitmaps
987 // a caller supplied having been borrowed and long gone.
988 let mut atlas = Atlas::with_limit(32, 128);
989 let distinct = Coverage {
990 width: 3,
991 height: 2,
992 texels: vec![11, 22, 33, 44, 55, 66],
993 };
994 atlas.insert(key(0), &distinct).expect("insert");
995 fill_until_growth(&mut atlas, 1);
996 assert_eq!(atlas.growths(), 1);
997
998 let rect = atlas.get(key(0)).expect("survived");
999 let mut got = Vec::new();
1000 for row in 0..rect.height {
1001 let from = ((rect.y + row) * atlas.size() + rect.x) as usize;
1002 got.extend_from_slice(&atlas.texels()[from..from + rect.width as usize]);
1003 }
1004 assert_eq!(got, distinct.texels, "coverage was lost in growing");
1005 }
1006
1007 #[test]
1008 fn growth_stops_at_the_limit_and_says_so() {
1009 // The limit is the device's maximum texture size, which the atlas has
1010 // no way to ask about. Past it there is nowhere to go, and reporting
1011 // full is the honest answer rather than allocating what cannot be
1012 // uploaded.
1013 let mut atlas = Atlas::with_limit(16, 32);
1014 let mut glyph = 0u16;
1015 loop {
1016 match atlas.insert(key(glyph), &solid(6, 6, 255)) {
1017 Ok(_) => glyph += 1,
1018 Err(e) => {
1019 assert_eq!(e, AtlasError::Full);
1020 break;
1021 }
1022 }
1023 assert!(glyph < 200, "it never filled");
1024 }
1025 assert_eq!(atlas.size(), 32, "it did not grow to its limit");
1026 assert!(atlas.growths() >= 1);
1027 // And it stays usable at the limit: a glyph already present is still
1028 // found, rather than the atlas being poisoned by having filled.
1029 assert!(atlas.get(key(0)).is_some());
1030 }
1031
1032 #[test]
1033 fn a_limit_no_larger_than_the_atlas_means_it_never_grows() {
1034 // A caller who knows the size they want says so this way, and it is a
1035 // reasonable thing to ask for rather than a contradiction to reject.
1036 let mut atlas = Atlas::with_limit(16, 16);
1037 fill(&mut atlas, 0);
1038 assert_eq!(
1039 atlas.insert(key(500), &solid(6, 6, 255)),
1040 Err(AtlasError::Full)
1041 );
1042 assert_eq!(atlas.size(), 16);
1043 assert_eq!(atlas.growths(), 0);
1044 }
1045
1046 #[test]
1047 fn texture_coordinates_span_the_rectangle() {
1048 let rect = AtlasRect {
1049 x: 16,
1050 y: 32,
1051 width: 8,
1052 height: 4,
1053 };
1054 let [left, top, right, bottom] = rect.uv(64);
1055 assert!((left - 0.25).abs() < 1e-6);
1056 assert!((top - 0.5).abs() < 1e-6);
1057 assert!((right - 0.375).abs() < 1e-6);
1058 assert!((bottom - 0.5625).abs() < 1e-6);
1059 // V runs downward, matching the row order the texels are stored in.
1060 assert!(bottom > top);
1061 }
1062}