luv

Workshop wiki

slug.lisp

hal/shader/slug.lisp

system luv · 41 definitions · on GitHub

Slug's quadratic outline calculation, first as a fixed proof and then as the data-driven band texture path used by real outlines.

The shape of the shaders follows Lengyel's open reference implementation (github.com/EricLengyel/Slug, 2017-2026): sorted bands with an early exit, a nonzero or even-odd fill, an optional optical-weight boost, and a bounding polygon dilated per vertex rather than by a constant. The constants the reference bakes in are named values here, so a live pipeline can be retuned from a knob without editing shader source.

in-package#:luv.slug
defconstant+slug-root-epsilon+
/1.065536.0

--------------------------------------------------------------------- Tunable values.

Each special is one number the shaders would otherwise carry as a literal. A shader body names it by the unstarred symbol; the parser folds that name to a literal through shader-source-value and remembers it did, so a live pipeline rebuilds when the special moves. A game that owns a session wraps these in knobs (luvcraft::define-knob in luvcraft/text.lisp); here they are only the values.

defparameter*slug-filter-width*1.0

The box filter's width in pixels. One is the reference's exact pixel coverage; wider softens (and, past the dilation, clips) the edge.

defparameter*slug-fill-rule*0.0"0 fills by the nonzero winding rule, 1 by even-odd; fractions blend."
defparameter*slug-optical-weight*1.0

The exponent applied to coverage. 1 is linear coverage; the reference's SLUG_WEIGHT is 0.5, a square root that boosts thin strokes.

defparameter*slug-footprint-norm*0.0

How the pixel footprint in em space is measured from the coordinate derivatives: 0 is the gradient length (L2), 1 is fwidth (L1), which is what the reference uses.

defparameter*slug-early-exit*1.0

1 leaves a band's sorted curve list at the first curve wholly behind the sample; 0 walks every curve (the reference without its break).

defparameter*slug-root-epsilon*+slug-root-epsilon+

Below this |a| a curve's polynomial is solved as linear; also the floor of every division in the coverage combination.

defparameter*slug-debug-view*0.0

0 renders ink; 1 paints each glyph's quad with its band loads (red the horizontal band's curve count, green the vertical's, over sixteen).

defparameter*slug-dilation-pixels*0.6

How far a glyph's bounding quad grows past its outline, in filter widths (pixels when the filter is one pixel wide), so the filter's half-width and a little more always lie inside the quad. The reference dilates exactly half a pixel; a hair more forgives the per-vertex approximation of the pixel size across a perspective quad.

defparameter*slug-static-padding*0.0

A constant dilation of every glyph quad in em, added on the CPU when the quads are laid out. Zero leaves the work to the per-vertex dilation; the value luv used before dynamic dilation was 0.035.

defmacrodefine-slug-source-value
namespecial

Let name stand for special's value in shader source.

