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}