9 Glyph Atlas
dxtr edited this page 2026-10-19 19:21:23 +02:00

Glyph Atlas

The optional system cl-textrender/atlas is for renderers that draw text from a texture. It rasterizes each glyph once, with the raster backend, and packs its coverage into a cl-atlas atlas of one byte a texel. Each font has an atlas of its own for each scale, which the font keeps, as Godot's fonts keep their glyph textures, and which goes with it, or with the last view that draws at its scale. The renderer uploads the atlases' pages as textures and draws each glyph as a textured quad.

(asdf:load-system :cl-textrender/atlas)

(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 16)
  (let ((layout (tr:layout-text "Hi" font))
        (glyphs (cl-textrender/atlas:make-glyph-atlas))       ; :page-size, :hinting
        (quads '()))
    (cl-textrender/atlas:map-layout-glyphs
     (lambda (region x y glyph)      ; draw REGION of its page with its top left at X, Y
       (declare (ignore glyph))
       (push (list (cl-atlas:page-index (cl-atlas:region-page region)) x y) quads))
     glyphs layout :scale 2 :x 10 :y 20)
    (nreverse quads)))
;; => ((0 13 27) (0 37 26))

The layout, drawn at 2 pixels per layout unit with its top left at pixel (10, 20), has two inked glyphs, both on page 0: the H's image goes with its top left at pixel (13, 27), and the i's at (37, 26).

Making a glyph atlas

(cl-textrender/atlas:make-glyph-atlas &key (page-size 512) (hinting :light))
  • page-size is the width and height of every page of every font's atlas, in texels.
  • hinting is :light or :none, as for a rasterizer.

glyph-atlas-page-size and glyph-atlas-hinting return them. A glyph atlas is a plain Lisp object that needs no releasing: what it makes is kept with the fonts it draws, and goes with them, with the last hold on its scale (see Scales), or with the glyph atlas once the collector takes it. Make one and keep it for the life of the program, or of the views that share it.

(font-atlas glyph-atlas font :scale scale) returns the cl-atlas atlas a font's glyphs at a scale are packed into, made the first time. The font keeps it and frees it with itself. (atlas-font atlas) and (atlas-scale atlas) tell what it holds, so a renderer that holds one of its pages reaches the font, and keeps it alive.

Scales

Each scale's glyphs have atlases of their own, so that they can go when no one draws at that scale any more: zooming a view in steps draws its text at a new scale each step, and without this every step's glyphs would stay as long as their fonts. A view that draws at a scale holds it, and releases it when it draws at another or goes, as Godot 4.5's viewports reference their oversampling levels:

(cl-textrender/atlas:hold-scale glyphs 2)      ; a view starts drawing at 2
(cl-textrender/atlas:hold-scale glyphs 2)      ; and another
(cl-textrender/atlas:release-scale glyphs 2)   ; => NIL: one still draws at 2
(cl-textrender/atlas:release-scale glyphs 2)   ; => T: the scale's glyphs went
  • Holds are counted, so views that draw at one scale share its glyphs. scale-holds returns the count.
  • The last release frees every font's atlas at the scale. Glyphs asked for at it later are rasterized again.
  • Scales are compared with =, so 2 and 2.0 are one scale.
  • A scale never held keeps its glyphs while their fonts and the glyph atlas live, as before: hold scales only to free them.
  • Releasing a scale that is not held signals textrender-error.
  • Release a scale while no thread asks for glyphs at it: the release frees what those threads would read.

Drawing a layout

(cl-textrender/atlas:map-layout-glyphs function glyph-atlas layout
                                       &key (scale 1) (x 0) (y 0))

map-layout-glyphs calls function for each glyph of layout that has ink, with:

  1. the glyph's region in the atlas, a cl-atlas:region;
  2. the pixel column where the region's top left goes, a whole number;
  3. the pixel row where it goes, a whole number;
  4. the cl-textrender:glyph, for its font, style data and so on.

The layout is drawn at scale pixels per layout unit with its top left at pixel (x, y), which may be fractional. Positions are those the raster backend's map-layout-glyphs gives: each glyph's x is kept to a quarter of a pixel, through an image drawn at each quarter, and its y is rounded; rows grow downwards from the layout's top whatever its y axis. Glyphs are rasterized and packed the first time they are met.

For each glyph, draw a quad region-width by region-height pixels with its top left at the position given, textured from the region's page with the texture coordinates cl-atlas:region-uv returns. The texel is the glyph's coverage: multiply the text's color, or the glyph's glyph-data, by it as alpha. Glyphs land on whole pixels, so sample with nearest filtering, at one texel per pixel.

One glyph

(cl-textrender/atlas:glyph-region glyph-atlas font glyph-id
                                  &key (scale 1) (subpixel 0) rotated)

glyph-region returns the region of the font's atlas holding one glyph, drawn as rasterize-glyph draws it. glyph-image-offset says where the image sits relative to the glyph's origin, as two values: its top-left corner is left pixels right of the origin and top pixels above it.

(let ((region (cl-textrender/atlas:glyph-region glyphs font
                                                (tr:font-glyph-id font #\H)
                                                :scale 2)))
  (list* (cl-atlas:region-x region) (cl-atlas:region-y region)
         (cl-atlas:region-width region) (cl-atlas:region-height region)
         (multiple-value-list (cl-textrender/atlas:glyph-image-offset region))))
;; => (0 0 18 23 3 23)

A glyph without ink, such as a space, has an empty region, whose cl-atlas:region-page is nil. map-layout-glyphs skips those.

Uploading pages

A font's atlas tells what changed since the renderer last asked. Before drawing, upload the changes of the pages you draw from, of each font and scale you draw with:

(dolist (page (cl-atlas:atlas-pages (cl-textrender/atlas:font-atlas glyphs font :scale 2)))
  (multiple-value-bind (x y width height) (cl-atlas:take-page-changes page)
    (when x
      (upload-texture-region (texture-for-page (cl-atlas:page-index page))  ; yours
                             x y width height
                             (cl-atlas:copy-page-region page x y width height
                                                        *upload-buffer*)))))
  • Pages are page-size texels square, one byte a texel: a single-channel texture, such as R8 or GL_RED. Read the red channel as coverage.
  • take-page-changes returns the rectangle covering every glyph packed on the page since the last call, or nil four times, and forgets it.
  • copy-page-region returns that rectangle's texels row by row, without padding, as a texture upload takes them. Give it a buffer to reuse and it allocates nothing.
  • When every page is full, the atlas adds a page. Make its texture when cl-atlas:atlas-page-count grows. Pages never grow and regions never move, so texture coordinates you recorded stay right.
  • A renderer can keep its textures with the font too, with font-cache, a cache key of its own for each scale and a function that frees them, so they go when the font does, or when the renderer forgets the scale's key as it releases the scale's last hold.

What is cached

Each glyph is packed once per font, glyph ID, scale, quarter-pixel offset and rotation, in the font's atlas at that scale. Drawing at another scale packs it again, so draw at the scale of the frame. An atlas only grows: no single glyph is evicted, but a scale's atlases go with the last hold on it, and every atlas of a font goes with the font. The images are packed only: the rasterizer keeps no copy of them.

Once a layout's glyphs are packed, glyph-region and map-layout-glyphs allocate nothing, so a renderer can draw a layout from the atlas every frame.

Threads

A glyph atlas may be used from any number of threads at once. Glyphs are rasterized outside the atlas's lock, so threads rasterize in parallel, as the raster backend allows; if two threads rasterize the same glyph, the first packed is kept. Scales may be held and released from any thread, but release a scale while no thread asks for glyphs at it, as between the frames that draw at it.

Drawing with a destroyed face or font signals textrender-error; see Fonts. So does a glyph the raster backend cannot draw, such as one whose outline is damaged (see Errors). A glyph ID the font does not have is no error: its region is .notdef's, packed once for them all.