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/// One binding within a material's or compute pass's own bind group (see
249/// `MaterialDescriptor::entries`/`ComputeDescriptor::entries`).
250#[derive(Clone)]
251pub struct BindingEntry {
252    /// Shader-facing name, used only in panic/diagnostic messages — has no
253    /// effect on the actual binding.
254    pub name: &'static str,
255    /// The `@binding(N)` this entry occupies within its bind group. Explicit rather than
256    /// inferred from position in `entries`, so it matches the shader unambiguously.
257    pub binding: u32,
258    /// What resource this binding expects, its wgpu binding parameters,
259    /// and which shader stage(s) can see it.
260    pub kind: BindingKind,
261}
262
263/// Builds a `wgpu::BindGroupLayout` one [`BindingEntry`] at a time.
264///
265/// ```ignore
266/// let layout = BindGroupLayoutBuilder::new()
267///     .label("camera_layout")
268///     .entry("camera", 0, BindingKind::uniform_buffer(wgpu::ShaderStages::VERTEX))
269///     .build(&device);
270/// ```
271///
272/// [`build`](Self::build) panics if two entries claim the same `@binding(N)`
273/// — this makes a shader-mismatched binding layout fail loudly here instead
274/// of silently misbehaving at draw/dispatch time.
275#[derive(Default)]
276pub struct BindGroupLayoutBuilder<'a> {
277    label: Option<&'a str>,
278    entries: Vec<BindingEntry>,
279}
280
281impl<'a> BindGroupLayoutBuilder<'a> {
282    pub fn new() -> Self {
283        Self::default()
284    }
285
286    pub fn label(mut self, label: impl Into<Option<&'a str>>) -> Self {
287        self.label = label.into();
288        self
289    }
290
291    /// Appends one entry. Call repeatedly for a multi-entry layout.
292    pub fn entry(mut self, name: &'static str, binding: u32, kind: BindingKind) -> Self {
293        self.entries.push(BindingEntry { name, binding, kind });
294        self
295    }
296
297    /// Appends every entry from `entries` — for building from an
298    /// already-collected `Vec<BindingEntry>` (e.g.
299    /// `MaterialDescriptor::entries`) rather than one at a time.
300    pub fn entries(mut self, entries: impl IntoIterator<Item = BindingEntry>) -> Self {
301        self.entries.extend(entries);
302        self
303    }
304
305    pub fn build(self, device: &wgpu::Device) -> wgpu::BindGroupLayout {
306        let layout_entries: Vec<_> =
307            self.entries.iter().map(|e| e.kind.layout_entry(e.binding)).collect();
308
309        let mut seen = std::collections::HashSet::new();
310        for e in &self.entries {
311            if !seen.insert(e.binding) {
312                panic!(
313                    "binding {} assigned more than once building bind group layout{} (entry '{}')",
314                    e.binding,
315                    self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
316                    e.name
317                );
318            }
319        }
320
321        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
322            label: self.label,
323            entries: &layout_entries,
324        })
325    }
326}
327
328/// Implemented by [`GPUMaterial`](super::material::GPUMaterial) and
329/// [`GPUCompute`](super::compute::GPUCompute) — anything with its own bind
330/// group layout and named entries that a
331/// [`GPUBindingInstance`](super::instance::GPUBindingInstance) can bind
332/// concrete resources against.
333pub trait BindGroupTarget {
334    fn bind_group_layout(&self) -> &wgpu::BindGroupLayout;
335    fn binding_entries(&self) -> &[BindingEntry];
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    // Pure logic — no device needed.
343
344    #[test]
345    fn visibility_reports_back_exactly_what_each_constructor_was_given() {
346        let stages = wgpu::ShaderStages::VERTEX_FRAGMENT;
347        assert_eq!(BindingKind::texture_2d(stages).visibility(), stages);
348        assert_eq!(BindingKind::sampler(stages).visibility(), stages);
349        assert_eq!(BindingKind::uniform_buffer(stages).visibility(), stages);
350        assert_eq!(
351            BindingKind::storage_buffer_read_only(wgpu::ShaderStages::COMPUTE).visibility(),
352            wgpu::ShaderStages::COMPUTE
353        );
354        assert_eq!(
355            BindingKind::storage_buffer_read_write(wgpu::ShaderStages::COMPUTE).visibility(),
356            wgpu::ShaderStages::COMPUTE
357        );
358    }
359
360    #[test]
361    fn dynamic_storage_buffer_picks_read_only_or_read_write_by_flag() {
362        let read_only = BindingKind::dynamic_storage_buffer(wgpu::ShaderStages::COMPUTE, 16, true);
363        let read_write = BindingKind::dynamic_storage_buffer(wgpu::ShaderStages::COMPUTE, 16, false);
364        assert!(matches!(read_only, BindingKind::StorageBufferReadOnly { .. }));
365        assert!(matches!(read_write, BindingKind::StorageBufferReadWrite { .. }));
366    }
367
368    // Device-dependent — see `test_util` for why these skip instead of
369    // failing when no adapter is available.
370
371    #[test]
372    fn unique_bindings_build_without_panicking() {
373        crate::wgpu::test_util::with_device!(device, _queue, {
374            BindGroupLayoutBuilder::new()
375                .entry("a", 0, BindingKind::texture_2d(wgpu::ShaderStages::FRAGMENT))
376                .entry("b", 1, BindingKind::sampler(wgpu::ShaderStages::FRAGMENT))
377                .build(&device);
378        });
379    }
380
381    #[test]
382    fn two_entries_claiming_the_same_binding_panics() {
383        crate::wgpu::test_util::with_device!(device, _queue, {
384            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
385                BindGroupLayoutBuilder::new()
386                    .entry("a", 0, BindingKind::texture_2d(wgpu::ShaderStages::FRAGMENT))
387                    .entry("b", 0, BindingKind::sampler(wgpu::ShaderStages::FRAGMENT))
388                    .build(&device);
389            }));
390            assert!(result.is_err(), "expected a panic for a duplicate @binding(0)");
391        });
392    }
393}