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
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
313/// `MaterialDescriptor::extra_layouts` takes ownership (e.g. a camera's
314/// layout, wired into more than one material) — cheap, the same `Arc`-backed
315/// handle underneath.
316#[derive(Clone)]
317pub struct BindGroupLayout(wgpu::BindGroupLayout);
318
319impl BindGroupLayout {
320    pub(crate) fn raw(&self) -> &wgpu::BindGroupLayout {
321        &self.0
322    }
323}
324
325/// One binding within a material's or compute pass's own bind group (see
326/// `MaterialDescriptor::entries`/`ComputeDescriptor::entries`).
327#[derive(Clone)]
328pub struct BindingEntry {
329    /// Shader-facing name, used only in panic/diagnostic messages — has no
330    /// effect on the actual binding.
331    pub name: &'static str,
332    /// The `@binding(N)` this entry occupies within its bind group. Explicit rather than
333    /// inferred from position in `entries`, so it matches the shader unambiguously.
334    pub binding: u32,
335    /// What resource this binding expects, its wgpu binding parameters,
336    /// and which shader stage(s) can see it.
337    pub kind: BindingKind,
338}
339
340/// Builds a `wgpu::BindGroupLayout` one [`BindingEntry`] at a time.
341///
342/// ```ignore
343/// let layout = BindGroupLayoutBuilder::new()
344///     .label("camera_layout")
345///     .entry("camera", 0, BindingKind::uniform_buffer(ShaderStages::VERTEX))
346///     .build(&backend);
347/// ```
348///
349/// [`build`](Self::build) panics if two entries claim the same `@binding(N)`
350/// — this makes a shader-mismatched binding layout fail loudly here instead
351/// of silently misbehaving at draw/dispatch time.
352#[derive(Default)]
353pub struct BindGroupLayoutBuilder<'a> {
354    label: Option<&'a str>,
355    entries: Vec<BindingEntry>,
356}
357
358impl<'a> BindGroupLayoutBuilder<'a> {
359    pub fn new() -> Self {
360        Self::default()
361    }
362
363    pub fn label(mut self, label: impl Into<Option<&'a str>>) -> Self {
364        self.label = label.into();
365        self
366    }
367
368    /// Appends one entry. Call repeatedly for a multi-entry layout.
369    pub fn entry(mut self, name: &'static str, binding: u32, kind: BindingKind) -> Self {
370        self.entries.push(BindingEntry { name, binding, kind });
371        self
372    }
373
374    /// Appends every entry from `entries` — for building from an
375    /// already-collected `Vec<BindingEntry>` (e.g.
376    /// `MaterialDescriptor::entries`) rather than one at a time.
377    pub fn entries(mut self, entries: impl IntoIterator<Item = BindingEntry>) -> Self {
378        self.entries.extend(entries);
379        self
380    }
381
382    pub fn build(self, backend: &WGPUBackend) -> BindGroupLayout {
383        self.build_raw(&backend.device)
384    }
385
386    /// Internal primitive behind [`build`](Self::build) — used directly only
387    /// by tests, which have a raw `wgpu::Device` but no full [`WGPUBackend`].
388    pub(crate) fn build_raw(self, device: &wgpu::Device) -> BindGroupLayout {
389        let layout_entries: Vec<_> =
390            self.entries.iter().map(|e| e.kind.layout_entry(e.binding)).collect();
391
392        let mut seen = std::collections::HashSet::new();
393        for e in &self.entries {
394            if !seen.insert(e.binding) {
395                panic!(
396                    "binding {} assigned more than once building bind group layout{} (entry '{}')",
397                    e.binding,
398                    self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
399                    e.name
400                );
401            }
402        }
403
404        BindGroupLayout(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
405            label: self.label,
406            entries: &layout_entries,
407        }))
408    }
409}
410
411/// Implemented by [`GPUMaterial`](super::material::GPUMaterial) and
412/// [`GPUCompute`](super::compute::GPUCompute) — anything with its own bind
413/// group layout and named entries that a
414/// [`GPUBindingInstance`](super::instance::GPUBindingInstance) can bind
415/// concrete resources against.
416pub trait BindGroupTarget {
417    fn bind_group_layout(&self) -> &BindGroupLayout;
418    fn binding_entries(&self) -> &[BindingEntry];
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    // Pure logic — no device needed.
426
427    #[test]
428    fn visibility_reports_back_exactly_what_each_constructor_was_given() {
429        let stages = ShaderStages::VERTEX_FRAGMENT;
430        assert!(BindingKind::texture_2d(stages).visibility() == stages);
431        assert!(BindingKind::sampler(stages).visibility() == stages);
432        assert!(BindingKind::uniform_buffer(stages).visibility() == stages);
433        assert!(
434            BindingKind::storage_buffer_read_only(ShaderStages::COMPUTE).visibility()
435                == ShaderStages::COMPUTE
436        );
437        assert!(
438            BindingKind::storage_buffer_read_write(ShaderStages::COMPUTE).visibility()
439                == ShaderStages::COMPUTE
440        );
441    }
442
443    #[test]
444    fn dynamic_storage_buffer_picks_read_only_or_read_write_by_flag() {
445        let read_only = BindingKind::dynamic_storage_buffer(ShaderStages::COMPUTE, 16, true);
446        let read_write = BindingKind::dynamic_storage_buffer(ShaderStages::COMPUTE, 16, false);
447        assert!(matches!(read_only, BindingKind::StorageBufferReadOnly { .. }));
448        assert!(matches!(read_write, BindingKind::StorageBufferReadWrite { .. }));
449    }
450
451    // Device-dependent — see `test_util` for why these skip instead of
452    // failing when no adapter is available.
453
454    #[test]
455    fn unique_bindings_build_without_panicking() {
456        crate::wgpu::test_util::with_device!(device, _queue, {
457            BindGroupLayoutBuilder::new()
458                .entry("a", 0, BindingKind::texture_2d(ShaderStages::FRAGMENT))
459                .entry("b", 1, BindingKind::sampler(ShaderStages::FRAGMENT))
460                .build_raw(&device);
461        });
462    }
463
464    #[test]
465    fn two_entries_claiming_the_same_binding_panics() {
466        crate::wgpu::test_util::with_device!(device, _queue, {
467            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
468                BindGroupLayoutBuilder::new()
469                    .entry("a", 0, BindingKind::texture_2d(ShaderStages::FRAGMENT))
470                    .entry("b", 0, BindingKind::sampler(ShaderStages::FRAGMENT))
471                    .build_raw(&device);
472            }));
473            assert!(result.is_err(), "expected a panic for a duplicate @binding(0)");
474        });
475    }
476}