6 Shaders and Pipelines
dxtr edited this page 2026-10-16 21:42:20 +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.

Shaders and Pipelines

Shader modules

Shaders are SPIR-V. Compile GLSL with glslangValidator -V or glslc, or write them in Lisp with clsl, which compiles them to SPIR-V itself.

(gfx:create-shader-module device #p"shaders/triangle.vert.spv")   ; a file
(gfx:create-shader-module device "shaders/triangle.frag.spv")     ; a namestring
(gfx:create-shader-module device words)   ; (simple-array (unsigned-byte 32) (*))
(gfx:create-shader-module device bytes)   ; (simple-array (unsigned-byte 8) (*))

The code must be a whole number of 32-bit words. A module may hold several entry points; pipelines pick one by name, "main" by default.

create-shader-module reads what the module declares, and signals unsupported-error if it needs a device feature the device lacks: the SPIR-V capabilities Geometry, Tessellation, Float64, and StorageImageMultisample and ImageMSArray, which need :geometry-shaders, :tessellation-shaders, :float64 and :multisample-storage-textures; and the execution modes RoundingModeRTE and SignedZeroInfNanPreserve of 32- or 64-bit floats, which need :rounding-mode-rte-float32 and its kin.

Once a pipeline is made, you may destroy its modules with destroy-shader-module; the pipeline is unaffected.

Writing shaders for cl-gfx

  • Clip space is Vulkan's: y points down and depth runs from 0 to 1. Write gl_Position as you would for Vulkan; the OpenGL backend will convert, not your shader. A tessellation evaluation shader's ccw and cw are Vulkan's too, in a domain whose origin is the top left.
  • Bind groups are descriptor sets. Bind group n of a pipeline is layout(set = n, ...), and its bindings are binding = ....
  • Vertex attributes are layout(location = n) in, matching the location in the vertex buffer layout.
  • Uniform buffers use std140; storage buffers std430.

Render pipelines

A render pipeline is everything about how to draw that is fixed: shaders, vertex layout, attachment formats, depth testing, culling and blending. It is immutable.

(gfx:create-render-pipeline
 device
 :vertex (gfx:create-shader-module device "mesh.vert.spv")
 :fragment (gfx:create-shader-module device "mesh.frag.spv")
 :vertex-buffers '((:stride 32 :attributes ((0 :float32x3 0)      ; position
                                            (1 :float32x3 12)     ; normal
                                            (2 :float32x2 24))))  ; uv
 :color-formats (list (gfx:surface-format surface))
 :depth-format :depth32-float
 :cull-mode :back
 :bind-group-layouts (list frame-layout material-layout))

Shader stages

Key Entry key Meaning
:vertex :vertex-entry Required.
:tessellation-control :tessellation-control-entry Optional; with :tessellation-evaluation, both or neither. Needs :tessellation-shaders.
:tessellation-evaluation :tessellation-evaluation-entry As above.
:geometry :geometry-entry Optional. Needs :geometry-shaders.
:fragment :fragment-entry Optional: leave it out for a pipeline that only writes depth, such as a shadow pass.

Every entry defaults to "main".

Vertex input

:vertex-buffers lists one layout per vertex buffer slot, by slot number from 0:

(:stride bytes :attributes ((location format offset) ...) :step :vertex)
  • :stride is the distance in bytes from one vertex to the next.
  • Each attribute is (location format offset): the shader's location, a vertex format, and its byte offset in the vertex.
  • :step is :vertex (the default) or :instance, for per-instance data.
;; Slot 0: per-vertex positions.  Slot 1: per-instance offsets and colors.
:vertex-buffers '((:stride 12 :attributes ((0 :float32x3 0)))
                  (:stride 16 :step :instance :attributes ((1 :float32x2 0)
                                                           (2 :unorm8x4 8))))

Vertex formats:

Format Shader type Bytes
:float32, :float32x2, :float32x3, :float32x4 float, vec2 … vec4 4, 8, 12, 16
:float64, :float64x2, :float64x3, :float64x4 double, dvec2 … dvec4; needs :float64 8, 16, 24, 32
:uint32 … :uint32x4 uint, uvec2 … uvec4 4 … 16
:sint32 … :sint32x4 int, ivec2 … ivec4 4 … 16
:unorm8x4 vec4, each byte mapped to 0 to 1 4

vertex-format-size returns the size in bytes. Not every GPU reads doubles from vertex buffers even when it has :float64. A portable way is to pass each double as a pair of :uint32s and combine them in the shader with packDouble2x32, as clsl does with a vertex shader's double inputs.

The pipeline doesn't have a vertex buffer layout if its vertex shader makes its own vertices, as a full-screen triangle does: leave :vertex-buffers out.

Primitive assembly

Key Default Values
:topology :triangle-list :triangle-list, :triangle-strip, :line-list, :line-strip, :point-list, and with tessellation, :patch-list
:patch-size nil With tessellation: the number of vertices per patch, 1 to 32, as the tessellation control shader takes them. Required with tessellation.
:cull-mode :none :none, :front or :back
:front-face :counter-clockwise :counter-clockwise or :clockwise, as the triangle's corners run on the screen
(gfx:create-render-pipeline device :vertex vertex
                                   :tessellation-control control
                                   :tessellation-evaluation evaluation
                                   :geometry geometry
                                   :fragment fragment
                                   :topology :patch-list :patch-size 3
                                   :color-formats '(:rgba8-unorm))

Attachments

Key Default Meaning
:color-formats () The formats of the color attachments the pipeline draws into, in order. For a window, (list (gfx:surface-format surface)).
:depth-format nil The format of the depth attachment, or nil for none.
:depth-compare :less The depth test, a compare function.
:depth-write t Whether passing fragments write their depth.
:samples 1 The number of samples, the same as the attachments'.
:blend :replace :replace, :alpha or :premultiplied-alpha (below).

The formats and sample count must match the attachments of the passes the pipeline draws in.

Blending

:blend Color Alpha Use for
:replace src src.a opaque geometry
:alpha src × src.a + dst × (1 − src.a) src.a + dst.a × (1 − src.a) colors of straight alpha, as most PNGs store them
:premultiplied-alpha src + dst × (1 − src.a) src.a + dst.a × (1 − src.a) colors already multiplied by their alpha

Both blends keep the target's alpha a coverage, so a target drawn with either can itself be blended over another with :premultiplied-alpha.

Prefer premultiplied alpha for textures that are filtered or mipmapped: a fully transparent texel of straight alpha still has a color, which bleeds into its neighbours as a dark or coloured fringe. A shader can premultiply straight colors itself, color.rgb *= color.a, and use :premultiplied-alpha.

A quarter-opaque red, (1 0 0 0.25), drawn over opaque blue gives (64 0 191 255) with :alpha and (255 0 191 255) with :premultiplied-alpha, in 8 bits.

Bind group layouts

:bind-group-layouts lists the layouts of the bind groups the shaders read, by index from 0, up to +max-bind-groups+ (4). Group n is the shaders' set = n. See Bind Groups.

Compute pipelines

(gfx:create-compute-pipeline device :module (gfx:create-shader-module device "blur.comp.spv")
                                    :entry "main"
                                    :bind-group-layouts (list layout))

A compute pipeline runs one compute shader. Its :bind-group-layouts work as a render pipeline's do. See Compute.

Destroying

destroy-render-pipeline and destroy-compute-pipeline mark the pipeline destroyed at once and release it once no frame in flight uses it.