1 Running on the GPU
dxtr edited this page 2026-10-18 19:16:46 +02:00

Running on the GPU

clsl compiles; a GPU layer runs what it compiles. A program gives each stage's SPIR-V and describes its inputs and bindings in cl-gfx's terms, so that a pipeline is made of it in a few calls. The examples on this page run offscreen, on any Vulkan 1.4 device, Mesa's lavapipe among them.

cl-gfx needs The program gives
a shader module of each stage, create-shader-module (program-spirv program stage), entered at "main"
a render pipeline's :vertex-buffers (program-vertex-buffers program)
the bind group layouts, create-bind-group-layout, a pipeline's :bind-group-layouts (program-layout-entries program), by index from 0
the bind groups' entries the bindings program-bindings and program-resources list, by group and number
the bytes of each uniform buffer (pack-uniforms program group :values ...)
a compute dispatch's workgroup counts (program-workgroup-size program), which the counts divide the work by
a tessellation pipeline's :patch-size (program-patch-size program)
a device that has the features it needs (program-features program), a subset of (gfx:device-features device)

A group is the bind group uniform-group-index gives, :frame 0, :material 1 and :draw 2. Its uniform block is binding 0 and its textures, storage images and storage buffers follow, so set-bind-group takes the group's index, and a bind group's entries the program's binding numbers.

Computing

Getting Started multiplies a buffer of numbers, and Shared Memory and Atomics counts one into bins. Both make a compute pipeline of the program's one stage and its layouts, bind a group, and dispatch as many workgroups as the work needs, (ceiling count 64) for a workgroup size of 64.

Drawing

This draws the triangle of Home, with its tint, into a texture, and reads two pixels back:

(asdf:load-system :clsl/vulkan)
(asdf:load-system :cl-gfx/vulkan)

