Skip to main content

kui_core/
atlas.rs

1//! CPU-side glyph atlas: a single RGBA page with shelf packing. Renderers
2//! mirror it to a texture; `dirty`/`epoch` tell them when to re-upload.
3
4use cosmic_text::CacheKey;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::resources::ImageId;
8
9pub const ATLAS_SIZE: u32 = 1024;
10/// The atlas doubles up to this when content (typically images) won't fit.
11pub const MAX_ATLAS_SIZE: u32 = 4096;
12
13#[derive(Clone, Copy, Debug)]
14pub struct GlyphSlot {
15    pub x: u32,
16    pub y: u32,
17    pub w: u32,
18    pub h: u32,
19    /// Raster offset from the glyph origin.
20    pub left: i32,
21    pub top: i32,
22    /// Color bitmap (emoji) vs alpha mask.
23    pub color_glyph: bool,
24    /// LCD subpixel mask: rgb are per-channel coverages.
25    pub subpixel: bool,
26}
27
28pub struct RasterGlyph {
29    pub w: u32,
30    pub h: u32,
31    pub left: i32,
32    pub top: i32,
33    pub color: bool,
34    pub subpixel: bool,
35    /// RGBA, w*h*4 bytes.
36    pub data: Vec<u8>,
37}
38
39/// A glyph or shape refused for room on a page that began the frame
40/// empty.
41#[derive(Clone, Copy, PartialEq, Eq, Hash)]
42enum Refusal {
43    Glyph(CacheKey),
44    Synth(char, u32, u32),
45    /// A path's mask, by the key `get_or_insert_path` was given.
46    Path(u64),
47}
48
49struct Shelf {
50    y: u32,
51    h: u32,
52    cursor_x: u32,
53}
54
55/// The page as it was when `begin_frame` last emptied it, kept for that
56/// one frame: its pixels, and the glyphs and shapes it
57/// held, where.
58struct Prev {
59    size: u32,
60    pixels: Vec<u8>,
61    map: FxHashMap<CacheKey, Option<GlyphSlot>>,
62    synth: FxHashMap<(char, u32, u32), Option<GlyphSlot>>,
63    paths: FxHashMap<u64, Option<GlyphSlot>>,
64}
65
66impl Prev {
67    /// The texels of the `w × h` rect at (`x`, `y`), row after row.
68    fn texels(&self, x: u32, y: u32, w: u32, h: u32) -> Vec<u8> {
69        let stride = (self.size * 4) as usize;
70        let row = (w * 4) as usize;
71        let mut out = Vec::with_capacity(row * h as usize);
72        for r in 0..h as usize {
73            let at = (y as usize + r) * stride + (x * 4) as usize;
74            out.extend_from_slice(&self.pixels[at..at + row]);
75        }
76        out
77    }
78}
79
80/// How the page makes room. A slot handed out during a
81/// frame is never moved or overwritten before that frame is presented:
82/// the quads already emitted, the text templates built and the cell
83/// tables filled all carry its texel rect, and nothing walks them again.
84/// So a page that fills mid-frame is *extended* — doubled with its
85/// pixels kept where they are, which leaves every texel rect valid, since
86/// `uv` is in texels and the renderer divides by the page's size at draw
87/// time — and the reset that reclaims it waits for the next
88/// `begin_frame`, before anything is emitted. The page then starts that
89/// frame empty at its base size and holds that frame's set alone; if the
90/// set does not fit it, it is larger than the page and the page keeps the
91/// growth. Most fills never get as far as mid-frame:
92/// `begin_frame` sees one coming in the rows the last frames opened and
93/// empties the page first. A page at `MAX_ATLAS_SIZE` cannot extend: there
94/// the request is refused for this frame (the glyph is not drawn, an
95/// image draws from a texture of its own) and the next frame, which
96/// `short` asks for, starts on an empty page.
97///
98/// A page that began the frame empty and still refuses holds a set bigger
99/// than itself, and the next frame would refuse the same. Its
100/// refusals are kept, and until the page is next emptied the atlas
101/// measures what each frame looks up: `stamp` moves every frame, so the
102/// caches that keep slots look theirs up again, and each distinct slot's
103/// texels are counted once. A frame that wanted a refused glyph and whose
104/// set fits in what the page held is `short`, and the next frame begins
105/// on an empty page that takes it — the view has scrolled to a part of
106/// the set. One that did not fit keeps the page as it is.
107///
108/// An emptied page is not drawn from again, but it is kept for the frame
109/// that begins on the empty one. A glyph or shape that
110/// frame looks up and the old page held is copied across, not
111/// rasterized again: the frame after a reset is the whole visible set
112/// looked up at once — kawoosh's window, ~600 glyphs, was 2.5–5.3 ms of
113/// rasterizing where the copy is a fraction of one. Only what the frame
114/// looks up is copied, so the page still holds that frame's set alone.
115/// `clear`, for a raster mode that changed, keeps nothing.
116pub struct GlyphAtlas {
117    pub size: u32,
118    /// RGBA, size*size*4.
119    pub pixels: Vec<u8>,
120    /// Set when pixels changed since the renderer last consumed them.
121    pub dirty: bool,
122    /// Bumped whenever the page is replaced — reset, or resized — so
123    /// renderers re-upload it whole and caches that stamped it re-look
124    /// their slots up. A resize keeps every slot where it was.
125    pub epoch: u64,
126    /// What caches that keep slots across frames — text templates, cell
127    /// tables — key them on: it moves with `epoch`, and on every frame
128    /// while refusals are pending, so that those frames look every
129    /// slot they use up again and are measured.
130    pub stamp: u64,
131    map: FxHashMap<CacheKey, Option<GlyphSlot>>,
132    /// Registered images blitted into the same page (one texture, one draw
133    /// call). Keyed by handle; re-blitted from `Resources` after a reset.
134    images: FxHashMap<ImageId, Option<GlyphSlot>>,
135    /// Shapes drawn from a cell box rather than a font — box drawing,
136    /// blocks, Powerline — keyed on the character and the
137    /// cell size in physical px, so one cell size shares one slot and
138    /// another size does not. Plain masks, tinted like a glyph's.
139    synth: FxHashMap<(char, u32, u32), Option<GlyphSlot>>,
140    /// A `path` node's masks (`docs/adr/0040-a-path-is-a-mask-in-the-atlas.md`,
141    /// decision 6): the outline filled or stroked at one physical scale and
142    /// quarter-pixel bin, keyed on a hash of all of that, so one shape
143    /// at one place shares one slot and another does not. Plain masks,
144    /// tinted like a glyph's; copied across a reset like a glyph's.
145    paths: FxHashMap<u64, Option<GlyphSlot>>,
146    shelves: Vec<Shelf>,
147    next_shelf_y: u32,
148    /// The size the page settles at: what `begin_frame` resets an
149    /// extended page to. It grows when one frame's set does not fit it —
150    /// the page filled on a frame it began empty — or one item alone is
151    /// bigger than it, and never shrinks.
152    base: u32,
153    /// The page held nothing when this frame began, so whatever fills it
154    /// is this frame's own set.
155    fresh: bool,
156    /// A request was refused for room this frame on a page that could not
157    /// extend and still held earlier frames' glyphs: the next frame
158    /// starts on an empty page and should come.
159    short: bool,
160    /// The shelf rows a frame has been opening lately — the last frame's,
161    /// or a quarter less than the figure before, whichever is more; a
162    /// frame that began on an empty page counts none — and the page's
163    /// rows when this frame began, which the next frame's figure is read
164    /// against.
165    rows_per_frame: u32,
166    rows_at_begin: u32,
167    /// Frames begun since the page was last emptied. A page that needs
168    /// emptying again within two is too small for its set and how fast
169    /// it turns over, and grows instead (F83's thrash, measured).
170    frames_since_reset: u32,
171    /// A frame since the page was last emptied began on a page holding
172    /// something and opened rows on it: the set is turning over. Until
173    /// one has, `rows_per_frame` is what the frames before the reset
174    /// opened — a burst, a view's worth of new glyphs, that says nothing
175    /// of how fast the set that followed turns over — and the page is not
176    /// read as filling (otherwise a burst that fit would double the page
177    /// for good two frames later).
178    turned: bool,
179    /// The refusals pending: what a page that began the frame empty
180    /// could not take, with the texels each would use and the frame it
181    /// was last looked up on. Emptied with the page.
182    refused: FxHashMap<Refusal, (u64, u64)>,
183    /// Texels handed out since the page was last emptied, padding
184    /// included, and what they came to on the frame that began it empty
185    /// and refused: the part of its set the page was seen to hold.
186    placed: u64,
187    held: u64,
188    /// While refusals are pending: the texels of the distinct slots this
189    /// frame looked up, refused ones included, the slots already counted,
190    /// and whether a refused one was among them.
191    demand: u64,
192    seen: FxHashSet<(u32, u32)>,
193    wanted: bool,
194    /// Frames begun, for `refused`'s once-a-frame count.
195    frame: u64,
196    /// The page `begin_frame` emptied, for this frame only (see the type's
197    /// note).
198    prev: Option<Prev>,
199}
200
201impl GlyphAtlas {
202    pub fn new() -> Self {
203        Self::with_size(ATLAS_SIZE)
204    }
205
206    pub fn with_size(size: u32) -> Self {
207        Self {
208            size,
209            pixels: vec![0; (size * size * 4) as usize],
210            dirty: false,
211            epoch: 0,
212            stamp: 0,
213            map: FxHashMap::default(),
214            images: FxHashMap::default(),
215            synth: FxHashMap::default(),
216            paths: FxHashMap::default(),
217            shelves: Vec::new(),
218            next_shelf_y: 0,
219            base: size,
220            fresh: true,
221            short: false,
222            rows_per_frame: 0,
223            rows_at_begin: 0,
224            frames_since_reset: u32::MAX,
225            turned: false,
226            refused: FxHashMap::default(),
227            placed: 0,
228            held: 0,
229            demand: 0,
230            seen: FxHashSet::default(),
231            wanted: false,
232            frame: 0,
233            prev: None,
234        }
235    }
236
237    /// A frame begins, before anything is emitted — the one point where
238    /// the page can be emptied without a quad sampling what it dropped.
239    /// It is emptied, back at its base size, when the last frame extended
240    /// it or was refused room (`short`) — and when it is about to fill:
241    /// fewer rows free than two frames open at the rate they lately have.
242    /// A set that turns
243    /// over a little each frame, a list scrolling through fonts, fills
244    /// the page every so often, and the rows foresee it, so the frame
245    /// that would have extended the page mid-emit — a page four times
246    /// the size, its rows copied, a texture made and uploaded twice —
247    /// begins on an empty one instead. A fill it does not foresee still
248    /// extends.
249    ///
250    /// A page that needs emptying within two frames of the last time is
251    /// too small for its set and the rate it turns over at, and grows
252    /// instead: an extension is kept, and a page about to fill doubles
253    /// with its slots in place. That is F83's thrash, a set between one
254    /// page and two, measured by what the page does rather than by which
255    /// glyphs come back; a fill long after the last empties it.
256    ///
257    /// A page with refusals pending is emptied when the last frame wanted
258    /// one and its set fits (see the type's note); otherwise the frame
259    /// ahead is measured.
260    pub fn begin_frame(&mut self) {
261        // The page emptied a frame ago has served the frame it was kept for.
262        self.prev = None;
263        if self.fresh && !self.refused.is_empty() {
264            self.held = self.placed;
265        }
266        let refit = self.refit();
267        let rows = self.next_shelf_y;
268        // A frame that began on an empty page opened its whole set, which
269        // says nothing of how fast the set turns over.
270        let opened = if self.fresh {
271            0
272        } else {
273            rows.saturating_sub(self.rows_at_begin)
274        };
275        self.rows_per_frame = opened.max(self.rows_per_frame - self.rows_per_frame / 4);
276        self.frames_since_reset = self.frames_since_reset.saturating_add(1);
277        self.turned |= opened > 0;
278        let extended = self.size > self.base;
279        let filling = self.turned && self.size - rows < 2 * self.rows_per_frame;
280        let thrash = self.frames_since_reset <= 2;
281        if self.short || refit {
282            self.reset_to(self.base, true);
283        } else if extended && thrash {
284            self.base = self.size;
285        } else if extended {
286            self.reset_to(self.base, true);
287        } else if filling && thrash && self.size < MAX_ATLAS_SIZE {
288            self.extend_to((self.size * 2).min(MAX_ATLAS_SIZE));
289            self.base = self.size;
290        } else if filling {
291            self.reset_to(self.base, true);
292        }
293        self.short = false;
294        self.fresh = self.next_shelf_y == 0;
295        self.rows_at_begin = self.next_shelf_y;
296        self.frame += 1;
297        self.demand = 0;
298        self.seen.clear();
299        self.wanted = false;
300        if !self.refused.is_empty() {
301            self.stamp += 1;
302        }
303    }
304
305    /// The frame's glyphs are all looked up: the page `begin_frame`
306    /// emptied has served it. Dropped here rather than at the next
307    /// `begin_frame`, which on an idle window may never come — the frame
308    /// that shrinks an extended page, or empties a full 4096 one, would
309    /// otherwise hold up to 64 MiB for as long as nothing redraws.
310    pub(crate) fn end_frame(&mut self) {
311        self.prev = None;
312    }
313
314    /// For a frame-level test: the next `begin_frame` empties the page,
315    /// keeping the old one, as a refusal would.
316    #[cfg(test)]
317    pub(crate) fn reset_next_frame(&mut self) {
318        self.short = true;
319    }
320
321    /// For a frame-level test: whether an emptied page is still kept.
322    #[cfg(test)]
323    pub(crate) fn keeps_prev(&self) -> bool {
324        self.prev.is_some()
325    }
326
327    /// Whether this frame was refused room (see `short`), or wanted a
328    /// glyph refused earlier while its set fits the page: it drew
329    /// without some glyph, and the next frame, on an empty page, draws it.
330    pub(crate) fn short(&self) -> bool {
331        self.short || self.refit()
332    }
333
334    /// The frame wanted a pending refusal, and what it looked up fits in
335    /// what the page was seen to hold. Not on a frame that began empty,
336    /// which was measured only from its first refusal on.
337    fn refit(&self) -> bool {
338        !self.fresh && self.wanted && self.demand <= self.held
339    }
340
341    /// Counts a looked-up slot into the frame's demand, once a frame,
342    /// while refusals are pending.
343    fn note_slot(&mut self, slot: GlyphSlot) {
344        if !self.refused.is_empty() && self.seen.insert((slot.x, slot.y)) {
345            self.demand += texels(slot.w, slot.h);
346        }
347    }
348
349    /// Counts a looked-up refusal into the frame's demand, once a frame.
350    /// Not a refusal — a glyph with nothing to draw, or larger than any
351    /// page — counts nothing.
352    fn note_refusal(&mut self, refusal: Refusal) {
353        if let Some((cost, last)) = self.refused.get_mut(&refusal)
354            && *last != self.frame
355        {
356            *last = self.frame;
357            self.demand += *cost;
358            self.wanted = true;
359        }
360    }
361
362    /// Keeps a refusal for room made on a page that began the frame
363    /// empty, the first time it is refused.
364    fn refuse(&mut self, refusal: Refusal, w: u32, h: u32) {
365        if self.fresh && w < MAX_ATLAS_SIZE && h < MAX_ATLAS_SIZE {
366            self.refused.insert(refusal, (texels(w, h), self.frame));
367            self.demand += texels(w, h);
368            self.wanted = true;
369        }
370    }
371
372    /// Drops every cached glyph and image (they re-rasterize on demand) and
373    /// bumps the epoch so renderers re-upload. Used when the raster mode
374    /// changes under the cache — between frames, never during one.
375    pub fn clear(&mut self) {
376        self.prev = None;
377        self.reset_to(self.size, false);
378    }
379
380    /// Empties the page onto a `size` one: every slot dropped, the epoch
381    /// bumped. Only between frames. `keep` keeps the old page for the
382    /// frame ahead to copy from (see the type's note).
383    fn reset_to(&mut self, size: u32, keep: bool) {
384        if keep {
385            let pixels = std::mem::replace(&mut self.pixels, vec![0; (size * size * 4) as usize]);
386            self.prev = Some(Prev {
387                size: self.size,
388                pixels,
389                map: std::mem::take(&mut self.map),
390                synth: std::mem::take(&mut self.synth),
391                paths: std::mem::take(&mut self.paths),
392            });
393            self.size = size;
394        } else if size == self.size {
395            self.pixels.fill(0);
396        } else {
397            self.size = size;
398            self.pixels = vec![0; (size * size * 4) as usize];
399        }
400        self.map.clear();
401        self.images.clear();
402        self.synth.clear();
403        self.paths.clear();
404        self.shelves.clear();
405        self.next_shelf_y = 0;
406        self.epoch += 1;
407        self.stamp += 1;
408        self.dirty = true;
409        self.frames_since_reset = 0;
410        self.turned = false;
411        self.refused.clear();
412        self.placed = 0;
413        self.held = 0;
414    }
415
416    /// Doubles the page with every slot kept where it is: the rows copy
417    /// into the top-left of the bigger page, the shelves run on into the
418    /// new width and new ones open below. Nothing handed out is
419    /// invalidated, so it is safe mid-frame.
420    fn extend_to(&mut self, size: u32) {
421        let old = self.size as usize;
422        let mut pixels = vec![0; (size * size * 4) as usize];
423        for (row, src) in self.pixels.chunks_exact(old * 4).enumerate() {
424            let at = row * size as usize * 4;
425            pixels[at..at + old * 4].copy_from_slice(src);
426        }
427        self.pixels = pixels;
428        self.size = size;
429        self.epoch += 1;
430        self.stamp += 1;
431        self.dirty = true;
432    }
433
434    /// Alloc with room made: on a full page, extend it (see the type's
435    /// note) until the item fits or the page is at `MAX_ATLAS_SIZE`. The
436    /// growth is kept — `base` moves — when the page began the frame
437    /// empty, since then this frame's set alone overflowed it (AR19: a
438    /// set larger than the page; F83: one between one page and two), or
439    /// when the item alone does not fit the base page; otherwise it is
440    /// this frame's, and `begin_frame` resets to the base. Resetting here
441    /// instead, as this did until F99, overwrote the slots of every quad
442    /// the frame had emitted before the fill, and that frame was
443    /// presented with them blank or scrambled.
444    fn alloc_or_make_room(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
445        let pos = self.make_room(w, h);
446        if pos.is_some() {
447            self.placed += texels(w, h);
448        }
449        pos
450    }
451
452    fn make_room(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
453        if let Some(pos) = self.alloc(w, h) {
454            return Some(pos);
455        }
456        if w + 1 > MAX_ATLAS_SIZE || h + 1 > MAX_ATLAS_SIZE {
457            return None; // no page holds it: nothing to make room for
458        }
459        loop {
460            let bigger = (self.size * 2).min(MAX_ATLAS_SIZE);
461            if bigger == self.size {
462                // No room without dropping a slot this frame may have
463                // used. A page that began the frame empty would refuse
464                // this on any frame; one that held earlier frames' glyphs
465                // will not, once the next frame begins on an empty page.
466                if !self.fresh {
467                    self.short = true;
468                }
469                return None;
470            }
471            self.extend_to(bigger);
472            if self.fresh || w + 1 > self.base || h + 1 > self.base {
473                self.base = self.size;
474            }
475            if let Some(pos) = self.alloc(w, h) {
476                return Some(pos);
477            }
478        }
479    }
480
481    /// Finds space for a w*h glyph (padded by 1px to avoid sampling bleed).
482    fn alloc(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
483        let (pw, ph) = (w + 1, h + 1);
484        if pw > self.size || ph > self.size {
485            return None;
486        }
487        // Reuse a shelf that's tall enough but not wastefully so.
488        for shelf in &mut self.shelves {
489            if ph <= shelf.h && shelf.h <= ph.saturating_mul(2) && shelf.cursor_x + pw <= self.size
490            {
491                let pos = (shelf.cursor_x, shelf.y);
492                shelf.cursor_x += pw;
493                return Some(pos);
494            }
495        }
496        // Open a new shelf, height quantized to reduce fragmentation.
497        let shelf_h = ph.next_multiple_of(8);
498        if self.next_shelf_y + shelf_h > self.size {
499            return None;
500        }
501        let shelf = Shelf {
502            y: self.next_shelf_y,
503            h: shelf_h,
504            cursor_x: pw,
505        };
506        self.next_shelf_y += shelf_h;
507        let pos = (0, shelf.y);
508        self.shelves.push(shelf);
509        Some(pos)
510    }
511
512    fn blit(&mut self, x: u32, y: u32, w: u32, h: u32, data: &[u8]) {
513        let stride = (self.size * 4) as usize;
514        for row in 0..h as usize {
515            let src = row * (w as usize) * 4;
516            let dst = (y as usize + row) * stride + (x as usize) * 4;
517            self.pixels[dst..dst + (w as usize) * 4]
518                .copy_from_slice(&data[src..src + (w as usize) * 4]);
519        }
520        self.dirty = true;
521    }
522
523    /// A glyph the page held before `begin_frame` emptied it, its texels
524    /// copied out of the old page. Only one that was drawn: a
525    /// `None` there may be a refusal, which the empty page is for.
526    fn carried(&self, key: &CacheKey) -> Option<RasterGlyph> {
527        let prev = self.prev.as_ref()?;
528        let slot = (*prev.map.get(key)?)?;
529        Some(RasterGlyph {
530            w: slot.w,
531            h: slot.h,
532            left: slot.left,
533            top: slot.top,
534            color: slot.color_glyph,
535            subpixel: slot.subpixel,
536            data: prev.texels(slot.x, slot.y, slot.w, slot.h),
537        })
538    }
539
540    /// Cached lookup; rasterizes on miss — or, the frame after the page
541    /// was emptied, copies what the old page held. `None` means
542    /// unrasterizable (e.g. whitespace) and is cached as such.
543    pub fn get_or_insert(
544        &mut self,
545        key: CacheKey,
546        raster: impl FnOnce() -> Option<RasterGlyph>,
547    ) -> Option<GlyphSlot> {
548        if let Some(&slot) = self.map.get(&key) {
549            match slot {
550                Some(slot) => self.note_slot(slot),
551                None => self.note_refusal(Refusal::Glyph(key)),
552            }
553            return slot;
554        }
555        let Some(glyph) = self.carried(&key).or_else(raster) else {
556            self.map.insert(key, None);
557            return None;
558        };
559        let Some((x, y)) = self.alloc_or_make_room(glyph.w, glyph.h) else {
560            self.refuse(Refusal::Glyph(key), glyph.w, glyph.h);
561            self.map.insert(key, None);
562            return None;
563        };
564        self.blit(x, y, glyph.w, glyph.h, &glyph.data);
565        let slot = GlyphSlot {
566            x,
567            y,
568            w: glyph.w,
569            h: glyph.h,
570            left: glyph.left,
571            top: glyph.top,
572            color_glyph: glyph.color,
573            subpixel: glyph.subpixel,
574        };
575        self.note_slot(slot);
576        self.map.insert(key, Some(slot));
577        Some(slot)
578    }
579
580    /// Cached lookup for a shape drawn to a `w × h` cell;
581    /// `coverage` is called on a miss for `w * h` alpha bytes, which land
582    /// as a white mask the renderer tints like any glyph's. `None` means
583    /// the cell does not fit a `MAX_ATLAS_SIZE` page.
584    pub fn get_or_insert_synth(
585        &mut self,
586        ch: char,
587        w: u32,
588        h: u32,
589        coverage: impl FnOnce() -> Vec<u8>,
590    ) -> Option<GlyphSlot> {
591        if let Some(&slot) = self.synth.get(&(ch, w, h)) {
592            match slot {
593                Some(slot) => self.note_slot(slot),
594                None => self.note_refusal(Refusal::Synth(ch, w, h)),
595            }
596            return slot;
597        }
598        let Some((x, y)) = self.alloc_or_make_room(w, h) else {
599            self.refuse(Refusal::Synth(ch, w, h), w, h);
600            self.synth.insert((ch, w, h), None);
601            return None;
602        };
603        let carried = self.prev.as_ref().and_then(|prev| {
604            let slot = (*prev.synth.get(&(ch, w, h))?)?;
605            Some(prev.texels(slot.x, slot.y, w, h))
606        });
607        let rgba = carried.unwrap_or_else(|| {
608            let mask = coverage();
609            debug_assert_eq!(mask.len(), (w * h) as usize);
610            let mut rgba = Vec::with_capacity(mask.len() * 4);
611            for &a in &mask {
612                rgba.extend_from_slice(&[255, 255, 255, a]);
613            }
614            rgba
615        });
616        self.blit(x, y, w, h, &rgba);
617        let slot = GlyphSlot {
618            x,
619            y,
620            w,
621            h,
622            left: 0,
623            top: 0,
624            color_glyph: false,
625            subpixel: false,
626        };
627        self.note_slot(slot);
628        self.synth.insert((ch, w, h), Some(slot));
629        Some(slot)
630    }
631
632    /// Cached lookup for a path's mask under `key` (the hash of its ops,
633    /// scale, bin and paint — the caller's to make); `coverage` is called
634    /// on a miss for `w * h` alpha bytes, which land as a white mask the
635    /// renderer tints like any glyph's, or the mask is copied from the
636    /// page `begin_frame` emptied when that page held it. `None` means
637    /// the mask does not fit the page this frame, or no page at all; the
638    /// caller draws it from a texture of its own.
639    pub fn get_or_insert_path(
640        &mut self,
641        key: u64,
642        w: u32,
643        h: u32,
644        coverage: impl FnOnce() -> Vec<u8>,
645    ) -> Option<GlyphSlot> {
646        self.get_or_insert_keyed(key, w, h, false, || {
647            let mask = coverage();
648            debug_assert_eq!(mask.len(), (w * h) as usize);
649            let mut rgba = Vec::with_capacity(mask.len() * 4);
650            for &a in &mask {
651                rgba.extend_from_slice(&[255, 255, 255, a]);
652            }
653            rgba
654        })
655    }
656
657    /// Cached lookup for a gradient's raster
658    /// (`docs/adr/0042-a-gradient-is-an-image-the-core-paints.md`): `w × h`
659    /// texels of straight RGBA under `key`, made by `rgba` on a miss and
660    /// copied across a reset like a path's mask, whose table it shares —
661    /// the caller's key is of a different domain. `None` when no page can
662    /// hold it.
663    pub fn get_or_insert_gradient(
664        &mut self,
665        key: u64,
666        w: u32,
667        h: u32,
668        rgba: impl FnOnce() -> Vec<u8>,
669    ) -> Option<GlyphSlot> {
670        self.get_or_insert_keyed(key, w, h, true, rgba)
671    }
672
673    /// The slot under `key` in the keyed table, its `w * h * 4` texels
674    /// from `rgba` the first time and from the page before across a reset.
675    fn get_or_insert_keyed(
676        &mut self,
677        key: u64,
678        w: u32,
679        h: u32,
680        color: bool,
681        rgba: impl FnOnce() -> Vec<u8>,
682    ) -> Option<GlyphSlot> {
683        if let Some(&slot) = self.paths.get(&key) {
684            match slot {
685                Some(slot) => self.note_slot(slot),
686                None => self.note_refusal(Refusal::Path(key)),
687            }
688            return slot;
689        }
690        let Some((x, y)) = self.alloc_or_make_room(w, h) else {
691            self.refuse(Refusal::Path(key), w, h);
692            self.paths.insert(key, None);
693            return None;
694        };
695        let carried = self.prev.as_ref().and_then(|prev| {
696            let slot = (*prev.paths.get(&key)?)?;
697            (slot.w == w && slot.h == h).then(|| prev.texels(slot.x, slot.y, w, h))
698        });
699        let rgba = carried.unwrap_or_else(rgba);
700        debug_assert_eq!(rgba.len(), (w * h * 4) as usize);
701        self.blit(x, y, w, h, &rgba);
702        let slot = GlyphSlot {
703            x,
704            y,
705            w,
706            h,
707            left: 0,
708            top: 0,
709            color_glyph: color,
710            subpixel: false,
711        };
712        self.note_slot(slot);
713        self.paths.insert(key, Some(slot));
714        Some(slot)
715    }
716
717    /// Whether the page holds a mask under `key` this frame: for a test
718    /// of what a path's second frame costs.
719    pub fn has_path(&self, key: u64) -> bool {
720        matches!(self.paths.get(&key), Some(Some(_)))
721    }
722
723    /// Cached lookup for a registered image; blits `rgba` (w*h*4) on miss.
724    /// `None` means it can't fit even a `MAX_ATLAS_SIZE` page.
725    pub fn get_or_insert_image(
726        &mut self,
727        id: ImageId,
728        w: u32,
729        h: u32,
730        rgba: &[u8],
731    ) -> Option<GlyphSlot> {
732        if let Some(&slot) = self.images.get(&id) {
733            // A refused image draws from a texture of its own: it takes
734            // nothing of the page and wants nothing of it.
735            if let Some(slot) = slot {
736                self.note_slot(slot);
737            }
738            return slot;
739        }
740        debug_assert_eq!(rgba.len(), (w * h * 4) as usize);
741        let Some((x, y)) = self.alloc_or_make_room(w, h) else {
742            self.images.insert(id, None);
743            return None;
744        };
745        self.blit(x, y, w, h, rgba);
746        let slot = GlyphSlot {
747            x,
748            y,
749            w,
750            h,
751            left: 0,
752            top: 0,
753            color_glyph: true,
754            subpixel: false,
755        };
756        self.note_slot(slot);
757        self.images.insert(id, Some(slot));
758        Some(slot)
759    }
760
761    /// Forget an image's slot (its pixels are reclaimed at the next reset).
762    /// Call when the host removes the image from `Resources`.
763    pub fn evict_image(&mut self, id: ImageId) {
764        self.images.remove(&id);
765    }
766
767    /// Keeps the slots of the images `live` says still exist and forgets
768    /// the rest — how a window learns of removals made through another
769    /// window of its session.
770    pub fn retain_images(&mut self, live: impl Fn(ImageId) -> bool) {
771        self.images.retain(|id, _| live(*id));
772    }
773
774    /// Whether the atlas holds a slot for `id`.
775    pub fn has_image(&self, id: ImageId) -> bool {
776        self.images.contains_key(&id)
777    }
778}
779
780/// The texels a `w × h` item takes of the page, its padding included.
781fn texels(w: u32, h: u32) -> u64 {
782    u64::from(w + 1) * u64::from(h + 1)
783}
784
785impl Default for GlyphAtlas {
786    fn default() -> Self {
787        Self::new()
788    }
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    fn fake_key(i: u32) -> CacheKey {
796        // CacheKey is plain data; construct distinct ones via glyph id.
797        CacheKey {
798            font_id: cosmic_text::fontdb::ID::dummy(),
799            glyph_id: i as u16,
800            font_size_bits: (12.0f32 + (i / 65536) as f32).to_bits(),
801            x_bin: cosmic_text::SubpixelBin::Zero,
802            y_bin: cosmic_text::SubpixelBin::Zero,
803            font_weight: cosmic_text::fontdb::Weight::NORMAL,
804            flags: cosmic_text::CacheKeyFlags::empty(),
805        }
806    }
807
808    fn raster(w: u32, h: u32) -> RasterGlyph {
809        RasterGlyph {
810            w,
811            h,
812            left: 0,
813            top: 0,
814            color: false,
815            subpixel: false,
816            data: vec![0xff; (w * h * 4) as usize],
817        }
818    }
819
820    #[test]
821    fn slots_stay_in_bounds_and_do_not_overlap() {
822        let mut atlas = GlyphAtlas::with_size(256);
823        let mut slots = Vec::new();
824        for i in 0..200 {
825            let (w, h) = (5 + (i % 13), 7 + (i % 9));
826            if let Some(s) = atlas.get_or_insert(fake_key(i), || Some(raster(w, h))) {
827                assert!(
828                    s.x + s.w <= 256 && s.y + s.h <= 256,
829                    "slot out of bounds: {s:?}"
830                );
831                slots.push(s);
832            }
833        }
834        assert!(!slots.is_empty());
835        for (i, a) in slots.iter().enumerate() {
836            for b in slots.iter().skip(i + 1) {
837                let disjoint =
838                    a.x + a.w <= b.x || b.x + b.w <= a.x || a.y + a.h <= b.y || b.y + b.h <= a.y;
839                assert!(disjoint, "overlap: {a:?} vs {b:?}");
840            }
841        }
842    }
843
844    #[test]
845    fn lookup_is_cached() {
846        let mut atlas = GlyphAtlas::with_size(128);
847        let mut calls = 0;
848        let key = fake_key(1);
849        for _ in 0..3 {
850            atlas.get_or_insert(key, || {
851                calls += 1;
852                Some(raster(10, 10))
853            });
854        }
855        assert_eq!(calls, 1);
856    }
857
858    #[test]
859    fn unrasterizable_is_cached_as_none() {
860        let mut atlas = GlyphAtlas::with_size(128);
861        let mut calls = 0;
862        for _ in 0..3 {
863            let slot = atlas.get_or_insert(fake_key(2), || {
864                calls += 1;
865                None
866            });
867            assert!(slot.is_none());
868        }
869        assert_eq!(calls, 1);
870    }
871
872    fn filled(w: u32, h: u32, v: u8) -> RasterGlyph {
873        RasterGlyph {
874            data: vec![v; (w * h * 4) as usize],
875            ..raster(w, h)
876        }
877    }
878
879    /// Whether every texel of `slot` is `v`: the slot still holds what
880    /// was put there.
881    fn holds(atlas: &GlyphAtlas, slot: GlyphSlot, v: u8) -> bool {
882        (slot.y..slot.y + slot.h).all(|y| {
883            let at = ((y * atlas.size + slot.x) * 4) as usize;
884            atlas.pixels[at..at + (slot.w * 4) as usize]
885                .iter()
886                .all(|&p| p == v)
887        })
888    }
889
890    /// F99: a page that fills mid-frame makes room without moving or
891    /// overwriting a slot the frame already has — it extends, the pixels
892    /// kept in place — and resets only when the next frame begins, back
893    /// to the size it had. It used to reset on the spot, and every quad
894    /// emitted before the fill sampled the page packed over it.
895    #[test]
896    fn a_page_that_fills_mid_frame_keeps_the_frames_slots_until_it_ends() {
897        let mut atlas = GlyphAtlas::with_size(64);
898        // 30×30 glyphs; a 64 page holds four.
899        atlas.begin_frame();
900        for i in 0..4u32 {
901            atlas.get_or_insert(fake_key(i), || Some(filled(30, 30, i as u8 + 1)));
902        }
903        assert_eq!(atlas.size, 64);
904        // The next frame draws one glyph it had and four new ones: the
905        // fifth does not fit the page with the four old ones in it.
906        atlas.begin_frame();
907        let mut used = Vec::new();
908        for i in [0u32, 4, 5, 6, 7] {
909            let v = i as u8 + 1;
910            let slot = atlas
911                .get_or_insert(fake_key(i), || Some(filled(30, 30, v)))
912                .expect("room is made");
913            used.push((slot, v));
914            for &(slot, v) in &used {
915                assert!(
916                    holds(&atlas, slot, v),
917                    "a slot this frame used was overwritten"
918                );
919            }
920        }
921        assert_eq!(atlas.size, 128, "extended for the rest of the frame");
922        assert!(!atlas.short());
923        let epoch = atlas.epoch;
924        // Between frames the extension goes: the page is its old size,
925        // empty, and the frame's glyphs are copied from the page it
926        // replaced (DX26), not rasterized again.
927        atlas.begin_frame();
928        assert_eq!(atlas.size, 64);
929        assert!(atlas.epoch > epoch, "reset before anything is emitted");
930        let mut rasterized = 0;
931        for i in [4u32, 5, 6, 7] {
932            let slot = atlas
933                .get_or_insert(fake_key(i), || {
934                    rasterized += 1;
935                    Some(filled(30, 30, i as u8 + 1))
936                })
937                .expect("room");
938            assert!(holds(&atlas, slot, i as u8 + 1), "copied whole");
939        }
940        assert_eq!(rasterized, 0);
941        assert_eq!(atlas.size, 64, "the set fits the page: no growth kept");
942    }
943
944    /// AR19: a set that does not fit the page grows it, and from the
945    /// next frame on nothing resets. A set that turns over — one page's
946    /// worth of new keys a frame — never keeps a growth.
947    #[test]
948    fn a_working_set_larger_than_the_page_grows_it_and_then_holds() {
949        let mut atlas = GlyphAtlas::with_size(64);
950        atlas.begin_frame();
951        for i in 0..100 {
952            atlas.get_or_insert(fake_key(i), || Some(raster(30, 30)));
953        }
954        assert!(atlas.size >= 512, "grown to hold the set: {}", atlas.size);
955        let (size, epoch) = (atlas.size, atlas.epoch);
956        for _ in 0..3 {
957            atlas.begin_frame();
958            for i in 0..100 {
959                atlas.get_or_insert(fake_key(i), || panic!("cached"));
960            }
961        }
962        assert_eq!(
963            (atlas.size, atlas.epoch),
964            (size, epoch),
965            "still from then on"
966        );
967        // A set that turns over whole each frame — a page's worth of new
968        // keys every frame — grows only until a page lasts past two
969        // frames, and then holds its size, emptied between frames.
970        let mut atlas = GlyphAtlas::with_size(64);
971        let mut sizes = Vec::new();
972        for frame in 0..40u32 {
973            atlas.begin_frame();
974            let (size, epoch) = (atlas.size, atlas.epoch);
975            for i in 0..4 {
976                atlas.get_or_insert(fake_key(frame * 4 + i), || Some(raster(30, 30)));
977            }
978            if frame >= 20 {
979                assert_eq!(
980                    (atlas.size, atlas.epoch),
981                    (size, epoch),
982                    "no fill mid-frame"
983                );
984            }
985            sizes.push(atlas.size);
986        }
987        assert!(sizes[20..].iter().all(|&s| s == sizes[39]), "{sizes:?}");
988        assert!(sizes[39] <= 256, "bounded: {sizes:?}");
989    }
990
991    /// F99: a set that turns over a little each frame — a list scrolling
992    /// through fonts — is emptied at `begin_frame`, when fewer rows are
993    /// free than a frame has lately opened, and never fills mid-frame.
994    #[test]
995    fn a_turnover_the_rows_foresee_is_emptied_between_frames() {
996        // 30×30 glyphs; a 256 page's row of 32 holds eight, and it has
997        // eight rows. Eight new keys a frame is a row a frame.
998        let mut atlas = GlyphAtlas::with_size(256);
999        let mut resets = 0;
1000        for frame in 0..40u32 {
1001            let epoch = atlas.epoch;
1002            atlas.begin_frame();
1003            if atlas.epoch != epoch {
1004                resets += 1;
1005            }
1006            let (size, epoch) = (atlas.size, atlas.epoch);
1007            for i in 0..8 {
1008                atlas.get_or_insert(fake_key(frame * 8 + i), || Some(raster(30, 30)));
1009            }
1010            assert_eq!(
1011                (atlas.size, atlas.epoch),
1012                (size, epoch),
1013                "frame {frame}: the page filled mid-frame"
1014            );
1015        }
1016        assert_eq!(atlas.size, 256);
1017        assert!(resets >= 4, "emptied between frames: {resets}");
1018    }
1019
1020    /// F83's thrash, measured: a page about to fill within two frames of
1021    /// being emptied is too small for its set and its turnover, and
1022    /// doubles at `begin_frame` with every slot in place — the set it
1023    /// holds is not rasterized again.
1024    #[test]
1025    fn a_page_emptied_again_within_two_frames_grows_with_its_slots() {
1026        let mut atlas = GlyphAtlas::with_size(256);
1027        // Six rows that stay, a new row a frame, on a page of eight.
1028        let mut next = 1000;
1029        let mut frame = |atlas: &mut GlyphAtlas| {
1030            atlas.begin_frame();
1031            for i in 0..48 {
1032                atlas.get_or_insert(fake_key(i), || Some(raster(30, 30)));
1033            }
1034            for _ in 0..8 {
1035                next += 1;
1036                atlas.get_or_insert(fake_key(next), || Some(raster(30, 30)));
1037            }
1038        };
1039        frame(&mut atlas); // on an empty page: seven rows
1040        frame(&mut atlas); // eight: full, a row a frame measured
1041        let epoch = atlas.epoch;
1042        frame(&mut atlas); // emptied before it: seven rows again
1043        assert_eq!((atlas.size, atlas.epoch), (256, epoch + 1));
1044        frame(&mut atlas); // a row turned over on it: full again
1045        assert_eq!(atlas.size, 256);
1046        atlas.begin_frame(); // no row free, a row a frame: again, so soon
1047        assert_eq!(atlas.size, 512, "grown instead");
1048        for i in 0..48 {
1049            atlas.get_or_insert(fake_key(i), || panic!("kept in place"));
1050        }
1051        let epoch = atlas.epoch;
1052        for _ in 0..40 {
1053            frame(&mut atlas);
1054            assert_eq!(atlas.size, 512, "and it holds");
1055        }
1056        assert!(
1057            atlas.epoch > epoch,
1058            "emptied between frames as it turns over"
1059        );
1060    }
1061
1062    /// F83: a set between one page and two — the glyphs of a big font —
1063    /// that first arrives on a page holding older glyphs: the frame it
1064    /// fills extends the page for itself, the next begins empty at the
1065    /// old size, fills again with that set alone, and keeps the growth;
1066    /// from then on the page is still.
1067    #[test]
1068    fn a_set_between_one_page_and_two_grows_on_its_second_frame() {
1069        let mut atlas = GlyphAtlas::with_size(64);
1070        let frame = |atlas: &mut GlyphAtlas, keys: std::ops::Range<u32>| {
1071            atlas.begin_frame();
1072            for i in keys {
1073                atlas.get_or_insert(fake_key(i), || Some(raster(30, 30)));
1074            }
1075        };
1076        frame(&mut atlas, 100..102);
1077        // Six 30×30 glyphs; a 64 page holds four.
1078        frame(&mut atlas, 0..6);
1079        assert_eq!(atlas.size, 128, "extended for the frame");
1080        frame(&mut atlas, 0..6);
1081        assert_eq!(atlas.size, 128, "the set alone overflowed: kept");
1082        let epoch = atlas.epoch;
1083        frame(&mut atlas, 0..6);
1084        frame(&mut atlas, 0..6);
1085        assert_eq!(atlas.epoch, epoch, "and still from then on");
1086    }
1087
1088    /// RG23: a fill long after the last, one that happens to want back a
1089    /// glyph an earlier reset dropped — the chrome's letters are in every
1090    /// set — is an ordinary turnover, and the page keeps its size.
1091    #[test]
1092    fn a_fill_long_after_a_reset_resets_even_with_a_dropped_glyph_back() {
1093        let mut atlas = GlyphAtlas::with_size(64);
1094        // 30×30 glyphs; a 64 page holds four.
1095        let frame = |atlas: &mut GlyphAtlas, keys: &[u32]| {
1096            atlas.begin_frame();
1097            for &i in keys {
1098                atlas.get_or_insert(fake_key(i), || Some(raster(30, 30)));
1099            }
1100        };
1101        frame(&mut atlas, &[0, 1, 2, 3]);
1102        frame(&mut atlas, &[4, 5, 6, 7]);
1103        for _ in 0..3 {
1104            frame(&mut atlas, &[4, 5, 6, 7]);
1105        }
1106        assert_eq!(atlas.size, 64, "one turnover, one reset");
1107        frame(&mut atlas, &[0, 8, 9, 10]);
1108        frame(&mut atlas, &[0, 8, 9, 10]);
1109        assert_eq!(atlas.size, 64, "a later turnover resets, it does not grow");
1110    }
1111
1112    /// F99: a page at `MAX_ATLAS_SIZE` cannot extend, so a fill there
1113    /// refuses the glyph for this frame rather than drop a slot the frame
1114    /// used, says so (`short`, which asks for the next frame), and the
1115    /// next frame begins on an empty page that takes it. A page that
1116    /// began the frame empty and still cannot take it is not short:
1117    /// nothing the next frame does would change that.
1118    #[test]
1119    fn a_full_page_at_the_largest_size_refuses_for_one_frame() {
1120        let mut atlas = GlyphAtlas::with_size(MAX_ATLAS_SIZE);
1121        // 1000×1000 items: a 4096 page holds sixteen.
1122        atlas.begin_frame();
1123        let mut used = Vec::new();
1124        for i in 0..16u32 {
1125            let v = i as u8 + 1;
1126            let slot = atlas
1127                .get_or_insert(fake_key(i), || Some(filled(1000, 1000, v)))
1128                .expect("fits");
1129            used.push((slot, v));
1130        }
1131        assert!(!atlas.short());
1132        atlas.begin_frame();
1133        assert!(
1134            atlas
1135                .get_or_insert(fake_key(16), || Some(raster(1000, 1000)))
1136                .is_none()
1137        );
1138        assert!(atlas.short(), "refused on a page holding the last frame");
1139        for &(slot, v) in &used {
1140            assert!(holds(&atlas, slot, v), "nothing dropped under the frame");
1141        }
1142        let epoch = atlas.epoch;
1143        atlas.begin_frame();
1144        assert!(atlas.epoch > epoch && !atlas.short());
1145        assert!(
1146            atlas
1147                .get_or_insert(fake_key(16), || Some(raster(1000, 1000)))
1148                .is_some()
1149        );
1150        for i in 0..16u32 {
1151            atlas.get_or_insert(fake_key(i), || Some(raster(1000, 1000)));
1152        }
1153        assert!(
1154            !atlas.short(),
1155            "a set bigger than the page on an empty one is not short"
1156        );
1157    }
1158
1159    /// RG56: a set bigger than a `MAX_ATLAS_SIZE` page on a frame that
1160    /// began it empty refuses what does not fit, and nothing the next
1161    /// frame does would change that, so it is not `short`. When the view
1162    /// then shows a part of the set that wants a refused glyph and fits,
1163    /// that frame is short and the next one empties the page and draws
1164    /// the glyph. It used to stay blank for as long as the page lived.
1165    #[test]
1166    fn a_glyph_refused_on_a_fresh_full_page_gets_room_once_its_set_fits() {
1167        let mut atlas = GlyphAtlas::with_size(MAX_ATLAS_SIZE);
1168        // 1000×1000 items: a 4096 page holds sixteen.
1169        let frame = |atlas: &mut GlyphAtlas, keys: std::ops::Range<u32>| {
1170            atlas.begin_frame();
1171            keys.map(|i| atlas.get_or_insert(fake_key(i), || Some(raster(1000, 1000))))
1172                .collect::<Vec<_>>()
1173        };
1174        let slots = frame(&mut atlas, 0..17);
1175        assert!(slots[16].is_none() && !atlas.short(), "refused, not short");
1176        // The view scrolls to the last four: they fit, one is refused.
1177        let slots = frame(&mut atlas, 13..17);
1178        assert!(slots[3].is_none(), "the cached refusal, this frame");
1179        assert!(atlas.short(), "and the next frame is owed");
1180        let epoch = atlas.epoch;
1181        let slots = frame(&mut atlas, 13..17);
1182        assert!(atlas.epoch > epoch, "emptied before anything is emitted");
1183        assert!(slots.iter().all(Option::is_some), "and drawn: {slots:?}");
1184        assert!(!atlas.short());
1185        let (epoch, stamp) = (atlas.epoch, atlas.stamp);
1186        for _ in 0..3 {
1187            frame(&mut atlas, 13..17);
1188        }
1189        assert_eq!((atlas.epoch, atlas.stamp), (epoch, stamp), "then still");
1190    }
1191
1192    /// RG56: while the whole of a set bigger than the page stays on
1193    /// screen, the frames that measure it find it does not fit and keep
1194    /// the page — nothing is emptied or asked for, however many frames —
1195    /// while the stamp moves, so the caches that keep slots look them up
1196    /// and are counted.
1197    #[test]
1198    fn a_set_bigger_than_the_largest_page_keeps_it_still() {
1199        let mut atlas = GlyphAtlas::with_size(MAX_ATLAS_SIZE);
1200        let frame = |atlas: &mut GlyphAtlas| {
1201            atlas.begin_frame();
1202            for i in 0..17u32 {
1203                atlas.get_or_insert(fake_key(i), || Some(raster(1000, 1000)));
1204            }
1205        };
1206        frame(&mut atlas);
1207        let epoch = atlas.epoch;
1208        for _ in 0..5 {
1209            let stamp = atlas.stamp;
1210            frame(&mut atlas);
1211            assert!(!atlas.short(), "the set does not fit: nothing owed");
1212            assert_eq!(atlas.epoch, epoch, "the page is kept");
1213            assert!(atlas.stamp > stamp, "measured again");
1214        }
1215        // A page with nothing refused keeps its stamp from frame to frame.
1216        let mut atlas = GlyphAtlas::with_size(256);
1217        atlas.begin_frame();
1218        atlas.get_or_insert(fake_key(0), || Some(raster(30, 30)));
1219        let stamp = atlas.stamp;
1220        atlas.begin_frame();
1221        atlas.get_or_insert(fake_key(0), || panic!("cached"));
1222        assert_eq!(atlas.stamp, stamp);
1223    }
1224
1225    /// One view's worth of new glyphs arriving at once — a tab of other
1226    /// fonts opened beside a steady chrome — fits the page, and a page
1227    /// whose set then holds still keeps its size: a burst is not a
1228    /// turnover (the regression pass over F99: the burst's rows, decayed,
1229    /// read as a page about to fill again within two frames of emptying).
1230    #[test]
1231    fn a_burst_that_fits_does_not_grow_the_page() {
1232        // 30×30 glyphs: 33 to a 32 px row of a 1024 page.
1233        let mut atlas = GlyphAtlas::with_size(1024);
1234        let frame = |atlas: &mut GlyphAtlas, burst: bool| {
1235            atlas.begin_frame();
1236            for i in 0..297 {
1237                atlas.get_or_insert(fake_key(i), || Some(raster(30, 30)));
1238            }
1239            if burst {
1240                for i in 1000..1627 {
1241                    atlas.get_or_insert(fake_key(i), || Some(raster(30, 30)));
1242                }
1243            }
1244        };
1245        for _ in 0..4 {
1246            frame(&mut atlas, false);
1247        }
1248        for n in 0..12 {
1249            frame(&mut atlas, true);
1250            assert_eq!(atlas.size, 1024, "frame {n} after the burst");
1251        }
1252        atlas.begin_frame();
1253        assert_eq!(atlas.size, 1024);
1254    }
1255
1256    /// F66: a synthesized shape is one slot per character and cell size —
1257    /// the same size twice shares, another size does not — and a reset
1258    /// drops it with the glyphs.
1259    /// DX26: the frame that begins on an emptied page copies what the old
1260    /// page held and it looks up — texels, offsets, kind — rather than
1261    /// rasterizing it again, and only for that frame. What it did not look
1262    /// up is gone with the old page, and `clear` keeps nothing.
1263    #[test]
1264    fn the_frame_after_a_reset_copies_what_the_old_page_held() {
1265        let mut atlas = GlyphAtlas::with_size(64);
1266        atlas.begin_frame();
1267        let glyph = |v: u8| RasterGlyph {
1268            left: -2,
1269            top: 9,
1270            subpixel: true,
1271            ..filled(30, 30, v)
1272        };
1273        for i in 0..4u32 {
1274            atlas.get_or_insert(fake_key(i), || Some(glyph(i as u8 + 1)));
1275        }
1276        atlas.get_or_insert_synth('─', 8, 16, || vec![200; 8 * 16]);
1277        atlas.short = true; // what a refusal leaves: the next frame resets
1278        let epoch = atlas.epoch;
1279        atlas.begin_frame();
1280        assert!(atlas.epoch > epoch, "the page was emptied");
1281
1282        let mut rasterized = 0;
1283        for i in [2u32, 0] {
1284            let slot = atlas
1285                .get_or_insert(fake_key(i), || {
1286                    rasterized += 1;
1287                    Some(glyph(0))
1288                })
1289                .unwrap();
1290            assert!(holds(&atlas, slot, i as u8 + 1), "glyph {i}'s texels");
1291            assert_eq!((slot.left, slot.top, slot.subpixel), (-2, 9, true));
1292        }
1293        let mut drawn = 0;
1294        let shape = atlas
1295            .get_or_insert_synth('─', 8, 16, || {
1296                drawn += 1;
1297                vec![0; 8 * 16]
1298            })
1299            .unwrap();
1300        assert_eq!(
1301            atlas.pixels[((shape.y * atlas.size + shape.x) * 4 + 3) as usize],
1302            200
1303        );
1304        assert_eq!((rasterized, drawn), (0, 0), "copied, not drawn again");
1305        assert_eq!(
1306            atlas.placed,
1307            2 * texels(30, 30) + texels(8, 16),
1308            "only what was looked up is placed"
1309        );
1310
1311        // The frame's end: the old page is gone, and in the next frame a
1312        // glyph the reset frame did not ask for is rasterized.
1313        atlas.end_frame();
1314        assert!(atlas.prev.is_none(), "not held past its frame");
1315        atlas.begin_frame();
1316        atlas.get_or_insert(fake_key(1), || {
1317            rasterized += 1;
1318            Some(glyph(2))
1319        });
1320        assert_eq!(rasterized, 1);
1321
1322        // `clear` (a raster mode that changed) keeps nothing.
1323        atlas.clear();
1324        atlas.begin_frame();
1325        atlas.get_or_insert(fake_key(1), || {
1326            rasterized += 1;
1327            Some(glyph(2))
1328        });
1329        assert_eq!(rasterized, 2);
1330    }
1331
1332    #[test]
1333    fn a_synthesized_shape_is_keyed_on_its_cell_size() {
1334        use std::cell::Cell;
1335        let mut atlas = GlyphAtlas::with_size(128);
1336        let calls = Cell::new(0);
1337        let draw = |atlas: &mut GlyphAtlas, w, h| {
1338            atlas
1339                .get_or_insert_synth('│', w, h, || {
1340                    calls.set(calls.get() + 1);
1341                    vec![255; (w * h) as usize]
1342                })
1343                .expect("fits")
1344        };
1345        let a = draw(&mut atlas, 7, 16);
1346        let b = draw(&mut atlas, 7, 16);
1347        let c = draw(&mut atlas, 7, 20);
1348        assert_eq!((a.x, a.y), (b.x, b.y), "the same size shares a slot");
1349        assert_ne!((a.x, a.y), (c.x, c.y), "another size does not");
1350        assert_eq!(calls.get(), 2);
1351        assert!(!a.color_glyph && !a.subpixel && a.left == 0 && a.top == 0);
1352        // The mask landed as a tinted white mask.
1353        let i = ((a.y * atlas.size + a.x) * 4) as usize;
1354        assert_eq!(&atlas.pixels[i..i + 4], &[255, 255, 255, 255]);
1355        atlas.clear();
1356        draw(&mut atlas, 7, 16);
1357        assert_eq!(calls.get(), 3, "a reset drops the shape with the glyphs");
1358    }
1359
1360    #[test]
1361    fn oversized_content_grows_the_page() {
1362        let mut atlas = GlyphAtlas::with_size(64);
1363        let s = atlas.get_or_insert(fake_key(5), || Some(raster(200, 200)));
1364        assert!(s.is_some(), "the page should double until it fits");
1365        assert!(atlas.size >= 256, "size is {}", atlas.size);
1366        assert!(
1367            atlas
1368                .get_or_insert(fake_key(6), || Some(raster(10, 10)))
1369                .is_some()
1370        );
1371    }
1372
1373    #[test]
1374    fn impossible_content_is_rejected() {
1375        let mut atlas = GlyphAtlas::with_size(64);
1376        let big = MAX_ATLAS_SIZE + 1;
1377        let s = atlas.get_or_insert(fake_key(7), || Some(raster(big, 1)));
1378        assert!(s.is_none());
1379        // Still functional afterwards.
1380        assert!(
1381            atlas
1382                .get_or_insert(fake_key(8), || Some(raster(10, 10)))
1383                .is_some()
1384        );
1385    }
1386}