2 Bind Groups
dxtr edited this page 2026-10-11 16:26:10 +02:00

Bind Groups

Shaders read uniforms, textures and storage through bind groups, as in WebGPU. There are three steps:

  1. A bind group layout describes what each binding holds and which stages see it.
  2. A pipeline is made with the layouts of the groups it reads, by index.
  3. A bind group fills a layout with real resources, and set-bind-group makes a pass's draws or dispatches use it.
(let* ((layout (gfx:create-bind-group-layout device '((0 :uniform-buffer)
                                                      (1 :texture :visibility (:fragment)))))
       (pipeline (gfx:create-render-pipeline device ... :bind-group-layouts (list layout)))
       (uniforms (gfx:create-buffer device :size 64 :usage '(:uniform)))
       (image (gfx:create-texture device :width 2 :height 2 :format :rgba8-srgb
                                         :usage '(:sampled :copy-dst)))
       (group (gfx:create-bind-group device layout
                                     (list (list 0 uniforms)
                                           (list 1 image (gfx:create-sampler device))))))
  (gfx:write-texture image texels)
  ;; In a pass:
  (gfx:set-pipeline pass pipeline)
  (gfx:set-bind-group pass 0 group)
  (gfx:draw pass 6))

The matching GLSL:

layout(set = 0, binding = 0, std140) uniform Uniforms { mat4 transform; };
layout(set = 0, binding = 1) uniform sampler2D image;

Layouts

(gfx:create-bind-group-layout device entries)

Each entry is (binding kind &key visibility):

  • binding: the binding number in the shaders.
  • kind: what it holds (below).
  • :visibility: the shader stages that use it, by default every stage the device runs. The stages are :vertex, :tessellation-control, :tessellation-evaluation, :geometry, :fragment and :compute; the tessellation and geometry stages count only on a device with :tessellation-shaders or :geometry-shaders. Naming fewer stages documents the shader's use of the binding, and may let a driver do less.

entries may be empty, for a group index a pipeline leaves unused. A binding number may appear only once.

Binding kinds

Kind Shaders see Bind group entry Resource needs
:uniform-buffer a uniform block (binding buffer &key offset size) buffer usage :uniform
:storage-buffer a buffer block they read and write (binding buffer &key offset size) buffer usage :storage
:read-only-storage-buffer a buffer block they only read (binding buffer &key offset size) buffer usage :storage
:texture a combined image sampler (binding texture sampler) texture usage :sampled
:storage-texture an image they read and write texel by texel (binding texture) texture usage :storage
:uniform-texel-buffer a texel buffer they read (binding buffer :format format &key offset size) buffer usage :uniform-texel
:storage-texel-buffer a texel buffer they read and write (binding buffer :format format &key offset size) buffer usage :storage-texel

bind-group-layout-entries returns a layout's entries as (binding kind visibility), sorted by binding.

Bind groups

(gfx:create-bind-group device layout entries)

entries has exactly one entry per binding of the layout, in any order. A missing, extra or duplicated entry signals an error that names the binding. The layout and every resource must come from device.

Buffer ranges

A buffer entry binds a range of the buffer, by default all of it:

(list 0 uniforms :offset 256 :size 64)

:offset must be a multiple of 256 (+uniform-offset-alignment+ and +storage-offset-alignment+), the largest alignment any GPU asks for. That makes it easy to keep one region per frame in flight in one buffer, with one bind group per region:

(let* ((slots (gfx:device-frames-in-flight device))
       (uniforms (gfx:create-buffer device :size (* 256 slots) :usage '(:uniform)
                                           :memory :upload))
       (groups (coerce (loop for slot below slots
                             collect (gfx:create-bind-group
                                      device layout
                                      (list (list 0 uniforms :offset (* 256 slot) :size 64))))
                       'vector)))
  ;; Each frame:
  (let ((slot (gfx:frame-slot frame)))
    (gfx:write-buffer uniforms transform :offset (* 256 slot))
    (gfx:set-bind-group pass 0 (svref groups slot))))

Texel buffers

A texel buffer is a range of a buffer that shaders read as a row of texels of a texture format, without a texture. Give its :format; the range must be a whole number of texels:

(list 1 table :format :rgba32-float)

Textures and samplers

A :texture entry takes a texture with :sampled usage and a sampler. A depth texture with a comparing sampler (:compare) gives a shadow sampler:

(list 2 shadow-map (gfx:create-sampler device :compare :less-equal))
layout(set = 0, binding = 2) uniform sampler2DShadow shadow_map;

A :storage-texture entry takes only a texture with :storage usage.

Lifetimes

The objects a bind group holds must outlive it. bind-group-textures returns a vector of the textures it holds. Destroy a group with destroy-bind-group, and a layout with destroy-bind-group-layout; destroying a layout does not affect the pipelines and groups made from it.

Setting bind groups

(gfx:set-bind-group pass index group)

Sets group as bind group index, from 0 to 3, for the draws or dispatches that follow in pass. It allocates nothing.

  • Compatibility: a bind group fits every pipeline made with a layout of the same entries, not only the layout object it was made from.
  • Setting a pipeline keeps the bind groups set. You can set a per-frame group once and switch pipelines under it.
  • Render bundles start with no bind groups set, and leave none set after them. See Render Passes and Bundles.

Hazards that are refused

The backend inserts barriers between passes, so a texture drawn into in one pass can be sampled in the next, and a dispatch sees what the previous one wrote. Two uses within one pass cannot be synchronised, and are refused as soon as they are recorded:

  • Reading a texture the render pass draws into. Draw into another texture, or read it in a later pass.
  • Sampling and storing to the same texture in one pass (through :texture and :storage-texture bindings). Use one storage texture to read and write, or use it each way in a pass of its own.

Both signal gfx-error with a message that says which texture and what to do.