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.