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    pub fn add_texture_array_view(&mut self, binding: u32, visibility: ShaderStages) -> &mut Self {
234        // Create bind group layout
235        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
236            binding,
237            visibility,
238            ty: wgpu::BindingType::Texture {
239                multisampled: false,
240                view_dimension: wgpu::TextureViewDimension::D2Array,
241                sample_type: wgpu::TextureSampleType::Float { filterable: true }
242            },
243            count: None
244        });
245
246        self
247    }
248
249    /// Add a depth texture to the bind group.
250    ///
251    /// # Arguments
252    ///
253    /// * `binding` - The binding index of the texture.
254    /// * `visibility` - The shader stages that can access the texture.
255    /// * `multisampled` - Whether the texture is multisampled.
256    pub fn add_depth_texture_view(
257        &mut self,
258        binding: u32,
259        visibility: ShaderStages,
260        multisampled: bool
261    ) -> &mut Self {
262        // Create bind group layout
263        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
264            binding,
265            visibility,
266            ty: wgpu::BindingType::Texture {
267                multisampled,
268                view_dimension: wgpu::TextureViewDimension::D2,
269                sample_type: wgpu::TextureSampleType::Depth
270            },
271            count: None
272        });
273
274        self
275    }
276
277    /// Add a texture to the bind group.
278    ///
279    /// # Arguments
280    ///
281    /// * `binding` - The binding index of the texture sampler.
282    /// * `visibility` - The shader stages that can access the sampler.
283    pub fn add_texture_sampler(&mut self, binding: u32, visibility: ShaderStages) -> &mut Self {
284        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
285            binding,
286            visibility,
287            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
288            count: None
289        });
290
291        self
292    }
293
294    /// Add a depth texture sampler to the bind group.
295    ///
296    /// # Arguments
297    ///
298    /// * `binding` - The binding index of the texture sampler.
299    /// * `visibility` - The shader stages that can access the sampler.
300    pub fn add_depth_texture_sampler(
301        &mut self,
302        binding: u32,
303        visibility: ShaderStages
304    ) -> &mut Self {
305        self.layout_entries.push(wgpu::BindGroupLayoutEntry {
306            binding,
307            visibility,
308            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
309            count: None
310        });
311
312        self
313    }
314}
315
316/// Structure for a bind group layout.
317/// Stores the layout description and builder data.
318#[derive(Clone)]
319pub struct BindGroupLayout {
320    // Bind group description
321    pub label: String,
322    // Access to builder data
323    pub builder: BindGroupLayoutBuilder
324}
325impl BindGroupLayout {
326    /// Create a new bind group layout.
327    ///
328    /// # Arguments
329    ///
330    /// * `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.
331    /// * `build_func` - The function to build the bind group layout.
332    pub fn new(label: &str, build_func: impl FnOnce(&mut BindGroupLayoutBuilder)) -> Self {
333        let mut builder = BindGroupLayoutBuilder {
334            layout_entries: Vec::new()
335        };
336
337        build_func(&mut builder);
338
339        BindGroupLayout {
340            label: label.to_string(),
341            builder
342        }
343    }
344
345    /// Create a dummy empty bind group layout. This can be used for render bindings that don't need a bind group.
346    pub fn empty() -> Self {
347        BindGroupLayout {
348            label: "".to_string(),
349            builder: BindGroupLayoutBuilder {
350                layout_entries: Vec::new()
351            }
352        }
353    }
354
355    /// Build the bind group layout.
356    ///
357    /// # Arguments
358    ///
359    /// * `instance` - The render instance data.
360    pub fn build(&self, instance: &RenderInstanceData) -> Result<WgpuBindGroupLayout, RenderError> {
361        event!(
362            LogLevel::TRACE,
363            "Creating bind group layout: {}.",
364            self.label
365        );
366
367        // Add validation to intercept potential errors in bind group layout creation
368        instance
369            .device
370            .push_error_scope(wgpu::ErrorFilter::Validation);
371
372        // Create bind group layout
373        let layout = instance
374            .device
375            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
376                label: Some(format!("{}-bind-group-layout", self.label).as_str()),
377                entries: &self.builder.layout_entries
378            });
379
380        // Check for errors
381        let mut res = Ok(layout);
382        future::block_on(async {
383            let error = instance.device.pop_error_scope().await;
384            match error {
385                Some(wgpu::Error::Validation {
386                    source,
387                    description
388                }) => {
389                    error!(
390                        self.label,
391                        "Failed to create bind group layout with source error: {:#?}. Description: {}. Bind group layout description: {:#?}.",
392                        source,
393                        description,
394                        self.builder.layout_entries
395                    );
396                    res = Err(RenderError::CannotCreateBindGroupLayout);
397                }
398                Some(e) => {
399                    error!(
400                        self.label,
401                        "Failed to create bind group layout with unexpected error: {:#?}. Bind group layout description: {:#?}.",
402                        e,
403                        self.builder.layout_entries
404                    );
405                    res = Err(RenderError::CannotCreateBindGroupLayout);
406                }
407                None => ()
408            }
409        });
410        res
411    }
412}
413
414/// Structure for a bind group.
415pub struct BindGroupBuilder;
416impl BindGroupBuilder {
417    /// Build a bind group.
418    ///
419    /// # Arguments
420    ///
421    /// * `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.
422    /// * `instance` - The render instance data.
423    /// * `layout` - The bind group layout.
424    /// * `entries` - The bind group entries.
425    pub fn build(
426        label: &str,
427        instance: &RenderInstanceData,
428        layout: &wgpu::BindGroupLayout,
429        entries: &Vec<wgpu::BindGroupEntry>
430    ) -> Result<BindGroup, RenderError> {
431        event!(LogLevel::TRACE, "Creating bind group: {}.", label);
432
433        // Add validation to intercept potential errors in bind group creation
434        instance
435            .device
436            .push_error_scope(wgpu::ErrorFilter::Validation);
437
438        // Create bind group
439        let bind_group = instance
440            .device
441            .create_bind_group(&wgpu::BindGroupDescriptor {
442                label: Some(format!("{}-bind-group", label).as_str()),
443                layout,
444                entries
445            });
446
447        // Check for errors
448        let mut res = Ok(bind_group);
449        future::block_on(async {
450            let error = instance.device.pop_error_scope().await;
451            match error {
452                Some(wgpu::Error::Validation {
453                    source,
454                    description
455                }) => {
456                    error!(
457                        label,
458                        "Failed to create bind group with source error: {:#?}. Description: {}.",
459                        source,
460                        description
461                    );
462                    res = Err(RenderError::CannotCreateBindGroup);
463                }
464                Some(e) => {
465                    error!(
466                        label,
467                        "Failed to create bind group with unexpected error: {:#?}.", e
468                    );
469                    res = Err(RenderError::CannotCreateBindGroup);
470                }
471                None => ()
472            }
473        });
474        res.map(BindGroup::from)
475    }
476
477    /// Add a buffer to the bind group.
478    ///
479    /// # Arguments
480    ///
481    /// * `binding` - The binding index of the buffer.
482    /// * `buffer` - The buffer to add to the bind group.
483    pub fn buffer(binding: u32, buffer: &'_ Buffer) -> wgpu::BindGroupEntry<'_> {
484        wgpu::BindGroupEntry {
485            binding,
486            resource: buffer.buffer.as_entire_binding()
487        }
488    }
489
490    /// Add a texture view to the bind group.
491    ///
492    /// # Arguments
493    ///
494    /// * `binding` - The binding index of the texture.
495    /// * `texture` - The texture to add to the bind group.
496    pub fn texture_view(binding: u32, texture: &'_ Texture) -> wgpu::BindGroupEntry<'_> {
497        wgpu::BindGroupEntry {
498            binding,
499            resource: wgpu::BindingResource::TextureView(&texture.view)
500        }
501    }
502
503    /// Add a texture sampler to the bind group.
504    ///
505    /// # Arguments
506    ///
507    /// * `binding` - The binding index of the texture.
508    /// * `texture` - The texture to add to the bind group.
509    pub fn texture_sampler(binding: u32, texture: &'_ Texture) -> wgpu::BindGroupEntry<'_> {
510        wgpu::BindGroupEntry {
511            binding,
512            resource: wgpu::BindingResource::Sampler(&texture.sampler)
513        }
514    }
515}