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