Skip to main content

pebble/wgpu/
material.rs

1use crate::{
2    app::App,
3    assets::{plugin::AssetPlugin, upload::Asset},
4    ecs::plugin::Plugin,
5    wgpu::backend::WGPUBackend,
6};
7
8#[derive(Copy, Clone, PartialEq, Eq, Hash)]
9pub enum MaterialBindingKind {
10    Texture {
11        sample_type: wgpu::TextureSampleType,
12        view_dimension: wgpu::TextureViewDimension,
13        multisampled: bool,
14    },
15    Sampler,
16    ComparisonSampler,
17    UniformBuffer {
18        visibility: wgpu::ShaderStages,
19        has_dynamic_offset: bool,
20        min_binding_size: Option<wgpu::BufferSize>,
21    },
22    StorageBufferReadOnly {
23        visibility: wgpu::ShaderStages,
24        has_dynamic_offset: bool,
25        min_binding_size: Option<wgpu::BufferSize>,
26    },
27    StorageBufferReadWrite {
28        visibility: wgpu::ShaderStages,
29        has_dynamic_offset: bool,
30        min_binding_size: Option<wgpu::BufferSize>,
31    },
32}
33
34impl MaterialBindingKind {
35    pub fn texture_2d() -> Self {
36        Self::Texture {
37            sample_type: wgpu::TextureSampleType::Float { filterable: true },
38            view_dimension: wgpu::TextureViewDimension::D2,
39            multisampled: false,
40        }
41    }
42
43    pub fn texture_2d_array() -> Self {
44        Self::Texture {
45            sample_type: wgpu::TextureSampleType::Float { filterable: true },
46            view_dimension: wgpu::TextureViewDimension::D2Array,
47            multisampled: false,
48        }
49    }
50
51    pub fn texture_cubemap() -> Self {
52        Self::Texture {
53            sample_type: wgpu::TextureSampleType::Float { filterable: true },
54            view_dimension: wgpu::TextureViewDimension::Cube,
55            multisampled: false,
56        }
57    }
58
59    pub fn uniform_buffer() -> Self {
60        Self::UniformBuffer {
61            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
62            has_dynamic_offset: false,
63            min_binding_size: None,
64        }
65    }
66
67    /// A uniform buffer bound with a per-draw dynamic offset, e.g. one large buffer
68    /// holding many objects' data, rebound at a different offset via
69    /// `RenderPass::set_bind_group`'s dynamic offsets slice instead of a bind group per object.
70    /// `element_size` is the size in bytes of a single element (before alignment padding).
71    /// Use [`crate::wgpu::buffers::build_dynamic_uniform_buffer`] to allocate the backing
72    /// buffer and [`crate::wgpu::buffers::dynamic_buffer_binding`] (not
73    /// `buffer.as_entire_binding()`) to build the bind group entry for it — the entry must
74    /// be scoped to one element's size, not the whole buffer, or dynamic offsets will fail
75    /// validation.
76    pub fn dynamic_uniform_buffer(element_size: u64) -> Self {
77        Self::UniformBuffer {
78            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
79            has_dynamic_offset: true,
80            min_binding_size: wgpu::BufferSize::new(element_size),
81        }
82    }
83
84    /// A storage buffer bound with a per-draw dynamic offset. See [`Self::dynamic_uniform_buffer`].
85    pub fn dynamic_storage_buffer(element_size: u64, read_only: bool) -> Self {
86        let visibility = wgpu::ShaderStages::VERTEX_FRAGMENT;
87        let has_dynamic_offset = true;
88        let min_binding_size = wgpu::BufferSize::new(element_size);
89        if read_only {
90            Self::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size }
91        } else {
92            Self::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size }
93        }
94    }
95
96    pub fn layout_entry(&self, binding: u32) -> wgpu::BindGroupLayoutEntry {
97        match self {
98            MaterialBindingKind::Texture { sample_type, view_dimension, multisampled } => wgpu::BindGroupLayoutEntry {
99                binding,
100                visibility: wgpu::ShaderStages::FRAGMENT,
101                ty: wgpu::BindingType::Texture {
102                    sample_type: *sample_type,
103                    view_dimension: *view_dimension,
104                    multisampled: *multisampled,
105                },
106                count: None,
107            },
108            MaterialBindingKind::Sampler => wgpu::BindGroupLayoutEntry {
109                binding,
110                visibility: wgpu::ShaderStages::FRAGMENT,
111                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
112                count: None,
113            },
114            MaterialBindingKind::ComparisonSampler => wgpu::BindGroupLayoutEntry {
115                binding,
116                visibility: wgpu::ShaderStages::FRAGMENT,
117                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
118                count: None,
119            },
120            MaterialBindingKind::UniformBuffer { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
121                binding,
122                visibility: *visibility,
123                ty: wgpu::BindingType::Buffer {
124                    ty: wgpu::BufferBindingType::Uniform,
125                    has_dynamic_offset: *has_dynamic_offset,
126                    min_binding_size: *min_binding_size,
127                },
128                count: None,
129            },
130            MaterialBindingKind::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
131                binding,
132                visibility: *visibility,
133                ty: wgpu::BindingType::Buffer {
134                    ty: wgpu::BufferBindingType::Storage { read_only: true },
135                    has_dynamic_offset: *has_dynamic_offset,
136                    min_binding_size: *min_binding_size,
137                },
138                count: None,
139            },
140            MaterialBindingKind::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
141                binding,
142                visibility: *visibility,
143                ty: wgpu::BindingType::Buffer {
144                    ty: wgpu::BufferBindingType::Storage { read_only: false },
145                    has_dynamic_offset: *has_dynamic_offset,
146                    min_binding_size: *min_binding_size,
147                },
148                count: None,
149            },
150        }
151    }
152}
153
154#[derive(Clone)]
155pub struct MaterialBindingEntry {
156    pub name: &'static str,
157    /// The `@binding(N)` this entry occupies within its bind group. Explicit rather than
158    /// inferred from position in `entries`, so it matches the shader unambiguously.
159    pub binding: u32,
160    pub kind: MaterialBindingKind,
161}
162
163pub struct MaterialDescriptor<'a> {
164    pub label: Option<&'a str>,
165    pub shader_source: &'a str,
166    pub vertex_entry: Option<&'a str>,
167    pub fragment_entry: Option<&'a str>,
168    pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
169    pub entries: Vec<MaterialBindingEntry>,
170    pub cull_mode: Option<wgpu::Face>,
171    pub depth: Option<wgpu::DepthStencilState>,
172    pub targets: Vec<wgpu::ColorTargetState>,
173    pub polygon_mode: wgpu::PolygonMode,
174    /// Which `@group(N)` the layout built from `entries` occupies in the pipeline, or
175    /// `None` if this material has no entries of its own (e.g. it only uses `extra_layouts`).
176    pub own_group: Option<u32>,
177    /// Additional bind group layouts, each tagged with the `@group(N)` it occupies.
178    /// Every index from 0 up to the highest one used (including `own_group`, if set) must
179    /// be covered exactly once, or `build_material` panics — this makes group assignment
180    /// explicit instead of inferred from field order.
181    pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
182}
183
184pub const DEFAULT_TARGET: [wgpu::ColorTargetState; 1] = [wgpu::ColorTargetState {
185    format: wgpu::TextureFormat::Rgba8Unorm,
186    blend: None,
187    write_mask: wgpu::ColorWrites::ALL,
188}];
189
190impl<'a> Default for MaterialDescriptor<'a> {
191    fn default() -> Self {
192        Self {
193            label: None,
194            shader_source: "",
195            vertex_entry: Some("vs_main"),
196            fragment_entry: Some("fs_main"),
197            vertex_layouts: Vec::new(),
198            entries: Vec::new(),
199            cull_mode: Some(wgpu::Face::Back),
200            depth: None,
201            targets: Vec::new(),
202            own_group: Some(0),
203            extra_layouts: Vec::new(),
204            polygon_mode: wgpu::PolygonMode::Fill,
205        }
206    }
207}
208
209pub fn build_bind_group_layout(
210    device: &wgpu::Device,
211    label: Option<&str>,
212    entries: &[MaterialBindingEntry],
213) -> wgpu::BindGroupLayout {
214    let layout_entries: Vec<_> = entries.iter().map(|e| e.kind.layout_entry(e.binding)).collect();
215
216    let mut seen = std::collections::HashSet::new();
217    for e in entries {
218        if !seen.insert(e.binding) {
219            panic!(
220                "binding {} assigned more than once building bind group layout{} (entry '{}')",
221                e.binding,
222                label.map(|l| format!(" '{l}'")).unwrap_or_default(),
223                e.name
224            );
225        }
226    }
227
228    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
229        label,
230        entries: &layout_entries,
231    })
232}
233
234pub fn build_material(
235    device: &wgpu::Device,
236    desc: &MaterialDescriptor,
237) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
238    let layout = build_bind_group_layout(device, desc.label, &desc.entries);
239
240    let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
241        label: desc.label,
242        source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
243    });
244
245    let mut slots: Vec<super::layout::GroupLayout> = desc
246        .extra_layouts
247        .iter()
248        .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
249        .collect();
250    if let Some(own_group) = desc.own_group {
251        slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
252    }
253    let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
254
255    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
256        label: desc.label,
257        bind_group_layouts: &bind_group_layouts,
258        immediate_size: 0,
259    });
260
261    let targets: Vec<Option<wgpu::ColorTargetState>> =
262        desc.targets.iter().cloned().map(Some).collect();
263
264    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
265        label: desc.label,
266        layout: Some(&pipeline_layout),
267        vertex: wgpu::VertexState {
268            module: &module,
269            entry_point: desc.vertex_entry,
270            compilation_options: Default::default(),
271            buffers: &desc.vertex_layouts,
272        },
273        primitive: wgpu::PrimitiveState {
274            topology: wgpu::PrimitiveTopology::TriangleList,
275            strip_index_format: None,
276            front_face: wgpu::FrontFace::Ccw,
277            cull_mode: desc.cull_mode,
278            unclipped_depth: false,
279            polygon_mode: desc.polygon_mode,
280            conservative: false,
281        },
282        depth_stencil: desc.depth.clone(),
283        multisample: wgpu::MultisampleState::default(),
284        fragment: Some(wgpu::FragmentState {
285            module: &module,
286            entry_point: desc.fragment_entry,
287            compilation_options: Default::default(),
288            targets: &targets,
289        }),
290        multiview_mask: None,
291        cache: None,
292    });
293
294    (pipeline, layout)
295}
296
297pub struct GPUMaterial {
298    pub pipeline: wgpu::RenderPipeline,
299    pub layout: wgpu::BindGroupLayout,
300    pub entries: Vec<MaterialBindingEntry>,
301}
302
303impl Asset<WGPUBackend> for GPUMaterial {
304    type Source = MaterialDescriptor<'static>;
305    type Deps<'a> = ();
306
307    fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
308        let (pipeline, layout) = build_material(&backend.device, source);
309
310        Some(Self {
311            pipeline,
312            layout,
313            entries: source.entries.to_vec(),
314        })
315    }
316}
317
318#[derive(Default)]
319pub struct MaterialPlugin;
320impl MaterialPlugin {
321    pub fn new() -> Self {
322        Self
323    }
324}
325
326impl Plugin for MaterialPlugin {
327    fn build(&self, app: &mut App) {
328        app.add_plugin(AssetPlugin::<super::backend::WGPUBackend, GPUMaterial>::new());
329    }
330}