8 Threads and Lifetimes
dxtr edited this page 2026-10-19 19:21:23 +02:00

Threads and Lifetimes

Threads

Object May be shared between threads?
face, font Yes. Several threads may lay out text with one font at once, and ask a face for fonts.
layout, and its lines, runs and glyphs Yes, while it does not change: hit testing only reads it. layout-text's into lays it out again in place, so do that only where no other thread reads it.
outline Yes. It never changes.
rect Yes. One a layout holds changes as the layout is laid out again; one selection-rectangles returns never does.
rasterizer Yes, from any number of threads at once. FreeType draws with a face from one thread at a time, so the glyphs of one face are drawn one at a time.
glyph-atlas Yes. Release a scale while no thread asks for glyphs at it.
field-atlas Yes. Fields for different glyphs are made in parallel.

cached-outline and cached-quadratics may be called from any thread. Arrays returned from caches, such as the coverage rasterize-glyph returns and the vectors cached-quadratics returns, are shared with later callers: read them, but do not modify them.

A common arrangement for a game: lay out text on whichever thread owns it, with faces made at startup; make distance fields or rasterized glyphs on job-system workers through a shared field or glyph atlas; and upload the atlases' changed pages on the render thread at a frame boundary.

Lifetimes

Nothing needs releasing by hand. Faces and fonts hold HarfBuzz's objects, and are freed once nothing holds them; everything renderers make from them is kept with them and goes with them.

Object What happens to it
face Freed, with what renderers keep with it, once nothing holds it or any of its fonts. destroy-face frees it and its fonts at once; with-face does when its body is left.
font Freed, with what renderers keep with it, once nothing holds it. destroy-font frees it at once, and its face too if make-font made it; with-font does when its body is left.
rasterizer, glyph-atlas, field-atlas Plain Lisp objects: what they make is kept with the faces and fonts they draw.
layout, outline, rect Plain Lisp data, collected like any other.

Destroying a face or font twice does nothing. See Fonts.

What holds what

  • A font holds its face, and a face the fonts it gave. Keep a face, and face-font gives the same font for a size each time.
  • A layout holds its fonts. No font is freed while text laid out with it is around, so a layout can always be drawn.
  • A page holds its font or face. A glyph atlas's font-atlas holds its font, which atlas-font reads, and the atlas-data of a field atlas's face-atlas is the face, so a renderer that keeps a page keeps what it was made from.
  • Layouts outlive destroyed fonts. After destroy-face or destroy-font, a layout keeps its glyphs and positions, and hit testing it still works, but it cannot be laid out or drawn again.

What renderers keep

Each face and font keeps what renderers make from it, under each renderer's cache key, with face-cache and font-cache, and a font keeps its own outlines:

  • a font keeps its outlines, from cached-outline and cached-quadratics;
  • a face keeps a FreeType library and face of its own for each rasterizer of the raster backend, and a font the glyph images each draws;
  • a font keeps its atlas at each scale of each glyph atlas, font-atlas, until the last hold on the scale is released, and a face its atlas of each distance field atlas, face-atlas.

Nothing is evicted glyph by glyph while the face or font lives: a font keeps every glyph it has drawn, and its atlases add pages as they fill. All of it goes when the face or font does, so an application that draws text at many sizes over a long time frees what it no longer uses by dropping the fonts of those sizes. A glyph atlas's glyphs at a scale go once the last hold on the scale is released, so text drawn at a zoom no view draws at any more keeps nothing. What one renderer made goes too when its key is forgotten, with forget-cache-key, as the renderer is released, and what a rasterizer, a glyph atlas or a distance field atlas made once the collector takes it, so a face or font that outlives them keeps nothing of them.

Allocation

Measured on SBCL, with DejaVu Sans and a 43-character, four-line layout:

Call Allocates
layout-text Much: about 59 KB here, 13 KB for a short line such as "-123.456". Lay out when the text changes, not every frame.
layout-text with into, of a simple line Nothing, once the layout has held a line as long: a label whose text changes every frame lays it out again into its layout.
do-glyphs, layout, line, run and glyph accessors Nothing.
caret-position, caret-segment Nothing.
next-caret-index, previous-caret-index Nothing.
hit-test, move-caret A few hundred bytes, on input events only.
selection-rectangles Its result: a list of rectangles.
layout-glyphs Its result: a fresh vector.
cached-outline, cached-quadratics, rasterize-glyph, glyph-region (both atlases), face-font for a size asked before Nothing, once the glyph is cached.
map-layout-glyphs (both), map-layout-fields Nothing, once the layout's glyphs are cached.

So a layout drawn every frame allocates nothing in steady state, whichever way it is drawn: walk it with do-glyphs, draw the caret with caret-segment, and look its glyphs up with cached-quadratics, a rasterizer, a glyph atlas or a field atlas. The test suite checks this. Selection rectangles change only when the selection does: keep them until then.