Skip to main content

pebble/wgpu/
binding.rs

1//! Shared bind-group vocabulary for [`material`](super::material) and
2//! [`compute`](super::compute) — a material's own bind group and a compute
3//! pass's own bind group are described the same way, differing only in
4//! which shader stage(s) can see each entry (a material entry can be
5//! `FRAGMENT`/`VERTEX`/`VERTEX_FRAGMENT`; a compute entry is always exactly
6//! `COMPUTE`). Every constructor below takes `visibility` explicitly rather
7//! than guessing a default per module — [`build_material`](super::material::build_material)/
8//! [`build_compute`](super::compute::build_compute) validate it's
9//! appropriate for the pipeline kind they're building, panicking with a
10//! clear message otherwise.
11//!
12//! Also useful directly (not just via `MaterialDescriptor`/`ComputeDescriptor`)
13//! any time you're building a bind group layout by hand — [`BindGroupLayoutBuilder`]
14//! catches a duplicate `@binding(N)` with a clear panic instead of a wgpu
15//! validation failure at draw time. Re-exported, along with [`buffers`](super::buffers),
16//! from [`wgpu::prelude`](super::prelude).
17
18/// What kind of resource a single [`BindingEntry`] binds, the wgpu binding
19/// parameters that go with it, and which shader stage(s) can see it.
20/// Construct via the `texture_*`/`*_buffer`/`sampler`/`storage_texture`
21/// associated functions rather than the variants directly — they fill in
22/// the usual defaults (filterable float textures, non-dynamic buffers) so
23/// only the cases that actually differ need spelling out.
24#[derive(Copy, Clone, PartialEq, Eq, Hash)]
25pub enum BindingKind {
26    /// A sampled texture (`texture_2d<f32>` and friends in WGSL).
27    Texture {
28        visibility: wgpu::ShaderStages,
29        sample_type: wgpu::TextureSampleType,
30        view_dimension: wgpu::TextureViewDimension,
31        multisampled: bool,
32    },
33    /// A texture bound for direct read/write access (`textureStore`/
34    /// `textureLoad` in WGSL) rather than sampling.
35    StorageTexture {
36        visibility: wgpu::ShaderStages,
37        format: wgpu::TextureFormat,
38        access: wgpu::StorageTextureAccess,
39        view_dimension: wgpu::TextureViewDimension,
40    },
41    /// A filtering sampler.
42    Sampler { visibility: wgpu::ShaderStages },
43    /// A comparison sampler (e.g. for shadow-map `textureSampleCompare`).
44    ComparisonSampler { visibility: wgpu::ShaderStages },
45    /// A uniform buffer.
46    UniformBuffer {
47        visibility: wgpu::ShaderStages,
48        has_dynamic_offset: bool,
49        min_binding_size: Option<wgpu::BufferSize>,
50    },
51    /// A read-only storage buffer.
52    StorageBufferReadOnly {
53        visibility: wgpu::ShaderStages,
54        has_dynamic_offset: bool,
55        min_binding_size: Option<wgpu::BufferSize>,
56    },
57    /// A read-write storage buffer.
58    StorageBufferReadWrite {
59        visibility: wgpu::ShaderStages,
60        has_dynamic_offset: bool,
61        min_binding_size: Option<wgpu::BufferSize>,
62    },
63}
64
65impl BindingKind {
66    /// A filterable, non-multisampled 2D texture — the common case.
67    pub fn texture_2d(visibility: wgpu::ShaderStages) -> Self {
68        Self::Texture {
69            visibility,
70            sample_type: wgpu::TextureSampleType::Float { filterable: true },
71            view_dimension: wgpu::TextureViewDimension::D2,
72            multisampled: false,
73        }
74    }
75
76    /// Same as [`texture_2d`](Self::texture_2d) but for a 2D texture array
77    /// (see [`GPUTextureArray`](super::texture_array::GPUTextureArray)).
78    pub fn texture_2d_array(visibility: wgpu::ShaderStages) -> Self {
79        Self::Texture {
80            visibility,
81            sample_type: wgpu::TextureSampleType::Float { filterable: true },
82            view_dimension: wgpu::TextureViewDimension::D2Array,
83            multisampled: false,
84        }
85    }
86
87    /// Same as [`texture_2d`](Self::texture_2d) but for a cubemap (see
88    /// [`GPUCubemap`](super::cubemap::GPUCubemap)).
89    pub fn texture_cubemap(visibility: wgpu::ShaderStages) -> Self {
90        Self::Texture {
91            visibility,
92            sample_type: wgpu::TextureSampleType::Float { filterable: true },
93            view_dimension: wgpu::TextureViewDimension::Cube,
94            multisampled: false,
95        }
96    }
97
98    /// A storage texture bound for direct read/write/read-write access
99    /// (per `access`) rather than sampling.
100    pub fn storage_texture(
101        visibility: wgpu::ShaderStages,
102        format: wgpu::TextureFormat,
103        access: wgpu::StorageTextureAccess,
104        view_dimension: wgpu::TextureViewDimension,
105    ) -> Self {
106        Self::StorageTexture { visibility, format, access, view_dimension }
107    }
108
109    /// A filtering sampler.
110    pub fn sampler(visibility: wgpu::ShaderStages) -> Self {
111        Self::Sampler { visibility }
112    }
113
114    /// A comparison sampler (e.g. for shadow-map `textureSampleCompare`).
115    pub fn comparison_sampler(visibility: wgpu::ShaderStages) -> Self {
116        Self::ComparisonSampler { visibility }
117    }
118
119    /// A uniform buffer, bound as a whole (no dynamic offset) — the common
120    /// case. See [`dynamic_uniform_buffer`](Self::dynamic_uniform_buffer)
121    /// for the per-draw/per-dispatch-offset variant.
122    pub fn uniform_buffer(visibility: wgpu::ShaderStages) -> Self {
123        Self::UniformBuffer { visibility, has_dynamic_offset: false, min_binding_size: None }
124    }
125
126    /// A uniform buffer bound with a dynamic offset, e.g. one large buffer
127    /// holding many objects'/elements' data, rebound at a different offset
128    /// via `set_bind_group`'s dynamic offsets slice instead of a bind group
129    /// per object/dispatch. `element_size` is the size in bytes of a single
130    /// element (before alignment padding). Use
131    /// [`DynamicBufferBuilder`](crate::wgpu::buffers::DynamicBufferBuilder)
132    /// to allocate the backing buffer and
133    /// [`BindGroupBuilder::dynamic_buffer`](crate::wgpu::buffers::BindGroupBuilder::dynamic_buffer)
134    /// (not `.buffer()`/`buffer.as_entire_binding()`) to bind it — the entry
135    /// must be scoped to one element's size, not the whole buffer, or
136    /// dynamic offsets will fail validation.
137    pub fn dynamic_uniform_buffer(visibility: wgpu::ShaderStages, element_size: u64) -> Self {
138        Self::UniformBuffer {
139            visibility,
140            has_dynamic_offset: true,
141            min_binding_size: wgpu::BufferSize::new(element_size),
142        }
143    }
144
145    /// A read-only storage buffer, bound as a whole (no dynamic offset).
146    pub fn storage_buffer_read_only(visibility: wgpu::ShaderStages) -> Self {
147        Self::StorageBufferReadOnly { visibility, has_dynamic_offset: false, min_binding_size: None }
148    }
149
150    /// A read-write storage buffer, bound as a whole (no dynamic offset).
151    pub fn storage_buffer_read_write(visibility: wgpu::ShaderStages) -> Self {
152        Self::StorageBufferReadWrite { visibility, has_dynamic_offset: false, min_binding_size: None }
153    }
154
155    /// A storage buffer bound with a dynamic offset. See
156    /// [`Self::dynamic_uniform_buffer`].
157    pub fn dynamic_storage_buffer(visibility: wgpu::ShaderStages, element_size: u64, read_only: bool) -> Self {
158        let has_dynamic_offset = true;
159        let min_binding_size = wgpu::BufferSize::new(element_size);
160        if read_only {
161            Self::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size }
162        } else {
163            Self::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size }
164        }
165    }
166
167    /// Which shader stage(s) this binding is visible to.
168    pub fn visibility(&self) -> wgpu::ShaderStages {
169        match self {
170            Self::Texture { visibility, .. }
171            | Self::StorageTexture { visibility, .. }
172            | Self::Sampler { visibility }
173            | Self::ComparisonSampler { visibility }
174            | Self::UniformBuffer { visibility, .. }
175            | Self::StorageBufferReadOnly { visibility, .. }
176            | Self::StorageBufferReadWrite { visibility, .. } => *visibility,
177        }
178    }
179
180    pub fn layout_entry(&self, binding: u32) -> wgpu::BindGroupLayoutEntry {
181        match self {
182            Self::Texture { visibility, sample_type, view_dimension, multisampled } => wgpu::BindGroupLayoutEntry {
183                binding,
184                visibility: *visibility,
185                ty: wgpu::BindingType::Texture {
186                    sample_type: *sample_type,
187                    view_dimension: *view_dimension,
188                    multisampled: *multisampled,
189                },
190                count: None,
191            },
192            Self::StorageTexture { visibility, format, access, view_dimension } => wgpu::BindGroupLayoutEntry {
193                binding,
194                visibility: *visibility,
195                ty: wgpu::BindingType::StorageTexture {
196                    access: *access,
197                    format: *format,
198                    view_dimension: *view_dimension,
199                },
200                count: None,
201            },
202            Self::Sampler { visibility } => wgpu::BindGroupLayoutEntry {
203                binding,
204                visibility: *visibility,
205                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
206                count: None,
207            },
208            Self::ComparisonSampler { visibility } => wgpu::BindGroupLayoutEntry {
209                binding,
210                visibility: *visibility,
211                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
212                count: None,
213            },
214            Self::UniformBuffer { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
215                binding,
216                visibility: *visibility,
217                ty: wgpu::BindingType::Buffer {
218                    ty: wgpu::BufferBindingType::Uniform,
219                    has_dynamic_offset: *has_dynamic_offset,
220                    min_binding_size: *min_binding_size,
221                },
222                count: None,
223            },
224            Self::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
225                binding,
226                visibility: *visibility,
227                ty: wgpu::BindingType::Buffer {
228                    ty: wgpu::BufferBindingType::Storage { read_only: true },
229                    has_dynamic_offset: *has_dynamic_offset,
230                    min_binding_size: *min_binding_size,
231                },
232                count: None,
233            },
234            Self::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
235                binding,
236                visibility: *visibility,
237                ty: wgpu::BindingType::Buffer {
238                    ty: wgpu::BufferBindingType::Storage { read_only: false },
239                    has_dynamic_offset: *has_dynamic_offset,
240                    min_binding_size: *min_binding_size,
241                },
242                count: None,
243            },
244        }
245    }
246}
247
248/// A `wgpu::BindGroupLayout`, opaque — built only via
249/// [`BindGroupLayoutBuilder::build`]. There's no way to reach the underlying
250/// `wgpu::BindGroupLayout` from outside this crate. `Clone` because
251/// `MaterialDescriptor::extra_layouts` takes ownership (e.g. a camera's
252/// layout, wired into more than one material) — cheap, the same `Arc`-backed
253/// handle underneath.
254#[derive(Clone)]
255pub struct BindGroupLayout(wgpu::BindGroupLayout);
256
257impl BindGroupLayout {
258    pub(crate) fn raw(&self) -> &wgpu::BindGroupLayout {
259        &self.0
260    }
261}
262
263/// One binding within a material's or compute pass's own bind group (see
264/// `MaterialDescriptor::entries`/`ComputeDescriptor::entries`).
265#[derive(Clone)]
266pub struct BindingEntry {
267    /// Shader-facing name, used only in panic/diagnostic messages — has no
268    /// effect on the actual binding.
269    pub name: &'static str,
270    /// The `@binding(N)` this entry occupies within its bind group. Explicit rather than
271    /// inferred from position in `entries`, so it matches the shader unambiguously.
272    pub binding: u32,
273    /// What resource this binding expects, its wgpu binding parameters,
274    /// and which shader stage(s) can see it.
275    pub kind: BindingKind,
276}
277
278/// Builds a `wgpu::BindGroupLayout` one [`BindingEntry`] at a time.
279///
280/// ```ignore
281/// let layout = BindGroupLayoutBuilder::new()
282///     .label("camera_layout")
283///     .entry("camera", 0, BindingKind::uniform_buffer(wgpu::ShaderStages::VERTEX))
284///     .build(&device);
285/// ```
286///
287/// [`build`](Self::build) panics if two entries claim the same `@binding(N)`
288/// — this makes a shader-mismatched binding layout fail loudly here instead
289/// of silently misbehaving at draw/dispatch time.
290#[derive(Default)]
291pub struct BindGroupLayoutBuilder<'a> {
292    label: Option<&'a str>,
293    entries: Vec<BindingEntry>,
294}
295
296impl<'a> BindGroupLayoutBuilder<'a> {
297    pub fn new() -> Self {
298        Self::default()
299    }
300
301    pub fn label(mut self, label: impl Into<Option<&'a str>>) -> Self {
302        self.label = label.into();
303        self
304    }
305
306    /// Appends one entry. Call repeatedly for a multi-entry layout.
307    pub fn entry(mut self, name: &'static str, binding: u32, kind: BindingKind) -> Self {
308        self.entries.push(BindingEntry { name, binding, kind });
309        self
310    }
311
312    /// Appends every entry from `entries` — for building from an
313    /// already-collected `Vec<BindingEntry>` (e.g.
314    /// `MaterialDescriptor::entries`) rather than one at a time.
315    pub fn entries(mut self, entries: impl IntoIterator<Item = BindingEntry>) -> Self {
316        self.entries.extend(entries);
317        self
318    }
319
320    pub fn build(self, device: &wgpu::Device) -> BindGroupLayout {
321        let layout_entries: Vec<_> =
322            self.entries.iter().map(|e| e.kind.layout_entry(e.binding)).collect();
323
324        let mut seen = std::collections::HashSet::new();
325        for e in &self.entries {
326            if !seen.insert(e.binding) {
327                panic!(
328                    "binding {} assigned more than once building bind group layout{} (entry '{}')",
329                    e.binding,
330                    self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
331                    e.name
332                );
333            }
334        }
335
336        BindGroupLayout(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
337            label: self.label,
338            entries: &layout_entries,
339        }))
340    }
341}
342
343/// Implemented by [`GPUMaterial`](super::material::GPUMaterial) and
344/// [`GPUCompute`](super::compute::GPUCompute) — anything with its own bind
345/// group layout and named entries that a
346/// [`GPUBindingInstance`](super::instance::GPUBindingInstance) can bind
347/// concrete resources against.
348pub trait BindGroupTarget {
349    fn bind_group_layout(&self) -> &BindGroupLayout;
350    fn binding_entries(&self) -> &[BindingEntry];
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    // Pure logic — no device needed.
358
359    #[test]
360    fn visibility_reports_back_exactly_what_each_constructor_was_given() {
361        let stages = wgpu::ShaderStages::VERTEX_FRAGMENT;
362        assert_eq!(BindingKind::texture_2d(stages).visibility(), stages);
363        assert_eq!(BindingKind::sampler(stages).visibility(), stages);
364        assert_eq!(BindingKind::uniform_buffer(stages).visibility(), stages);
365        assert_eq!(
366            BindingKind::storage_buffer_read_only(wgpu::ShaderStages::COMPUTE).visibility(),
367            wgpu::ShaderStages::COMPUTE
368        );
369        assert_eq!(
370            BindingKind::storage_buffer_read_write(wgpu::ShaderStages::COMPUTE).visibility(),
371            wgpu::ShaderStages::COMPUTE
372        );
373    }
374
375    #[test]
376    fn dynamic_storage_buffer_picks_read_only_or_read_write_by_flag() {
377        let read_only = BindingKind::dynamic_storage_buffer(wgpu::ShaderStages::COMPUTE, 16, true);
378        let read_write = BindingKind::dynamic_storage_buffer(wgpu::ShaderStages::COMPUTE, 16, false);
379        assert!(matches!(read_only, BindingKind::StorageBufferReadOnly { .. }));
380        assert!(matches!(read_write, BindingKind::StorageBufferReadWrite { .. }));
381    }
382
383    // Device-dependent — see `test_util` for why these skip instead of
384    // failing when no adapter is available.
385
386    #[test]
387    fn unique_bindings_build_without_panicking() {
388        crate::wgpu::test_util::with_device!(device, _queue, {
389            BindGroupLayoutBuilder::new()
390                .entry("a", 0, BindingKind::texture_2d(wgpu::ShaderStages::FRAGMENT))
391                .entry("b", 1, BindingKind::sampler(wgpu::ShaderStages::FRAGMENT))
392                .build(&device);
393        });
394    }
395
396    #[test]
397    fn two_entries_claiming_the_same_binding_panics() {
398        crate::wgpu::test_util::with_device!(device, _queue, {
399            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
400                BindGroupLayoutBuilder::new()
401                    .entry("a", 0, BindingKind::texture_2d(wgpu::ShaderStages::FRAGMENT))
402                    .entry("b", 0, BindingKind::sampler(wgpu::ShaderStages::FRAGMENT))
403                    .build(&device);
404            }));
405            assert!(result.is_err(), "expected a panic for a duplicate @binding(0)");
406        });
407    }
408}