luv

Workshop wiki

slug-bezier.org

Slug: Bézier outlines in the pixel shader

The 2026 release removes two different restrictions #VDGHWX

Slug is Eric Lengyel's method for evaluating quadratic outline curves directly for each covered pixel. It does not rasterize a glyph into a bitmap or a signed-distance field first. The original description is the 2017 JCGT paper “GPU-Centered Font Rendering Directly from Glyph Outlines”. The current reference is the shader repository at commit be3c13eb7d63f9e8aa5c583e42d92c374cb91d98 (15 April 2026), not the paper's sample code.

There are two separate freedom claims:

These are the patent owner's announcement and the repository's license terms, not legal advice. The Google Patents snapshot for US10373352B1 still labels the patent “Active”, but that site also says its legal-status field is an unverified assumption and shows no 2026 event. It is therefore a lagging aggregator, not evidence against the later dedication.

A pixel asks the outline for its winding #3T0JD9

For a quadratic curve with control points p1, p2, and p3, Slug writes

C(t) = a t^2 - 2 b t + p1
a    = p1 - 2 p2 + p3
b    = p1 - p2

At a pixel, the control points are translated so that the pixel is the origin. For a horizontal ray, the shader solves C_y(t) = 0 and accumulates a signed contribution from the x coordinate of each eligible intersection. It repeats the calculation with x and y exchanged for a vertical ray. The magnitude of the resulting winding quantity distinguishes the inside from the outside, while the continuous intersection coordinate supplies antialiasing across the boundary.

For one axis, the quadratic roots are

t1 = (b - sqrt(max(b^2 - a p1, 0))) / a
t2 = (b + sqrt(max(b^2 - a p1, 0))) / a

where a, b, and p1 above mean the selected scalar components. The current reference treats abs(a) < 1/65536 as linear and uses t = p1 / (2 b). Clamping the discriminant to zero keeps the square root defined in finite-precision arithmetic.

Root eligibility, not a floating-point range test, supplies robustness #S2F8SA

Testing whether a computed root lies in [0,1] is numerically unstable at shared endpoints. Slug instead classifies the signs of the three control coordinates before calculating roots. Let s1, s2, and s3 be one only when the corresponding coordinate is strictly positive; zero is non-positive. The exact eligibility table from the paper is:

s3s2s1root 2root 1
00000
00101
01011
01101
10010
10111
11010
11100

The reference shader extracts the two bits from the constant 0x2E74. Luv's proof emits the same table as float arithmetic so that it needs no integer bit operators. In particular,

e1 = s1 (1 - s2 s3) + (1 - s1) s2 (1 - s3)
e2 = s3 (1 - s1 s2) + (1 - s3) s2 (1 - s1)

The source-level signum operator lowers to GLSL.std.450 FSign for SPIR-V and sign for MSL. Its zero result preserves the table's strict inequality at an on-curve endpoint.

defun slug-root-eligibility slug.lisp:118
defunslug-root-eligibility
y1y2y3

Return the two Slug root-eligibility bits as numeric masks.

Only strict positivity matters. The arithmetic form is Table 1 of Lengyel's 2017 paper without an integer lookup, and is the form generated into the proof pixel shader. #S2F8SA

let
s1
if
pluspy1
10
s2
if
pluspy2
10
s3
if
pluspy3
10

Coverage turns intersections into antialiasing #P9FDUS

If r is an intersection's coordinate along the ray in pixel units, its coverage contribution is saturate(r + 0.5), multiplied by its eligibility bit and added or subtracted according to which root it is. This gives a one-pixel-wide linear transition without supersampling.

The current 2026 pixel shader combines horizontal and vertical results instead of trusting one ray direction:

w(r)     = saturate(1 - 2 abs(r))
coverage = max(abs(xcov*xweight + ycov*yweight)
               / max(xweight + yweight, 1/65536),
               min(abs(xcov), abs(ycov)))

Each axis weight is the maximum w of its eligible intersections. The weighted term prefers the ray whose crossing is closest to the pixel centre; the minimum term preserves coverage when both weights vanish. The reference then clamps for nonzero fill, or applies abs(fmod(coverage + 1, 2) - 1) for even-odd fill. An optional square root supplies optical-weight adjustment.

Bands make exact curves affordable #8FJF9U

The full algorithm does not test every outline curve at every pixel. A glyph is divided into equal-thickness horizontal and vertical bands, each pointing to the subset of curves that could cross rays in that band. Adjacent bands may share an identical list or a contiguous subrange.

The current reference has two textures:

The reference fixes the texture row width at 4096 texels. Curves in a horizontal band are sorted by descending maximum x; vertical bands use descending maximum y. The pixel shader can therefore stop once that maximum, in pixel units, is less than -0.5. The README recommends a 1/1024 em overlap between bands. Horizontal straight lines must be omitted from horizontal bands and vertical straight lines from vertical bands because a ray parallel to such a line has no winding contribution. A straight segment from p1 to p2 is encoded as {p1,p2,p2}.

