2 Programs
dxtr edited this page 2026-10-19 18:22:40 +02:00

Programs

A program is the stages that draw or compute together: a vertex shader and a fragment shader, with tessellation control and evaluation shaders, both or neither, and a geometry shader between them; or a compute shader alone. define-program names them, and compile-program compiles them for a target into a program, which says everything a GPU layer needs to run them.

define-program

(define-program name [documentation] stage shader ...)
(define-shader triangle-vertex :vertex ((position :vec2) (color :vec3))
  "Place each vertex at its POSITION, and pass its COLOR on."
  (values (vec4 position 0 1) color))

(define-shader triangle-fragment :fragment ((color :vec3) &uniform (tint :vec4))
  "Draw the COLOR the vertices pass, times TINT."
  (* (vec4 color 1) tint))

(define-program triangle
  "A triangle in its vertices' colours."
  :vertex triangle-vertex :fragment triangle-fragment)

Each stage is followed by the name of its shader: :vertex and :fragment, either or both, with :tessellation-control and :tessellation-evaluation, both or neither, and :geometry between them, the last three after a :vertex; or :compute alone. The stages are checked against each other when it is evaluated: each takes the values the one before gives, of the same types, a tessellation or geometry shader in arrays of a value for each vertex; a tessellation evaluation shader's :patch-size is the control shader's :vertices; a geometry shader after tessellation takes the primitives it makes; and two stages that read a uniform of one name declare it alike. They are checked again whenever a definition they use is redefined (see Redefinition).

compile-program

