Skip to main content

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}