The 2026 reference is not the 2017 sample #45K8GR

The decade retrospective records several material changes:

The repository's shader comments and data contracts should therefore be the implementation reference; the paper remains the clearest derivation and historical measurement. Its benchmark and storage numbers describe 2017 hardware and the older layout, not current performance promises.

Dynamic dilation is a rasterization guarantee #J0H590

The reference vertex shader expands the quad by half a pixel in screen space. It transforms a per-vertex dilation normal through the inverse of the local-to-device Jacobian, while the model-view-projection matrix and viewport size determine the em-space size of that half pixel. This is not decorative boldening: without conservative geometry, rasterization can omit a pixel whose centre lies just outside the undilated quad even though the analytic filter would assign it nonzero coverage.

The full vertex record is five float4 values carrying position plus dilation normal, texture coordinate plus packed glyph location and maxima, inverse Jacobian, band transform, and colour. Band and glyph data are marked non-interpolated; only the render coordinate is interpolated.

DONE The reference's shape, with its constants as knobs #3YHNO3

Read against the 2026 reference shaders and the retrospective, luv's Slug had the algorithm -- eligibility, two rays, weighted combination, sorted bands -- but not the reference's shape around it. This figure closes the gaps and names what the reference hard-codes.

The values live in luv.slug as specials -- *slug-filter-width*, *slug-fill-rule*, *slug-optical-weight*, *slug-footprint-norm*, *slug-early-exit*, *slug-root-epsilon*, *slug-debug-view*, *slug-dilation-pixels* -- and stand in shader source by their unstarred names, folded through shader-source-value. For that to be live, the two band shaders are define-live-shader forms, reparsed on every pipeline build, where define-shader parses once at load; luvcraft wraps each value in a :text knob in make-world-text-run's file, so the metabar turns them and the terminal walls rebuild at the next frame. *slug-debug-view* paints every quad with its two bands' loads, red and green over sixteen, which is the quickest way to see what a band count buys.

