2 Programs
dxtr edited this page 2026-10-14 03:35:08 +02:00

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:

  1. checks every stage and every shader function they call, including functions that were forward references when defined;
  2. 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;
  3. lays out the uniforms of each group and numbers the bindings;
  4. translates each stage to Vulkan GLSL 4.50;
  5. 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.