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