- Common Lisp 100%
|
All checks were successful
CI / test (push) Successful in 1m25s
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. |
||
|---|---|---|
| .forgejo/workflows | ||
| clsl | ||
| src | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| cl-slug.asd | ||
| CLAUDE.md | ||
| LICENSE | ||
| NOTICE | ||
| README.org | ||
| stiftfile | ||
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-addadds a shape and returns it. Its curves are a vector of reals, six per curve, in ems with y up, forming closed contours.:fill-ruleis:nonzero, the rule of fonts, or:even-odd.store-find-or-addadds 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-findonly looks.store-shape-countcounts the shapes added.
Shapes
A shape tells a renderer how to draw it:
shape-xmin,shape-ymin,shape-xmaxandshape-ymaxbound 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-pis true for a shape with no curves, such as a space; it draws nothing.write-shape-datawrites 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-xandshape-band-offset-yare those numbers one by one, for a renderer that packs them its own way.shape-glyph-wordspacks them as the reference vertex shader'stex.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.