3 Concepts
dxtr edited this page 2026-10-15 09:00:37 +02:00

Concepts

What Slug does

Most renderers draw text from images: each glyph is rasterized at a size into a texture, and drawn as a textured quad. That is sharp at the size it was rasterized for, and blurry or blocky when zoomed, scaled or turned.

Slug, by Eric Lengyel, draws a glyph from its outline instead. The outline is quadratic Bézier curves, which live in a texture. A quad is drawn over the glyph's bounds, and for each pixel the pixel shader works out how much of it the glyph covers, from the curves themselves. So the edge is exact at any size, under any transform, even in perspective, and is anti-aliased without multisampling.

The pixel shader:

  1. Casts a ray from the pixel to the right, and one upwards.
  2. Finds where each curve crosses each ray, by solving a quadratic.
  3. Adds up the crossings, each counting by how far into the pixel it lies, signed by the curve's direction: the coverage along each ray, which is the winding number blurred over the pixel's width.
  4. Combines the two rays' coverages, weighted by how near their crossings are to the pixel, which smooths edges at any angle.

Looking at every curve for every pixel would be slow. So a shape's bounds are cut into bands: horizontal bands for the rays that go right, vertical bands for the rays that go up. A pixel looks only at the curves of the one horizontal band and the one vertical band it lies in. Each band's curves are sorted, so the shader stops as soon as the rest lie behind the pixel.

cl-slug does the CPU side of this: it cuts shapes into bands and packs the curves and bands into textures. cl-slug/clsl holds the pixel shader.

Units

Shapes are in ems, with y up: a glyph from a font of size 1, its origin on the baseline. A renderer maps ems to the screen with whatever transform it likes, such as the font size, a scale, a turn.

Ems matter because the curve texture holds 16-bit floats, whose precision is best near 1 and which top out at 65504. Coordinates are rounded to them when a shape is added, about a two-thousandth of an em near 1. Shapes in pixels or font units lose precision or overflow.

Stores, shapes and textures

A store holds every shape a renderer draws, packed into the data of two textures:

Texture Format Each texel holds
Curve texture :rgba16-float Two control points, x and y each
Band texture :rg16-uint A band's header, or a curve's place in the curve texture

Both are +texture-width+, 4096, texels wide, and grow downwards as shapes are added. Data never moves once placed, so what a renderer recorded about a shape stays right. See Data Format for the layout of each texel.

A shape is what store-add returns: an immutable record of where the shape's data lies and how to draw it. It has

  • its bounds in ems, to draw a quad over;
  • where its band data starts, and how many bands it has each way;
  • the scale and offset that turn an em coordinate into a band index;
  • its fill rule, :nonzero or :even-odd.

A renderer passes the bounds to its vertex shader, and the rest as the eight numbers write-shape-data writes into its instance data, which the pixel shader reads. The shape itself holds no curves: they are in the store's data.

The flow

  1. Make a store, once.
  2. Add shapes, from any thread: with store-add, or with store-find-or-add, which adds a key's shape the first time it is asked for, such as a font's glyph.
  3. Each frame, before drawing, upload what changed: take-curve-changes and take-band-changes give the rows written since they were last asked, and copy-curve-rows and copy-band-rows read them. A texture too short for the rows is made anew, taller, with every row uploaded.
  4. Draw each shape as a quad over its bounds, a pixel larger on every side, with write-shape-data's eight numbers in its instance data. The pixel shader calls slug-coverage with each pixel's position in ems and those numbers.

Threads

A store and its shapes may be used from any number of threads at once. A store has one lock; the slow work of adding a shape, rounding its curves and choosing its bands, happens outside it, and so does the function store-find-or-add calls for curves. Job-pool workers can add glyphs while they draw.

Compared with the other ways to draw text

Way Best for Weakness
Hinted raster atlas Small text at fixed sizes, such as a user interface Does not scale or turn
Slug (this library) Zoomed, rotated or in-world text, vector icons More work per pixel; no hinting
Multi-channel signed distance fields Cheap outlines, glows and soft shadows Limited scale range; atlas memory

Pyssel's 2D renderer offers the first two: draw-text takes :painter :raster or :painter :slug.