Table of Contents
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.
Compute Shaders
A compute shader runs many invocations in workgroups. It has no inputs and returns nothing: it reads uniforms and writes storage buffers and storage images, and its invocations tell themselves apart by their IDs.
(define-struct particle
(position :vec4)
(velocity :vec4))
(define-shader advance (:compute :workgroup-size (64))
(&uniform (particles (:array particle) :storage :read-write)
(counter (:array :uint 1) :storage :read-write)
(picture :image2d :format :rgba8-unorm :access :write)
(dt :float :group :frame))
(let ((i (x (global-invocation-id))))
(when (< i (uint (length particles)))
(let ((p (aref particles i)))
(incf (particle-position p) (* dt (particle-velocity p)))
(setf (aref particles i) p)
(atomic-incf (aref counter 0))
(image-store picture (ivec2 (int i) 0) (particle-position p))))))
(defvar *advance* (compile-program :compute 'advance))
(program-workgroup-size *advance*)
;; => (64 1 1)
(mapcar #'resource-name (program-resources *advance* :kind :storage-buffer))
;; => (PARTICLES COUNTER)
(program-resources *advance* :kind :image)
;; => (#S(RESOURCE :NAME PICTURE :TYPE :IMAGE2D :GROUP :MATERIAL :BINDING 3
;; :KIND :IMAGE :ACCESS :WRITE :FORMAT :RGBA8-UNORM))
(program-bindings *advance*)
;; => (#S(PROGRAM-BINDING :GROUP :FRAME :SET 0 :NUMBER 0 :KIND :UNIFORM-BLOCK :STAGES (:COMPUTE))
;; #S(PROGRAM-BINDING :GROUP :MATERIAL :SET 1 :NUMBER 1 :KIND :STORAGE-BUFFER :STAGES (:COMPUTE))
;; #S(PROGRAM-BINDING :GROUP :MATERIAL :SET 1 :NUMBER 2 :KIND :STORAGE-BUFFER :STAGES (:COMPUTE))
;; #S(PROGRAM-BINDING :GROUP :MATERIAL :SET 1 :NUMBER 3 :KIND :IMAGE2D :STAGES (:COMPUTE)))
A compute program has the compute stage alone: (compile-program :compute 'advance).
Workgroups
The stage designator gives the workgroup size, one to three counts, with missing ones defaulting to 1:
(:compute :workgroup-size (64)) ; 64 × 1 × 1
(:compute :workgroup-size (8 8)) ; 8 × 8 × 1
(:compute :workgroup-size (4 4 4))
A dispatch of X × Y × Z workgroups runs X times the first count invocations across, and so on. To cover N elements with workgroups of 64, dispatch (ceiling n 64) workgroups and check the bound in the shader, as advance does.
Which invocation this is
| Form | Type | What it is |
|---|---|---|
(global-invocation-id) |
:uvec3 |
Which invocation of the whole dispatch: (+ (* (workgroup-id) (workgroup-size)) (local-invocation-id)). |
(local-invocation-id) |
:uvec3 |
Which invocation of its workgroup. |
(local-invocation-index) |
:uint |
The same, counted in one number across the workgroup's three dimensions. |
(workgroup-id) |
:uvec3 |
Which workgroup of the dispatch. |
(workgroup-count) |
:uvec3 |
How many workgroups the dispatch has across each dimension. |
(workgroup-size) |
:uvec3 |
The shader's workgroup size. |
The IDs are unsigned. Compare them with :uints, as (< i (uint (length particles))) does, and convert with int where an :int is needed.
Shared variables
After &shared in a compute shader's parameter list come its shared variables, (name type). There is one of each per workgroup, which its invocations read and write together. They start undefined.
(define-shader tile-sum (:compute :workgroup-size (16 16))
(&uniform (data (:array :float) :storage :read-write)
&shared (tile (:array :float 256)))
(let ((i (local-invocation-index)))
(setf (aref tile i) (aref data (x (global-invocation-id))))
(memory-barrier-shared)
(barrier)
(when (= i 0)
(let ((sum 0.0))
(dotimes (k 256) (incf sum (aref tile k)))
(setf (aref data (* (x (workgroup-id)) 256)) sum)))))
A shared variable holds a value type; it cannot hold a texture or image. Only compute shaders have them.
Atomics
An atomic operation changes a place in a storage buffer or shared variable at once for every invocation, and gives the value the place held before. The place is an :int or :uint; a :uint wraps around.
| Form | Does |
|---|---|
(atomic-incf place [delta]) |
Adds delta, 1 by default, as sb-ext:atomic-incf does. |
(atomic-decf place [delta]) |
Subtracts delta, 1 by default. |
(atomic-min place value) |
Sets it to the lesser of it and value. |
(atomic-max place value) |
Sets it to the greater. |
(atomic-logand place value) |
Bitwise and. |
(atomic-logior place value) |
Bitwise inclusive or. |
(atomic-logxor place value) |
Bitwise exclusive or. |
(atomic-exchange place value) |
Sets it to value. |
(compare-and-swap place old new) |
Sets it to new if it holds old, as sb-ext:compare-and-swap does. |
(let ((slot (atomic-incf (aref counter 0)))) ; reserve an index
(setf (aref output slot) value))
Atomics work in any stage that has storage buffers, and on shared variables in compute shaders.
Barriers
| Form | Does | Stages |
|---|---|---|
(barrier) |
Waits until every invocation of the workgroup reaches it. | Compute |
(memory-barrier) |
Makes this invocation's writes to storage buffers, storage images and shared variables before it visible to other invocations before any after it. | Any |
(memory-barrier-buffer) |
The same, for storage buffers only. | Any |
(memory-barrier-image) |
The same, for storage images only. | Any |
(memory-barrier-shared) |
The same, for shared variables only. | Compute |
(group-memory-barrier) |
memory-barrier, seen only by the invocations of this workgroup. |
Compute |
To share values through a shared variable, write it, then (memory-barrier-shared) and (barrier), then read it, as tile-sum does.
Every invocation of a workgroup must reach the same barriers. A barrier inside a branch that some invocations skip hangs or misbehaves on the GPU.