Skip to main content

densa_text/
cache.rs

1use etagere::{size2, AllocId, Allocation, AtlasAllocator};
2use rustc_hash::FxHashMap;
3use std::fmt;
4
5use crate::{rasterize, Font, FontId, GlyphId};
6
7/// Subpixel positions per pixel: glyphs are rasterized at 0, 1/4, 1/2 and 3/4 px offsets.
8pub const SUBPIXEL_STEPS: u8 = 4;
9
10/// Transparent border written around every glyph, so sampling at a glyph's edge (even with
11/// bilinear filtering) never picks up a neighbour or a previous occupant of the space.
12const PADDING: u32 = 1;
13
14/// Pixels antialiasing and subpixel shifts may add to an outline's bounding box.
15const RASTER_SLACK: f32 = 2.0;
16
17/// Largest atlas side in texels: within every desktop GPU's 2D texture limit.
18const MAX_ATLAS_SIDE: u32 = 8192;
19
20/// Entries holding no atlas space (whitespace, too large, waiting) kept before the ones not
21/// used this frame are forgotten, so ever-changing sizes cannot grow the cache without bound.
22const MAX_UNPLACED: usize = 4096;
23
24/// Splits a horizontal position into a whole pixel and a subpixel bin (`0..SUBPIXEL_STEPS`),
25/// rounding to the nearest bin. Draw the glyph cached for `bin` at the returned pixel.
26pub fn quantize_x(x: f32) -> (i32, u8) {
27    let steps = i32::from(SUBPIXEL_STEPS);
28    // Lossless enough: screen coordinates are far below 2^31 / SUBPIXEL_STEPS.
29    let scaled = (x * steps as f32).round() as i32;
30    // Lossless: rem_euclid keeps the value in 0..SUBPIXEL_STEPS.
31    (scaled.div_euclid(steps), scaled.rem_euclid(steps) as u8)
32}
33
34/// Shape of the glyph atlas: square pages stacked vertically in one texture.
35///
36/// The atlas starts as one page and grows a page at a time, up to `max_pages`; growing never
37/// moves a glyph. Once at its limit, the least recently used glyphs are evicted.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct AtlasConfig {
41    /// Side of one page in texels; also the largest glyph the atlas holds.
42    pub page_size: u32,
43    /// Pages the atlas may grow to.
44    pub max_pages: u32,
45}
46
47impl AtlasConfig {
48    /// 1024 x 1024 pages, up to 4 (a 1024 x 4096 texture, 4 MiB).
49    pub const DEFAULT: Self = Self::new(1024, 4);
50
51    /// `max_pages` pages of `page_size` x `page_size` texels.
52    pub const fn new(page_size: u32, max_pages: u32) -> Self {
53        Self {
54            page_size,
55            max_pages,
56        }
57    }
58
59    /// The config clamped to what the cache supports: a page of 1 to 4096 texels, at least
60    /// one page, and a texture no taller than 8192 texels.
61    pub fn normalized(self) -> Self {
62        let page_size = self.page_size.clamp(1, 4096);
63        Self {
64            page_size,
65            max_pages: self.max_pages.clamp(1, MAX_ATLAS_SIDE / page_size),
66        }
67    }
68}
69
70impl Default for AtlasConfig {
71    fn default() -> Self {
72        Self::DEFAULT
73    }
74}
75
76/// A glyph ready to draw from the atlas.
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct CachedGlyph {
79    /// `[x0, y0, x1, y1]` of the glyph's coverage in atlas texels (not normalized: stays
80    /// valid when the atlas grows).
81    pub atlas_rect: [f32; 4],
82    /// Bitmap size in pixels; `[0, 0]` for glyphs without ink.
83    pub size: [f32; 2],
84    /// Offset from the pen position (x) and baseline (y, negative = up) to the bitmap's
85    /// top-left corner, in pixels.
86    pub offset: [f32; 2],
87}
88
89impl CachedGlyph {
90    const EMPTY: Self = Self {
91        atlas_rect: [0.0; 4],
92        size: [0.0; 2],
93        offset: [0.0; 2],
94    };
95}
96
97/// Why [`GlyphCache::get`] has no glyph to draw.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[non_exhaustive]
100pub enum GlyphUnavailable {
101    /// No room this frame: everything evictable was evicted and it still did not fit. The
102    /// rasterized glyph is kept and placed by a later lookup once there is room.
103    NoRoom,
104    /// The glyph at this size is larger than an atlas page; it never will fit.
105    TooLarge,
106    /// The size is not a finite, positive number of pixels.
107    InvalidSize,
108}
109
110impl fmt::Display for GlyphUnavailable {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.write_str(match self {
113            Self::NoRoom => "no room in the glyph atlas this frame",
114            Self::TooLarge => "glyph is larger than a glyph atlas page",
115            Self::InvalidSize => "font size is not a finite, positive number of pixels",
116        })
117    }
118}
119
120impl std::error::Error for GlyphUnavailable {}
121
122/// The device cannot hold the atlas: its texture limit is below the atlas already in use.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct AtlasLimitError {
125    /// The device's largest texture side.
126    pub max_side: u32,
127    /// Texels the atlas needs at least: its page side, or its current height.
128    pub needed: u32,
129}
130
131impl fmt::Display for AtlasLimitError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(
134            f,
135            "glyph atlas needs textures of {} texels, the device allows {}",
136            self.needed, self.max_side
137        )
138    }
139}
140
141impl std::error::Error for AtlasLimitError {}
142
143/// Pixels a renderer must copy into its atlas texture.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct GlyphUpload {
146    /// Left edge in the atlas, in texels.
147    pub x: u32,
148    /// Top edge in the atlas, in texels.
149    pub y: u32,
150    /// Width in texels.
151    pub width: u32,
152    /// Height in texels.
153    pub height: u32,
154    /// Row-major coverage, `width` bytes per row; includes the glyph's transparent border.
155    pub coverage: Vec<u8>,
156}
157
158/// A change the renderer must apply to its atlas texture, in queue order.
159#[derive(Debug, Clone, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum AtlasUpdate {
162    /// Make the texture at least `width` x `height`, keeping existing texels where they are.
163    /// Always the first update, so the renderer learns the size from the cache itself.
164    Resize {
165        /// Width in texels.
166        width: u32,
167        /// Height in texels.
168        height: u32,
169    },
170    /// Copy pixels into the texture.
171    Write(GlyphUpload),
172}
173
174/// Cumulative cache counters.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
176#[non_exhaustive]
177pub struct CacheStats {
178    /// Lookups answered from the cache.
179    pub hits: u64,
180    /// Lookups of a glyph not cached yet.
181    pub misses: u64,
182    /// Glyphs evicted to make room.
183    pub evictions: u64,
184    /// Lookups that returned no glyph (see [`GlyphUnavailable`]).
185    pub dropped: u64,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189struct GlyphKey {
190    font: FontId,
191    glyph: GlyphId,
192    /// Size in 1/64 px, so nearly equal float sizes share entries (and rasterize alike).
193    size_64: u32,
194    subpixel: u8,
195}
196
197impl GlyphKey {
198    /// The size the glyph is rasterized at: the key's, not the caller's unquantized one.
199    fn px(self) -> f32 {
200        self.size_64 as f32 / 64.0
201    }
202}
203
204/// Rasterized coverage with its transparent border, waiting for (or written to) atlas space.
205#[derive(Debug)]
206struct Padded {
207    width: u32,
208    height: u32,
209    coverage: Vec<u8>,
210}
211
212#[derive(Debug)]
213enum Slot {
214    /// In the atlas.
215    Placed { page: usize, id: AllocId },
216    /// Nothing to draw (whitespace).
217    NoInk,
218    /// Rasterized, no room yet. `failed_at` is the [`GlyphCache::room_stamp`] of the last
219    /// attempt: retrying before it changes would fail again, so it is skipped.
220    Waiting {
221        padded: Padded,
222        failed_at: (u64, u64),
223    },
224    /// Larger than a page: can never be placed.
225    TooLarge,
226}
227
228#[derive(Debug)]
229struct Entry {
230    /// Metrics; `atlas_rect` is meaningful only when placed.
231    glyph: CachedGlyph,
232    slot: Slot,
233    /// Frame of the last lookup.
234    last_used: u64,
235}
236
237/// Rasterized glyphs packed into a paged coverage atlas with per-glyph LRU eviction.
238///
239/// Call [`begin_frame`](Self::begin_frame) once per frame, [`get`](Self::get) for every
240/// glyph drawn, then apply [`drain_updates`](Self::drain_updates) to the GPU texture before
241/// drawing. A glyph looked up in the current frame is never evicted, so every atlas rectangle
242/// handed out during a frame stays valid until the next `begin_frame`. One cache serves one
243/// frame loop: two render loops sharing it would evict each other's glyphs mid-frame.
244///
245/// When full, the atlas grows a page; at its page limit it evicts the glyphs used least
246/// recently (never this frame's). A glyph is unavailable only if it does not fit even then.
247///
248/// Allocation: rasterizing a glyph for the first time allocates, and so does adding a page.
249/// Hits, evictions and retries of waiting glyphs do not (the eviction order reuses its
250/// buffer; it grows only past its largest size so far).
251pub struct GlyphCache {
252    config: AtlasConfig,
253    pages: Vec<AtlasAllocator>,
254    entries: FxHashMap<GlyphKey, Entry>,
255    updates: Vec<AtlasUpdate>,
256    /// Glyphs not used this frame, least recently used first; built on the frame's first
257    /// eviction and consumed from `evict_next`.
258    evict_order: Vec<(u64, GlyphKey)>,
259    evict_next: usize,
260    evict_frame: u64,
261    /// Entries without atlas space, and the count that triggers forgetting cold ones.
262    unplaced: usize,
263    unplaced_limit: usize,
264    frame: u64,
265    /// Changes whenever atlas space is freed or added.
266    room: u64,
267    generation: u64,
268    stats: CacheStats,
269}
270
271impl fmt::Debug for GlyphCache {
272    // Hand-written: etagere's allocator does not implement Debug.
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.debug_struct("GlyphCache")
275            .field("config", &self.config)
276            .field("pages", &self.pages.len())
277            .field("glyphs", &self.entries.len())
278            .field("pending_updates", &self.updates.len())
279            .field("frame", &self.frame)
280            .field("generation", &self.generation)
281            .field("stats", &self.stats)
282            .finish_non_exhaustive()
283    }
284}
285
286impl GlyphCache {
287    /// An empty cache of one page; `config` is [normalized](AtlasConfig::normalized).
288    pub fn new(config: AtlasConfig) -> Self {
289        let config = config.normalized();
290        let mut cache = Self {
291            config,
292            pages: Vec::with_capacity(config.max_pages as usize),
293            entries: FxHashMap::default(),
294            updates: Vec::new(),
295            evict_order: Vec::new(),
296            evict_next: 0,
297            evict_frame: 0,
298            unplaced: 0,
299            unplaced_limit: MAX_UNPLACED,
300            frame: 0,
301            room: 0,
302            generation: 0,
303            stats: CacheStats::default(),
304        };
305        cache.add_page();
306        cache
307    }
308
309    /// The atlas shape (normalized, and capped by [`limit_side`](Self::limit_side)).
310    pub fn config(&self) -> AtlasConfig {
311        self.config
312    }
313
314    /// Caps growth so the texture never exceeds `max_side` texels (the device's
315    /// `max_texture_dimension_2d`).
316    ///
317    /// # Errors
318    /// If a page, or the atlas already grown, is larger than `max_side`.
319    pub fn limit_side(&mut self, max_side: u32) -> Result<(), AtlasLimitError> {
320        let needed = self.config.page_size.max(self.atlas_extent().1);
321        if needed > max_side {
322            return Err(AtlasLimitError { max_side, needed });
323        }
324        let pages_fitting = max_side / self.config.page_size;
325        self.config.max_pages = self.config.max_pages.min(pages_fitting);
326        Ok(())
327    }
328
329    /// Current texture size in texels: `(page_size, page_size * pages)`.
330    pub fn atlas_extent(&self) -> (u32, u32) {
331        let page = self.config.page_size;
332        // Lossless: pages are capped so the height stays within MAX_ATLAS_SIDE.
333        (page, page * self.pages.len() as u32)
334    }
335
336    /// Glyphs cached, with or without atlas space.
337    pub fn cached_glyphs(&self) -> usize {
338        self.entries.len()
339    }
340
341    /// Starts a frame. Glyphs used in earlier frames become evictable.
342    pub fn begin_frame(&mut self) {
343        self.frame += 1;
344    }
345
346    /// Increments whenever glyphs are evicted: atlas rectangles from earlier frames may now
347    /// hold other glyphs.
348    pub fn generation(&self) -> u64 {
349        self.generation
350    }
351
352    /// Cumulative counters.
353    pub fn stats(&self) -> CacheStats {
354        self.stats
355    }
356
357    /// The atlas placement of `glyph` at `px` pixels per em and subpixel bin `subpixel`
358    /// (see [`quantize_x`]), rasterizing it on first use.
359    ///
360    /// # Errors
361    /// [`GlyphUnavailable`] when there is nothing to draw this frame; counted in
362    /// [`CacheStats::dropped`].
363    pub fn get(
364        &mut self,
365        font: &Font,
366        glyph: GlyphId,
367        px: f32,
368        subpixel: u8,
369    ) -> Result<CachedGlyph, GlyphUnavailable> {
370        if !(px.is_finite() && px > 0.0) {
371            self.stats.dropped += 1;
372            return Err(GlyphUnavailable::InvalidSize);
373        }
374        let key = GlyphKey {
375            font: font.id(),
376            glyph,
377            // Saturating cast: absurd sizes become a huge key, rejected as TooLarge below.
378            size_64: (px * 64.0).round() as u32,
379            subpixel: subpixel % SUBPIXEL_STEPS,
380        };
381        let room = self.room_stamp();
382        if let Some(entry) = self.entries.get_mut(&key) {
383            entry.last_used = self.frame;
384            match &entry.slot {
385                Slot::Placed { .. } => {
386                    self.stats.hits += 1;
387                    return Ok(entry.glyph);
388                }
389                Slot::NoInk => {
390                    self.stats.hits += 1;
391                    return Ok(CachedGlyph::EMPTY);
392                }
393                Slot::TooLarge => {
394                    self.stats.dropped += 1;
395                    return Err(GlyphUnavailable::TooLarge);
396                }
397                Slot::Waiting { failed_at, .. } if *failed_at == room => {
398                    self.stats.dropped += 1;
399                    return Err(GlyphUnavailable::NoRoom);
400                }
401                Slot::Waiting { .. } => {}
402            }
403            let Some(Entry {
404                glyph,
405                slot: Slot::Waiting { padded, .. },
406                ..
407            }) = self.entries.remove(&key)
408            else {
409                unreachable!("the entry was just seen waiting");
410            };
411            self.unplaced -= 1;
412            return self.place(key, glyph, padded);
413        }
414        self.stats.misses += 1;
415        self.rasterize_new(key, font)
416    }
417
418    /// Takes the atlas changes queued since the last call, in the order they must be applied.
419    /// Apply all of them: the queue is emptied even if the iterator is dropped early.
420    pub fn drain_updates(&mut self) -> impl ExactSizeIterator<Item = AtlasUpdate> + '_ {
421        self.updates.drain(..)
422    }
423
424    /// Identifies the free space: retrying a placement that failed under the same stamp
425    /// cannot succeed (nothing was freed or added, and nothing became evictable).
426    fn room_stamp(&self) -> (u64, u64) {
427        (self.frame, self.room)
428    }
429
430    fn rasterize_new(
431        &mut self,
432        key: GlyphKey,
433        font: &Font,
434    ) -> Result<CachedGlyph, GlyphUnavailable> {
435        let px = key.px();
436        // Bound the work before rasterizing: a huge size must not allocate a huge bitmap.
437        // Lenient by the raster slack (the exact check follows rasterization), so the bitmap
438        // allocated is at most about a page.
439        let limit = self.config.page_size.saturating_sub(2 * PADDING) as f32 + RASTER_SLACK;
440        let may_fit = match font.glyph_extent(key.glyph, px) {
441            Some([w, h]) => w <= limit && h <= limit,
442            // Unknown extent: assume up to two ems.
443            None => 2.0 * px <= limit,
444        };
445        if !may_fit {
446            return self.too_large(key, CachedGlyph::EMPTY);
447        }
448
449        let x_offset = f32::from(key.subpixel) / f32::from(SUBPIXEL_STEPS);
450        let bitmap = rasterize(font, key.glyph, px, x_offset);
451        if bitmap.width == 0 {
452            self.insert(key, CachedGlyph::EMPTY, Slot::NoInk);
453            return Ok(CachedGlyph::EMPTY);
454        }
455        let glyph = CachedGlyph {
456            atlas_rect: [0.0; 4],
457            size: [bitmap.width as f32, bitmap.height as f32],
458            offset: [bitmap.left as f32, bitmap.top as f32],
459        };
460        let max = self.config.page_size.saturating_sub(2 * PADDING);
461        if bitmap.width > max || bitmap.height > max {
462            return self.too_large(key, glyph); // The estimate was optimistic.
463        }
464        let padded = pad(bitmap.width, bitmap.height, &bitmap.coverage);
465        self.place(key, glyph, padded)
466    }
467
468    fn too_large(
469        &mut self,
470        key: GlyphKey,
471        glyph: CachedGlyph,
472    ) -> Result<CachedGlyph, GlyphUnavailable> {
473        self.stats.dropped += 1;
474        self.insert(key, glyph, Slot::TooLarge);
475        Err(GlyphUnavailable::TooLarge)
476    }
477
478    /// Finds atlas space for `padded` and queues its upload, or parks it as waiting.
479    fn place(
480        &mut self,
481        key: GlyphKey,
482        glyph: CachedGlyph,
483        padded: Padded,
484    ) -> Result<CachedGlyph, GlyphUnavailable> {
485        let Some((page, allocation)) = self.allocate(padded.width, padded.height) else {
486            self.stats.dropped += 1;
487            let failed_at = self.room_stamp();
488            self.insert(key, glyph, Slot::Waiting { padded, failed_at });
489            return Err(GlyphUnavailable::NoRoom);
490        };
491        // Lossless: etagere rectangles lie inside the page, so they are non-negative, and
492        // page offsets stay within MAX_ATLAS_SIDE.
493        let x = allocation.rectangle.min.x as u32;
494        let y = page as u32 * self.config.page_size + allocation.rectangle.min.y as u32;
495        let (inner_x, inner_y) = ((x + PADDING) as f32, (y + PADDING) as f32);
496        let placed = CachedGlyph {
497            atlas_rect: [
498                inner_x,
499                inner_y,
500                inner_x + glyph.size[0],
501                inner_y + glyph.size[1],
502            ],
503            ..glyph
504        };
505        self.updates.push(AtlasUpdate::Write(GlyphUpload {
506            x,
507            y,
508            width: padded.width,
509            height: padded.height,
510            coverage: padded.coverage,
511        }));
512        let slot = Slot::Placed {
513            page,
514            id: allocation.id,
515        };
516        self.insert(key, placed, slot);
517        Ok(placed)
518    }
519
520    /// Space for a `width` x `height` rectangle (no larger than a page): first fit in an
521    /// existing page, else a new page, else evicting least recently used glyphs not used
522    /// this frame until it fits.
523    fn allocate(&mut self, width: u32, height: u32) -> Option<(usize, Allocation)> {
524        // Lossless: callers guarantee the size fits in a page (≤ 4096).
525        let size = size2(width as i32, height as i32);
526        let fit = self
527            .pages
528            .iter_mut()
529            .enumerate()
530            .find_map(|(index, page)| page.allocate(size).map(|a| (index, a)));
531        if fit.is_some() {
532            return fit;
533        }
534        if self.pages.len() < self.config.max_pages as usize {
535            self.add_page();
536            let index = self.pages.len() - 1;
537            // An empty page always fits a rectangle no larger than itself.
538            return self.pages[index].allocate(size).map(|a| (index, a));
539        }
540
541        if self.evict_frame != self.frame {
542            self.build_evict_order();
543        }
544        while let Some(&(_, key)) = self.evict_order.get(self.evict_next) {
545            self.evict_next += 1;
546            let Some(page) = self.evict(key) else {
547                continue; // Gone already, or used since the order was built.
548            };
549            // Freed space merges with its neighbours on that page only.
550            if let Some(allocation) = self.pages[page].allocate(size) {
551                return Some((page, allocation));
552            }
553        }
554        None
555    }
556
557    /// Orders the glyphs placed before this frame, least recently used first. Reuses the
558    /// buffer, so it allocates only past its largest size so far.
559    fn build_evict_order(&mut self) {
560        let frame = self.frame;
561        self.evict_order.clear();
562        self.evict_order.extend(
563            self.entries
564                .iter()
565                .filter(|(_, e)| e.last_used < frame && matches!(e.slot, Slot::Placed { .. }))
566                .map(|(key, e)| (e.last_used, *key)),
567        );
568        self.evict_order
569            .sort_unstable_by_key(|(last_used, _)| *last_used);
570        self.evict_next = 0;
571        self.evict_frame = frame;
572    }
573
574    /// Evicts `key` if it is still placed and unused this frame; returns its page.
575    fn evict(&mut self, key: GlyphKey) -> Option<usize> {
576        let entry = self.entries.get(&key)?;
577        let Slot::Placed { page, id } = entry.slot else {
578            return None;
579        };
580        if entry.last_used >= self.frame {
581            return None;
582        }
583        self.entries.remove(&key);
584        self.pages[page].deallocate(id);
585        self.stats.evictions += 1;
586        self.generation += 1;
587        self.room += 1;
588        Some(page)
589    }
590
591    fn add_page(&mut self) {
592        // Lossless enough: page sizes are clamped to 4096.
593        let side = self.config.page_size as i32;
594        self.pages.push(AtlasAllocator::new(size2(side, side)));
595        self.room += 1;
596        let (width, height) = self.atlas_extent();
597        self.updates.push(AtlasUpdate::Resize { width, height });
598    }
599
600    fn insert(&mut self, key: GlyphKey, glyph: CachedGlyph, slot: Slot) {
601        if !matches!(slot, Slot::Placed { .. }) {
602            self.unplaced += 1;
603            if self.unplaced > self.unplaced_limit {
604                self.forget_cold_unplaced();
605            }
606        }
607        let last_used = self.frame;
608        self.entries.insert(
609            key,
610            Entry {
611                glyph,
612                slot,
613                last_used,
614            },
615        );
616    }
617
618    /// Drops entries without atlas space that were not used this frame. If this frame alone
619    /// uses more than the limit, the limit rises so this does not repeat on every insert.
620    fn forget_cold_unplaced(&mut self) {
621        let frame = self.frame;
622        self.entries
623            .retain(|_, e| matches!(e.slot, Slot::Placed { .. }) || e.last_used >= frame);
624        self.unplaced = self
625            .entries
626            .values()
627            .filter(|e| !matches!(e.slot, Slot::Placed { .. }))
628            .count()
629            + 1; // The entry being inserted.
630        self.unplaced_limit = MAX_UNPLACED.max(2 * self.unplaced);
631    }
632}
633
634/// `coverage` (`width` x `height`) surrounded by a transparent border of `PADDING`.
635fn pad(width: u32, height: u32, coverage: &[u8]) -> Padded {
636    let (padded_width, padded_height) = (width + 2 * PADDING, height + 2 * PADDING);
637    let (w, p, pw) = (width as usize, PADDING as usize, padded_width as usize);
638    let mut padded = vec![0u8; pw * padded_height as usize];
639    for (row, src) in coverage.chunks_exact(w).enumerate() {
640        let start = (row + p) * pw + p;
641        padded[start..start + w].copy_from_slice(src);
642    }
643    Padded {
644        width: padded_width,
645        height: padded_height,
646        coverage: padded,
647    }
648}