Definitions and Stages
define-function, define-shader, define-struct, define-macro,
define-type-alias, define-uniform and define-program make the parts of
programs. They are macros, evaluated as Lisp evaluates them, at the REPL or
when a file loads. Each is checked when it is evaluated: an error names the
definition and the form at fault, and the definition before it stays.
define-uniform is described on Uniforms and
Resources, and define-program on
Programs.
define-function
(define-function name ((parameter type) ...)
[documentation] [(declare ...)]
form ...)
Defines the shader function name, which shader code calls as (name argument ...). A parameter's type is a type of a value or a texture. The
last form gives the value, whose type is inferred, or several with values,
or none. A struct or an array argument is copied: the function changes its
own copy. A shader function reads the uniforms of define-uniform, and the
shader that calls it passes it its own textures.
(define-function brightness ((color :vec3))
"The perceived brightness of the linear COLOR."
(dot color (vec3 0.2126 0.7152 0.0722)))
A shader function may call a function not defined yet. The definition is
made, and signals a forward-reference-warning, a style warning, when it is
evaluated, at the REPL or as a file loads; it is checked whole when a program
that uses it is compiled. At the REPL, SBCL prints:
(define-function tone ((color :vec3))
(gamma-correct (* color 1.2)))
WARNING:
TONE calls GAMMA-CORRECT, which is not a shader function yet: define it with
DEFINE-FUNCTION before a program that uses TONE is compiled.
A program that calls it compiles once it is defined, and until then signals an
undefined-shader-function; a shader that calls tone warns of it again as
it is defined:
(define-shader toned :fragment ((color :vec3))
(vec4 (tone color) 1))
(compile-program '(:fragment toned) :target :vulkan)
In TONE: GAMMA-CORRECT is not a shader function: neither built into clsl's
language nor defined with DEFINE-FUNCTION.
in: (GAMMA-CORRECT (* COLOR 1.2))
(define-function gamma-correct ((color :vec3))
(expt color (vec3 (/ 1 2.2))))
(program-stages (compile-program '(:fragment toned) :target :vulkan))
;; => (:FRAGMENT)
No shader code calls itself, through other functions or not. The name of a built-in names no definition.
define-shader
(define-shader name stage (input ... [&uniform uniform ...] [&shared variable ...])
[documentation] [(declare ...)]
form ...)
Defines the shader name: the stage of a program, a function from its
inputs, the parameters before &uniform, to its outputs, its values. A shader
cannot be called. The uniforms after &uniform are on Uniforms and
Resources, and the shared variables after &shared
on Shared Memory and Atomics. The stages:
| Stage | Takes | Gives |
|---|---|---|
:vertex |
inputs (name type [:location n]), from vertex buffers |
the position in clip space, a :vec4, then the values the next stage takes |
(:tessellation-control :patch-size n :vertices n) |
patches of :patch-size vertices, each input (name (:array type n)) |
each vertex of the patches it makes, its position and values; and the tessellation levels |
(:tessellation-evaluation :patch-size n [:domain d] [:spacing s] [:winding w] [:point-mode b]) |
the patches the control shader makes, inputs as the control shader's | each vertex the tessellator makes, placed: its position and values |
(:geometry :input p :output p :max-vertices n [:invocations n]) |
primitives of :input, each input an array of a value for each vertex |
strips of :output, with emit-vertex |
:fragment |
inputs (name type [:interpolation i] [:sampling s]), the values of the stage before |
a value for each render target, or none |
(:compute :workgroup-size (x [y [z]])) |
nothing | nothing; it writes storage buffers and storage images |
Vertex shaders
A vertex shader's inputs are scalars and vectors of numbers, which a vertex
buffer holds, at locations 0, 1 and so on: an input without a :location
takes the one after the input before it. It gives the position in clip space,
a :vec4, then the values the next stage takes, with values: scalars,
vectors and matrices of numbers, and structs of them. Clip space is Vulkan's
on every target, as cl-gfx's is: y points down, so the top of the target is at
y -1, and depth runs from 0 to 1.
A vertex shader may take no inputs, and place its vertices by
(vertex-index) alone; its program then needs no vertex buffer:
(define-shader fullscreen :vertex ()
"A triangle that covers the target, placed by the vertex index alone:
draw three vertices, with no vertex buffer."
(let ((uv (vec2 (float (logand (ash (vertex-index) 1) 2))
(float (logand (vertex-index) 2)))))
(values (vec4 (- (* uv 2) 1) 0 1) uv)))
(define-shader show-uv :fragment ((uv :vec2))
"UV as red and green."
(vec4 uv 0 1))
(define-program fullscreen-uv :vertex fullscreen :fragment show-uv)
(program-vertex-buffers (compile-program 'fullscreen-uv :target :vulkan))
;; => NIL
Tessellation shaders
A tessellation control shader takes patches of :patch-size vertices, as a
pipeline draws them, and makes patches of :vertices, from 1 to 32 each. It
runs once for each vertex it makes, which (invocation-id) numbers from 0,
and gives that vertex's position and values, as a vertex shader does. Its
inputs are (name (:array type n)), a value for each vertex of the patch, n
its :patch-size; (input-position index) reads their positions.
(set-tessellation-levels outer inner) says how finely the tessellator
divides the patch: every invocation of the patch sets the same levels, and a
shader that never sets them is refused (see Uniform Control
Flow).
A tessellation evaluation shader takes the patches the control shader makes,
its :patch-size the control shader's :vertices, and runs once for each
vertex the tessellator makes, which it places. (tessellation-coord) is where
the vertex is in the :domain: :triangles, the default, the weights of the
patch's three corners; :quads, x and y from 0 to 1 across it; or
:isolines, lines across a quad. :spacing is how the levels divide an edge:
:equal, the default, :fractional-even or :fractional-odd. :winding,
:counter-clockwise, the default, or :clockwise, is the order the corners
of its triangles run in the domain, whose origin is its top left, as Vulkan's
is. :point-mode t makes a point of each vertex instead.
(define-shader detailed-vertex :vertex ((position :vec2) (detail :float))
(values (vec4 position 0 1) detail))
(define-shader refine (:tessellation-control :patch-size 3 :vertices 3)
((detail (:array :float 3)))
"Divide each triangle as finely as its most detailed corner asks."
(let ((level (max (aref detail 0) (aref detail 1) (aref detail 2))))
(set-tessellation-levels (vec4 level) (vec2 level)))
(input-position (invocation-id)))
(define-shader place (:tessellation-evaluation :patch-size 3)
()
"Place each vertex the tessellator makes, by its weights of the corners."
(let ((weights (tessellation-coord)))
(+ (* (x weights) (input-position 0))
(* (y weights) (input-position 1))
(* (z weights) (input-position 2)))))
(define-shader white :fragment ()
(vec4 1))
(define-program tessellated
"Triangles, divided as finely as their corners ask."
:vertex detailed-vertex
:tessellation-control refine
:tessellation-evaluation place
:fragment white)
(let ((program (compile-program 'tessellated :target :vulkan)))
(values (program-stages program) (program-patch-size program) (program-features program)))
;; => (:VERTEX :TESSELLATION-CONTROL :TESSELLATION-EVALUATION :FRAGMENT)
;; 3
;; (:TESSELLATION-SHADERS)
A pipeline of the program draws lists of patches of program-patch-size
vertices, and needs the device's :tessellation-shaders feature.
Geometry shaders
A geometry shader takes primitives of its :input, :points, :lines,
:lines-adjacency, :triangles or :triangles-adjacency, of 1, 2, 4, 3 and
6 vertices, its inputs arrays of a value for each, and makes strips of its
:output, :points, :line-strip or :triangle-strip. (emit-vertex position value ...) emits a vertex, passing the values on, and
(end-primitive) ends a strip. It emits at most :max-vertices vertices:
past them, at the default safety, emit-vertex emits nothing. It runs
:invocations times for each primitive, from 1, the default, to 32, which
(invocation-id) tells apart.
(define-shader colored-vertex :vertex ((position :vec2) (color :vec3))
(values (vec4 position 0 1) color))
(define-shader outline (:geometry :input :triangles :output :line-strip :max-vertices 4)
((color (:array :vec3 3)))
"Draw the edges of each triangle, back to its first corner."
(dotimes (corner 4)
(let ((i (mod corner 3)))
(emit-vertex (input-position i) (aref color i)))))
(define-shader flat-color :fragment ((color :vec3))
(vec4 color 1))
(define-program wireframe
"Triangles drawn as their edges."
:vertex colored-vertex :geometry outline :fragment flat-color)
(program-features (compile-program 'wireframe :target :vulkan))
;; => (:GEOMETRY-SHADERS)
Every emit-vertex of a geometry shader, and of the functions it calls,
passes values of the same types, which the next stage takes. A geometry
shader after tessellation takes the primitives the tessellator makes:
:points in :point-mode, :lines of :isolines, and :triangles
otherwise.
Fragment shaders
A fragment shader's inputs take the values of the stage before, by position.
A value is interpolated :smooth, perspective-correct, the default; :linear,
without perspective; or :flat, not interpolated, from one vertex, as
integers, doubles and float pairs always are. It is sampled at the pixel's
:center, the default, the primitive's :centroid, or each :sample. It
gives a value for each render target, a scalar or vector of numbers, or none,
and (discard) makes the fragment draw nowhere.
Compute shaders
A compute shader has no inputs and gives nothing. It runs in workgroups of
x by y by z invocations, which tell themselves apart by
(global-invocation-id), and writes storage buffers and storage images. The
invocations of a workgroup share its variables after &shared. Getting
Started runs one.
Built-in inputs
The built-in inputs are functions of no arguments. One used in another stage, by the shader or a function it calls, is refused.
| Function | Stage | Gives |
|---|---|---|
(vertex-index) |
vertex | the index of the vertex being placed, an :int |
(instance-index) |
vertex | the index of the instance being drawn, an :int |
(invocation-id) |
tessellation control, geometry | which invocation this is, an :int from 0: the vertex it places of the patch it makes, or which of a geometry shader's :invocations |
(primitive-id) |
tessellation, geometry, fragment | the index of the primitive in the draw, an :int: of the patch, of the primitive taken, or of the primitive the fragment is of. A fragment shader that reads it needs the device feature of geometry shaders |
(tessellation-coord) |
tessellation evaluation | where the vertex is in the patch, a :vec3 |
(input-position index) |
tessellation, geometry | the position the stage before gave the vertex at index of the patch or primitive, a :vec4 |
(fragment-coord) |
fragment | where the fragment is, a :vec4: x and y in pixels from the target's top left, at the pixel's centre, z its depth, w 1 over the clip-space w |
(front-facing-p) |
fragment | true if the fragment's primitive faces the viewer |
(helper-invocation-p) |
fragment | true if the fragment is a helper of its quad's derivatives, which draws nowhere |
(point-coord) |
fragment | where the fragment is in its point, a :vec2 from 0 to 1 from the top left |
(sample-index) |
fragment | which sample of its pixel the fragment is, an :int; the shader runs once for each sample |
(sample-position) |
fragment | where the fragment's sample is in its pixel, a :vec2 from 0 to 1 |
(global-invocation-id) |
compute | which invocation of the whole dispatch this is, a :uvec3 |
(local-invocation-id) |
compute | which invocation of its workgroup this is, a :uvec3 |
(local-invocation-index) |
compute | the same, counted across the workgroup's dimensions, a :uint |
(workgroup-id) |
compute | which workgroup of the dispatch this is, a :uvec3 |
(workgroup-count) |
compute | how many workgroups the dispatch has across each dimension, a :uvec3 |
(workgroup-size) |
compute | the size of the shader's workgroups, a :uvec3 |
A function that uses one of these may be defined anywhere, and is refused in a shader of another stage, which calls it, when that shader is defined:
(define-function pixel-x ()
(x (fragment-coord)))
(define-shader wrong-stage :vertex ((position :vec2))
(vec4 position (pixel-x) 1))
In PIXEL-X: FRAGMENT-COORD belongs to fragment shaders, and PIXEL-X, which uses
it, is called by the vertex shader WRONG-STAGE.
in: (FRAGMENT-COORD)
Stage functions
| Function | Stage | Does |
|---|---|---|
(discard) |
fragment | makes the fragment draw nowhere, and gives no value, so (if test (discard) color) is a colour. Nothing it writes after counts, to a render target or a storage buffer; it runs on, as a helper of its neighbours' derivatives |
(set-tessellation-levels outer inner) |
tessellation control | sets the levels of the patch's edges, a :vec4, and inside it, a :vec2: :triangles use the first three of outer and the first of inner, :quads all of them, :isolines the first two of outer |
(emit-vertex position value ...) |
geometry | emits a vertex at position, a :vec4 in clip space, passing each value on |
(end-primitive) |
geometry | ends the strip of the vertices emitted since the last one |
(interpolate-at-centroid input) |
fragment | input, an input of the fragment shader or a slot or component of one, of floats, interpolated at the centroid of the part of the pixel the primitive covers |
(interpolate-at-sample input sample) |
fragment | input interpolated at the position of the :int sample of the pixel |
(interpolate-at-offset input offset) |
fragment | input interpolated offset, a :vec2 in pixels from -0.5 to 0.5, from the pixel's centre |
The derivatives, dfdx, dfdy and fwidth and their -fine and -coarse
kinds, are on Uniform Control Flow.
interpolate-at-centroid and its kin interpolate an input as the vertex
shader gave it, so they take the input itself, and an input that shader code
assigns is refused there; a shader function's parameter is not an input.
Stage interfaces
A vertex shader's inputs are at their locations, each taking one, but an
input of three or four doubles, or of float pairs, two. Its values after the
position go to the next stage at locations 0, 1 and so on, a matrix taking one
location for each column, three or four doubles two, and a vector of float
pairs one for each pair; so do the values of the other stages before the
fragment shader. A tessellation or geometry shader takes them as arrays of a
value for each vertex of its patch or primitive. A fragment shader's values go
to render targets 0, 1 and so on; a :complex to two channels, and doubles to
none, as no render target holds them.
define-program checks that each stage takes what the one before gives, in
number, order and type. With colored-vertex from above, which gives a
:vec3:
(define-shader misfit-fragment :fragment ((color :vec4))
color)
(define-program misfit :vertex colored-vertex :fragment misfit-fragment)
In MISFIT-FRAGMENT: The vertex shader COLORED-VERTEX passes :VEC3 to the
fragment shader MISFIT-FRAGMENT, which takes :VEC4: the values after the
position are the fragment shader's parameters, in number, order and type.
A struct passes whole, as a variable of the interface for each of its slots,
a nested struct's slots in turn, at consecutive locations, each interpolated
as its slot says, or by its type: integers, doubles and float pairs :flat,
floats :smooth, at the :center. A struct that holds a boolean or an array
does not pass between stages.
(define-struct surface
(normal :vec3)
(uv :vec2)
(material :uint)) ; :flat, as integers are
(define-shader surface-vertex :vertex ((position :vec3) (normal :vec3) (uv :vec2)
(material :uint))
(values (vec4 position 1) (make-surface :normal normal :uv uv :material material)))
(define-shader surface-fragment :fragment ((point surface))
(vec4 (* 0.5 (+ 1 (surface-normal point))) 1))
(define-program surfaces :vertex surface-vertex :fragment surface-fragment)
The fragment shader's input point is at locations 0 to 2: normal at 0,
uv at 1 and material at 2.
define-struct
(define-struct name [documentation]
(slot type [:interpolation i] [:sampling s])
...)
Defines the struct name, as defstruct defines one: (make-name :slot value ...) makes one, each slot's value given after a keyword of its name, in
any order, and evaluated in the order written; (copy-name struct) copies
one; and (name-slot struct) reads a slot, a place that setf assigns, as
(slot-value struct 'slot) and with-slots do. The constructor takes every
slot: a slot has no default, as shader code has no nil to leave in it.
(define-struct light
"A point light."
(position :vec3)
(color :vec3)
(radius :float))
(shader-documentation 'make-light)
;; => "(make-light :position value :color value :radius value): a LIGHT of the slots'
;; values, each given after a keyword of the slot's name, every slot given."
A slot's type is a scalar, vector or matrix, a struct, or an array of a
length. The last slot of a struct that only a storage buffer holds may be an
array whose length is known only as the shader runs. A struct does not hold
itself, through other structs or not. Its functions are named by the
struct's and the slots' names, in the package define-struct is evaluated in;
a name that is already a shader function, macro or another struct's function
is refused. :interpolation and :sampling say how a slot passes between
stages. Redefining a struct makes the programs that use it stale, as
redefining a function does.
define-macro
(define-macro name lambda-list
[documentation] [(declare ...)]
form ...)
Defines name as a shader macro, as defmacro defines a macro: shader code
that uses it, (name . arguments), stands for what the body, Lisp code,
returns, with the lambda list bound as defmacro binds it, &whole,
&optional, &rest, &body, &key and destructuring included. There is no
&environment: shader code is expanded as it is checked, which no Lisp
environment describes.
(define-macro square (x)
"X times itself, X evaluated once."
(let ((value (gensym "VALUE")))
`(let ((,value ,x)) (* ,value ,value))))
A macro is expanded each time the shader code that uses it is checked, so the
definitions that use it follow its redefinitions, which make their programs
stale. The names a macro makes with gensym never reach the SPIR-V: a program
compiles to the same words whatever they are.
define-type-alias
(define-type-alias name type [documentation])
Defines name, a symbol that is not a keyword, as another name of type,
wherever a type is written: in parameter lists, struct slots, uniforms, the,
type declarations and make-array's :element-type. A name is a struct or
a type alias, not both. Programs follow an alias's redefinitions.
(define-type-alias color :vec4 "A linear RGBA colour.")
(define-function grey ((c color))
"C's brightness, squared, as a grey of C's alpha."
(let ((b (square (brightness (swizzle c :xyz)))))
(vec4 b b b (w c))))
remove-definition
(remove-definition name) removes the shader, shader function, shader macro,
struct, type alias, uniform or program name, and returns true if there was
one. Removing a struct removes its functions; a uniform's special variable
stays. Programs that use what was removed fail to compile until it is defined
again. A struct's function is removed with its struct:
(remove-definition 'square) ; => T
(remove-definition 'square) ; => NIL
(remove-definition 'make-light)
MAKE-LIGHT is a function of the struct LIGHT, which goes with it: remove the
struct, LIGHT.
Documentation
shader-documentation returns the documentation of a definition, a struct's
function, an operator of the language, a type, shader-type, &uniform or
&shared:
(shader-documentation 'color)
;; => "A linear RGBA colour."