3 Compute
dxtr edited this page 2026-10-14 03:45:48 +02:00

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.