src/flit/rendering/glyph_atlas

Glyph atlas: cached rasterized text. The first time a piece of text is drawn at a given font+size+color, it is rasterized via Pixie into an SDL_Texture. Every subsequent draw composites the cached texture with SDL_RenderCopy (GPU). Stable labels stop costing per-frame CPU work entirely.

This is a string-level atlas, not a per-glyph atlas. The trade-off is simpler code and zero text-shaping overhead at the cost of memory when the app draws many unique strings. For typical UIs with a stable set of button labels, headings, and body copy, the working set is small and the cache hit rate is essentially 100%.

Integration with HarfBuzz for proper text shaping (ligatures, complex scripts, contextual forms) is a follow-up; this atlas works on top of Pixie's typeset which handles Latin scripts fully.

Types

GlyphAtlas = ref object
  renderer*: RendererPtr
  cache*: Table[GlyphKey, GlyphEntry]
  order*: seq[GlyphKey]
  maxEntries*: int
  hbFonts*: Table[int, HbFont]

Owns the cache. renderer is the SDL renderer to upload textures with. maxEntries caps memory; oldest entries (by insertion order) are evicted past the cap.

When HarfBuzz is available, callers may register per-font HarfBuzz handles via registerHbFont so the atlas can shape text with proper ligatures and kerning before rasterization. The mapping key is the same fontHash used in cache keys.

GlyphEntry = ref object
  texture*: TexturePtr
  width*: int
  height*: int
A cached rasterized text run. texture is GPU-resident. width and height are the pixel dims used to size the destination rect when compositing.

Procs

proc `==`(a, b: GlyphKey): bool {....raises: [], tags: [], forbids: [].}
proc clear(a: GlyphAtlas) {....raises: [], tags: [], forbids: [].}
Drops every cached entry, destroying textures. Call before tearing down the renderer to avoid leaking GPU memory.
proc evictOldest(a: GlyphAtlas) {....raises: [KeyError], tags: [], forbids: [].}
Drops the oldest entry from the cache and destroys its texture. Called automatically when the cache exceeds maxEntries; can be called manually to trim memory.
proc getOrRasterize(a: GlyphAtlas; font: Font; text: string; fontSize: float32;
                    color: uint32; fontHash: int): GlyphEntry {.
    ...raises: [KeyError, PixieError], tags: [RootEffect], forbids: [].}

Looks up a cached entry; rasterizes and caches one if missing. Returns the entry whose texture is GPU-ready for SDL_RenderCopy. Texture lifetime is managed by the atlas; callers must not destroy it.

Inputs:

  • font: a Pixie font. The font's size is mutated during rasterization but restored to fontSize on each call.
  • text: the string to draw.
  • fontSize: pixel size.
  • color: ARGB8 fill color.
  • fontHash: a stable hash distinguishing this font from others. Callers typically use cast[int](font) for ref identity, or hash by font family name.
proc hash(k: GlyphKey): Hash {....raises: [], tags: [], forbids: [].}
proc measureShaped(a: GlyphAtlas; fontHash: int; text: string; fontSize: float32): tuple[
    width, height: float32] {....raises: [KeyError], tags: [], forbids: [].}
Returns the shaped width and font ascent height for text at fontSize using the HarfBuzz font registered for fontHash. Returns (0, 0) if no HarfBuzz font is registered.
proc newGlyphAtlas(renderer: RendererPtr; maxEntries = 512): GlyphAtlas {.
    ...raises: [], tags: [], forbids: [].}
Builds a fresh atlas backed by renderer. maxEntries caps the cache; older textures are destroyed and removed past that point. Tune based on UI complexity; 512 covers a decent-sized desktop app.
proc registerHbFont(a: GlyphAtlas; fontHash: int; fontPath: string) {.
    ...raises: [], tags: [ReadIOEffect], forbids: [].}
Registers a HarfBuzz font for shaping. Subsequent getOrRasterize calls with the matching fontHash will shape the text through HarfBuzz before rasterization. If HarfBuzz is not available or the font can't be loaded, this is a no-op and the atlas falls back to Pixie's internal typeset.