Skip to main content

wde_wgpu/pipelines/
bind_group.rs

1//! Bind groups bind buffers, textures, and samplers into a shader-visible layout.
2//!
3//! # Overview
4//! 1. Create a [`BindGroupLayout`] that matches your WGSL `@group` / `@binding` declarations.
5//! 2. Populate GPU resources and build the concrete [`wgpu::BindGroup`].
6//! 3. Set the bind group inside a render or compute pass.
7//!
8//! # Example: PBR material
9//! ```rust,no_run
10//! use wde_wgpu::{
11//!     bind_group::*,
12//!     buffer::{Buffer, BufferUsage},
13//!     instance::RenderInstanceData,
14//!     render_pipeline::ShaderStages,
15//!     texture::{Texture, TextureFormat, TextureUsages},
16//! };
17//!
18//! let uniform_buffer = Buffer::new(instance, "material-uniform", 64, BufferUsage::UNIFORM, None);
19//! let albedo = Texture::new(instance, "albedo", (512, 512), TextureFormat::Rgba8Unorm, TextureUsages::TEXTURE_BINDING, 1, 1);
20//! let normal = Texture::new(instance, "normal", (512, 512), TextureFormat::Rgba8Unorm, TextureUsages::TEXTURE_BINDING, 1, 1);
21//!
22//! let layout = BindGroupLayout::new("material-layout", |builder| {
23//!     builder.add_buffer(0, ShaderStages::FRAGMENT, BufferBindingType::Uniform);
24//!     builder.add_texture_view(1, ShaderStages::FRAGMENT);
25//!     builder.add_texture_sampler(2, ShaderStages::FRAGMENT);
26//!     builder.add_texture_view(3, ShaderStages::FRAGMENT);
27//!     builder.add_texture_sampler(4, ShaderStages::FRAGMENT);
28//! });
29//! let wgpu_layout = layout.build(instance);
30//!
31//! let bind_group = BindGroup::build(
32//!     "material-bind-group",
33//!     instance,
34//!     &wgpu_layout,
35//!     &vec![
36//!         BindGroup::buffer(0, &uniform_buffer),
37//!         BindGroup::texture_view(1, &albedo),
38//!         BindGroup::texture_sampler(2, &albedo),
39//!         BindGroup::texture_view(3, &normal),
40//!         BindGroup::texture_sampler(4, &normal),
41//!     ],
42//! );
43//!
44//! (wgpu_layout, bind_group)
45//! ```
46//!
47//! Depth-only layouts follow the same pattern using `add_depth_texture_view` and
48//! `add_depth_texture_sampler`.
49//!
50//! # Tips
51//! - Keep per-frame data in group 0, per-material in group 1, and per-object in group 2 to
52//!   minimize rebinding during draws.
53//! - The order of `set_bind_groups` on pipelines must match the order of layouts you provide
54//!   here.
55
56use futures_lite::future;
57use wde_logger::prelude::*;
58
59use crate::{
60    buffer::Buffer,
61    instance::{RenderError, RenderInstanceData},
62    render_pipeline::ShaderStages,
63    texture::{Texture, TextureFormat}
64};
65
66// The wgpu bind group type.
67#[derive(Clone)]
68pub struct BindGroup(pub Option<wgpu::BindGroup>);
69impl BindGroup {
70    pub fn empty() -> Self {
71        BindGroup(None)
72    }
73}
74impl From<wgpu::BindGroup> for BindGroup {
75    fn from(value: wgpu::BindGroup) -> Self {
76        BindGroup(Some(value))
77    }
78}
79
80/// The buffer binding type.
81pub type BufferBindingType = wgpu::BufferBindingType;
82
83/// The wgpu bind group layout type.
84pub type WgpuBindGroupLayout = wgpu::BindGroupLayout;
85
86/// The wgpu bind group entry type.
87pub type BindGroupEntry<'a> = wgpu::BindGroupEntry<'a>;
88
89/// Builder for a bind group layout.
90#[derive(Debug, Clone)]
91pub struct BindGroupLayoutBuilder {
92    layout_entries: Vec<wgpu::BindGroupLayoutEntry>
93}
94
95impl BindGroupLayoutBuilder {
96    /// Add a buffer to the bind group.
97    ///
98    /// # Arguments
99    ///
100    /// * `binding` - The binding index of the buffer.
101    /// * `visibility` - The shader stages that can access the buffer.
102    /// * `binding_type` - The type of the buffer binding.
103    pub fn add_buffer(
104        &mut self,
105        binding: u32,
106        visibility: ShaderStages,
107        binding_type: BufferBindingType
108    ) -> &mut Self {
109        // Create bind group layout
110        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
111            binding,
112            visibility,
113            ty: wgpu::BindingType::Buffer {
114                has_dynamic_offset: false,
115                min_binding_size: None,
116                ty: binding_type
117            },
118            count: None
119        });
120
121        self
122    }
123
124    /// Add a texture to the bind group.
125    ///
126    /// # Arguments
127    ///
128    /// * `binding` - The binding index of the texture.
129    /// * `visibility` - The shader stages that can access the texture.
130    /// * `multisampled` - Whether the texture is multisampled.
131    pub fn add_texture_view(
132        &mut self,
133        binding: u32,
134        visibility: ShaderStages,
135        multisampled: bool
136    ) -> &mut Self {
137        // Create bind group layout
138        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
139            binding,
140            visibility,
141            ty: wgpu::BindingType::Texture {
142                multisampled,
143                view_dimension: wgpu::TextureViewDimension::D2,
144                sample_type: wgpu::TextureSampleType::Float {
145                    filterable: !multisampled
146                }
147            },
148            count: None
149        });
150
151        self
152    }
153
154    /// Add a texture to the bind group.
155    ///
156    /// # Arguments
157    ///
158    /// * `binding` - The binding index of the texture.
159    /// * `visibility` - The shader stages that can access the texture.
160    /// * `multisampled` - Whether the texture is multisampled.
161    /// * `filterable` - Whether the texture is filterable (only relevant if not multisampled).
162    pub fn add_texture_view_filterable(
163        &mut self,
164        binding: u32,
165        visibility: ShaderStages,
166        multisampled: bool,
167        filterable: bool
168    ) -> &mut Self {
169        // Create bind group layout
170        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
171            binding,
172            visibility,
173            ty: wgpu::BindingType::Texture {
174                multisampled,
175                view_dimension: wgpu::TextureViewDimension::D2,
176                sample_type: wgpu::TextureSampleType::Float { filterable }
177            },
178            count: None
179        });
180
181        self
182    }
183
184    /// Add a storage texture view to the bind group.
185    /// This is used for textures that will be read and written to in a compute shader.
186    ///
187    /// # Arguments
188    ///
189    /// * `binding` - The binding index of the texture.
190    /// * `format` - The format of the texture.
191    /// * `atomic` - Whether the texture will be used for atomic operations.
192    pub fn add_storage_texture_view(&mut self, binding: u32, format: TextureFormat) -> &mut Self {
193        // Create bind group layout
194        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
195            binding,
196            visibility: ShaderStages::COMPUTE,
197            ty: wgpu::BindingType::StorageTexture {
198                access: wgpu::StorageTextureAccess::ReadWrite,
199                view_dimension: wgpu::TextureViewDimension::D2,
200                format
201            },
202            count: None
203        });
204
205        self
206    }
207
208    /// Add a storage texture array view to the bind group (for compute read/write of array layers).
209    pub fn add_storage_texture_array_view(
210        &mut self,
211        binding: u32,
212        format: TextureFormat
213    ) -> &mut Self {
214        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
215            binding,
216            visibility: ShaderStages::COMPUTE,
217            ty: wgpu::BindingType::StorageTexture {
218                access: wgpu::StorageTextureAccess::ReadWrite,
219                view_dimension: wgpu::TextureViewDimension::D2Array,
220                format
221            },
222            count: None
223        });
224        self
225    }
226
227    /// Add a texture array to the bind group.
228    ///
229    /// # Arguments
230    ///
231    /// * `binding` - The binding index of the texture array.
232    /// * `visibility` - The shader stages that can access the texture array.
233    /// * `filterable` - Whether the texture is filterable. Formats that aren't filterable by
234    ///   default (e.g. `R32Float`) must pass `false` here and be read with `textureLoad` in the
235    ///   shader, not `textureSample`.
236    pub fn add_texture_array_view(
237        &mut self,
238        binding: u32,
239        visibility: ShaderStages,
240        filterable: bool
241    ) -> &mut Self {
242        // Create bind group layout
243        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
244            binding,
245            visibility,
246            ty: wgpu::BindingType::Texture {
247                multisampled: false,
248                view_dimension: wgpu::TextureViewDimension::D2Array,
249                sample_type: wgpu::TextureSampleType::Float { filterable }
250            },
251            count: None
252        });
253
254        self
255    }
256
257    /// Add a depth texture to the bind group.
258    ///
259    /// # Arguments
260    ///
261    /// * `binding` - The binding index of the texture.
262    /// * `visibility` - The shader stages that can access the texture.
263    /// * `multisampled` - Whether the texture is multisampled.
264    pub fn add_depth_texture_view(
265        &mut self,
266        binding: u32,
267        visibility: ShaderStages,
268        multisampled: bool
269    ) -> &mut Self {
270        // Create bind group layout
271        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
272            binding,
273            visibility,
274            ty: wgpu::BindingType::Texture {
275                multisampled,
276                view_dimension: wgpu::TextureViewDimension::D2,
277                sample_type: wgpu::TextureSampleType::Depth
278            },
279            count: None
280        });
281
282        self
283    }
284
285    /// Add a texture to the bind group.
286    ///
287    /// # Arguments
288    ///
289    /// * `binding` - The binding index of the texture sampler.
290    /// * `visibility` - The shader stages that can access the sampler.
291    pub fn add_texture_sampler(&mut self, binding: u32, visibility: ShaderStages) -> &mut Self {
292        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
293            binding,
294            visibility,
295            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
296            count: None
297        });
298
299        self
300    }
301
302    /// Add a depth texture sampler to the bind group.
303    ///
304    /// # Arguments
305    ///
306    /// * `binding` - The binding index of the texture sampler.
307    /// * `visibility` - The shader stages that can access the sampler.
308    pub fn add_depth_texture_sampler(
309        &mut self,
310        binding: u32,
311        visibility: ShaderStages
312    ) -> &mut Self {
313        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
314            binding,
315            visibility,
316            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
317            count: None
318        });
319
320        self
321    }
322}
323
324/// Structure for a bind group layout.
325/// Stores the layout description and builder data.
326#[derive(Clone)]
327pub struct BindGroupLayout {
328    // Bind group description
329    pub label: String,
330    // Access to builder data
331    pub builder: BindGroupLayoutBuilder
332}
333impl BindGroupLayout {
334    /// Create a new bind group layout.
335    ///
336    /// # Arguments
337    ///
338    /// * `label` - The label of the bind group layout. Note that this label is only for GPU debugging purposes and is only used if GPU debugging is enabled.
339    /// * `build_func` - The function to build the bind group layout.
340    pub fn new(label: &str, build_func: impl FnOnce(&mut BindGroupLayoutBuilder)) -> Self {
341        let mut builder = BindGroupLayoutBuilder {
342            layout_entries: Vec::new()
343        };
344
345        build_func(&mut builder);
346
347        BindGroupLayout {
348            label: label.to_string(),
349            builder
350        }
351    }
352
353    /// Create a dummy empty bind group layout. This can be used for render bindings that don't need a bind group.
354    pub fn empty() -> Self {
355        BindGroupLayout {
356            label: "".to_string(),
357            builder: BindGroupLayoutBuilder {
358                layout_entries: Vec::new()
359            }
360        }
361    }
362
363    /// Build the bind group layout.
364    ///
365    /// # Arguments
366    ///
367    /// * `instance` - The render instance data.
368    pub fn build(&self, instance: &RenderInstanceData) -> Result<WgpuBindGroupLayout, RenderError> {
369        event!(
370            LogLevel::TRACE,
371            "Creating bind group layout: {}.",
372            self.label
373        );
374
375        // Add validation to intercept potential errors in bind group layout creation
376        instance
377            .device
378            .push_error_scope(wgpu::ErrorFilter::Validation);
379
380        // Create bind group layout
381        let layout = instance
382            .device
383            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
384                label: Some(format!("{}-bind-group-layout", self.label).as_str()),
385                entries: &self.builder.layout_entries
386            });
387
388        // Check for errors
389        let mut res = Ok(layout);
390        future::block_on(async {
391            let error = instance.device.pop_error_scope().await;
392            match error {
393                Some(wgpu::Error::Validation {
394                    source,
395                    description
396                }) => {
397                    error!(
398                        self.label,
399                        "Failed to create bind group layout with source error: {:#?}. Description: {}. Bind group layout description: {:#?}.",
400                        source,
401                        description,
402                        self.builder.layout_entries
403                    );
404                    res = Err(RenderError::CannotCreateBindGroupLayout);
405                }
406                Some(e) => {
407                    error!(
408                        self.label,
409                        "Failed to create bind group layout with unexpected error: {:#?}. Bind group layout description: {:#?}.",
410                        e,
411                        self.builder.layout_entries
412                    );
413                    res = Err(RenderError::CannotCreateBindGroupLayout);
414                }
415                None => ()
416            }
417        });
418        res
419    }
420}
421
422/// Structure for a bind group.
423pub struct BindGroupBuilder;
424impl BindGroupBuilder {
425    /// Build a bind group.
426    ///
427    /// # Arguments
428    ///
429    /// * `label` - The label of the bind group. Note that this label is only for GPU debugging purposes and is only used if GPU debugging is enabled.
430    /// * `instance` - The render instance data.
431    /// * `layout` - The bind group layout.
432    /// * `entries` - The bind group entries.
433    pub fn build(
434        label: &str,
435        instance: &RenderInstanceData,
436        layout: &wgpu::BindGroupLayout,
437        entries: &Vec<wgpu::BindGroupEntry>
438    ) -> Result<BindGroup, RenderError> {
439        event!(LogLevel::TRACE, "Creating bind group: {}.", label);
440
441        // Add validation to intercept potential errors in bind group creation
442        instance
443            .device
444            .push_error_scope(wgpu::ErrorFilter::Validation);
445
446        // Create bind group
447        let bind_group = instance
448            .device
449            .create_bind_group(&wgpu::BindGroupDescriptor {
450                label: Some(format!("{}-bind-group", label).as_str()),
451                layout,
452                entries
453            });
454
455        // Check for errors
456        let mut res = Ok(bind_group);
457        future::block_on(async {
458            let error = instance.device.pop_error_scope().await;
459            match error {
460                Some(wgpu::Error::Validation {
461                    source,
462                    description
463                }) => {
464                    error!(
465                        label,
466                        "Failed to create bind group with source error: {:#?}. Description: {}.",
467                        source,
468                        description
469                    );
470                    res = Err(RenderError::CannotCreateBindGroup);
471                }
472                Some(e) => {
473                    error!(
474                        label,
475                        "Failed to create bind group with unexpected error: {:#?}.", e
476                    );
477                    res = Err(RenderError::CannotCreateBindGroup);
478                }
479                None => ()
480            }
481        });
482        res.map(BindGroup::from)
483    }
484
485    /// Add a buffer to the bind group.
486    ///
487    /// # Arguments
488    ///
489    /// * `binding` - The binding index of the buffer.
490    /// * `buffer` - The buffer to add to the bind group.
491    pub fn buffer(binding: u32, buffer: &'_ Buffer) -> wgpu::BindGroupEntry<'_> {
492        wgpu::BindGroupEntry {
493            binding,
494            resource: buffer.buffer.as_entire_binding()
495        }
496    }
497
498    /// Add a texture view to the bind group.
499    ///
500    /// # Arguments
501    ///
502    /// * `binding` - The binding index of the texture.
503    /// * `texture` - The texture to add to the bind group.
504    pub fn texture_view(binding: u32, texture: &'_ Texture) -> wgpu::BindGroupEntry<'_> {
505        wgpu::BindGroupEntry {
506            binding,
507            resource: wgpu::BindingResource::TextureView(&texture.view)
508        }
509    }
510
511    /// Add a texture sampler to the bind group.
512    ///
513    /// # Arguments
514    ///
515    /// * `binding` - The binding index of the texture.
516    /// * `texture` - The texture to add to the bind group.
517    pub fn texture_sampler(binding: u32, texture: &'_ Texture) -> wgpu::BindGroupEntry<'_> {
518        wgpu::BindGroupEntry {
519            binding,
520            resource: wgpu::BindingResource::Sampler(&texture.sampler)
521        }
522    }
523}