Skip to main content

myth_resources/
builder.rs

1//! Resource builder for collecting GPU binding descriptions.
2//!
3//! [`ResourceBuilder`] accumulates [`Binding`] entries that describe both the
4//! CPU-side resource data and the layout metadata needed by the renderer.
5//! Each [`Binding`] combines a [`BindingResource`] with a [`BindingDesc`],
6//! shader visibility, and an optional WGSL struct name.
7//!
8//! A single texture [`Binding`] automatically expands into two GPU binding
9//! slots — one for the texture view and one for the paired sampler — so
10//! callers never need to manage sampler bindings manually.
11
12use crate::binding::BindingResource;
13use crate::buffer::BufferRef;
14use crate::buffer::{CpuBuffer, GpuData};
15use crate::texture::TextureSource;
16use crate::uniforms::WgslStruct;
17use std::fmt::Write;
18use wgpu::ShaderStages;
19
20type WgslStructGenerator = fn(&str) -> String;
21
22/// Identifies the WGSL struct type associated with a binding.
23pub enum WgslStructName {
24    /// Generates the struct definition at code-gen time.
25    Generator(WgslStructGenerator),
26    /// A pre-existing struct name (no generation needed).
27    Name(String),
28}
29
30// ============================================================================
31// Binding descriptor types
32// ============================================================================
33
34/// Type-specific layout descriptor for a collected binding.
35///
36/// Each variant carries the metadata needed to generate
37/// [`wgpu::BindGroupLayoutEntry`] entries and WGSL declarations.
38pub enum BindingDesc {
39    /// Uniform or storage buffer.
40    Buffer {
41        ty: wgpu::BufferBindingType,
42        has_dynamic_offset: bool,
43        min_binding_size: Option<std::num::NonZeroU64>,
44    },
45    /// Texture with auto-paired sampler.
46    ///
47    /// A single `Texture` binding produces **two** GPU binding slots:
48    /// one for the texture view and one for the sampler. The sampler is
49    /// resolved automatically from the [`Texture`](crate::Texture) asset's
50    /// [`TextureSampler`](crate::TextureSampler) configuration.
51    Texture {
52        sample_type: wgpu::TextureSampleType,
53        view_dimension: wgpu::TextureViewDimension,
54        /// Sampler binding type for the auto-paired sampler entry.
55        sampler_binding_type: wgpu::SamplerBindingType,
56    },
57}
58
59/// A single resource binding collected by [`ResourceBuilder`].
60///
61/// Combines CPU-side resource data ([`BindingResource`]) with layout
62/// metadata ([`BindingDesc`]) and shader stage visibility.
63pub struct Binding<'a> {
64    /// The actual resource data for bind group creation.
65    pub resource: BindingResource<'a>,
66    /// Shader stage visibility.
67    pub visibility: ShaderStages,
68    /// Binding name (used for WGSL variable naming: `t_name`, `s_name`, `u_name`).
69    pub name: &'a str,
70    /// Optional WGSL struct type for code generation.
71    pub struct_name: Option<WgslStructName>,
72    /// Type-specific layout descriptor.
73    pub desc: BindingDesc,
74}
75
76// ============================================================================
77// ResourceBuilder
78// ============================================================================
79
80/// Collects resource bindings for automatic layout and bind group generation.
81///
82/// After populating the builder via `add_*` methods, use
83/// [`generate_layout_entries`](Self::generate_layout_entries) to produce
84/// `wgpu::BindGroupLayoutEntry` arrays and
85/// [`generate_wgsl`](Self::generate_wgsl) for WGSL declarations.
86pub struct ResourceBuilder<'a> {
87    pub bindings: Vec<Binding<'a>>,
88}
89
90impl Default for ResourceBuilder<'_> {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl<'a> ResourceBuilder<'a> {
97    #[must_use]
98    pub fn new() -> Self {
99        Self {
100            bindings: Vec::new(),
101        }
102    }
103
104    pub fn add_uniform_buffer(
105        &mut self,
106        name: &'a str,
107        buffer: &BufferRef,
108        data: Option<&'a [u8]>,
109        visibility: ShaderStages,
110        has_dynamic_offset: bool,
111        min_binding_size: Option<std::num::NonZeroU64>,
112        struct_name: Option<WgslStructName>,
113    ) {
114        self.bindings.push(Binding {
115            resource: BindingResource::Buffer {
116                buffer: buffer.clone(),
117                offset: 0,
118                size: min_binding_size.as_ref().map(|s| s.get()),
119                data,
120            },
121            visibility,
122            name,
123            struct_name,
124            desc: BindingDesc::Buffer {
125                ty: wgpu::BufferBindingType::Uniform,
126                has_dynamic_offset,
127                min_binding_size,
128            },
129        });
130    }
131
132    pub fn add_uniform<T: WgslStruct + GpuData>(
133        &mut self,
134        name: &'a str,
135        cpu_buffer: &'a CpuBuffer<T>,
136        visibility: ShaderStages,
137    ) {
138        self.add_uniform_buffer(
139            name,
140            &cpu_buffer.handle(),
141            None,
142            visibility,
143            false,
144            None,
145            Some(WgslStructName::Generator(T::wgsl_struct_def)),
146        );
147    }
148
149    pub fn add_dynamic_uniform<T: WgslStruct>(
150        &mut self,
151        name: &'a str,
152        buffer_ref: &BufferRef,
153        data: Option<&'a [u8]>,
154        min_binding_size: std::num::NonZeroU64,
155        visibility: ShaderStages,
156    ) {
157        self.add_uniform_buffer(
158            name,
159            buffer_ref,
160            data,
161            visibility,
162            true,
163            Some(min_binding_size),
164            Some(WgslStructName::Generator(T::wgsl_struct_def)),
165        );
166    }
167
168    /// Adds a texture binding with an auto-paired sampler.
169    ///
170    /// This produces **two** GPU binding slots: the texture view at the
171    /// current index and the sampler at the next index. The sampler
172    /// binding type is inferred from `sample_type` (depth → comparison,
173    /// otherwise → filtering).
174    pub fn add_texture(
175        &mut self,
176        name: &'a str,
177        source: Option<impl Into<TextureSource>>,
178        sample_type: wgpu::TextureSampleType,
179        view_dimension: wgpu::TextureViewDimension,
180        visibility: ShaderStages,
181    ) {
182        let sampler_binding_type = if matches!(sample_type, wgpu::TextureSampleType::Depth) {
183            wgpu::SamplerBindingType::Comparison
184        } else {
185            wgpu::SamplerBindingType::Filtering
186        };
187        self.bindings.push(Binding {
188            resource: BindingResource::Texture(source.map(std::convert::Into::into)),
189            visibility,
190            name,
191            struct_name: None,
192            desc: BindingDesc::Texture {
193                sample_type,
194                view_dimension,
195                sampler_binding_type,
196            },
197        });
198    }
199
200    pub fn add_storage_buffer(
201        &mut self,
202        name: &'a str,
203        buffer: &BufferRef,
204        data: Option<&'a [u8]>,
205        read_only: bool,
206        visibility: ShaderStages,
207        struct_name: Option<WgslStructName>,
208    ) {
209        self.bindings.push(Binding {
210            resource: BindingResource::Buffer {
211                buffer: buffer.clone(),
212                offset: 0,
213                size: None,
214                data,
215            },
216            visibility,
217            name,
218            struct_name,
219            desc: BindingDesc::Buffer {
220                ty: wgpu::BufferBindingType::Storage { read_only },
221                has_dynamic_offset: false,
222                min_binding_size: None,
223            },
224        });
225    }
226
227    pub fn add_storage<T: WgslStruct>(
228        &mut self,
229        name: &'a str,
230        buffer: &BufferRef,
231        data: Option<&'a [u8]>,
232        read_only: bool,
233        visibility: ShaderStages,
234    ) {
235        self.add_storage_buffer(
236            name,
237            buffer,
238            data,
239            read_only,
240            visibility,
241            Some(WgslStructName::Generator(T::wgsl_struct_def)),
242        );
243    }
244
245    /// Generates [`wgpu::BindGroupLayoutEntry`] array from collected bindings.
246    ///
247    /// Each buffer binding produces one layout entry. Each texture binding
248    /// produces two entries (texture view + auto-paired sampler).
249    #[must_use]
250    pub fn generate_layout_entries(&self) -> Vec<wgpu::BindGroupLayoutEntry> {
251        // Pre-allocate: buffers produce 1 entry, textures produce 2 entries
252        let capacity = self
253            .bindings
254            .iter()
255            .map(|b| match &b.desc {
256                BindingDesc::Buffer { .. } => 1,
257                BindingDesc::Texture { .. } => 2,
258            })
259            .sum();
260        let mut entries = Vec::with_capacity(capacity);
261        let mut idx = 0u32;
262        for b in &self.bindings {
263            match &b.desc {
264                BindingDesc::Buffer {
265                    ty,
266                    has_dynamic_offset,
267                    min_binding_size,
268                } => {
269                    entries.push(wgpu::BindGroupLayoutEntry {
270                        binding: idx,
271                        visibility: b.visibility,
272                        ty: wgpu::BindingType::Buffer {
273                            ty: *ty,
274                            has_dynamic_offset: *has_dynamic_offset,
275                            min_binding_size: *min_binding_size,
276                        },
277                        count: None,
278                    });
279                    idx += 1;
280                }
281                BindingDesc::Texture {
282                    sample_type,
283                    view_dimension,
284                    sampler_binding_type,
285                } => {
286                    entries.push(wgpu::BindGroupLayoutEntry {
287                        binding: idx,
288                        visibility: b.visibility,
289                        ty: wgpu::BindingType::Texture {
290                            sample_type: *sample_type,
291                            view_dimension: *view_dimension,
292                            multisampled: false,
293                        },
294                        count: None,
295                    });
296                    idx += 1;
297                    entries.push(wgpu::BindGroupLayoutEntry {
298                        binding: idx,
299                        visibility: b.visibility,
300                        ty: wgpu::BindingType::Sampler(*sampler_binding_type),
301                        count: None,
302                    });
303                    idx += 1;
304                }
305            }
306        }
307        entries
308    }
309
310    /// Generates WGSL binding declarations and struct definitions.
311    ///
312    /// Buffer bindings produce a single `var<uniform>` or `var<storage>`
313    /// declaration. Texture bindings produce both a `var t_` texture
314    /// declaration and a `var s_` sampler declaration.
315    #[must_use]
316    pub fn generate_wgsl(&self, group_index: u32) -> String {
317        let mut bindings_code = String::new();
318        let mut struct_defs = String::new();
319        let mut idx = 0u32;
320
321        for b in &self.bindings {
322            let name = b.name;
323
324            let struct_type_name = if let Some(generator) = &b.struct_name {
325                let sn = match generator {
326                    WgslStructName::Generator(g) => {
327                        let auto = format!("Struct_{name}");
328                        writeln!(struct_defs, "{}", g(&auto)).unwrap();
329                        auto
330                    }
331                    WgslStructName::Name(n) => n.clone(),
332                };
333                Some(sn)
334            } else {
335                None
336            };
337
338            match &b.desc {
339                BindingDesc::Buffer { ty, .. } => {
340                    match ty {
341                        wgpu::BufferBindingType::Uniform => {
342                            writeln!(
343                                bindings_code,
344                                "@group({group_index}) @binding({idx}) var<uniform> u_{name}: {};",
345                                struct_type_name
346                                    .as_deref()
347                                    .expect("buffer binding needs a struct name")
348                            )
349                            .unwrap();
350                        }
351                        wgpu::BufferBindingType::Storage { read_only } => {
352                            let access = if *read_only { "read" } else { "read_write" };
353                            let stn = struct_type_name
354                                .as_deref()
355                                .expect("storage binding needs a struct name");
356                            writeln!(
357                                bindings_code,
358                                "@group({group_index}) @binding({idx}) var<storage, {access}> st_{name}: array<{stn}>;"
359                            ).unwrap();
360                        }
361                    }
362                    idx += 1;
363                }
364                BindingDesc::Texture {
365                    sample_type,
366                    view_dimension,
367                    sampler_binding_type,
368                } => {
369                    // Texture declaration
370                    let type_str = match (view_dimension, sample_type) {
371                        (wgpu::TextureViewDimension::D2, wgpu::TextureSampleType::Depth) => {
372                            "texture_depth_2d"
373                        }
374                        (wgpu::TextureViewDimension::D2Array, wgpu::TextureSampleType::Depth) => {
375                            "texture_depth_2d_array"
376                        }
377                        (
378                            wgpu::TextureViewDimension::Cube,
379                            wgpu::TextureSampleType::Float { .. },
380                        ) => "texture_cube<f32>",
381                        (
382                            wgpu::TextureViewDimension::D2Array,
383                            wgpu::TextureSampleType::Float { .. },
384                        ) => "texture_2d_array<f32>",
385                        _ => "texture_2d<f32>",
386                    };
387                    writeln!(
388                        bindings_code,
389                        "@group({group_index}) @binding({idx}) var t_{name}: {type_str};"
390                    )
391                    .unwrap();
392                    idx += 1;
393
394                    // Sampler declaration (auto-paired)
395                    let sampler_type_str = match sampler_binding_type {
396                        wgpu::SamplerBindingType::Comparison => "sampler_comparison",
397                        _ => "sampler",
398                    };
399                    writeln!(
400                        bindings_code,
401                        "@group({group_index}) @binding({idx}) var s_{name}: {sampler_type_str};"
402                    )
403                    .unwrap();
404                    idx += 1;
405                }
406            }
407        }
408        format!(
409            "// --- Auto Generated Bindings (Group {group_index}) ---\n{struct_defs}\n{bindings_code}\n"
410        )
411    }
412}