4 Stages
dxtr edited this page 2026-10-14 19:51:48 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Stages

A shader stage is defined with define-shader:

(define-shader name stage-designator (parameter ... &uniform uniform ... &shared shared ...)
  [docstring]
  body ...)

Stages are functions from their inputs to their outputs. A stage's parameters are its inputs, and the values its body returns are its outputs. cl-gsl checks that each stage's outputs match the next stage's inputs when a program is compiled.

&uniform and &shared are cl-gsl's exported symbols. In a package that uses cl-gsl, as the examples here do, they need no prefix; elsewhere write them through a nickname, as gsl:&uniform. Uniforms are covered in Uniforms and Resources, and shared variables in Compute Shaders.

Stage designator Stage
:vertex Vertex shader
(:tessellation-control :patch-size n :vertices n) Tessellation control shader
(:tessellation-evaluation :patch-size n [:domain d :spacing s :winding w :point-mode b]) Tessellation evaluation shader
(:geometry :input primitive :output primitive :max-vertices n [:invocations n]) Geometry shader
:fragment Fragment shader
(:compute :workgroup-size (x [y [z]])) Compute shader

A graphics pipeline runs: vertex → [tessellation control → tessellation evaluation] → [geometry] → fragment.

Vertex shaders

The parameters are vertex inputs, (name type &key location). Each is a scalar or vector of ints, uints, floats or doubles. Locations count from 0 in order, unless given with :location; an input without one follows the input before it. A :dvec3 or :dvec4 takes two locations, every other input one.

(define-shader example-vertex :vertex ((p :vec3 :location 7) (q :vec2) (d :dvec4))
  (values (vec4 p 1) q))

