Shader Code
Shader code is Common Lisp, where a GPU can give Lisp's meaning. It is written
in the package of the program that defines it, which uses clsl for the
language's own names, such as vec3, dot and &uniform; Common Lisp's
names, such as let, + and floor, mean what they mean in Common Lisp.
This page describes the language. Language Functions
lists its functions, and doc/language.org in clsl's repository is the full
reference, which describes every form and built-in.
Principles
- On scalars, every operator means what it means in Common Lisp. Arithmetic
and numeric functions extend component by component to vectors, and to
matrices where linear algebra says so. Predicates stay scalar: a
comparison gives one boolean, and vectors compare through
every,someandmap, as Lisp compares sequences. - Where a GPU cannot give Lisp's meaning, the form is refused when its definition is evaluated, with a message that says what to write instead. The exceptions, which static types force, are listed below under Where clsl differs from Common Lisp.
- Every operation has a defined result for every input, at the default safety: see Numbers and Precision.
- Types are written where values enter, in parameter lists, and inferred everywhere else.
- There is one way to do each thing. Names come from Common Lisp where the
meaning matches, and from the GPU's vocabulary, as
dot,mixandsmoothstep, where Lisp has none. - Mistakes are found when a definition is evaluated. The error names the definition and the form, and says what would fit.
Types
| Type | Values |
|---|---|
:bool |
t and nil |
:int, :uint |
integers of 32 bits, signed and unsigned, which wrap |
:float, :double |
IEEE floats of 32 and 64 bits, Lisp's single-float and double-float |
:vec2 to :vec4, :dvec2 to :dvec4, :ivec2 to :ivec4, :uvec2 to :uvec4, :bvec2 to :bvec4 |
vectors of 2 to 4 floats, doubles, integers, unsigned integers or booleans |
:mat2, :mat3, :mat4, :mat2x3 and the others, :dmat2 to :dmat4x3 |
matrices of floats, or doubles, stored by column: :mat2x3 has two columns of three |
:complex, :dcomplex |
complex numbers of floats or doubles, Lisp's (complex single-float) and (complex double-float) |
:dfloat, :dfvec2 to :dfvec4 |
float pairs, a float and the float nearest what it misses, about 14 significant digits at the speed of floats; vectors of them |
:sampler2d, :isampler3d, :usampler-cube-array, :sampler2d-ms, :sampler-buffer, :sampler2d-shadow and so on |
textures of one, two or three dimensions, cubes, arrays, multisampled, buffers and shadow textures, of floats or integers, with their samplers |
:image2d, :uimage3d and so on |
storage images of the same shapes, which shader code reads and writes a texel at a time |
| the name of a struct | a struct that define-struct defines |
(:array type length) |
an array of length values of type, a positive integer |
(:array type) |
an array whose length is known only as the shader runs, which a storage buffer holds |
define-type-alias names a type, and the alias may be written wherever the
type is. (shader-documentation 'shader-type) lists every type, and
(shader-documentation :vec3) describes one:
(shader-documentation :vec3)
;; => "A vector of 3 :FLOATs, which (vec3 ...) makes."
A struct and an array of a length are values, as a vector is. A texture is
no value: shader code passes it to sample, fetch, gather and the
queries, or to a shader function, as it is. Nor is a storage image, nor an
array whose length is known only as the shader runs: shader code reads and
writes them a part at a time.
Numbers written in the code
An integer written in the code is an :int, or a :uint above an :int's
range; a single-float is a :float, a double-float a :double, as pi is,
and t and nil are :bool values. A constant, such as
single-float-epsilon or one of defconstant, is its value.
A number written in the code fits any type that holds it exactly, where it
meets one, as Lisp's contagion would make it: (* x 2) of a :float x
multiplies by 2.0, (* x 1/2) by 0.5, and (+ u 1) of a :uint adds a
:uint. 16777217 fits no :float, which cannot hold it, and 1/3 fits
nothing:
(define-function third ((x :float))
(* x 1/3))
In THIRD: 1/3 is a ratio, and clsl has no ratios, whose integers would overflow
where Lisp's never do. A ratio fits a :FLOAT, a :DOUBLE or a :DFLOAT where it
is exactly one, and division of floats gives floats: write 0.33333334, or
(/ 1.0 3.0), or for doubles and float pairs 0.3333333333333333d0.
in: 1/3
Arithmetic of numbers written in the code is done exactly, as Lisp does it,
before the result fits: (/ 1 2) is 1/2, which fits a :float, and
(+ 1 4294967296) is refused, as 4294967297 does not fit in 32 bits. A
double-float fits doubles, and float pairs as the pair nearest it, but not a
:float: (* x 1d0) of a :float is refused, and (float pi 1.0) is the
:float nearest pi.
Numbers written in the code that meet where one value is wanted, as the
branches of an if, take the type Lisp's contagion gives them: (if c 1 2.5) is a :float, and (if c 1 2.5d0) a :double.
Conversions
Nothing else converts silently. (+ x i) of a :float and an :int is
refused:
(define-function mixed ((x :float) (i :int))
(+ x i))
In MIXED: + takes numbers of one type, :INT, :UINT, :FLOAT, :DOUBLE or :DFLOAT,
scalars or vectors, or a vector and a scalar of its components; matrices of one
type; or complex numbers, or a complex number and a real of its parts' kind,
not :FLOAT, :INT.
in: (+ X I)
and (+ x (float i)) adds them. The conversions:
| Form | Gives |
|---|---|
(float x) |
a float: an integer as a :float, a float as it is, a float pair as the :float nearest it |
(float x 1.0), (float x 1d0) |
the :float, or :double, nearest x: of a vector, a matrix or a complex number too |
(truncate x), (floor x), (ceiling x), (round x) |
a float as an integer, rounding as each says; floats become integers only so |
(int i), (uint i) |
the :int, or :uint, of an integer's bits: one that fits stays, one that does not wraps |
(dfloat x) |
a float pair of a float, an integer or a double |
A real meets a complex number of its kind as Lisp's contagion has it, as the
complex number of it and 0: (* 2.0 z).
Forms
| Form | Meaning |
|---|---|
(progn form ...) |
the forms in order; the last gives the value, or values |
(let ((variable value) ...) (declare ...) form ...), (let* ...) |
binds variables, each of its value's type, or of the type a type declaration gives it |
(locally (declare ...) form ...) |
the forms with the declarations in effect |
(if test then [else]) |
then if the :bool test is true; a value if both branches give one of one type |
(when test form ...), (unless test form ...) |
the forms if test is true, or false; no value |
(cond (test form ...) ... [(t form ...)]) |
the forms of the first true clause; a value if it ends with a t clause |
(case key (keys form ...) ... [(otherwise form ...)]) |
the clause whose keys, integers written in the code, hold the :int or :uint key |
(and boolean ...), (or boolean ...) |
:bool values, evaluated until one is false, or true |
(the type value) |
value, of type; a number written in the code becomes type if type holds it exactly |
(setq variable value ...), (setf place value ...) |
stores each value in its variable or place, and gives the last |
(incf place [delta]), (decf place [delta]) |
adds or subtracts delta, 1 by default; the place's parts are evaluated once |
(dotimes (variable count [result]) ...) |
the body count times, then result |
(loop form ...) |
the simple loop, until a return leaves |
(return [value]) |
leaves the innermost dotimes or loop |
(values value ...) and the forms of several values |
see Several values |
(flet ...), (labels ...), (funcall ...), (lambda ...) |
see Local functions |
(macrolet ...), (symbol-macrolet ...) |
see Macros |
(slot-value struct 'slot), (with-slots (slot ...) struct ...) |
a struct's slots, as places |
(make-array length :initial-element value [:element-type type]) |
an array of a length written in the code |
(every predicate vector ...), (some ...), (notany ...), (notevery ...) |
as Common Lisp's, of the vectors' components at each position |
(map type function vector ...) |
the vector of type of function of the components at each position |
(swizzle vector :components) |
the components the keyword names: (swizzle color :zyx) |
#'name names, in every, some, notany, notevery, map, funcall
and multiple-value-call, the function they apply: a local function, a
shader function, a struct's function or a function of the language. every
and its kin take a lambda form too: (every (lambda (c) (< 0 c 1)) color).
A comparison of vectors, (every #'< a b), compares them component by
component at once.
Places
A place is a variable; a component of a vector, as x, y, z, w and
swizzle read it, where swizzle names no component twice; an element, as
aref reads it, of a vector, a matrix or an array; a slot of a struct, as
its accessor and slot-value read it; a byte of an integer, as ldb and
mask-field read it; (the type place); a symbol macro, or a macro form,
whose expansion is a place; and a part of any of these, as (x (light-color (aref lights i))). Parameters and inputs are variables too. Uniforms and
textures are not places, and a part of a storage buffer is one only with
:storage :read-write. setf, incf, decf and (setf (values ...))
assign places, each place's parts evaluated once, from left to right.
Loops
(define-function first-set-bit ((bits :uint))
"The index of the lowest 1 bit of BITS, or -1 if there is none."
(dotimes (i 32 -1)
(when (logbitp i bits)
(return i))))
A loop gives values of one type: those its return forms give, and a
dotimes's result, which it gives when its count runs out. A number written
in the code takes the type of the other values, as -1 above is an :int.
dotimes counts with an :int, or a :uint, its count's type; a type
declaration at the start of its body declares its variable, (dotimes (i 10) (declare (type :uint i)) ...). Shader code does not assign the variable of
dotimes. loop is Common Lisp's simple loop, and a loop that no
return leaves never ends. return leaves the loop around it in the same
shader function: a return with no loop around it is refused.
Declarations
declare is written at the start of the body of let, let*, locally,
dotimes and the definitions:
(optimize (safety 0))leaves out the guards that give every operation a defined result, in its body. The default safety, 1, keeps them. The other qualities are accepted and change nothing.(type type variable ...)gives variables ofletandlet*their type, which their values fit:(let ((x 0)) (declare (type :float x)) ...).(derivative-uniformity severity),:error,:warningor:off, says what becomes of a derivative where control flow may not be uniform: see Uniform Control Flow.(ignore variable ...)and(ignorable variable ...)are accepted.
Several values
A form may give several values, as in Lisp: values makes them, and so do
truncate and its kin, decode-float, add-carry and its kin, and the
functions whose last form gives several. Where one value is wanted, the first
is used, and the others are evaluated and dropped.
(define-function split ((v :vec2))
"The whole parts of V, and what is left of each."
(multiple-value-bind (whole fraction) (floor v)
(values whole fraction)))
(define-function cell-colour ((v :vec2))
"V's whole parts and what is left of each, in one :VEC4."
(multiple-value-call #'vec4 (ffloor v)))
multiple-value-bind, nth-value, multiple-value-setq, (setf (values place ...) form), multiple-value-prog1 and multiple-value-call take them
as in Lisp, and (return (values ...)) gives several from a loop. A static
type has no nil for a value a form does not give, so a variable, a position
or a place beyond the values is refused, where Lisp would give nil. Only the
values that are used are computed: (nth-value 1 (floor x)) computes no
quotient.
Structs and arrays
A struct, which define-struct defines, is made by its constructor, copied
by its copier, and read and written a slot at a time; an array of a length is
made by (vector element ...) or (make-array length :initial-element value), read and written by aref, counted by length and copied by
copy-seq. An array has one dimension, and an array of arrays makes more:
(aref (aref grid row) column).
(define-struct light
"A point light."
(position :vec3)
(color :vec3)
(radius :float))
(define-function brightest ((lights (:array light 4)) (point :vec3))
"The index of the light of LIGHTS that shines brightest at POINT."
(let ((best 0)
(most 0.0))
(dotimes (i 4)
(with-slots (position color radius) (aref lights i)
(let ((shine (/ (norm color) (+ 1 (/ (distance position point) radius)))))
(when (> shine most)
(setf best i
most shine)))))
best))
Structs and arrays are values, as numbers and vectors are: binding one, assigning it, passing it and returning it copies it, so that a change to one copy is not seen through another. In Lisp they would be one object:
(define-function copies ()
"1.0: B is a copy of A."
(let* ((a (make-light :position (vec3 0) :color (vec3 1) :radius 1))
(b a))
(setf (light-radius b) 2.0)
(light-radius a)))
Code that copies before it changes, with copy-seq or a struct's copier,
means the same in both. An index computed as the shader runs reads zero out
of range, and a write there does nothing; an index written in the code is
checked when the definition is evaluated:
(define-function past-end ((v :vec3))
(aref v 3))
In PAST-END: :VEC3 has 3 components, from 0 to 2, and none at 3.
in: (AREF V 3)
Local functions
flet and labels define functions in shader code, as in Lisp. A local
function's parameters are typed, as a shader function's are, ((parameter type) ...), and its last form gives its value, or several. The functions of
flet do not see each other; those of labels do. A local function sees
the variables around it, and assigns them as Lisp does:
(define-function blend ((a :float) (b :float))
(let ((total 0.0))
(flet ((add ((weight :float) (value :float))
(incf total (* weight value))))
(add 0.25 a)
(add 0.75 b))
total))
A lambda form applied to arguments, ((lambda (x) (* x x)) a), binds its
parameters as let does, and one in funcall, map or every takes the
types of what it is given. A function is no value: funcall and its kin take
#'name or a lambda form written in the form. No shader code calls itself,
through other functions or not:
(define-function no-recursion ((n :int))
(if (> n 0) (no-recursion (- n 1)) 0))
In NO-RECURSION: NO-RECURSION calls itself, through other functions or not, and
shader code cannot recurse.
Macros
A shader macro, which define-macro defines, and a local macro of
macrolet, expand as Lisp's macros do, before the code is checked; their
bodies are Lisp code, run as the shader code is checked. symbol-macrolet
makes a symbol stand for a form, a place where it is assigned:
(define-function brighter ((color :vec3))
(macrolet ((twice (form) `(* 2 ,form)))
(symbol-macrolet ((red (x color)))
(setf red (twice red))))
color)
A local function or macro shadows a shader function or macro of its name, and a variable shadows a symbol macro. A name built into the language names no macro or local function.
Two shaders
A fragment shader that draws a tile map from a texture of integers, one tile a texel:
(define-shader tile-map :fragment ((uv :vec2) &uniform (tiles :usampler2d))
"Draw TILES, a texture of one tile a texel, stretched over UV: each tile
in the colour its low three bytes give, with dark lines a pixel wide
between the tiles."
(let* ((cell (* uv (float (texture-size tiles))))
(line (fwidth cell))
(tile (x (fetch tiles (truncate cell)))))
(if (some #'< (mod cell 1.0) line)
(vec4 0.1 0.1 0.1 1)
(let ((red (ldb (byte 8 16) tile))
(green (ldb (byte 8 8) tile))
(blue (ldb (byte 8 0) tile)))
(vec4 (/ (float (uvec3 red green blue)) 255) 1)))))
fwidth is computed before the if, where every fragment computes it: in a
branch that some fragments of a quad take and others do not, it is refused
(see Uniform Control Flow).
Complex numbers are numbers, as in Lisp:
(define-shader mandelbrot :fragment ((uv :vec2))
"Shade UV, from 0 to 1, by how soon the Mandelbrot set's iteration escapes
there, and black where it does not."
(let ((c (complex (- (* 3 (x uv)) 2) (- (* 2 (y uv)) 1)))
(z (complex 0.0)))
(dotimes (i 100 (vec4 0 0 0 1))
(setf z (+ (* z z) c))
(when (> (abs z) 2)
(return (vec4 (vec3 (/ (float i) 100)) 1))))))
Where clsl differs from Common Lisp
What static types and a GPU force:
- Integers are 32 bits, and wrap; a ratio is no value.
/divides floats only, andtruncateand its kin divide integers. - A float function keeps its type:
(sqrt -1.0)is a NaN, where Lisp's is a complex number, and(asin 2.0)too.(sqrt (complex -1.0))is the complex number. - Floats, doubles and integers do not convert into one another in arithmetic,
as Lisp's contagion would convert them:
floatconverts. A real does meet a complex number of its parts' kind as Lisp's contagion has it. - Complex numbers are of floats: a complex number of rationals is no value, though one written in the code fits a complex number exactly.
- GPUs compute trigonometry, exponentials and logarithms of
:floatvalues only: doubles are refused there, and a:dcomplexhas noexp,log,phaseor trigonometry. floatconverts vectors, matrices and complex numbers too, whichcl:floatdoes not take.- A variable keeps the type of its first value.
- A struct and an array are values: binding, assigning, passing and returning one copies it.
- A form has as many values as it gives: a variable, a position or a place
beyond them is refused, where Lisp would give
nil. A struct's constructor takes every slot, andmake-arrayan:initial-element, for the same reason. A struct's slots have no defaults, anddefine-structtakes no options. - An array has one dimension, and a length written in the code, or known only as a storage buffer's.
casetakes integers written in the code as its keys.- What Lisp signals an error for, such as a division by zero, or a negative byte size computed as the shader runs, has a defined result instead: a shader cannot signal.
loopis the simplelooponly, andreturnleaves the innermostdotimesorloop, never a function. Shader code does not assign the variable ofdotimes, and has nogoto the tags of its body.- A function, local or not, is no value. No function calls itself.
- A shader macro has no
&environment: shader code is expanded as it is checked, which no Lisp environment describes. - A byte,
(byte size position), is written in the forms ofldband its kin, and is no value of its own.
Asking clsl
shader-documentation describes any name in shader code: a definition, a
struct's function, an operator of the language, a type, shader-type,
&uniform or &shared.
(shader-documentation 'dot)
;; => "(dot a b): the dot product of the vectors A and B, a scalar of their
;; components."
(shader-documentation 'light-radius)
;; => "(light-radius light): the RADIUS slot of the LIGHT, of the type :FLOAT. A place, which SETF assigns."