Slug glyph rendering data for Common Lisp: builds the curve and band data that GPU shaders use to draw quadratic outlines exactly at any size, rotation or zoom.
  • Common Lisp 100%
Find a file
dxtr ac2f770803
All checks were successful
CI / test (push) Successful in 1m25s
Move the pins on the workspace's libraries
To the latest commits of clsl, which keeps the programs it compiled, and
of the libraries that pin cl-ffi's whose allocate boxes no pointer, of
Pyssel's step 25.
2026-10-19 21:32:07 +02:00
.forgejo/workflows Run on SBCL only 2026-10-17 04:48:00 +02:00
clsl Move the pixel shader to clsl: cl-slug/clsl in place of cl-slug/gsl 2026-10-15 08:56:41 +02:00
src Declare the arguments' full domain types 2026-10-18 16:23:05 +02:00
tests Declare the arguments' full domain types 2026-10-18 16:23:05 +02:00
.gitattributes Keep LF line endings in every checkout 2026-10-16 15:49:25 +02:00
.gitignore Pin the dependencies with stift in place of qlot 2026-10-16 11:33:20 +02:00
cl-slug.asd Move the pixel shader to clsl: cl-slug/clsl in place of cl-slug/gsl 2026-10-15 08:56:41 +02:00
CLAUDE.md Follow the template's new Types section in the CLAUDE.md 2026-10-18 16:23:10 +02:00
LICENSE Add cl-slug: shapes packed into Slug's curve and band textures 2026-10-11 16:52:15 +02:00
NOTICE Move the pixel shader to clsl: cl-slug/clsl in place of cl-slug/gsl 2026-10-15 08:56:41 +02:00
README.org Name what exists now in the README and two docstrings 2026-10-18 15:38:51 +02:00
stiftfile Move the pins on the workspace's libraries 2026-10-19 21:32:07 +02:00

cl-slug

The data of the Slug algorithm for Common Lisp. Slug draws shapes made of quadratic Bézier curves, such as glyphs and vector icons, straight from their curves on the GPU: sharp at any size, turned or in perspective, without a texture of pixels per size.

cl-slug is the CPU half: a band builder that packs shapes into the two textures the Slug shaders read. It knows nothing of GPUs or fonts. The optional system cl-slug/clsl holds the shaders' pixel half, ported to clsl.

(asdf:load-system :cl-slug)

(defvar *store* (cl-slug:make-store))

;; A triangle in ems, y up: three curves of six numbers, X0 Y0 CX CY X1 Y1.
;; A straight line is a curve whose control point lies on it.
(defvar *triangle*
  (cl-slug:store-add *store* #(0.1 0.1  0.5 0.1  0.9 0.1
                               0.9 0.1  0.7 0.5  0.5 0.9
                               0.5 0.9  0.3 0.5  0.1 0.1)))

;; What a renderer gives the shaders: the bounds to draw a quad over, in
;; ems, and eight numbers for the pixel shader, written into its instance data.
(list (cl-slug:shape-xmin *triangle*) (cl-slug:shape-ymin *triangle*)
      (cl-slug:shape-xmax *triangle*) (cl-slug:shape-ymax *triangle*))
