Table of Contents
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-shaderdefined 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, andfactor, 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-programnamed the program's one stage, andcompile-programmade its SPIR-V for the:vulkantarget.- Uniforms are in the
:materialgroup 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:numbersis at binding 1.program-bindingslists them. program-layout-entriesgave each bind group's layout as cl-gfx'screate-bind-group-layouttakes it, group 0 with no entries, andpack-uniformsthe 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
- Shader Code describes the language.
- Definitions and Stages covers each kind of shader.
- Programs and Running on the GPU show what a compiled program gives a GPU layer.