(defpackage #:drawing
  (:use #:cl #:clsl)
  (:local-nicknames (#:gfx #:cl-gfx)))
(in-package #:drawing)

(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)

(defun pixel (pixels width x y)
  "The red, green, blue and alpha of the pixel at X, Y of PIXELS, an RGBA8
image WIDTH pixels wide, as a list."
  (let ((start (* 4 (+ x (* width y)))))
    (coerce (subseq pixels start (+ start 4)) 'list)))

(gfx:with-device (device)
  (let* ((program (compile-program 'triangle :target :vulkan))
         (vertex (gfx:create-shader-module device (program-spirv program :vertex)))
         (fragment (gfx:create-shader-module device (program-spirv program :fragment)))
         (layouts (mapcar (lambda (entries) (gfx:create-bind-group-layout device entries))
                          (program-layout-entries program)))
         (pipeline (gfx:create-render-pipeline device
                                               :vertex vertex :fragment fragment
                                               :vertex-buffers (program-vertex-buffers program)
                                               :bind-group-layouts layouts
                                               :color-formats '(:rgba8-unorm)))
         (target (gfx:create-texture device :width 64 :height 64 :format :rgba8-unorm
                                            :usage '(:render-target :copy-src)))
         (vertices (gfx:create-buffer device :size (* 4 15) :usage '(:vertex)))
         (block (pack-uniforms program :material :values '(:tint #(1.0 0.5 0.5 1.0))))
         (uniforms (gfx:create-buffer device :size (length block) :usage '(:uniform)))
         (group (gfx:create-bind-group device (nth (uniform-group-index :material) layouts)
                                       (list (list 0 uniforms)))))
    (gfx:destroy-shader-module vertex)
    (gfx:destroy-shader-module fragment)
    ;; Each vertex's position, then its color, as PROGRAM-VERTEX-BUFFERS lays them out.
    (gfx:write-buffer vertices (make-array 15 :element-type 'single-float
                                              :initial-contents '( 0.0 -0.5  1.0 1.0 1.0
                                                                  -0.5  0.5  1.0 1.0 1.0
                                                                   0.5  0.5  1.0 1.0 1.0)))
    (gfx:write-buffer uniforms block)
    (gfx:with-frame (frame device)
      (gfx:with-render-pass (pass frame :color ((target :clear '(0 0 0 1))))
        (gfx:set-pipeline pass pipeline)
        (gfx:set-bind-group pass (uniform-group-index :material) group)
        (gfx:set-vertex-buffer pass 0 vertices)
        (gfx:draw pass 3)))
    (let ((pixels (gfx:read-texture target)))
      (list (pixel pixels 64 32 40) (pixel pixels 64 2 2)))))
;; => ((255 128 128 255) (0 0 0 255))

The white vertices times the tint give (255 128 128 255) inside the triangle, and the corner keeps the clear colour. The vertex buffer's layout, ((:stride 20 :attributes ((0 :float32x2 0) (1 :float32x3 8)) :step :vertex)), puts each vertex's position and color side by side; clip space is Vulkan's, y down, so the apex at y -0.5 is at the top.

Uniforms every frame

pack-uniforms given a :buffer writes into it and allocates nothing, so a frame loop packs each frame's values into one vector of bytes and writes it into the uniform buffer during the frame. cl-gfx copies a :device buffer's write into place before the frame's passes, without waiting, and allocates nothing in steady state. Each frame here draws in another tint:

(gfx:with-device (device)
  (let* ((program (compile-program 'triangle :target :vulkan))
         (vertex (gfx:create-shader-module device (program-spirv program :vertex)))
         (fragment (gfx:create-shader-module device (program-spirv program :fragment)))
         (layouts (mapcar (lambda (entries) (gfx:create-bind-group-layout device entries))
                          (program-layout-entries program)))
         (pipeline (gfx:create-render-pipeline device
                                               :vertex vertex :fragment fragment
                                               :vertex-buffers (program-vertex-buffers program)
                                               :bind-group-layouts layouts
                                               :color-formats '(:rgba8-unorm)))
         (target (gfx:create-texture device :width 64 :height 64 :format :rgba8-unorm
                                            :usage '(:render-target :copy-src)))
         (vertices (gfx:create-buffer device :size (* 4 15) :usage '(:vertex)))
         (block (make-array (program-uniform-block-size program :material)
                            :element-type '(unsigned-byte 8) :initial-element 0))
         (uniforms (gfx:create-buffer device :size (length block) :usage '(:uniform)))
         (group (gfx:create-bind-group device (nth (uniform-group-index :material) layouts)
                                       (list (list 0 uniforms))))
         (colours '()))
    (gfx:destroy-shader-module vertex)
    (gfx:destroy-shader-module fragment)
    (gfx:write-buffer vertices (make-array 15 :element-type 'single-float
                                              :initial-contents '( 0.0 -0.5  1.0 1.0 1.0
                                                                  -0.5  0.5  1.0 1.0 1.0
                                                                   0.5  0.5  1.0 1.0 1.0)))
    (dolist (tint '(#(1.0 0.0 0.0 1.0) #(0.0 1.0 0.0 1.0) #(0.0 0.0 1.0 1.0)))
      (gfx:with-frame (frame device)
        (pack-uniforms program :material :values (list :tint tint) :buffer block)
        (gfx:write-buffer uniforms block)
        (gfx:with-render-pass (pass frame :color ((target :clear '(0 0 0 1))))
          (gfx:set-pipeline pass pipeline)
          (gfx:set-bind-group pass (uniform-group-index :material) group)
          (gfx:set-vertex-buffer pass 0 vertices)
          (gfx:draw pass 3)))
      (push (pixel (gfx:read-texture target) 64 32 40) colours))
    (reverse colours)))
;; => ((255 0 0 255) (0 255 0 255) (0 0 255 255))

The values of define-uniform, such as a camera's, come from their variables as the packing thread sees them, so a frame loop binds or sets them and packs. An :upload buffer, which the CPU writes directly, is not double-buffered: a frame in flight may still read what is overwritten, so it needs a region of its own for each frame in flight; cl-gfx's manual describes it.

Textures

A texture uniform is a :texture binding of an image and a sampler. Its binding number is the program's, which program-resources gives:

(define-shader fullscreen :vertex ()
  "A triangle that covers the target, placed by the vertex index alone:
draw three vertices, with no vertex buffer."
  (let ((uv (vec2 (float (logand (ash (vertex-index) 1) 2))
                  (float (logand (vertex-index) 2)))))
    (values (vec4 (- (* uv 2) 1) 0 1) uv)))

(define-shader picture :fragment ((uv :vec2) &uniform (image :sampler2d))
  "IMAGE, stretched over the target."
  (sample image uv))

(define-program show-picture :vertex fullscreen :fragment picture)

(mapcar (lambda (resource)
          (list (resource-name resource) (resource-group resource) (resource-binding resource)))
        (program-resources (compile-program 'show-picture :target :vulkan)))
;; => ((IMAGE :MATERIAL 1))

(gfx:with-device (device)
  (let* ((program (compile-program 'show-picture :target :vulkan))
         (vertex (gfx:create-shader-module device (program-spirv program :vertex)))
         (fragment (gfx:create-shader-module device (program-spirv program :fragment)))
         (layouts (mapcar (lambda (entries) (gfx:create-bind-group-layout device entries))
                          (program-layout-entries program)))
         (pipeline (gfx:create-render-pipeline device
                                               :vertex vertex :fragment fragment
                                               :bind-group-layouts layouts
                                               :color-formats '(:rgba8-unorm)))
         (target (gfx:create-texture device :width 64 :height 64 :format :rgba8-unorm
                                            :usage '(:render-target :copy-src)))
         (image (gfx:create-texture device :width 2 :height 2 :format :rgba8-unorm
                                           :usage '(:sampled :copy-dst)))
         (group (gfx:create-bind-group device (nth (uniform-group-index :material) layouts)
                                       (list (list 1 image (gfx:create-sampler device
                                                                               :filter :nearest))))))
    (gfx:destroy-shader-module vertex)
    (gfx:destroy-shader-module fragment)
    ;; Red and green on top, blue and white below.
    (gfx:write-texture image (make-array 16 :element-type '(unsigned-byte 8)
                                            :initial-contents '(255 0 0 255  0 255 0 255
                                                                0 0 255 255  255 255 255 255)))
    (gfx:with-frame (frame device)
      (gfx:with-render-pass (pass frame :color ((target :clear '(0 0 0 1))))
        (gfx:set-pipeline pass pipeline)
        (gfx:set-bind-group pass (uniform-group-index :material) group)
        (gfx:draw pass 3)))
    (let ((pixels (gfx:read-texture target)))
      (list (pixel pixels 64 16 16) (pixel pixels 64 48 16)
            (pixel pixels 64 16 48) (pixel pixels 64 48 48)))))
;; => ((255 0 0 255) (0 255 0 255) (0 0 255 255) (255 255 255 255))

The vertex shader takes no inputs, so the pipeline has no vertex buffer and the draw none. Texture coordinates run from the top left, as clip space's y does, so the picture comes out as it is stored. texture-type-shape gives the dimension of the texture to make for a texture type, such as :2d-array for a :sampler2d-array, and whether it is multisampled or holds depths.

Storage

A storage buffer is a buffer with the :storage usage, bound as a :storage-buffer or :read-only-storage-buffer, as the program's layout entries say. A storage image is a texture with the :storage usage, of the format its uniform declares, resource-format, bound as a :storage-texture; an :image-buffer is a buffer bound as a :storage-texel-buffer, with that format.

Following redefinitions

A program compiled once is kept, and compiled again when a definition it uses changes, as Programs shows: the frame loop compares definitions-generation with what it saw last, and makes a new pipeline of the program recompile-program returns when program-stale-p says so. The stages may be compiled in another thread, as programs may be compiled in several at once, and the pipeline swapped in at a frame's start.