`
defmethodshader:shader-source-value
name
eql',name
declare
ignorename
values,specialnilt
defunslug-dilation-em
pixels-per-em

The em distance a glyph quad laid out at pixels-per-em should grow on each side: the live dilation in filter widths, plus the static padding. For a stage that cannot dilate per vertex (a flat screen quad, whose pixel scale is known when it is laid out).

defunslug-font-cap-height
font-loader

font-loader's sCapHeight from its OS/2 table, in font units, or NIL when the font has no OS/2 table or one too old (version < 2) to carry it. ZPB-TTF does not read OS/2, so this seeks the table itself.

let
table
zpb-ttf::table-info"OS/2"font-loader
when
andtable
>=
zpb-ttf::sizetable
90
zpb-ttf::seek-to-tabletablefont-loader
let*
stream
zpb-ttf::input-streamfont-loader
version
zpb-ttf::read-uint16stream
when
>=version2

sCapHeight sits at byte 88 of the table; the version was 2.

zpb-ttf::advance-file-positionstream86
let
cap-height
zpb-ttf::read-int16stream
and
pluspcap-height
cap-height
defunslug-cap-height-aligned-size
font-loadersize

The font size nearest size (in pixels per em) at which font-loader's cap height lands on a whole number of pixels, so the tops of most capitals share the pixel grid: the reference's substitute for hinting. Falls back to size when the font declares no cap height.

let
cap-height
units-per-em
zpb-ttf:units/emfont-loader
if
andcap-height
pluspunits-per-em
let*
cap-em
/cap-heightunits-per-em
pixels
max1
round
*sizecap-em
/pixelscap-em
size
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
shader:define-shader-functionslug-axis-contribution
p1-axisp2-axisp3-axisp1-otherp2-otherp3-otherpixels-per-em
"Return coverage1, coverage2, weight1, and weight2 for one ray axis."
let*
axis-a
+
-p1-axis
*p2-axis2.0
p3-axis
axis-b
-p1-axisp2-axis
other-a
+
-p1-other
*p2-other2.0
p3-other
other-b
-p1-otherp2-other
safe-a
shader:mixaxis-a1.0linear
discriminant
max
-
*axis-baxis-b
*axis-ap1-axis
0.0
root-distance
sqrtdiscriminant
quadratic-t1
/
-axis-broot-distance
safe-a
quadratic-t2
/
+axis-broot-distance
safe-a
linear-t
/
*p1-axis0.5
safe-b
t1
shader:mixquadratic-t1linear-tlinear
t2
shader:mixquadratic-t2linear-tlinear

FSign makes zero exactly "not positive", matching the eligibility table without epsilon classification at shared control points.

s1
max
signump1-axis
0.0
s2
max
signump2-axis
0.0
s3
max
signump3-axis
0.0
crossing1
+
*
-
*other-at1
*other-b2.0
t1
p1-other
crossing2
+
*
-
*other-at2
*other-b2.0
t2
p1-other
scaled1
*crossing1pixels-per-em
scaled2
*crossing2pixels-per-em
coverage1
*eligible1
shader:clamp
+scaled10.5
0.01.0
coverage2
*eligible2
shader:clamp
+scaled20.5
0.01.0
weight1
*eligible1
shader:clamp
-1.0
*
absscaled1
2.0
0.01.0
weight2
*eligible2
shader:clamp
-1.0
*
absscaled2
2.0
0.01.0
shader:vec4coverage1coverage2weight1weight2
shader:define-shader-functionslug-horizontal-contribution
p1p2p3pixels-per-em
"Evaluate one quadratic against the pixel's horizontal winding ray."
shader:define-shader-functionslug-vertical-contribution
p1p2p3pixels-per-em
"Evaluate one quadratic against the pixel's vertical winding ray."
shader:define-shader-functionslug-combine-coverage
horizontal0horizontal1horizontal2horizontal3vertical0vertical1vertical2vertical3
"Combine four curves' horizontal and vertical ray contributions."
let*
xcov
+0.0
-
shader:swizzlehorizontal0:x
shader:swizzlehorizontal0:y
-
shader:swizzlehorizontal1:x
shader:swizzlehorizontal1:y
-
shader:swizzlehorizontal2:x
shader:swizzlehorizontal2:y
-
shader:swizzlehorizontal3:x
shader:swizzlehorizontal3:y
ycov
+0.0
-
shader:swizzlevertical0:y
shader:swizzlevertical0:x
-
shader:swizzlevertical1:y
shader:swizzlevertical1:x
-
shader:swizzlevertical2:y
shader:swizzlevertical2:x
-
shader:swizzlevertical3:y
shader:swizzlevertical3:x
xweight
max0.0
shader:swizzlehorizontal0:z
shader:swizzlehorizontal0:w
shader:swizzlehorizontal1:z
shader:swizzlehorizontal1:w
shader:swizzlehorizontal2:z
shader:swizzlehorizontal2:w
shader:swizzlehorizontal3:z
shader:swizzlehorizontal3:w
yweight
max0.0
shader:swizzlevertical0:z
shader:swizzlevertical0:w
shader:swizzlevertical1:z
shader:swizzlevertical1:w
shader:swizzlevertical2:z
shader:swizzlevertical2:w
shader:swizzlevertical3:z
shader:swizzlevertical3:w
slug-finish-coverage
max
/
abs
+
*xcovxweight
*ycovyweight
max
+xweightyweight
slug-root-epsilon
min
absxcov
absycov
shader:define-shader-functionslug-finish-coverage
winding

Turn an unbounded signed WINDING estimate into a fill by the fill rule, then boost it by the optical weight.

The nonzero rule saturates; even-odd folds the winding number back and forth between zero and one, as the reference's SLUG_EVENODD does. The optical weight is an exponent: one leaves coverage linear, the reference's SLUG_WEIGHT is one half.

let*
nonzero
shader:clampwinding0.01.0
even-odd
-1.0
abs
-1.0
*
shader:fract
*winding0.5
2.0
filled
shader:define-shader-functionslug-combine-band-coverage
xcovxweightycovyweight
"Combine the accumulated horizontal and vertical band traversals."
slug-finish-coverage
max
/
abs
+
*xcovxweight
*ycovyweight
max
+xweightyweight
slug-root-epsilon
min
absxcov
absycov
shader:define-shader-functionslug-pixels-per-em
render-coordinate

The pixel scale of the em square along each axis, from the sample coordinate's screen derivatives, already divided by the filter width so the rest of the pipeline works in filter widths rather than pixels.

The footprint norm chooses between the gradient length and fwidth.

let*
coordinate-dx
shader:derivative-xrender-coordinate
coordinate-dy
shader:derivative-yrender-coordinate
x-gradient
shader:vec2
shader:swizzlecoordinate-dx:x
shader:swizzlecoordinate-dy:x
y-gradient
shader:vec2
shader:swizzlecoordinate-dx:y
shader:swizzlecoordinate-dy:y
length-footprint
shader:vec2
sqrt
shader:dotx-gradientx-gradient
sqrt
shader:doty-gradienty-gradient
width-footprint
+
abscoordinate-dx
abscoordinate-dy
ems-per-pixel
shader:mixlength-footprintwidth-footprintslug-footprint-norm
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
shader:define-shader-functionslug-vertical-band-step
statecurvenextrender-coordinatepixels-per-em

Fold one vertical-band curve into STATE = (ycov, yweight, done); the band is sorted by descending maximum y.

let*
p1
-render-coordinate
p2
-render-coordinate
p3
-render-coordinate
contribution
slug-vertical-contributionp1p2p3pixels-per-em
shader:vec3
+
-
shader:swizzlecontribution:y
shader:swizzlecontribution:x
max
shader:swizzlecontribution:z
shader:swizzlecontribution:w
-1.0
shader:step-0.5reach
shader:define-shader-functionslug-band-done-p
state

Whether a band fold with STATE = (cov, weight, done) may stop: the last curve was wholly behind the sample and the early exit is on.

shader:define-shader-functionslug-texel-coordinate
address
"Map Slug's fixed-width linear texture ADDRESS to exact uint coordinates."
shader:uvec2
modaddress
/address
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
shader:define-shaderslug-bezier-vertex-specification
:stage:vertex:inputs
position:vec3:location0
outline-coordinate:vec3:location1
pixels-per-em:vec3:location2
:outputs
clip-position:vec4:built-in:position
render-coordinate:vec2:location0
render-pixels-per-em:vec2:location1
let*
shader:set-outputclip-positionclip
shader:set-outputrender-coordinate
shader:swizzleoutline-coordinate:xy
shader:set-outputrender-pixels-per-em
shader:swizzlepixels-per-em:xy
shader:define-shaderslug-bezier-fragment-specification
:stage:fragment:inputs
render-coordinate:vec2:location0
pixels-per-em:vec2:location1
:outputs
color-output:vec4:location0
shader:set-outputcolor-output
slug-quadratic-outlinerender-coordinatepixels-per-em
shader:vec40.960.320.481.0
shader:vec20.500.08
shader:vec20.080.38
shader:vec20.140.70
shader:vec20.180.98
shader:vec20.500.74
shader:vec20.820.98
shader:vec20.860.70
shader:vec20.920.38
shader:define-live-shaderslug-banded-fragment-specification
:stage:fragment:inputs
render-coordinate:vec2:location0
pixels-per-em:vec2:location1
:resources
band-data:uint-texture-2d:binding0
curve-data:texture-2d:binding1
:outputs
color-output:vec4:location0

One band per axis is the complete correctness path for an outline: every serialized curve participates. Subdividing the same lists into spatial bands is the subsequent culling optimization, not a different algorithm.

let*
horizontal-header-location
shader:uvec2zerozero
vertical-header-location
horizontal-header
shader:texel-loadband-datahorizontal-header-location
vertical-header
shader:texel-loadband-datavertical-header-location
horizontal-count
shader:swizzlehorizontal-header:x
horizontal-offset
shader:swizzlehorizontal-header:y
vertical-count
shader:swizzlevertical-header:x
vertical-offset
shader:swizzlevertical-header:y
horizontal
shader:counted-fold
indexhorizontal-countstate
shader:vec30.00.00.0
:until
let*
entry-address
+horizontal-offsetindex
curve-location
next-location
curve
shader:texel-loadcurve-datacurve-location
next
shader:texel-loadcurve-datanext-location
slug-horizontal-band-stepstatecurvenextrender-coordinatepixels-per-em
vertical
shader:counted-fold
indexvertical-countstate
shader:vec30.00.00.0
:until
let*
entry-address
+vertical-offsetindex
curve-location
next-location
curve
shader:texel-loadcurve-datacurve-location
next
shader:texel-loadcurve-datanext-location
slug-vertical-band-stepstatecurvenextrender-coordinatepixels-per-em
shader:set-outputcolor-output
*
shader:vec40.960.320.481.0
coverage
shader:define-live-shaderslug-atlas-fragment-specification
:stage:fragment:inputs
render-coordinate:vec2:location0
atlas-base:vec2:location1
band-bounds:vec4:location2
band-counts:vec2:location3
render-color:vec4:location4
:resources
band-data:uint-texture-2d:binding0
curve-data:texture-2d:binding1
:outputs
color-output:vec4:location0

The reference's SlugRender: pick the pixel's horizontal and vertical band, walk each band's sorted curve list until a curve is wholly behind the sample, and combine the two rays' coverage by their weights. The glyph's data lives at ATLAS-BASE inside shared band and curve atlases; every address below is relative to it.

let*
band-base
curve-base
horizontal-band-count
vertical-band-count
pixels-per-em
slug-pixels-per-emrender-coordinate
horizontal-position
shader:clamp
/
-
shader:swizzlerender-coordinate:y
shader:swizzleband-bounds:y
0.01.0
vertical-position
shader:clamp
/
-
shader:swizzlerender-coordinate:x
shader:swizzleband-bounds:x
0.01.0
horizontal-band-candidate
shader:uint
*horizontal-position
shader:floathorizontal-band-count
vertical-band-candidate
shader:uint
*vertical-position
shader:floatvertical-band-count
horizontal-band
if
<horizontal-band-candidatehorizontal-band-count
horizontal-band-candidate
-horizontal-band-countone
vertical-band
if
<vertical-band-candidatevertical-band-count
vertical-band-candidate
-vertical-band-countone
horizontal-header-address
+band-basehorizontal-band
vertical-header-address
+band-basehorizontal-band-countvertical-band
horizontal-header-location
shader:uvec2
modhorizontal-header-addresswidth
/horizontal-header-addresswidth
vertical-header-location
shader:uvec2
modvertical-header-addresswidth
/vertical-header-addresswidth
horizontal-header
shader:texel-loadband-datahorizontal-header-location
vertical-header
shader:texel-loadband-datavertical-header-location
horizontal-count
shader:swizzlehorizontal-header:x
horizontal-offset
shader:swizzlehorizontal-header:y
vertical-count
shader:swizzlevertical-header:x
vertical-offset
shader:swizzlevertical-header:y
horizontal
shader:counted-fold
indexhorizontal-countstate
shader:vec30.00.00.0
:until
let*
entry-address
+band-basehorizontal-offsetindex
entry-location
shader:uvec2
modentry-addresswidth
/entry-addresswidth
local-location
shader:swizzle
shader:texel-loadband-dataentry-location
:xy
curve-address
+curve-base
shader:swizzlelocal-location:x
*
shader:swizzlelocal-location:y
width
curve-location
shader:uvec2
modcurve-addresswidth
/curve-addresswidth
next-location
shader:uvec2
mod
+curve-addressone
width
/
+curve-addressone
width
curve
shader:texel-loadcurve-datacurve-location
next
shader:texel-loadcurve-datanext-location
slug-horizontal-band-stepstatecurvenextrender-coordinatepixels-per-em
vertical
shader:counted-fold
indexvertical-countstate
shader:vec30.00.00.0
:until
let*
entry-address
+band-basevertical-offsetindex
entry-location
shader:uvec2
modentry-addresswidth
/entry-addresswidth
local-location
shader:swizzle
shader:texel-loadband-dataentry-location
:xy
curve-address
+curve-base
shader:swizzlelocal-location:x
*
shader:swizzlelocal-location:y
width
curve-location
shader:uvec2
modcurve-addresswidth
/curve-addresswidth
next-location
shader:uvec2
mod
+curve-addressone
width
/
+curve-addressone
width
curve
shader:texel-loadcurve-datacurve-location
next
shader:texel-loadcurve-datanext-location
slug-vertical-band-stepstatecurvenextrender-coordinatepixels-per-em
ink
*render-colorcoverage

The debug view paints the whole quad with the two bands' loads, so the banding of a glyph -- and what the band count buys -- can be seen at a glance.

band-load
shader:vec4
/
shader:floathorizontal-count
16.0
/
shader:floatvertical-count
16.0
*coverage0.5
1.0
shader:set-outputcolor-output