(compile-program 'triangle :target :vulkan)
;; => #<PROGRAM TRIANGLE :vertex TRIANGLE-VERTEX :fragment TRIANGLE-FRAGMENT for :vulkan {120555AD43}>
(compile-program '(:vertex triangle-vertex :fragment triangle-fragment) :target :vulkan)
;; => #<PROGRAM :vertex TRIANGLE-VERTEX :fragment TRIANGLE-FRAGMENT for :vulkan {120557AAE3}>

compiles a program, by its name or its stages, for a target, as the definitions are now: each stage is lowered to IR, which the target makes its code of, SPIR-V for :vulkan. The same definitions always give the same words.

The image keeps the programs it compiles. Until a definition a program was compiled from changes, as program-stale-p says, or its target is loaded again, the same program and target give back the program compiled before, at once and without warning again, so that every engine and renderer that draws with a program compiles it once:

(eq (compile-program 'triangle :target :vulkan)
    (compile-program 'triangle :target :vulkan))
;; => T

A program that no longer compiles is never given back: compiling it signals why. (forget-compiled-programs) forgets the programs kept, so that the next calls compile afresh, as after a change to clsl's own compiler, which program-stale-p does not see.

:target is required; without one, compile-program signals a clsl-error:

COMPILE-PROGRAM needs a :TARGET, such as :VULKAN, the API the program's code is
for.

and a target no system loaded has defined is a no-target-error, which names the system to load (see Targets). compile-program signals a shader-error if the stages or the code they use do not fit, an undefined-shader-function for a call of a function no one defined, an unsupported-error if the target cannot express what the program asks, and a uniformity-warning for a derivative a declaration lets run where control flow may not be uniform. Programs may be compiled in several threads at once.

What a program tells

A compiled program never changes, and may be shared between threads. Its readers:

Reader Gives
program-target the target's name, as compile-program took it
program-stages its stages, in the order they run
program-shader-name stage the name of its shader of a stage, or nil
program-spirv stage a stage's SPIR-V, a vector of 32-bit words entered at "main", which cl-gfx's create-shader-module takes as it is. It is the program's own: read it, and copy it to change it
program-ir stage the IR the stage became, in its external form
disassemble-program stage prints the stage's code as its target prints it: SPIR-V assembly
program-workgroup-size a compute shader's workgroup size, three counts, or nil
program-patch-size how many vertices each patch has, as a pipeline draws them, or nil without tessellation
program-features the optional device features it needs, named as cl-gfx's device-features names them
program-vertex-inputs the vertex shader's inputs, as vertex-inputs
program-vertex-buffers &key step the layout of one vertex buffer that feeds every input, as cl-gfx's create-render-pipeline takes :vertex-buffers
program-uniforms the members of its uniform blocks, as uniforms, :frame's first
program-uniform-block-size group the size of a group's uniform block in bytes, or 0
program-resources &key kind its textures, storage buffers and storage images, as resources
program-bindings every binding of every group, as program-bindings
program-layout-entries for each bind group, by index from 0, the entries of its layout, as cl-gfx's create-bind-group-layout takes them

For triangle:

(let ((program (compile-program 'triangle :target :vulkan)))
  (values (program-target program)
          (program-stages program)
          (program-shader-name program :fragment)
          (program-shader-name program :geometry)
          (length (program-spirv program :vertex))
          (program-vertex-buffers program)
          (program-vertex-buffers program :step :instance)
          (program-uniform-block-size program :material)
          (program-layout-entries program)
          (program-features program)
          (program-workgroup-size program)
          (program-patch-size program)))
;; => :VULKAN
;;    (:VERTEX :FRAGMENT)
;;    TRIANGLE-FRAGMENT
;;    NIL
;;    156
;;    ((:STRIDE 20 :ATTRIBUTES ((0 :FLOAT32X2 0) (1 :FLOAT32X3 8)) :STEP :VERTEX))
;;    ((:STRIDE 20 :ATTRIBUTES ((0 :FLOAT32X2 0) (1 :FLOAT32X3 8)) :STEP :INSTANCE))
;;    16
;;    (NIL ((0 :UNIFORM-BUFFER :VISIBILITY (:FRAGMENT))))
;;    NIL
;;    NIL
;;    NIL

Asking for a stage the program does not have is a clsl-error:

(program-spirv (compile-program '(:fragment triangle-fragment) :target :vulkan) :vertex)
#<PROGRAM :fragment TRIANGLE-FRAGMENT for :vulkan {120230F783}> has no vertex
shader: its stages are :fragment.

Device features

program-features lists the optional features of a device the program needs, so that it runs where (subsetp (program-features program) (gfx:device-features device)):

Feature Needed for
:geometry-shaders a geometry shader, or a fragment shader that reads primitive-id
:tessellation-shaders tessellation shaders
:float64 doubles
:multisample-storage-textures a multisampled storage image
:vertex-stores a storage buffer or image that a stage before the fragment shader may write
:rounding-mode-rte-float32 float pairs, which need floats rounded to the nearest
:signed-zero-inf-nan-preserve-float32, :signed-zero-inf-nan-preserve-float64 the defined results of NaNs and infinities, of floats or doubles

The last three are properties of a device's float controls, which nearly every Vulkan 1.4 device has. Numbers and Precision says when a program needs them.

Vertex inputs and buffers

Each vertex-input has its vertex-input-name, vertex-input-location, vertex-input-type, and vertex-input-formats, the formats of the vertex attributes it is read from, one for each location, named as cl-gfx's vertex formats are: (:float32x3) for a :vec3, (:sint32) for an :int, (:float32x2) for a :complex, (:uint32x4 :uint32x2) for a :dvec3, whose doubles are read as their words, and (:float32x3 :float32x3) for a :dfvec3, whose pairs are read as their high parts, then their low parts.

program-vertex-buffers lays every input out side by side in one buffer, at slot 0, in the order of their locations, the buffer advancing a vertex at a time, or with :step :instance an instance at a time. A vertex shader that takes no inputs needs no buffer, and gives nil.

Uniforms, resources and bindings

Each uniform has its uniform-name, uniform-type, uniform-group, uniform-offset in its block, laid out by std140, and its uniform-documentation and uniform-metadata from define-uniform. Each resource has its resource-name, resource-type, resource-group, resource-binding, resource-kind, :texture, :image or :storage-buffer, resource-access, and a storage image's resource-format; program-resources takes :kind to list one kind. Each program-binding has its program-binding-group, program-binding-number, program-binding-kind, :uniform-block, :storage-buffer, :read-only-storage-buffer, or the type of a texture or storage image, such as :sampler2d or :image2d, and program-binding-stages. Uniforms and Resources shows them, and pack-uniforms, which writes a group's uniform block.

Looking inside

program-ir returns the IR a stage became, in its external form, which The IR describes: what lowering made of the Lisp, before the target made its code. Labels the IR makes are interned in *package*.

(program-ir (compile-program 'triangle :target :vulkan) :fragment)
;; => (:MODULE :FRAGMENT (:STRUCT MATERIAL-BLOCK (TINT :VEC4))
;;     (:GLOBAL COLOR :INPUT :VEC3 :LOCATION 0) (:GLOBAL OUTPUT-0 :OUTPUT :VEC4 :LOCATION 0)
;;     (:GLOBAL MATERIAL-UNIFORMS :UNIFORM MATERIAL-BLOCK :GROUP 1 :BINDING 0)
;;     (:FUNCTION MAIN NIL :VOID (%1 (:LOAD COLOR) :NAME COLOR)
;;      (%2 (:CONSTRUCT :VEC4 %1 (:FLOAT 1.0))) (%3 (:ACCESS MATERIAL-UNIFORMS (:INT 0)))
;;      (%4 (:LOAD %3)) (%5 (:MULTIPLY %2 %4)) (:STORE OUTPUT-0 %5) (:RETURN))
;;     (:ENTRY-POINT MAIN))

disassemble-program prints a stage's code as its target prints it, SPIR-V assembly for :vulkan, which spirv-as reads, to *standard-output*, a stream given as :stream, or with :stream nil into a string, as cl:disassemble prints a function:

(disassemble-program (compile-program 'triangle :target :vulkan) :fragment)
; SPIR-V
; Version: 1.6
; Generator: 0; 0
; Bound: 24
; Schema: 0
               OpCapability Shader
               OpMemoryModel Logical GLSL450
               OpEntryPoint Fragment %main "main" %color %output_0 %material_uniforms
               OpExecutionMode %main OriginUpperLeft
               OpName %color "color"
               OpName %output_0 "output-0"
               OpName %material_block "material-block"
               OpMemberName %material_block 0 "tint"
               OpName %material_uniforms "material-uniforms"
               OpName %main "main"
               OpName %color_0 "color"
               OpDecorate %color Location 0
               OpDecorate %output_0 Location 0
               OpMemberDecorate %material_block 0 Offset 0
               OpDecorate %material_block Block
               OpDecorate %material_uniforms DescriptorSet 1
               OpDecorate %material_uniforms Binding 0
      %float = OpTypeFloat 32
    %v3float = OpTypeVector %float 3
%_ptr_Input_v3float = OpTypePointer Input %v3float
      %color = OpVariable %_ptr_Input_v3float Input
    %v4float = OpTypeVector %float 4
%_ptr_Output_v4float = OpTypePointer Output %v4float
   %output_0 = OpVariable %_ptr_Output_v4float Output
%material_block = OpTypeStruct %v4float
%_ptr_Uniform_material_block = OpTypePointer Uniform %material_block
%material_uniforms = OpVariable %_ptr_Uniform_material_block Uniform
       %void = OpTypeVoid
         %13 = OpTypeFunction %void
    %float_1 = OpConstant %float 1
        %int = OpTypeInt 32 1
      %int_0 = OpConstant %int 0
%_ptr_Uniform_v4float = OpTypePointer Uniform %v4float
       %main = OpFunction %void None %13
         %14 = OpLabel
    %color_0 = OpLoad %v3float %color
         %17 = OpCompositeConstruct %v4float %color_0 %float_1
         %20 = OpAccessChain %_ptr_Uniform_v4float %material_uniforms %int_0
         %22 = OpLoad %v4float %20
         %23 = OpFMul %v4float %17 %22
               OpStore %output_0 %23
               OpReturn
               OpFunctionEnd

Redefinition

Programs follow redefinitions, as at the REPL. Every definition changes definitions-generation, which costs nothing to read. program-stale-p says whether a definition a compiled program was compiled from, a shader, shader function, shader macro, struct, type alias, uniform or the program itself, has been redefined or removed since, and recompile-program compiles it again, for its target, as the definitions are now; the program it was given does not change.

(let ((program (compile-program 'triangle :target :vulkan))
      (generation (definitions-generation)))
  (define-shader triangle-fragment :fragment ((color :vec3) &uniform (tint :vec4))
    "Draw the COLOR the vertices pass, times TINT, twice as bright."
    (* 2 (vec4 color 1) tint))
  (values (> (definitions-generation) generation)
          (program-stale-p program)
          (program-stale-p (recompile-program program))))
;; => T
;;    T
;;    NIL

A frame loop that keeps programs compares definitions-generation with the number it saw last, and asks program-stale-p only when it has changed:

(when (/= (definitions-generation) generation)
  (setf generation (definitions-generation))
  (when (program-stale-p program)
    (setf program (recompile-program program))))

A redefinition that breaks a program define-program defined is made, so that a change to several of a program's stages can be made one stage at a time, as reloading a file makes it, and signals a program-warning, which names the program and holds the error compiling it would signal:

(define-shader triangle-vertex :vertex ((position :vec2) (color :vec4))
  (values (vec4 position 0 1) color))
WARNING:
   The program TRIANGLE no longer compiles: In TRIANGLE-FRAGMENT: The vertex
   shader TRIANGLE-VERTEX passes :VEC4 to the fragment shader
   TRIANGLE-FRAGMENT, which takes :VEC3: the values after the position are the
   fragment shader's parameters, in number, order and type.

program-warning-program is triangle, and program-warning-error the shader-error. compile-program signals that error until the stages fit again, as they do once the fragment shader takes a :vec4.