1use crate::{
2 assets::upload::Asset,
3 wgpu::{backend::WGPUBackend, binding::{BindGroupLayoutBuilder, BindingEntry}},
4};
5
6pub struct MaterialDescriptor<'a> {
11 pub label: Option<&'a str>,
14 pub shader_source: &'a str,
16 pub vertex_entry: Option<&'a str>,
18 pub fragment_entry: Option<&'a str>,
20 pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
23 pub entries: Vec<BindingEntry>,
28 pub cull_mode: Option<wgpu::Face>,
30 pub depth: Option<wgpu::DepthStencilState>,
32 pub targets: Vec<wgpu::ColorTargetState>,
35 pub polygon_mode: wgpu::PolygonMode,
37 pub own_group: Option<u32>,
40 pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
45}
46
47pub const DEFAULT_TARGET: [wgpu::ColorTargetState; 1] = [wgpu::ColorTargetState {
54 format: wgpu::TextureFormat::Rgba8Unorm,
55 blend: None,
56 write_mask: wgpu::ColorWrites::ALL,
57}];
58
59impl<'a> Default for MaterialDescriptor<'a> {
60 fn default() -> Self {
61 Self {
62 label: None,
63 shader_source: "",
64 vertex_entry: Some("vs_main"),
65 fragment_entry: Some("fs_main"),
66 vertex_layouts: Vec::new(),
67 entries: Vec::new(),
68 cull_mode: Some(wgpu::Face::Back),
69 depth: None,
70 targets: Vec::new(),
71 own_group: Some(0),
72 extra_layouts: Vec::new(),
73 polygon_mode: wgpu::PolygonMode::Fill,
74 }
75 }
76}
77
78pub fn build_material(
93 device: &wgpu::Device,
94 desc: &MaterialDescriptor,
95) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
96 for entry in &desc.entries {
97 if entry.kind.visibility().intersects(wgpu::ShaderStages::COMPUTE) {
98 panic!(
99 "material{}: entry '{}' is visible to the compute stage ({:?}) — material bind \
100 group entries must not be COMPUTE-visible",
101 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
102 entry.name,
103 entry.kind.visibility()
104 );
105 }
106 }
107
108 let layout = BindGroupLayoutBuilder::new()
109 .label(desc.label)
110 .entries(desc.entries.iter().cloned())
111 .build(device);
112
113 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
114 label: desc.label,
115 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
116 });
117
118 let mut slots: Vec<super::layout::GroupLayout> = desc
119 .extra_layouts
120 .iter()
121 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
122 .collect();
123 if let Some(own_group) = desc.own_group {
124 slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
125 }
126 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
127
128 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
129 label: desc.label,
130 bind_group_layouts: &bind_group_layouts,
131 immediate_size: 0,
132 });
133
134 let targets: Vec<Option<wgpu::ColorTargetState>> =
135 desc.targets.iter().cloned().map(Some).collect();
136
137 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
138 label: desc.label,
139 layout: Some(&pipeline_layout),
140 vertex: wgpu::VertexState {
141 module: &module,
142 entry_point: desc.vertex_entry,
143 compilation_options: Default::default(),
144 buffers: &desc.vertex_layouts,
145 },
146 primitive: wgpu::PrimitiveState {
147 topology: wgpu::PrimitiveTopology::TriangleList,
148 strip_index_format: None,
149 front_face: wgpu::FrontFace::Ccw,
150 cull_mode: desc.cull_mode,
151 unclipped_depth: false,
152 polygon_mode: desc.polygon_mode,
153 conservative: false,
154 },
155 depth_stencil: desc.depth.clone(),
156 multisample: wgpu::MultisampleState::default(),
157 fragment: Some(wgpu::FragmentState {
158 module: &module,
159 entry_point: desc.fragment_entry,
160 compilation_options: Default::default(),
161 targets: &targets,
162 }),
163 multiview_mask: None,
164 cache: None,
165 });
166
167 (pipeline, layout)
168}
169
170pub struct GPUMaterial {
175 pub pipeline: wgpu::RenderPipeline,
176 pub layout: wgpu::BindGroupLayout,
177 pub entries: Vec<BindingEntry>,
178}
179
180impl super::binding::BindGroupTarget for GPUMaterial {
181 fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
182 &self.layout
183 }
184 fn binding_entries(&self) -> &[BindingEntry] {
185 &self.entries
186 }
187}
188
189impl Asset<WGPUBackend> for GPUMaterial {
190 type Source = MaterialDescriptor<'static>;
191 type Deps<'a> = ();
192
193 fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
194 let (pipeline, layout) = build_material(&backend.device, source);
195
196 Some(Self {
197 pipeline,
198 layout,
199 entries: source.entries.to_vec(),
200 })
201 }
202}
203
204crate::wgpu::plugin_macros::asset_plugin! {
205 MaterialPlugin, GPUMaterial
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::wgpu::binding::{BindingEntry, BindingKind};
216 use crate::wgpu::test_util::with_device;
217
218 const MINIMAL_SHADER: &str = r#"
219 @vertex
220 fn vs_main() -> @builtin(position) vec4<f32> {
221 return vec4<f32>(0.0, 0.0, 0.0, 1.0);
222 }
223 @fragment
224 fn fs_main() -> @location(0) vec4<f32> {
225 return vec4<f32>(1.0, 1.0, 1.0, 1.0);
226 }
227 "#;
228
229 #[test]
230 fn a_compute_visible_entry_panics_before_touching_the_device() {
231 with_device!(device, _queue, {
232 let desc = MaterialDescriptor {
233 shader_source: MINIMAL_SHADER,
234 entries: vec![BindingEntry {
235 name: "bad",
236 binding: 0,
237 kind: BindingKind::storage_buffer_read_write(wgpu::ShaderStages::COMPUTE),
238 }],
239 targets: DEFAULT_TARGET.to_vec(),
240 ..Default::default()
241 };
242 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
243 build_material(&device, &desc);
244 }));
245 assert!(result.is_err(), "expected a panic for a COMPUTE-visible material entry");
246 });
247 }
248
249 #[test]
250 fn a_fragment_visible_entry_builds_without_panicking() {
251 with_device!(device, _queue, {
252 let desc = MaterialDescriptor {
253 shader_source: MINIMAL_SHADER,
254 entries: vec![],
255 own_group: None,
256 targets: DEFAULT_TARGET.to_vec(),
257 ..Default::default()
258 };
259 build_material(&device, &desc);
260 });
261 }
262}