Compute
A compute pass runs compute pipelines on workgroups.
A complete example
This squares 100 floats on the GPU. The shader, square.comp:
#version 450
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) buffer Values { float values[]; };
void main() {
uint i = gl_GlobalInvocationID.x;
if (i < values.length()) values[i] = values[i] * values[i];
}
Compile it with glslangValidator -V square.comp -o square.comp.spv. Then:
(asdf:load-system :cl-gfx/vulkan)
(gfx:with-device (device)
(let* ((count 100)
(layout (gfx:create-bind-group-layout device '((0 :storage-buffer))))
(values (gfx:create-buffer device :size (* 4 count) :usage '(:storage :copy-src)))
(pipeline (gfx:create-compute-pipeline
device :module (gfx:create-shader-module device "square.comp.spv")
:bind-group-layouts (list layout)))
(group (gfx:create-bind-group device layout (list (list 0 values)))))
(let ((input (make-array count :element-type 'single-float)))
(dotimes (i count) (setf (aref input i) (float i)))
(gfx:write-buffer values input))
(gfx:with-frame (frame device)
(gfx:with-compute-pass (pass frame)
(gfx:set-pipeline pass pipeline)
(gfx:set-bind-group pass 0 group)
(gfx:dispatch pass (ceiling count 64))))
(gfx:read-buffer values :offset 12 :size 8 :element-type 'single-float)))
;; => #(9.0 16.0), elements 3 and 4, squared
Compute passes
(gfx:with-compute-pass (pass frame)
(gfx:set-pipeline pass pipeline)
(gfx:set-bind-group pass 0 group)
(gfx:dispatch pass x y z))
with-compute-pass ends the pass however its body is left, and allocates
nothing. begin-compute-pass and end-compute-pass are the functions under
it.
| Function | Does |
|---|---|
(set-pipeline pass compute-pipeline) |
Dispatch with this pipeline from here on. |
(set-bind-group pass index group) |
Use group as bind group index. |
(dispatch pass x &optional (y 1) (z 1)) |
Run the pipeline on x by y by z workgroups, each of the size the shader declares with local_size_x and so on. |
Round the workgroup count up, (ceiling count 64), and have the shader skip
invocations past the end, as square.comp does.
Ordering
Compute and render passes run in the order they are recorded, and each sees what the passes before it wrote. Within a compute pass, each dispatch sees what the dispatches before it wrote. The backend works out the barriers:
(gfx:with-frame (frame device)
;; Move particles in a storage buffer that is also a vertex buffer.
(gfx:with-compute-pass (pass frame)
(gfx:set-pipeline pass simulate)
(gfx:set-bind-group pass 0 particles-group)
(gfx:dispatch pass (ceiling particle-count 64)))
;; Then draw them.
(gfx:with-render-pass (pass frame :color ((texture :clear '(0 0 0 1))))
(gfx:set-pipeline pass draw-particles)
(gfx:set-vertex-buffer pass 0 particles)
(gfx:draw pass particle-count)))
Here particles is made with :usage '(:storage :vertex).
Storage textures
A compute shader writes images through a :storage-texture binding. The
texture needs :storage usage:
(let* ((picture (gfx:create-texture device :width 256 :height 256 :format :rgba8-unorm
:usage '(:storage :sampled)))
(layout (gfx:create-bind-group-layout
device '((0 :storage-texture :visibility (:compute)))))
(group (gfx:create-bind-group device layout (list (list 0 picture)))))
...)
layout(local_size_x = 8, local_size_y = 8) in;
layout(set = 0, binding = 0, rgba8) uniform writeonly image2D picture;
A later render pass can sample picture. One dispatch cannot both sample a
texture and store to it; see Bind Groups.
Reading results
read-buffer and read-texture wait for the GPU and return bytes, or the
:element-type you ask for, such as single-float. Call them
between frames. They suit tests, tools and offline work; for results that
feed the next frame, keep the data on the GPU and bind it.
Writing from vertex and fragment shaders
Storage buffers and textures are always writable from compute and fragment
shaders. Writing them from the stages before the fragment shader, such as
the vertex shader, needs the :vertex-stores device feature.