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::dynamic_uniform_offset_stride`] or
72    /// [`crate::wgpu::buffers::build_dynamic_uniform_buffer`] to compute the actual stride.
73    pub fn dynamic_uniform_buffer(element_size: u64) -> Self {
74        Self::UniformBuffer {
75            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
76            has_dynamic_offset: true,
77            min_binding_size: wgpu::BufferSize::new(element_size),
78        }
79    }
80
81    /// A storage buffer bound with a per-draw dynamic offset. See [`Self::dynamic_uniform_buffer`].
82    pub fn dynamic_storage_buffer(element_size: u64, read_only: bool) -> Self {
83        let visibility = wgpu::ShaderStages::VERTEX_FRAGMENT;
84        let has_dynamic_offset = true;
85        let min_binding_size = wgpu::BufferSize::new(element_size);
86        if read_only {
87            Self::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size }
88        } else {
89            Self::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size }
90        }
91    }
92
93    pub fn layout_entry(&self, binding: u32) -> wgpu::BindGroupLayoutEntry {
94        match self {
95            MaterialBindingKind::Texture { sample_type, view_dimension, multisampled } => wgpu::BindGroupLayoutEntry {
96                binding,
97                visibility: wgpu::ShaderStages::FRAGMENT,
98                ty: wgpu::BindingType::Texture {
99                    sample_type: *sample_type,
100                    view_dimension: *view_dimension,
101                    multisampled: *multisampled,
102                },
103                count: None,
104            },
105            MaterialBindingKind::Sampler => wgpu::BindGroupLayoutEntry {
106                binding,
107                visibility: wgpu::ShaderStages::FRAGMENT,
108                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
109                count: None,
110            },
111            MaterialBindingKind::ComparisonSampler => wgpu::BindGroupLayoutEntry {
112                binding,
113                visibility: wgpu::ShaderStages::FRAGMENT,
114                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
115                count: None,
116            },
117            MaterialBindingKind::UniformBuffer { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
118                binding,
119                visibility: *visibility,
120                ty: wgpu::BindingType::Buffer {
121                    ty: wgpu::BufferBindingType::Uniform,
122                    has_dynamic_offset: *has_dynamic_offset,
123                    min_binding_size: *min_binding_size,
124                },
125                count: None,
126            },
127            MaterialBindingKind::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
128                binding,
129                visibility: *visibility,
130                ty: wgpu::BindingType::Buffer {
131                    ty: wgpu::BufferBindingType::Storage { read_only: true },
132                    has_dynamic_offset: *has_dynamic_offset,
133                    min_binding_size: *min_binding_size,
134                },
135                count: None,
136            },
137            MaterialBindingKind::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
138                binding,
139                visibility: *visibility,
140                ty: wgpu::BindingType::Buffer {
141                    ty: wgpu::BufferBindingType::Storage { read_only: false },
142                    has_dynamic_offset: *has_dynamic_offset,
143                    min_binding_size: *min_binding_size,
144                },
145                count: None,
146            },
147        }
148    }
149}
150
151#[derive(Clone)]
152pub struct MaterialBindingEntry {
153    pub name: &'static str,
154    pub kind: MaterialBindingKind,
155}
156
157pub struct MaterialDescriptor<'a> {
158    pub label: Option<&'a str>,
159    pub shader_source: &'a str,
160    pub vertex_entry: Option<&'a str>,
161    pub fragment_entry: Option<&'a str>,
162    pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
163    pub entries: Vec<MaterialBindingEntry>,
164    pub cull_mode: Option<wgpu::Face>,
165    pub depth: Option<wgpu::DepthStencilState>,
166    pub targets: Vec<wgpu::ColorTargetState>,
167    pub polygon_mode: wgpu::PolygonMode,
168    pub extra_layouts: Vec<wgpu::BindGroupLayout>,
169}
170
171pub const DEFAULT_TARGET: [wgpu::ColorTargetState; 1] = [wgpu::ColorTargetState {
172    format: wgpu::TextureFormat::Rgba8Unorm,
173    blend: None,
174    write_mask: wgpu::ColorWrites::ALL,
175}];
176
177impl<'a> Default for MaterialDescriptor<'a> {
178    fn default() -> Self {
179        Self {
180            label: None,
181            shader_source: "",
182            vertex_entry: Some("vs_main"),
183            fragment_entry: Some("fs_main"),
184            vertex_layouts: Vec::new(),
185            entries: Vec::new(),
186            cull_mode: Some(wgpu::Face::Back),
187            depth: None,
188            targets: Vec::new(),
189            extra_layouts: Vec::new(),
190            polygon_mode: wgpu::PolygonMode::Fill,
191        }
192    }
193}
194
195pub fn build_bind_group_layout(
196    device: &wgpu::Device,
197    label: Option<&str>,
198    entries: &[MaterialBindingEntry],
199) -> wgpu::BindGroupLayout {
200    let layout_entries: Vec<_> = entries
201        .iter()
202        .enumerate()
203        .map(|(i, e)| e.kind.layout_entry(i as u32))
204        .collect();
205
206    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
207        label,
208        entries: &layout_entries,
209    })
210}
211
212pub fn build_material(
213    device: &wgpu::Device,
214    desc: &MaterialDescriptor,
215) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
216    let layout = build_bind_group_layout(device, desc.label, &desc.entries);
217
218    let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
219        label: desc.label,
220        source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
221    });
222
223    let mut bind_group_layouts: Vec<&wgpu::BindGroupLayout> = desc.extra_layouts.iter().collect();
224    bind_group_layouts.push(&layout);
225    let bind_group_layouts: Vec<Option<&wgpu::BindGroupLayout>> =
226        bind_group_layouts.into_iter().map(Some).collect();
227
228    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
229        label: desc.label,
230        bind_group_layouts: &bind_group_layouts,
231        immediate_size: 0,
232    });
233
234    let targets: Vec<Option<wgpu::ColorTargetState>> =
235        desc.targets.iter().cloned().map(Some).collect();
236
237    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
238        label: desc.label,
239        layout: Some(&pipeline_layout),
240        vertex: wgpu::VertexState {
241            module: &module,
242            entry_point: desc.vertex_entry,
243            compilation_options: Default::default(),
244            buffers: &desc.vertex_layouts,
245        },
246        primitive: wgpu::PrimitiveState {
247            topology: wgpu::PrimitiveTopology::TriangleList,
248            strip_index_format: None,
249            front_face: wgpu::FrontFace::Ccw,
250            cull_mode: desc.cull_mode,
251            unclipped_depth: false,
252            polygon_mode: desc.polygon_mode,
253            conservative: false,
254        },
255        depth_stencil: desc.depth.clone(),
256        multisample: wgpu::MultisampleState::default(),
257        fragment: Some(wgpu::FragmentState {
258            module: &module,
259            entry_point: desc.fragment_entry,
260            compilation_options: Default::default(),
261            targets: &targets,
262        }),
263        multiview_mask: None,
264        cache: None,
265    });
266
267    (pipeline, layout)
268}
269
270pub struct GPUMaterial {
271    pub pipeline: wgpu::RenderPipeline,
272    pub layout: wgpu::BindGroupLayout,
273    pub entries: Vec<MaterialBindingEntry>,
274}
275
276impl Asset<WGPUBackend> for GPUMaterial {
277    type Source = MaterialDescriptor<'static>;
278    type Deps<'a> = ();
279
280    fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
281        let (pipeline, layout) = build_material(&backend.device, source);
282
283        Some(Self {
284            pipeline,
285            layout,
286            entries: source.entries.to_vec(),
287        })
288    }
289}
290
291#[derive(Default)]
292pub struct MaterialPlugin;
293impl MaterialPlugin {
294    pub fn new() -> Self {
295        Self
296    }
297}
298
299impl Plugin for MaterialPlugin {
300    fn build(&self, app: &mut App) {
301        app.add_plugin(AssetPlugin::<super::backend::WGPUBackend, GPUMaterial>::new());
302    }
303}