;; => (0.099975586 0.099975586 0.89990234 0.89990234)
(let ((instance (make-array 8 :element-type 'single-float)))
  (cl-slug:write-shape-data *triangle* instance 0)
  instance)
;; => #(1.2501144 1.2501144 -0.12498093 -0.12498093 0.0 0.0 0.0 0.0)

;; Before drawing: upload the rows that changed, into an :RGBA16-FLOAT
;; curve texture and an :RG16-UINT band texture, both 4096 texels wide.
(multiple-value-bind (row count) (cl-slug:take-curve-changes *store*)
  (list row count (length (cl-slug:copy-curve-rows *store* row count))))
;; => (0 1 16384)
(multiple-value-bind (row count) (cl-slug:take-band-changes *store*)
  (list row count (length (cl-slug:copy-band-rows *store* row count))))
;; => (0 1 8192)

The bounds are a little off 0.1 and 0.9 because the curve texture holds 16-bit floats: shapes are rounded to them when they are added.

Glyphs

A font's glyph is a shape. With cl-textrender, make the font at size 1, so its outlines are in ems, and add each glyph the first time it is drawn:

(cl-textrender:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 1)
  (let ((glyph (cl-textrender:font-glyph-id font #\A)))
    (cl-slug:store-find-or-add
     *store* (list font glyph)
     (lambda ()
       (cl-textrender:outline-quadratics
        (cl-textrender:glyph-outline font glyph :quadratic t))))))
;; => #<CL-SLUG:SHAPE 11 curves, 7 by 3 bands {...}>

store-find-or-add compares keys with equal and calls the function only the first time, outside the store's lock, so job-pool workers can add glyphs while they draw.

Stores

make-store takes :epsilon, how far a shape's bands overlap in ems (1/1024 by default), and :max-rows, the most rows either texture may have: 4096, the tallest texture every Vulkan device takes, or up to 65536.

  • store-add adds a shape and returns it. Its curves are a vector of reals, six per curve, in ems with y up, forming closed contours. :fill-rule is :nonzero, the rule of fonts, or :even-odd.
  • store-find-or-add adds the shape a function returns the first time a key is asked for, and returns the same shape after. The function may return the fill rule as a second value. store-find only looks.
  • store-shape-count counts the shapes added.

Shapes

A shape tells a renderer how to draw it:

  • shape-xmin, shape-ymin, shape-xmax and shape-ymax bound it in ems. Draw a quad over them, a pixel larger on every side for the anti-aliased edge, and give the pixel shader each pixel's position in ems.
  • shape-empty-p is true for a shape with no curves, such as a space; it draws nothing.
  • write-shape-data writes the +shape-data-floats+, eight, single-floats the pixel shader reads the shape's data with into a vector, such as a renderer's instance data, allocating nothing: the band scales and offsets, then where the shape's data starts and the indices of its last bands, with the even-odd rule's flag.
  • shape-data-x, shape-data-y, shape-vertical-band-count, shape-horizontal-band-count, shape-band-scale-x, shape-band-scale-y, shape-band-offset-x and shape-band-offset-y are those numbers one by one, for a renderer that packs them its own way.
  • shape-glyph-words packs them as the reference vertex shader's tex.zw.

Shapes never change, and data in the textures never moves.

Textures

Both textures are +texture-width+, 4096, texels wide and grow downwards:

  • The curve texture, :rgba16-float: each texel holds two control points. A run of curves that each start where the one before ended shares those points, so a contour of N curves takes N + 1 texels.
  • The band texture, :rg16-uint: for each shape, the headers of its bands, then the lists of the curves in each band.

store-curve-rows and store-band-rows tell how many rows hold data, so how tall to make each texture. take-curve-changes and take-band-changes return the first row and the number of rows written since they were last called, and copy-curve-rows and copy-band-rows return rows as vectors of 16-bit integers, as a GPU API takes them. A renderer that outgrows its texture makes a taller one and uploads every row again: places are in texels, so nothing recorded about a shape changes.

Shaders

cl-slug/clsl exports slug-coverage, a clsl shader function that returns how much of a pixel a shape covers. In a package that uses CLSL:

(asdf:load-system :cl-slug/clsl)

(define-shader text-fragment :fragment
    ((color :vec4 :interpolation :flat) (em :vec2)
     (banding :vec4 :interpolation :flat) (data :ivec4 :interpolation :flat)
     &uniform (curves :sampler2d) (bands :usampler2d))
  (* color (cl-slug/clsl:slug-coverage curves bands em banding data)))

em is the pixel's position in ems. banding and data are the eight numbers write-shape-data writes, the first four as a vec4 and the last four as an ivec4. The vertex shader places the quad and passes these on; Pyssel's 2D renderer has one for 2D canvases. A program that calls slug-coverage runs on a device that keeps the NaNs of floats, which clsl's guards check for, as program-features reports: :signed-zero-inf-nan-preserve-float32.

How it works

The pixel shader casts a ray right and a ray up from the pixel and adds up where curves cross them, weighted by how near the crossing is, which both fills the shape and anti-aliases its edges. To look at few curves, a shape's bounds are cut into horizontal bands, for the rays that go right, and vertical bands, for the rays that go up. The band builder picks the number of bands, up to 32 each way, whose fullest band holds the fewest curves, and sorts each band's curves so the shader stops early. Horizontal lines are in no horizontal band and vertical lines in no vertical band, as rays along them cross nothing. Bands with the same curves share one list.

A run of curves never crosses the end of a row, nor does a band's list, because the shaders read them without wrapping. A band holds at most 4096 curves, and a shape's band data spans at most 65536 texels; a shape that needs more signals a slug-error saying to split it.

Thread rules

A store and its shapes may be used from any number of threads at once.

Credit

cl-slug/clsl is ported from Eric Lengyel's reference shaders, https://github.com/EricLengyel/Slug, under the MIT License. Software that uses it and is distributed must give credit; the text is in NOTICE. The Slug patent was dedicated to the public domain on 2026-03-17.

Tests

stift test

The tests run the pixel shader's algorithm on the CPU, reading the uploaded texture data, and compare its coverage with winding numbers computed from the curves; and they compile slug-coverage with clsl, and check that it takes the pixel's size from derivatives, reads the textures unfiltered and loops over each band's curves.

License

MIT. See LICENSE, and NOTICE for the reference shaders.