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