Programs
A program is a set of stages compiled together: GLSL and SPIR-V for each stage, and the metadata a GPU layer needs to build a pipeline and bind its resources.
Compiling
(compile-program &key vertex tessellation-control tessellation-evaluation geometry fragment compute)
Each argument names a shader defined with define-shader for that stage.
(compile-program :vertex 'triangle-vertex :fragment 'triangle-fragment)
(compile-program :vertex 'v :tessellation-control 'tc :tessellation-evaluation 'te :fragment 'f)
(compile-program :vertex 'v :geometry 'g :fragment 'f)
(compile-program :fragment 'post-process)
(compile-program :compute 'blur)
Which stages go together:
- A graphics program has a vertex shader, a fragment shader, or both.
- Tessellation and geometry shaders need a vertex shader, and the two tessellation stages go together.
- A compute shader stands alone.
compile-program:
- checks every stage and every shader function they call, including functions that were forward references when defined;
- checks that what each stage passes on is what the next stage takes, in order and type, and that each built-in is used in a stage that has it;
- lays out the uniforms of each group and numbers the bindings;
- translates each stage to Vulkan GLSL 4.50;
- compiles each to SPIR-V for Vulkan 1.3 with glslang.
It signals shader-error for a mistake in the shader code, naming the definition and the form. GLSL that was compiled before reuses its SPIR-V from a cache, so compiling an unchanged program again costs only the check and the translation.
What a program tells you
| Function | Returns |
|---|---|
(program-stages program) |
The stages, such as (:vertex :fragment) or (:compute). |
(program-shader-name program stage) |
The name of the shader compiled for stage, or nil. |
(program-spirv program stage) |
The SPIR-V, a (simple-array (unsigned-byte 32) (*)). |
(program-glsl program stage) |
The GLSL it was compiled from, as a string, for reading. |
(program-vertex-inputs program) |
The vertex shader's inputs, as vertex-inputs, for the vertex buffer layout. |
(program-patch-size program) |
The vertices per patch the tessellation control shader takes, or nil. The pipeline draws patch lists of this size. |
(program-workgroup-size program) |
The compute shader's workgroup size, (x y z), or nil. |
(program-uniforms program) |
The uniform blocks' members, as uniforms. |
(program-uniform-block-size program group) |
The size in bytes of a buffer holding the group's block, or 0. |
(program-resources program &key kind) |
The textures, storage buffers and storage images, as resources; with kind, :texture, :storage-buffer or :image, only those. |
(program-bindings program) |
Every resource to bind, by set, as program-bindings. |
The queries return structures that never change, read with these readers:
| Structure | Readers |
|---|---|
vertex-input |
vertex-input-name, vertex-input-location, vertex-input-type |
uniform |
uniform-name, uniform-type, uniform-group, and uniform-offset, in bytes into the group's block |
resource |
resource-name, resource-type, resource-group, resource-binding, resource-kind (:texture, :storage-buffer or :image), resource-access (:read, :write or :read-write, and nil for a texture) and resource-format (an image's format, and nil for the others) |
program-binding |
program-binding-group, program-binding-set, program-binding-number, program-binding-kind and program-binding-stages |
(dolist (binding (program-bindings *program*))
(format t "~&set ~D, binding ~D: ~S~%" (program-binding-set binding)
(program-binding-number binding) (program-binding-kind binding)))
program-bindings is the one a GPU layer builds its bind group layouts from. For each group, the uniform block is at binding 0, of kind :uniform-block; textures, storage buffers and images follow from binding 1. A binding's kind is:
| Kind | Resource |
|---|---|
:uniform-block |
The group's uniform buffer. |
:storage-buffer |
A storage buffer some stage writes. |
:read-only-storage-buffer |
A storage buffer no stage writes. |
a texture type, such as :sampler2d |
A texture and its sampler. texture-type-p is true of it. |
an image type, such as :image2d |
A storage image. image-type-p is true of it. |
texture-type-shape tells a GPU layer what shape of view a texture or image binding takes. program-binding-stages lists the stages that use the binding, for its visibility.
Uniforms and Resources explains groups and layouts; pack-uniforms fills the uniform buffers.
Using a program with cl-gfx
cl-gfx takes the SPIR-V as it is:
(gfx:create-render-pipeline
device
:vertex (gfx:create-shader-module device (program-spirv *program* :vertex))
:fragment (gfx:create-shader-module device (program-spirv *program* :fragment))
:vertex-buffers '((:stride 24 :attributes ((0 :float32x3 0) (1 :float32x3 12))))
:color-formats (list (gfx:surface-format surface)))
The attribute locations come from program-vertex-inputs; the bind group layouts come from program-bindings; uniform buffers are program-uniform-block-size bytes and filled with pack-uniforms.
Following redefinitions
A program keeps the definitions it was compiled from, and never changes. Code that holds programs and draws with them, such as a frame loop, can follow redefinitions made at the REPL:
| Function | Does |
|---|---|
(definitions-generation) |
A number that grows whenever a shader, shader function, struct or shader variable is defined, redefined or removed. Allocates nothing. |
(program-stale-p program) |
True once something the program was compiled from has been redefined or removed. Allocates nothing. |
(recompile-program program) |
Compiles the same stages again from their current definitions and returns a new program. The old one does not change. |
A frame loop compares the generation with the one it saw last, and looks further only when it has changed:
(defvar *generation* (definitions-generation))
(defun refresh-program (program)
"Return PROGRAM, or a recompiled one if a definition it uses has changed."
(let ((now (definitions-generation)))
(if (= now *generation*)
program
(progn
(setf *generation* now)
(if (program-stale-p program)
(handler-case (recompile-program program)
(shader-error (condition)
(warn "Keeping the old program: ~A" condition)
program))
program)))))
recompile-program signals shader-error if the stages no longer fit together, for example when the vertex shader now passes values the fragment shader does not take.
A definition that fails its check is never stored, so a mistake at the REPL leaves programs, and what is drawn with them, as they were.
Threads
A program does not change once compiled, so it may be shared between threads. Programs may be compiled from any thread; compiling serialises its use of glslang. See Errors and Threads.