Bind Groups
Shaders read uniforms, textures and storage through bind groups, as in WebGPU. There are three steps:
- A bind group layout describes what each binding holds and which stages see it.
- A pipeline is made with the layouts of the groups it reads, by index.
- A bind group fills a layout with real resources, and
set-bind-groupmakes 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,:fragmentand:compute; the tessellation and geometry stages count only on a device with:tessellation-shadersor: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
:textureand:storage-texturebindings). 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.