1use crate::{
2 assets::upload::Asset,
3 wgpu::{backend::WGPUBackend, binding::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 = super::binding::build_bind_group_layout(device, desc.label, &desc.entries);
109
110 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
111 label: desc.label,
112 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
113 });
114
115 let mut slots: Vec<super::layout::GroupLayout> = desc
116 .extra_layouts
117 .iter()
118 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
119 .collect();
120 if let Some(own_group) = desc.own_group {
121 slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
122 }
123 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
124
125 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
126 label: desc.label,
127 bind_group_layouts: &bind_group_layouts,
128 immediate_size: 0,
129 });
130
131 let targets: Vec<Option<wgpu::ColorTargetState>> =
132 desc.targets.iter().cloned().map(Some).collect();
133
134 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
135 label: desc.label,
136 layout: Some(&pipeline_layout),
137 vertex: wgpu::VertexState {
138 module: &module,
139 entry_point: desc.vertex_entry,
140 compilation_options: Default::default(),
141 buffers: &desc.vertex_layouts,
142 },
143 primitive: wgpu::PrimitiveState {
144 topology: wgpu::PrimitiveTopology::TriangleList,
145 strip_index_format: None,
146 front_face: wgpu::FrontFace::Ccw,
147 cull_mode: desc.cull_mode,
148 unclipped_depth: false,
149 polygon_mode: desc.polygon_mode,
150 conservative: false,
151 },
152 depth_stencil: desc.depth.clone(),
153 multisample: wgpu::MultisampleState::default(),
154 fragment: Some(wgpu::FragmentState {
155 module: &module,
156 entry_point: desc.fragment_entry,
157 compilation_options: Default::default(),
158 targets: &targets,
159 }),
160 multiview_mask: None,
161 cache: None,
162 });
163
164 (pipeline, layout)
165}
166
167pub struct GPUMaterial {
172 pub pipeline: wgpu::RenderPipeline,
173 pub layout: wgpu::BindGroupLayout,
174 pub entries: Vec<BindingEntry>,
175}
176
177impl super::binding::BindGroupTarget for GPUMaterial {
178 fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
179 &self.layout
180 }
181 fn binding_entries(&self) -> &[BindingEntry] {
182 &self.entries
183 }
184}
185
186impl Asset<WGPUBackend> for GPUMaterial {
187 type Source = MaterialDescriptor<'static>;
188 type Deps<'a> = ();
189
190 fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
191 let (pipeline, layout) = build_material(&backend.device, source);
192
193 Some(Self {
194 pipeline,
195 layout,
196 entries: source.entries.to_vec(),
197 })
198 }
199}
200
201crate::wgpu::plugin_macros::asset_plugin! {
202 MaterialPlugin, GPUMaterial
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::wgpu::binding::{BindingEntry, BindingKind};
213 use crate::wgpu::test_util::with_device;
214
215 const MINIMAL_SHADER: &str = r#"
216 @vertex
217 fn vs_main() -> @builtin(position) vec4<f32> {
218 return vec4<f32>(0.0, 0.0, 0.0, 1.0);
219 }
220 @fragment
221 fn fs_main() -> @location(0) vec4<f32> {
222 return vec4<f32>(1.0, 1.0, 1.0, 1.0);
223 }
224 "#;
225
226 #[test]
227 fn a_compute_visible_entry_panics_before_touching_the_device() {
228 with_device!(device, _queue, {
229 let desc = MaterialDescriptor {
230 shader_source: MINIMAL_SHADER,
231 entries: vec![BindingEntry {
232 name: "bad",
233 binding: 0,
234 kind: BindingKind::storage_buffer_read_write(wgpu::ShaderStages::COMPUTE),
235 }],
236 targets: DEFAULT_TARGET.to_vec(),
237 ..Default::default()
238 };
239 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
240 build_material(&device, &desc);
241 }));
242 assert!(result.is_err(), "expected a panic for a COMPUTE-visible material entry");
243 });
244 }
245
246 #[test]
247 fn a_fragment_visible_entry_builds_without_panicking() {
248 with_device!(device, _queue, {
249 let desc = MaterialDescriptor {
250 shader_source: MINIMAL_SHADER,
251 entries: vec![],
252 own_group: None,
253 targets: DEFAULT_TARGET.to_vec(),
254 ..Default::default()
255 };
256 build_material(&device, &desc);
257 });
258 }
259}