What was studied and left alone: the reference's packed vertex record and its 8-bit band maxima (luv's instance record already carries the counts and bounds it needs), and the multi-layer emoji path, which luv has no outlines for yet. The reference's CalcRootCode bit trick and luv's arithmetic eligibility (#S2F8SA) are the same table.

define-shader-function slug-horizontal-band-step slug.lisp:305
shader:define-shader-functionslug-horizontal-band-step
statecurvenextrender-coordinatepixels-per-em

Fold one horizontal-band curve into STATE = (xcov, xweight, done).

CURVE holds p1 and p2, NEXT's first two lanes p3. DONE becomes one when the curve lies wholly more than half a filter width left of the sample: the band is sorted by descending maximum x, so nothing after it can contribute and the fold's :UNTIL leaves the loop. #3YHNO3

let*
p1
-render-coordinate
p2
-render-coordinate
p3
-render-coordinate
contribution
slug-horizontal-contributionp1p2p3pixels-per-em
shader:vec3
+
-
shader:swizzlecontribution:x
shader:swizzlecontribution:y
max
shader:swizzlecontribution:z
shader:swizzlecontribution:w
-1.0
shader:step-0.5reach

DONE One fixed contour proves luv's pixel-stage mathematics #OWR8OZ

Intent: prove that luv's shader DSL can state, lower, compile, and actually draw the numerically important pixel-stage core before designing texture and font-data machinery.

Evidence:

Done when (met): a fixed four-quadratic heart crosses the normal mathematical shader path, both backend compilers accept it, root classification has an executable table oracle, and a Metal pixel shader produces the expected antialiased outline from Bézier data rather than a sampled image.

define-shader-function slug-quadratic-outline slug.lisp:359
shader:define-shader-functionslug-quadratic-outline
coordinatepixels-per-emcolorp0c0p1c1p2c2p3c3

Render one connected four-quadratic contour with Slug's two-ray pixel math.

This is a typed shader function: its LET* bindings and nested calls are parsed directly into shader objects. The fixed curve count remains an atelier proof, not the band-texture font renderer. #OWR8OZ

let*
q0p0
-p0coordinate
q0c0
-c0coordinate
q0p1
-p1coordinate
q1c1
-c1coordinate
q1p2
-p2coordinate
q2c2
-c2coordinate
q2p3
-p3coordinate
q3c3
-c3coordinate
horizontal0
slug-horizontal-contributionq0p0q0c0q0p1pixels-per-em
horizontal1
slug-horizontal-contributionq0p1q1c1q1p2pixels-per-em
horizontal2
slug-horizontal-contributionq1p2q2c2q2p3pixels-per-em
horizontal3
slug-horizontal-contributionq2p3q3c3q0p0pixels-per-em
vertical0
slug-vertical-contributionq0p0q0c0q0p1pixels-per-em
vertical1
slug-vertical-contributionq0p1q1c1q1p2pixels-per-em
vertical2
slug-vertical-contributionq1p2q2c2q2p3pixels-per-em
vertical3
slug-vertical-contributionq2p3q3c3q0p0pixels-per-em
coverage
slug-combine-coveragehorizontal0horizontal1horizontal2horizontal3vertical0vertical1vertical2vertical3
*colorcoverage

The fixed proof delineates the first milestone #LB46YZ

The proof bakes four curves and 204.8 pixels per em into one shader. It has no curve or band textures, variable-length band loop, font parser, glyph layout, dynamic dilation, fill-rule flag, or optical-weight flag. The quad already includes ample margin, so it does not test the reference vertex dilation. SPIR-V validation proves the portable module, while the executed offscreen probe is presently Metal-specific because that backend already has a no-resource mathematical-pipeline seam.

This boundary matters historically: that image proves the hard per-pixel mathematics and the DSL capability, not Slug's data-scale performance or a usable text API. The later data-driven and font proofs below cross this boundary without removing the small fixed oracle.

Full Slug is a structured fold over indexed outline data #ZXTZ5J

The 2026 reference pixel shader makes the next language requirements concrete. For each horizontal and vertical band it loads a curve count and list offset, iterates the indexed curves, loads two curve texels, exits when the sorted maximum coordinate cannot reach the pixel, conditionally accumulates the two eligible roots, and carries coverage and weight forward. The required source vocabulary is therefore:

This is not a reason to embed MSL statements or SPIR-V branches in the source language. The loop is a value-producing fold in #8RQOF0. Its result is the four-value coverage state; target lowering chooses mutable locals in MSL and structured loop/header/continue/merge blocks with phi values in SPIR-V.

McCLIM already exposes the TrueType contour boundary #7IVPIE

McCLIM's native TrueType renderer is a useful source path even though luv will not use its software rasterizer. mcclim-native-ttf.lisp obtains a glyph with zpb-ttf:find-glyph and walks it with do-contours and do-contour-segments. Each visit supplies start point, optional quadratic control point, and end point; consecutive off-curve TrueType points have already acquired their implicit on-curve midpoint. McCLIM then turns those segments into cl-vectors paths and sweeps them into an alpha bitmap. Luv can branch at the earlier segment boundary and pack the exact quadratic outline instead.

TrueType glyf contours therefore need no curve fitting. Lines use the Slug encoding {p1,p2,p2}. Cubic outlines, such as CFF/CFF2 or arbitrary vector paths, are a preprocessing concern: subdivide and approximate them with quadratics under an explicit font-unit error bound. Current HarfBuzz GPU code uses exactly this shape, adapting FontTools cu2qu with a default half-font-unit tolerance before its Slug encoder. HarfBuzz 14's experimental libharfbuzz-gpu is consequently a valuable format and pixel oracle, but the first luv path can remain Lisp-native through zpb-ttf.

DONE Teach the shared language Slug's structured band fold #HMNMBU

Intent: add the smallest shared control-flow and indexed-data vocabulary that states one Slug band traversal without burying its concrete data contract in a generic scene abstraction.

Evidence needed: after shared function application #YZS04W, add represented integer/index values, exact loads, comparison and conditional expressions, and a counted value-producing loop with early termination. Execute a CPU realization over a small curve array, lower the same body to valid MSL and SPIR-V structured control flow, and keep the current fixed shader as the pixel oracle.

The neutral loop skeleton is now present: counted-fold accepts a runtime count and one carried scalar or vector state, executes as dotimes on the CPU, as an ordinary for loop in direct Metal, and as validated structured SPIR-V with phi-carried index and state. Its update is an expression body rather than an artificially single call: a leading let* produces per-iteration lexical bindings in the common AST, an inner Lisp let*, loop-local SSA values, and named locals inside the emitted Metal loop.

Conditional accumulation is shared too. if is a first-class value node in the common expression graph, with checked scalar comparisons <, <=, >, >=, and ===. Lisp retains ordinary branch semantics; SPIR-V emits the ordered comparison plus OpSelect; and direct Metal emits a ternary inside the same for loop. A bounded triangular fold executes on the CPU and compiles from the same shape on both GPU targets.

The shader representation layer now supplies uint and uvec2/3/4 values plus an exact texel-load extension. A count loaded from a uint-texture-2d selects an unsigned fold index automatically; address addition, unsigned division and the common mod operation stay typed throughout. The same probe emits OpImageFetch, OpULessThan, OpUDiv, OpUMod, OpIAdd, and OpConvertUToF in SPIR-V, while Metal emits texture2d<uint>.read(uint2) and a for (uint ...) loop with readable address locals. Both spirv-val and the Metal 4 compiler accept those generated products. mod itself belongs to the shared arithmetic vocabulary and executes through Common Lisp's ordinary mod; the texture resource and exact load remain properly shader-specific.

slug-banded-fragment-specification now supplies the Slug-shaped traversal. Two runtime unsigned curve counts control two counted-fold values. Each iteration reads its RG16U curve location and two adjacent RGBA16F curve texels, creates ordinary lexical let* bindings for the reusable axis-contribution function, and carries coverage plus maximum weight. There is no host unrolling and no form-generating Slug macro.

This exercised one subtle language rule that the scalar loop probes did not: function-local bindings called from a fold belong to that iteration. Direct MSL lowering now materializes them inside the for body, while SPIR-V only reuses a cached resource load in the basic block that owns it. The latter rule prevents a value defined inside one loop from being reused in another loop where it does not dominate. Regression tests inspect both generated products; spirv-val and Apple's Metal 4 compiler accept them.

The first correctness shader intentionally uses one complete band per axis. Sorted-coordinate early exit and multiple spatial bands are culling optimizations over this same value-producing traversal, not prerequisites for rendering an arbitrary outline.

Done when: a runtime curve count rather than source unrolling controls the number of evaluated quadratics; CPU and GPU results agree at edge, shared-endpoint, empty-band, and early-exit cases; and source/lowering tools retain the loop, carried values, and emitted block provenance.

DONE Pack arbitrary quadratic contours into Slug bands #TTE5JG

Intent: turn arbitrary connected quadratic contours into dense curve and band data suitable for the shared Slug fold.

Evidence needed: a Lisp outline model preserving contour boundaries and orientation, line-to-quadratic encoding, conservative band overlap, omission of parallel straight lines, per-axis descending sort, and visible band-length statistics. Exercise an outer contour plus an inner contour before involving a font loader.

The CPU-owned model and the first preprocessing boundary are now concrete. slug-outline keeps closed contours, validates every curve junction, computes quadratic contour orientation from the exact area integral, and flattens curves in contour order. Lines use {p1,p2,p2}. pack-slug-outline builds horizontal and vertical bands with the reference 1/1024 conservative overlap, omits curves parallel to the relevant ray, and retains both descending-maximum and ascending-minimum orders. Tests exercise a counterclockwise outer square with a clockwise inner hole and reject a disconnected contour.

The serialization boundary now preserves the released shader's exact texture contract. Luv first rounds every point through binary16, decodes those actual shader-visible values, and only then recomputes bounds, bands, and sort keys. The 4096-wide RGBA16F curve texture stores one {p1,p2} texel per quadratic; the following texel supplies the shared p3, with one endpoint sentinel after each contour. The 4096-wide RG16U band texture stores all horizontal headers, then all vertical headers, followed by their descending-maximum (curve-texture-x, curve-texture-y) lists. Tests deliberately use a one-third coordinate to prove that packing observes the half-quantized value rather than the original Lisp rational.

The portable GPU vocabulary now names the exact storage formats too: :rg16-uint is four bytes per texel and :rgba16-float is eight. The Metal backend maps those names to MTLPixelFormatRG16Uint 63 and MTLPixelFormatRGBA16Float 115; Vulkan maps them to VK_FORMAT_R16G16_UINT 81 and VK_FORMAT_R16G16B16A16_SFLOAT 97. Both upload paths accept the serializer's two-dimensional specialized arrays directly and compute row length, byte size, alignment, and foreign element width from the format instead of assuming RGBA8.

The first serializer still describes one glyph beginning at texture origin zero. Atlas allocation and relocation remain open, but the complete one-glyph data is now uploaded and rendered from those resources while preserving the fixed-width addressing assumed by the released pixel shader.

Done when: several differently sized and holed quadratic outlines render from uploaded data without changing shader source, and a CPU winding oracle agrees with GPU pixels around extrema, shared endpoints, and contour holes.

HarfBuzz places a run; Slug draws indexed outlines #4G7064

Text shaping and outline rendering meet at a deliberately narrow seam. shape-slug-text gives HarfBuzz the UTF-8 run and font bytes, then returns dense records containing glyph IDs, byte clusters, advances, and offsets in font units. It does not ask HarfBuzz to rasterize. The Slug side uses each glyph ID with zpb-ttf:index-glyph, normalizes that outline to em units, and places its quad from the corresponding HarfBuzz record.

The first run proof keeps resource ownership literal rather than anticipating an atlas. One shared vertex buffer contains six vertices per drawable glyph; each slice carries clip position, outline coordinate, and pixels per em. Each glyph temporarily owns its own serialized band texture, curve texture, views, and bind group. The render pass switches bind groups and uses first-vertex to draw each six-vertex slice. This is intentionally expensive but makes glyph selection, placement, outline data, and draw ownership visible before a packing or caching policy hides them.

DejaVu Sans shapes office affinity into 11 glyphs rather than 14 drawable characters: each ffi sequence becomes one ligature glyph. The regression test checks the office clusters (0 1 4 5), proving that the three UTF-8 characters beginning at cluster 1 became one selected glyph. The cold Metal probe then renders the complete shaped run through the same public Slug vertex and band-fragment specifications used by the earlier “O” proof:

The image also forced a useful correction that the symmetric “O” could not show: target Y had been inverted. Visual inspection of letters with ascenders and descenders caught it, and the fitted text vertices now map font-up to image-up. Its blue background is also a composition check: the Slug fragment emits premultiplied color and coverage, and the render target explicitly uses one / one-minus-source-alpha blending instead of relying on the black background that concealed transparent quad pixels in the first proof. HarfBuzz 13.2.1 is part of the owned Nix runtime rather than an ambient host dependency.

defun render-metal-slug-text examples.lisp:875
defunrender-metal-slug-text
providerstringfont-pathname

Shape string with HarfBuzz and render one Slug quad per drawable glyph.

The proof target is 768 by 256 pixels. HarfBuzz owns glyph selection, ligatures, clusters, advances, and offsets; ZPB-TTF supplies outlines by the resulting glyph IDs. Return pixels, width, height, format, and shaped text. #4G7064

let
devicenil
vertex-modulenil
fragment-modulenil
pipelinenil
targetnil
verticesnil
readbacknil
encodernil
command-buffernil
glyph-resourcesnil
format:rgba8-unorm
shaped
luv.slug:shape-slug-textstringfont-pathname
zpb-ttf:with-font-loader
font-loaderfont-pathname
let
draws
make-slug-text-drawsshapedfont-loader
unlessdraws
error'luv.slug:slug-shaping-error:reason:no-drawable-glyphs:detailsstring
multiple-value-bind
min-xmin-ymax-xmax-y
slug-text-extentsdrawsshapedfont-loader
let
vertex-data
make-slug-text-verticesdrawswidthheightmin-xmin-ymax-xmax-y
unwind-protect
progn
setfdevicevertex-module
createdevice
make-shader-module-descriptor:label"Shaped Slug vertex":language:mathematical:code
fragment-module
createdevice
make-shader-module-descriptor:label"Shaped Slug fragment":language:mathematical:code
layout
createdevice
make-bind-group-layout-descriptor:label"Shaped Slug outline textures":entries'
:binding0:type:texture
:binding1:type:texture
pipeline
createdevice
make-render-pipeline-descriptor:label"HarfBuzz shaped Slug text":layoutlayout:vertex`
:module,vertex-module:buffers
:array-stride36:attributes
:shader-location0:offset0:format:float32x3
:shader-location1:offset12:format:float32x3
:shader-location2:offset24:format:float32x3
:fragment`
:module,fragment-module:targets
:format,format:blend:premultiplied-alpha
target
createdevice
make-texture-descriptor:label"Shaped Slug text target":size:dimensions:2d:formatformat:usage'
:render-attachment:copy-src
vertices
createdevice
make-buffer-descriptor:label"Shaped Slug glyph quads":size
*
lengthvertex-data
4
:usage'
:vertex:copy-dst
readback
createdevice
make-buffer-descriptor:label"Shaped Slug text readback":size:usage'
:copy-dst
write-bufferverticesvertex-data
dolist
drawdraws
setfglyph-resources
setfencoder
createdevice
make-command-encoder-descriptor:label"Shaped Slug text commands"
let
pass
begin-render-passencoder
make-render-pass-descriptor:color-attachments`
:view,target:load-op:clear:store-op:store:clear-value
0.040.120.201.0
set-pipelinepasspipeline
set-vertex-bufferpass0vertices
loopfordrawindrawsforfirst-vertexfrom0by6do
set-bind-grouppass0
slug-text-draw-bind-groupdraw
drawpass61first-vertex
encodeencoder
make-gpu-copy-texture-to-buffer-command:sourcetarget:destinationreadback
setfcommand-buffer
finishencoder
submitcommand-buffer
values
read-bufferreadback
widthheightformatshaped
whencommand-buffer
destroycommand-buffer
whenencoder
destroyencoder
dolist
resourceglyph-resources
destroyresource
whenreadback
destroyreadback
whenvertices
destroyvertices
whentarget
destroytarget
whenpipeline
destroypipeline
whenfragment-module
destroyfragment-module
whenvertex-module
destroyvertex-module
whendevice
destroydevice
defun shape-slug-text slug-harfbuzz.lisp:108
defunshape-slug-text
stringfont-pathname&keydirection

Shape string with HarfBuzz, returning glyph IDs and font-unit placements.

direction may be :LTR, :RTL, :TTB, or :BTT. With no direction HarfBuzz guesses the segment properties from the Unicode text. #4G7064

let
bytes
blobnil
facenil
fontnil
buffernil
unwind-protect
cffi:with-pointer-to-vector-data
font-databytes

HB_MEMORY_MODE_DUPLICATE copies the bytes before this dynamic pointer leaves scope.

setfblob
hb-blob-createfont-data
lengthbytes
0
cffi:null-pointer
cffi:null-pointer
face
hb-face-createblob0
font
hb-font-createface
buffer
hb-buffer-create
when
some#'cffi:null-pointer-p
listblobfacefontbuffer
error'slug-shaping-error:reason:harfbuzz-allocation-failed
let
units-per-em
hb-face-get-upemface
unless
pluspunits-per-em
error'slug-shaping-error:reason:invalid-units-per-em:detailsunits-per-em
hb-ot-font-set-funcsfont
hb-font-set-scalefontunits-per-emunits-per-em
cffi:with-foreign-string
textbyte-count
string:encoding:utf-8
let
length
1-byte-count
hb-buffer-add-utf8buffertextlength0length
whendirection
hb-buffer-set-directionbuffer
hb-buffer-guess-segment-propertiesbuffer
hb-shapefontbuffer
cffi:null-pointer
0
let*
x-advance
loopforglyphacrossglyphssum
slug-shaped-glyph-x-advanceglyph
y-advance
loopforglyphacrossglyphssum
slug-shaped-glyph-y-advanceglyph
make-slug-shaped-text:glyphsglyphs:units-per-emunits-per-em:x-advancex-advance:y-advancey-advance
whenbuffer
hb-buffer-destroybuffer
whenfont
hb-font-destroyfont
whenface
hb-face-destroyface
whenblob
hb-blob-destroyblob

DONE Shape and draw a HarfBuzz text run through Slug #DTBI99

Intent: join HarfBuzz shaping to the proven zpb-ttf outline boundary and render a run as placed Slug glyph quads without a software rasterizer.

Evidence: #4G7064 records the owned HarfBuzz binding, ffi ligature and cluster regression, glyph-ID-to-outline join, one-quad-per-glyph Metal draw, and visually inspected office affinity image. The complete luvcraft suite passes with the per-vertex pixels-per-em shader input.

The font boundary is now working before any rasterizer. The shader module in :luv depends directly on zpb-ttf and converts its resolved (start,control,end) segments into luv contours; a nil control becomes the standard Slug line quadratic. A slug-glyph also retains advance width, left side bearing, and units per em, and the converter accepts an already selected zpb glyph so a later HarfBuzz glyph index need not be turned back into a character. The reproducible test font is cl-dejavu's DejaVu Sans: its “O” produces two opposite-winding contours, 16 quadratics, a 1612-unit advance, and packs through the same arbitrary-outline path. No cl-vectors, cl-aa, or McCLIM raster object enters this system.

The Metal mark now crosses the complete boundary. normalize-slug-glyph-outline converts the font-unit points to em units while preserving the TrueType origin. The cold glyph probe loads DejaVu Sans “O”, serializes its 16 quadratics and two contours, uploads its exact band and curve arrays, binds the two texture views, draws the em-square quad through the public mathematical shader specifications, reads back the GPU target, and writes this image:

Changing the outline changes only the uploaded arrays. The fragment shader contains no “O” points, contour count, or source-unrolled curve calls. The generated SPIR-V has two structured loops and at least six exact image fetches; the generated MSL has two unsigned for loops with the reusable Slug function's lexical locals scoped inside them.

Done when: HarfBuzz-selected glyph IDs and placements render as a legible run through indexed Slug outlines, including a ligature that cannot be reproduced by one cmap lookup per character.

DONE Put a shaped run into the moving world #QW7P96

Intent: cross #4G7064 from an isolated target into the actual luvcraft scene, with world placement, camera projection, scene depth, and alpha composition all participating in the same frame as terrain.

make-world-text-run is now a semantic owner around dense HarfBuzz placement and vertex data. Its initial model transform fixes the text plane in world space from the camera's initial right and up basis. The :slug-world-text vertex stage then applies the scene frame uniform's camera view and perspective projection; the existing Slug fragment stage still owns coverage. The pipeline uses :less depth testing with depth writes disabled and premultiplied-alpha blending. It is encoded after opaque chunks and before the always-visible crosshair.

The fragment stage now derives pixels-per-em from the horizontal and vertical screen derivatives of the interpolated outline coordinate. Perspective, rotation, and camera distance therefore participate at the actual fragment; there is no run-center approximation or per-frame instance-buffer rewrite.

Evidence: the Metal validation layer accepts the complete hidden luvcraft frame, including its pipeline-layout transitions and teardown. This capture is the ordinary world smoke path, not an isolated text target:

The label's transparent quad exterior preserves sky and blocks; the pipeline's :less comparison puts terrain in front whenever their projections overlap. Frame metrics count six unit-quad vertices instanced across the run in one draw. The cache and atlas iterations below remove the original literal bind-and-draw-per-glyph baseline.

Done when: a HarfBuzz-shaped run is legible in the real scene, changes screen position and scale with its camera relationship, obeys terrain depth, and survives a full Metal-validation frame without a bind-state or lifetime error.

defun make-world-text-run text.lisp:287
defunmake-world-text-run
deviceglyph-cachecameratarget-formatstringfont-pathname&key
lift3.0
world-units-per-em0.55

Shape string once and create a depth-tested world text run on device.

The run owns dense placement/model data and its live pipeline; glyph-cache owns font-and-glyph device resources reusable across runs. See #QW7P96.

let*
shaped
luv.slug:cached-slug-shaped-textglyph-cachefont-pathnamestring
zpb-ttf:with-font-loader
font-loaderfont-pathname
let
glyphs
luv.slug:make-slug-glyph-placementsshapedfont-loaderglyph-cachefont-pathname
unlessglyphs
error'luv.slug:slug-shaping-error:reason:no-drawable-glyphs:detailsstring
multiple-value-bind
min-xmin-ymax-xmax-y
luv.slug:slug-text-extentsglyphsshapedfont-loader
multiple-value-bind
rightupforward
declare
ignoreforward
let*
atlas
instance-data
make-world-text-instancesglyphsatlascenterrightupworld-units-per-emmin-xmin-ymax-xmax-y
make-world-text-run-from-instancesdevicetarget-formatstringfont-pathnameshapedglyphsatlascenterworld-units-per-eminstance-data

DONE Cache shaping and device glyph outlines #U5X6DY

Intent: make text runs cheap to reconstruct without hiding GPU lifetime inside glyph occurrences.

make-world-text-glyph-cache is owned by the luvcraft session and tied to one device. It retains HarfBuzz results by canonical font path and exact string, and retains normalized, serialized Slug outlines by canonical font path and HarfBuzz glyph ID. Atlas objects own the uploaded textures; a run owns placement, dense instance data, layout, and pipeline. Stopping the session destroys frame bind groups, then runs, then atlas textures, then the device.

Repeated occurrences share the same serialized resource object. The "hello, world" proof has 11 drawable occurrences but only 8 cached outlines. That banner is a proof rather than scenery, so a caller asks for it by passing :world-text-string; the ordinary game sky carries no text at all. A second identical run reuses the slug-shaped-text, all eight outlines, and the matching atlas. Metal regressions construct both runs and exercise the ownership-order teardown.

Done when: duplicate glyph IDs within a run and across runs resolve to the same per-device outline, identical shaping is not repeated, and Metal teardown is clean.

DONE Pack one atlas and issue one instanced text draw #AT7L3S

Intent: remove the command and allocation multiplication caused by treating each glyph occurrence as an independently bound texture pair.

create-world-text-glyph-atlas concatenates each distinct serialized outline into one RG16U band texture and one RGBA16F curve texture. Its location table maps the cached outline object to linear band and curve bases. Each run has a six-vertex unit quad and one 96-byte instance record per drawable glyph: world origin, right/up edge vectors, outline bounds and band counts, and atlas bases. The final two lanes separately retain the exact packed band bounds; quad geometry can therefore stay padded without changing spatial band selection. The vertex stage expands these records; the render pass binds the two buffers and one atlas group, then calls draw once with the glyph count as the instance count.

The exact proof-run comparison is deliberately about owned objects and encoded commands, not a noisy whole-frame timing claim:

text cost, "hello, world"cached glyph pairsatlas instances
drawable / unique glyphs11 / 811 / 8
text draws per frame111
text bind calls per frame111
text encode calls245
per-frame text buffer writes10
glyph textures / views16 / 162 / 2
frame text bind groups81
allocated glyph texture data384 KiB48 KiB
text vertex-buffer data2,376 B1,128 B

The 24-to-5 encode count includes pipeline, vertex-buffer, bind-group, and draw calls. Texture allocation falls by 87.5 percent because every former glyph pair paid the 4096-texel minimum row; vertex data falls by 52.5 percent. The new spatial bands intentionally increase used band payload from 202 to 996 texels while curve payload remains 121 texels: roughly 4.8 KiB of useful data inside the 48 KiB allocated atlas. A 30-frame Apple M2 Pro steady run records 165 whole-scene draws, 0.173 ms median scene encoding, and 0.011 ms median uniform update. It is a current-path health measurement, not an A/B timing, because upstream terrain, particles, overlays, and lighting changed across the saved pre-atlas commit.

Done when: repeated glyphs share atlas storage, one run emits one instanced draw and one bind call, the backend validation layer accepts the frame, and the ordinary world capture remains legible.

DONE Select spatial bands and scale coverage from fragment derivatives #D4R1VX

The shared shader language now carries derivative-x and derivative-y through direct Metal dfdx/dfdy and SPIR-V OpDPdx/OpDPdy. The fragment shader forms a screen gradient for each em axis and takes its reciprocal length as pixels-per-em. The same fragment normalizes its x/y outline coordinate, chooses one of the serialized horizontal and vertical bands, and relocates that band's local curve addresses through the atlas bases. Default outlines use min(curve-count, 16) bands per axis rather than the one-band correctness fallback.

The generated MSL and SPIR-V are compiled and validated in the normal build, unit tests require derivative instructions and selected header addressing, and the Metal debug layer renders the multi-band world capture above. The default eight outlines use band counts from 4 through 16; the inspected atlas contains 996 band texels and 121 curve texels.

The remaining efficiency goblins are now explicit. Atlases and live pipelines are shared only by runs with the same exact glyph set, so many overlapping labels can duplicate storage and pipeline state. Tiny sets still pay one 4096-wide texture row. The sorted band lists do not yet early-exit their GPU fold, and multiple runs are not coalesced into a world-wide draw or culled as a batch. A terminal-scale client should next move atlas pages and pipeline ownership into the per-device cache, then measure whether page locality or world-wide instance aggregation wins before adding a generic text manager.

Done when: projected scale has no CPU estimate or frame upload, fragments select real spatial bands, both shader backends validate the derivative path, and an ordinary-distance debug-layer world frame is visually intact. The first close-up in #9G0Z19 subsequently falsified the bounds crossing; its corrected close-up now verifies that fragment selection agrees with preprocessing at the enlarged proof scale.

DONE One distance function draws the roundrect family analytically #LDAHBP

Intent: reuse the proof discipline learned from Slug without pretending that a fixed GUI primitive is an arbitrary outline.

The distinction is now executable. Slug's first proof #OWR8OZ puts reusable pixel mathematics in a typed function, exposes it through tiny portable shader specifications, inspects both generated targets, and finally demands a real GPU readback. The roundrect proof follows exactly that path while omitting curve textures, bands, winding traversal, and device outline identity.

roundrect-signed-distance is one shared arithmetic definition. It executes as an ordinary Lisp function for the scalar oracle and lowers into the same mathematical shader graph for both backends. A radius of zero is a rectangle; radius equal to the shorter half-extent yields capsules and circles. The shared arithmetic vocabulary gained raw abs and sqrt realization on this evidence, without claiming quantity semantics for either operation.

roundrect-coverage evaluates that distance in the fragment shader, uses derivative-x and derivative-y to measure its screen-space gradient, and maps the signed result through a one-pixel coverage ramp. One padded quad record carries clip position, local coordinate, half-size plus radius, opacity, and premultiplied colour. The cold Metal proof sends four such records through one resource-free pipeline: a roundrect, a capsule, a circle, and a rotated affine ellipse.

Evidence: the Lisp distance oracle checks straight edges, circle points, and radius normalization. The generated SPIR-V contains both fragment derivative instructions and validates under spirv-val. Generated MSL contains direct dfdx, dfdy, and sqrt calls and compiles under Metal 4. The image above is a 768 by 480 native Metal render and readback, not a software reconstruction. The complete luv, luvcraft, and wiki suites pass.

Done when (met): one shared distance definition produces exact scalar oracle values, both backend products compile, and a real GPU draw preserves smooth coverage across a card, capsule, circle, and non-uniform rotated transform.

NEXT Compare Slug coverage at glyph edges #9G0Z19

Intent: distinguish “a shaped run renders” from evidence that Slug's edge coverage and geometry expansion are correct across backends.

Evidence needed: exercise the portable draw on Vulkan, compare boundary pixels with a small CPU winding oracle, and test dynamic dilation on geometry whose undilated quad actually loses edge coverage. Atlas allocation and outline reuse are separate performance work and should follow measurement.

capture-hidden-luvcraft-text-closeup renders a 1280 by 360 logical Metal frame with the text enlarged in world space, producing a 2560 by 720 Retina image without post-render scaling. The elevated camera leaves enough world context to exercise the real depth path while giving the outline an unobstructed sky background:

The first native close-up was falsifying evidence: horizontal cuts through h and e and vertical slivers in h, r, and d coincided with band transitions, not font extrema or terrain edges. The instance record had sent the quad's 0.035-em padded outline bounds to the fragment shader, while pack-slug-outline partitioned the unpadded packed-outline bounds. The shader consequently mapped fragments into different spatial partitions than the serializer used to build their curve lists.

The corrected record carries both meanings. Padded low/high values still expand and parameterize the quad, while two additional attributes carry the exact packed min/max used only for band selection. The image above is the same close-up after that change: all transition-aligned cuts have disappeared, including the former horizontal break across h/e and the vertical breaks in h/r/d. A focused regression requires those padded and exact lanes to remain distinct. Metal validation accepts the enlarged Retina world render, and a cold Vulkan capture through the same shared specification produces the same continuous glyphs at its native 1280 by 360 drawable extent.

The world terminal #NMAD2U is a particularly demanding next client: repeated grid-aligned glyphs make edge quality, outline reuse, and batching visible, but the terminal should consume this evidence rather than silently becoming its oracle.

Done when: representative extrema, shared endpoints, holes, and clipped edges agree with the oracle on Metal and Vulkan, and dilation demonstrably recovers coverage rather than merely enlarging an already sufficient quad.

defun capture-hidden-luvcraft-text-closeup capture.lisp:228
defuncapture-hidden-luvcraft-text-closeup
pathname&key

Render a native-resolution close-up of the world Slug text proof.

The wide viewport and enlarged world-space text make outline, band-selection, and coverage defects visible without scaling up a smaller raster afterward. #9G0Z19

capture-hidden-luvcraft-screenshotpathname:title"luv Slug world text close-up":width1280:height360:providerprovider:camera
make-instance'fly-camera:position
make-vec38.024.0-6.0
:yaw0.0:pitch0.02
:world-text-string"hello, world":world-text-distance6.0:world-text-lift2.2:world-text-units-per-em3.4