(mapcar (lambda (input) (list (vertex-input-name input) (vertex-input-location input)
                              (vertex-input-type input)))
        (program-vertex-inputs (compile-program :vertex 'example-vertex)))
;; => ((P 7 :VEC3) (Q 8 :VEC2) (D 9 :DVEC4))

The vertex shader returns the clip-space position, a :vec4, and then any values for the next stage, with values.

(define-shader sprite-vertex :vertex ((position :vec2) (uv :vec2)
                                      &uniform (view :mat4 :group :frame))
  (values (* view (vec4 position 0 1)) uv))

Clip space is Vulkan's, as in cl-gfx: y points down, so the top of the target is at y −1, and depth runs from 0 to 1.

(program-vertex-inputs program) gives the inputs as vertex-inputs, with a vertex-input-name, vertex-input-location and vertex-input-type, for building a vertex buffer layout.

Built-ins: (vertex-index) and (instance-index), both :ints.

Fragment shaders

The parameters receive the previous stage's values, in the same order and with the same types: (name type &key interpolation).

Interpolation Meaning
:smooth Perspective-correct interpolation. The default for floats.
:noperspective Linear interpolation in screen space.
:flat The value of one vertex of the primitive. Integer and double inputs are always flat.

The fragment shader returns one colour per render target, with values if there are several. Colours are scalars or vectors of floats, ints or uints; not doubles. (discard) drops the fragment instead.

(define-shader sprite-fragment :fragment ((uv :vec2)
                                          &uniform (tint :vec4)
                                                   (image :sampler2d :group :draw))
  (* (sample image uv) tint))

Built-ins:

Form Type What it is
(fragment-coord) :vec4 Window position: X and Y in pixels from the top left at the pixel centre, Z the depth, W 1/w.
(front-facing-p) :bool Whether the primitive faces the front.
(point-coord) :vec2 Where the fragment is in its point, 0 to 1.
(sample-index) :int The sample being shaded. Using it shades every sample apart.
(sample-position) :vec2 Where that sample is in its pixel. Using it shades every sample apart.
(helper-invocation-p) :bool True if this fragment only runs to compute its neighbours' derivatives.
(primitive-id) :int The primitive's index in the draw. Needs the device's geometry shader feature, or a geometry shader before it.

A fragment shader may also take derivatives (dfdx, dfdy, fwidth and their -fine and -coarse kinds) and interpolate an input elsewhere in the pixel (interpolate-at-centroid, interpolate-at-sample, interpolate-at-offset). See Operator Reference.

Tessellation shaders

Tessellation takes both stages: a control shader and an evaluation shader. A program with one but not the other is an error.

A tessellation or geometry stage receives the values of every vertex of its patch or primitive: each of its parameters is declared with the type of one vertex's value, and in the body it is an array with one element per vertex. (input-position i) gives the position the previous stage gave vertex i.

Tessellation control

(define-shader patch-control (:tessellation-control :patch-size 3 :vertices 3)
    ((color :vec3) &uniform (level :float))
  (set-tessellation-levels (vec4 level level level 1) (vec2 level 0))
  (values (input-position (invocation-id)) (aref color (invocation-id))))
Option Meaning
:patch-size How many vertices each incoming patch has, 1 to 32. The pipeline draws patch lists of this size; program-patch-size returns it.
:vertices How many vertices each outgoing patch has, 1 to 32.

The shader runs once per outgoing vertex; (invocation-id) says which. It returns that vertex's position, a :vec4, and its values, as a vertex shader does.

(set-tessellation-levels outer inner) sets how finely the patch is divided: outer, a :vec4, along each edge, and inner, a :vec2, inside. A triangle uses the first three of outer and the first of inner; a quad uses all of them; isolines use the first two of outer, the number of lines and then the segments of each. Any invocation may set them; set them the same in every one.

Tessellation evaluation

(define-shader patch-evaluation (:tessellation-evaluation :patch-size 3 :spacing :fractional-odd)
    ((color :vec3))
  (let ((weights (tessellation-coord)))
    (values (+ (* (x weights) (input-position 0))
               (* (y weights) (input-position 1))
               (* (z weights) (input-position 2)))
            (+ (* (x weights) (aref color 0))
               (* (y weights) (aref color 1))
               (* (z weights) (aref color 2))))))
Option Values Default
:patch-size The control shader's :vertices, 1 to 32. cl-gsl checks that they agree. required
:domain :triangles, :quads, :isolines :triangles
:spacing :equal, :fractional-even, :fractional-odd :equal
:winding :counter-clockwise, :clockwise :counter-clockwise
:point-mode true to make points instead of triangles or lines nil

The shader runs once per vertex the tessellator makes. (tessellation-coord), a :vec3, says where in the patch: barycentric coordinates in the :triangles domain, and X and Y from 0 to 1 in the :quads and :isolines domains. It returns the vertex's position and values.

Built-ins in both tessellation stages: (input-position i), (primitive-id); in the control stage, (invocation-id).

Geometry shaders

A geometry shader receives one primitive's vertices and emits any number of vertices, which make strips of its output primitive.

(define-shader explode (:geometry :input :triangles :output :triangle-strip :max-vertices 3)
    ((color :vec3) &uniform (offset :float))
  (dotimes (corner 3)
    (emit-vertex (+ (input-position corner) (vec4 0 0 offset 0))
                 (aref color corner)))
  (end-primitive))
Option Values
:input :points (1 vertex), :lines (2), :lines-adjacency (4), :triangles (3), :triangles-adjacency (6). Each parameter is an array of this many values.
:output :points, :line-strip, :triangle-strip
:max-vertices The most vertices one invocation emits.
:invocations How many times the shader runs per primitive, 1 to 32. Defaults to 1. (invocation-id) says which run this is.
  • (emit-vertex position value ...) emits a vertex at position, a clip-space :vec4, passing the values on to the fragment shader. Every emit-vertex in a shader passes values of the same types, and a geometry shader must emit at least one vertex.
  • (end-primitive) ends the current strip, so the next emit-vertex starts another.
  • The geometry shader returns nothing; its outputs are what it emits.

If a tessellation evaluation shader comes before it, its :input must match what tessellation makes: :points with :point-mode, :lines for isolines, :triangles otherwise. cl-gsl checks this.

Built-ins: (input-position i), (invocation-id), (primitive-id).

Compute shaders

(define-shader double-all (:compute :workgroup-size (64))
    (&uniform (data (:array :float) :storage :read-write))
  (let ((i (x (global-invocation-id))))
    (when (< i (uint (length data)))
      (setf (aref data i) (* 2 (aref data i))))))

A compute shader has no inputs and returns nothing. It runs in workgroups of the size its designator gives, with missing counts defaulting to 1. It reads and writes storage buffers and images, and may share &shared variables across a workgroup. A compute program has the compute stage alone. See Compute Shaders.

Which built-ins each stage may use

A built-in that belongs to some stages only, such as (fragment-coord), emit-vertex or barrier, may still be used in a shader function. cl-gsl checks it when a program is compiled, against the stages that call the function, and the error names both:

(define-function fc () (fragment-coord))
(define-shader uses-fc :vertex () (fc))
(compile-program :vertex 'uses-fc)
;; error: In FC: FRAGMENT-COORD is available only in fragment shaders,
;;        and this is used by the vertex shader USES-FC.