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    pub kind: MaterialBindingKind,
158}
159
160pub struct MaterialDescriptor<'a> {
161    pub label: Option<&'a str>,
162    pub shader_source: &'a str,
163    pub vertex_entry: Option<&'a str>,
164    pub fragment_entry: Option<&'a str>,
165    pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
166    pub entries: Vec<MaterialBindingEntry>,
167    pub cull_mode: Option<wgpu::Face>,
168    pub depth: Option<wgpu::DepthStencilState>,
169    pub targets: Vec<wgpu::ColorTargetState>,
170    pub polygon_mode: wgpu::PolygonMode,
171    pub extra_layouts: Vec<wgpu::BindGroupLayout>,
172}
173
174pub const DEFAULT_TARGET: [wgpu::ColorTargetState; 1] = [wgpu::ColorTargetState {
175    format: wgpu::TextureFormat::Rgba8Unorm,
176    blend: None,
177    write_mask: wgpu::ColorWrites::ALL,
178}];
179
180impl<'a> Default for MaterialDescriptor<'a> {
181    fn default() -> Self {
182        Self {
183            label: None,
184            shader_source: "",
185            vertex_entry: Some("vs_main"),
186            fragment_entry: Some("fs_main"),
187            vertex_layouts: Vec::new(),
188            entries: Vec::new(),
189            cull_mode: Some(wgpu::Face::Back),
190            depth: None,
191            targets: Vec::new(),
192            extra_layouts: Vec::new(),
193            polygon_mode: wgpu::PolygonMode::Fill,
194        }
195    }
196}
197
198pub fn build_bind_group_layout(
199    device: &wgpu::Device,
200    label: Option<&str>,
201    entries: &[MaterialBindingEntry],
202) -> wgpu::BindGroupLayout {
203    let layout_entries: Vec<_> = entries
204        .iter()
205        .enumerate()
206        .map(|(i, e)| e.kind.layout_entry(i as u32))
207        .collect();
208
209    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
210        label,
211        entries: &layout_entries,
212    })
213}
214
215pub fn build_material(
216    device: &wgpu::Device,
217    desc: &MaterialDescriptor,
218) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
219    let layout = build_bind_group_layout(device, desc.label, &desc.entries);
220
221    let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
222        label: desc.label,
223        source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
224    });
225
226    let mut bind_group_layouts: Vec<&wgpu::BindGroupLayout> = desc.extra_layouts.iter().collect();
227    bind_group_layouts.push(&layout);
228    let bind_group_layouts: Vec<Option<&wgpu::BindGroupLayout>> =
229        bind_group_layouts.into_iter().map(Some).collect();
230
231    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
232        label: desc.label,
233        bind_group_layouts: &bind_group_layouts,
234        immediate_size: 0,
235    });
236
237    let targets: Vec<Option<wgpu::ColorTargetState>> =
238        desc.targets.iter().cloned().map(Some).collect();
239
240    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
241        label: desc.label,
242        layout: Some(&pipeline_layout),
243        vertex: wgpu::VertexState {
244            module: &module,
245            entry_point: desc.vertex_entry,
246            compilation_options: Default::default(),
247            buffers: &desc.vertex_layouts,
248        },
249        primitive: wgpu::PrimitiveState {
250            topology: wgpu::PrimitiveTopology::TriangleList,
251            strip_index_format: None,
252            front_face: wgpu::FrontFace::Ccw,
253            cull_mode: desc.cull_mode,
254            unclipped_depth: false,
255            polygon_mode: desc.polygon_mode,
256            conservative: false,
257        },
258        depth_stencil: desc.depth.clone(),
259        multisample: wgpu::MultisampleState::default(),
260        fragment: Some(wgpu::FragmentState {
261            module: &module,
262            entry_point: desc.fragment_entry,
263            compilation_options: Default::default(),
264            targets: &targets,
265        }),
266        multiview_mask: None,
267        cache: None,
268    });
269
270    (pipeline, layout)
271}
272
273pub struct GPUMaterial {
274    pub pipeline: wgpu::RenderPipeline,
275    pub layout: wgpu::BindGroupLayout,
276    pub entries: Vec<MaterialBindingEntry>,
277}
278
279impl Asset<WGPUBackend> for GPUMaterial {
280    type Source = MaterialDescriptor<'static>;
281    type Deps<'a> = ();
282
283    fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
284        let (pipeline, layout) = build_material(&backend.device, source);
285
286        Some(Self {
287            pipeline,
288            layout,
289            entries: source.entries.to_vec(),
290        })
291    }
292}
293
294#[derive(Default)]
295pub struct MaterialPlugin;
296impl MaterialPlugin {
297    pub fn new() -> Self {
298        Self
299    }
300}
301
302impl Plugin for MaterialPlugin {
303    fn build(&self, app: &mut App) {
304        app.add_plugin(AssetPlugin::<super::backend::WGPUBackend, GPUMaterial>::new());
305    }
306}