1 Getting Started
dxtr edited this page 2026-10-18 19:16:33 +02:00

Getting Started

Requirements

  • SBCL. clsl is plain Common Lisp, and writes SPIR-V itself: loading it needs no foreign library, no glslang and no C compiler, on Linux and Windows alike.
  • To run what it compiles: a GPU layer that takes SPIR-V, such as cl-gfx, and a Vulkan 1.4 driver. Any current GPU driver has Vulkan 1.4; Mesa's lavapipe is a software renderer that needs no GPU, and the examples in this wiki run on it.
  • For the tests: SPIRV-Tools, lavapipe and the Khronos validation layer. See Development and Testing.

On Debian or Ubuntu, the Vulkan loader and lavapipe are:

apt install libvulkan1 mesa-vulkan-drivers

The systems

System What it is
clsl The language, its definitions and programs, its conditions, and the IR they compile through.
clsl/spirv SPIR-V: the grammar's tables, a module builder, a reader and a disassembler; and the back end that translates the IR into SPIR-V.
clsl/vulkan The :vulkan target: SPIR-V 1.6 under Vulkan 1.4's rules. Programs load this one.
clsl/gmath cl-gmath's types and functions in shader code.
clsl/generator A build-time tool that writes clsl's SPIR-V tables from the grammar files.
clsl/test The test suite.

Systems says more about each, and about the packages.

Installing

clsl's dependencies are pinned by commit in its stiftfile, with stift. In a project of your own, require it, and cl-gfx to run shaders:

stift get https://fakehacker.cc/dxtr/clsl.git
stift get https://fakehacker.cc/dxtr/cl-gfx.git

and start Lisp with stift exec sbcl. Without stift, clone clsl and cl-threads where ASDF finds them, such as ~/common-lisp/, and cl-gmath too for clsl/gmath.

Loading

(asdf:load-system :clsl/vulkan)

loads the language, the SPIR-V back end and the :vulkan target. Load clsl/vulkan, not clsl alone: without a target, compile-program signals a no-target-error, whose message names the system to load:

The target :VULKAN is not loaded: load the system clsl/vulkan, which defines it.

Shader code is written in a package of your own that uses clsl beside common-lisp, so that both the language's names, such as vec3, dot and &uniform, and Lisp's, such as let, + and floor, read as they are written:

(defpackage #:my-shaders
  (:use #:cl #:clsl))

clsl exports the language's own names, among them short ones such as x, y, z, w, dot, norm, sample, fetch and program. None of them is Common Lisp's. A package that uses another library beside clsl chooses between the names both export with :shadowing-import-from: cl-gmath, for one, exports vec2 to vec4, dvec2 to dvec4, mat3, mat3x2, mat4, dmat3, dmat3x2, dmat4, clamp, smoothstep, radians and degrees too, and with clsl/gmath loaded either library's means the same in shader code (see Systems). A local nickname works as well, and shader code then writes the language's names with it, as in (c:vec4 color 1) and c:&uniform; a plain &uniform is refused, with a message that says so.

A first program

This multiplies 100 numbers on the GPU, through cl-gfx. Its package gives cl-gfx the local nickname gfx, and a local nickname needs its package to exist, so the package is defined after cl-gfx/vulkan is loaded:

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

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

(define-shader scale-numbers (:compute :workgroup-size (64))
    (&uniform (numbers (:array :float) :storage :read-write)
              (factor :float))
  "Multiply each of NUMBERS by FACTOR."
  (let ((i (x (global-invocation-id))))
    (when (< i (length numbers))
      (setf (aref numbers i) (* factor (aref numbers i))))))

(define-program scaling
  "Scale a buffer of numbers."
  :compute scale-numbers)

(program-bindings (compile-program 'scaling :target :vulkan))
;; => (#S(PROGRAM-BINDING :GROUP :MATERIAL :NUMBER 0 :KIND :UNIFORM-BLOCK :STAGES (:COMPUTE))
;;     #S(PROGRAM-BINDING :GROUP :MATERIAL :NUMBER 1 :KIND :STORAGE-BUFFER :STAGES (:COMPUTE)))

(gfx:with-device (device)
  (let* ((program (compile-program 'scaling :target :vulkan))
         (module (gfx:create-shader-module device (program-spirv program :compute)))
         (layouts (mapcar (lambda (entries) (gfx:create-bind-group-layout device entries))
                          (program-layout-entries program)))
         (pipeline (gfx:create-compute-pipeline device :module module
                                                       :bind-group-layouts layouts))
         (block (pack-uniforms program :material :values '(:factor 2.5)))
         (uniforms (gfx:create-buffer device :size (length block) :usage '(:uniform)))
         (numbers (gfx:create-buffer device :size (* 4 100) :usage '(:storage :copy-src)))
         (group (gfx:create-bind-group device (nth (uniform-group-index :material) layouts)
                                       (list (list 0 uniforms) (list 1 numbers)))))
    (gfx:destroy-shader-module module)
    (gfx:write-buffer uniforms block)
    (gfx:write-buffer numbers (let ((data (make-array 100 :element-type 'single-float)))
                                (dotimes (i 100 data)
                                  (setf (aref data i) (float i)))))
    (gfx:with-frame (frame device)
      (gfx:with-compute-pass (pass frame)
        (gfx:set-pipeline pass pipeline)
        (gfx:set-bind-group pass (uniform-group-index :material) group)
        (gfx:dispatch pass (ceiling 100 64))))
    (subseq (gfx:read-buffer numbers :element-type 'single-float) 0 5)))
;; => #(0.0 2.5 5.0 7.5 10.0)

What happened:

  • define-shader defined a compute shader, a function of no inputs that runs in workgroups of 64 invocations. Its uniforms come after &uniform: numbers, a storage buffer of floats whose length the buffer gives, and factor, a float. The definition was checked as it was evaluated: a mistake in it would have been refused there, with a message naming the form.
  • define-program named the program's one stage, and compile-program made its SPIR-V for the :vulkan target.
  • Uniforms are in the :material group by default, bind group 1. A group's scalars, vectors and matrices make its uniform block, at binding 0, and its storage buffers and textures follow: numbers is at binding 1. program-bindings lists them.
  • program-layout-entries gave each bind group's layout as cl-gfx's create-bind-group-layout takes it, group 0 with no entries, and pack-uniforms the bytes of the uniform block, laid out by std140.
  • (global-invocation-id) numbers the invocations of the whole dispatch. The dispatch is of whole workgroups, (ceiling 100 64), two of 64 invocations, so the shader skips the invocations past the buffer's end.

Running the tests

From clsl's own directory:

stift test

stift --no-workspace test runs them from the stiftfile's pins alone, as CI does. The suite holds clsl to SPIRV-Tools and runs shaders on a Vulkan 1.4 device under the Khronos validation layer; it needs, on Debian or Ubuntu:

apt install spirv-tools libvulkan1 mesa-vulkan-drivers vulkan-validationlayers

On Windows, the Vulkan SDK gives the tools and the layer. A missing tool, driver or layer fails the suite with a message that says what to install: the tests never skip. See Development and Testing.

Next