1use crate::{
2 assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3 wgpu::{
4 backend::WGPUBackend,
5 binding::{BindGroupLayout, BindGroupLayoutBuilder, BindingEntry},
6 flags::ShaderStages,
7 },
8};
9
10pub struct ComputePipeline(wgpu::ComputePipeline);
17
18impl ComputePipeline {
19 pub(crate) fn raw(&self) -> &wgpu::ComputePipeline {
20 &self.0
21 }
22}
23
24pub struct Compute {
29 label: Option<&'static str>,
32 shader_source: &'static str,
34 entry_point: Option<&'static str>,
36 groups: Vec<super::layout::GroupEntry>,
39}
40
41pub struct ComputeBuilder {
44 label: Option<&'static str>,
45 shader_source: &'static str,
46 entry_point: Option<&'static str>,
47 groups: Vec<super::layout::GroupEntry>,
48}
49
50impl Default for ComputeBuilder {
51 fn default() -> Self {
52 Self {
53 label: None,
54 shader_source: "",
55 entry_point: Some("cs_main"),
56 groups: Vec::new(),
57 }
58 }
59}
60
61impl ComputeBuilder {
62 pub fn new(shader_source: &'static str) -> Self {
65 Self { shader_source, ..Self::default() }
66 }
67
68 pub fn with_label(mut self, label: &'static str) -> Self {
69 self.label = Some(label);
70 self
71 }
72
73 pub fn with_entry_point(mut self, entry: &'static str) -> Self {
74 self.entry_point = Some(entry);
75 self
76 }
77
78 pub fn with_entries(mut self, groups: Vec<super::layout::GroupEntry>) -> Self {
97 self.groups = groups;
98 self
99 }
100
101 fn validate(&self) {
105 if self.groups.is_empty() {
106 tracing::warn!(
107 "ComputeBuilder{}: no bind groups at all — this pass can't read or write \
108 anything; consider calling .with_entries(...)",
109 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
110 );
111 }
112 }
113
114 pub fn build(self) -> Compute {
116 self.validate();
117 Compute {
118 label: self.label,
119 shader_source: self.shader_source,
120 entry_point: self.entry_point,
121 groups: self.groups,
122 }
123 }
124
125 pub fn build_asset(self, name: &str, assets: &mut Assets<Compute>) -> Handle<Compute> {
128 let compute = self.build();
129 assets.insert(name, compute)
130 }
131}
132
133pub fn build_compute(
153 backend: &WGPUBackend,
154 desc: &Compute,
155 pool: &super::layout::GlobalLayoutPool,
156) -> Option<(ComputePipeline, BindGroupLayout)> {
157 build_compute_raw(&backend.device, desc, pool)
158}
159
160pub(crate) fn build_compute_raw(
163 device: &wgpu::Device,
164 desc: &Compute,
165 pool: &super::layout::GlobalLayoutPool,
166) -> Option<(ComputePipeline, BindGroupLayout)> {
167 let own_entries =
168 super::layout::find_own_entries(desc.label, super::layout::PipelineKind::Compute, &desc.groups);
169 for entry in own_entries {
170 if entry.kind.visibility() != ShaderStages::COMPUTE {
171 panic!(
172 "compute pass{}: entry '{}' is not visible to exactly the compute stage — \
173 compute bind group entries must be visible to exactly COMPUTE",
174 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
175 entry.name,
176 );
177 }
178 }
179
180 let layout = BindGroupLayoutBuilder::new()
181 .with_label(desc.label)
182 .with_entries(own_entries.iter().cloned())
183 .build_raw(device);
184
185 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
186 label: desc.label,
187 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
188 });
189
190 let bind_group_layouts = super::layout::assemble_group_layouts(
191 desc.label,
192 &desc.groups,
193 &layout,
194 pool,
195 device.limits().max_bind_groups,
196 )?;
197
198 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
199 label: desc.label,
200 bind_group_layouts: &bind_group_layouts,
201 immediate_size: 0,
202 });
203
204 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
205 label: desc.label,
206 layout: Some(&pipeline_layout),
207 module: &module,
208 entry_point: desc.entry_point,
209 compilation_options: Default::default(),
210 cache: None,
211 });
212
213 Some((ComputePipeline(pipeline), layout))
214}
215
216pub struct GPUCompute {
219 pub pipeline: ComputePipeline,
220 layout: BindGroupLayout,
221 entries: Vec<BindingEntry>,
222}
223
224impl super::binding::BindGroupTarget for GPUCompute {
225 fn bind_group_layout(&self) -> &BindGroupLayout {
226 &self.layout
227 }
228 fn binding_entries(&self) -> &[BindingEntry] {
229 &self.entries
230 }
231}
232
233impl AssetSource for Compute {
234 type Processed = GPUCompute;
235}
236
237impl Asset<WGPUBackend> for Compute {
238 type Deps<'a> = crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>;
239
240 fn upload<'a>(
241 &self,
242 backend: &WGPUBackend,
243 pool: &crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>,
244 ) -> Option<GPUCompute> {
245 let (pipeline, layout) = build_compute(backend, self, pool)?;
246 let entries =
247 super::layout::find_own_entries(self.label, super::layout::PipelineKind::Compute, &self.groups)
248 .to_vec();
249
250 Some(GPUCompute { pipeline, layout, entries })
251 }
252}
253
254crate::wgpu::plugin_macros::asset_plugin! {
255 ComputePlugin, Compute
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::wgpu::binding::{BindingEntry, BindingKind};
265 use crate::wgpu::test_util::with_device;
266
267 const MINIMAL_COMPUTE_SHADER: &str = r#"
268 @compute @workgroup_size(1)
269 fn cs_main() {}
270 "#;
271
272 #[test]
273 fn a_fragment_visible_own_entry_panics_before_touching_the_device() {
274 with_device!(device, _queue, {
275 let pool = super::super::layout::GlobalLayoutPool::new();
276 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
277 .with_entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
278 name: "bad",
279 binding: 0,
280 kind: BindingKind::sampler(ShaderStages::FRAGMENT),
281 }])])
282 .build();
283 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
284 build_compute_raw(&device, &desc, &pool);
285 }));
286 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
287 });
288 }
289
290 #[test]
291 fn a_vertex_fragment_visible_own_entry_also_panics() {
292 with_device!(device, _queue, {
297 let pool = super::super::layout::GlobalLayoutPool::new();
298 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
299 .with_entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
300 name: "bad",
301 binding: 0,
302 kind: BindingKind::storage_buffer_read_write(
303 ShaderStages::COMPUTE | ShaderStages::FRAGMENT,
304 ),
305 }])])
306 .build();
307 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
308 build_compute_raw(&device, &desc, &pool);
309 }));
310 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
311 });
312 }
313
314 #[test]
315 fn no_entries_at_all_builds_without_panicking() {
316 with_device!(device, _queue, {
317 let pool = super::super::layout::GlobalLayoutPool::new();
318 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER).build();
319 build_compute_raw(&device, &desc, &pool).unwrap();
320 });
321 }
322
323 #[test]
324 fn a_layout_pulled_from_the_global_pool_ends_up_in_the_pipeline_layout() {
325 with_device!(device, _queue, {
326 let mut pool = super::super::layout::GlobalLayoutPool::new();
327 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
328
329 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
330 .with_entries(vec![super::super::layout::GroupEntry::Layout(pool.get("camera").unwrap())])
331 .build();
332
333 build_compute_raw(&device, &desc, &pool).unwrap();
334 });
335 }
336
337 #[test]
338 fn a_global_entry_resolves_from_the_pool_at_build_time() {
339 with_device!(device, _queue, {
340 let mut pool = super::super::layout::GlobalLayoutPool::new();
341 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
342
343 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
344 .with_entries(vec![super::super::layout::GroupEntry::Global("camera")])
345 .build();
346
347 build_compute_raw(&device, &desc, &pool).unwrap();
348 });
349 }
350
351 #[test]
352 fn a_global_entry_not_yet_registered_returns_none_instead_of_panicking() {
353 with_device!(device, _queue, {
354 let pool = super::super::layout::GlobalLayoutPool::new(); let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
356 .with_entries(vec![super::super::layout::GroupEntry::Global("camera")])
357 .build();
358
359 assert!(build_compute_raw(&device, &desc, &pool).is_none());
360 });
361 }
362
363 #[test]
364 fn own_and_layout_groups_are_ordered_by_position() {
365 with_device!(device, _queue, {
366 let pool = super::super::layout::GlobalLayoutPool::new();
367 let extra = crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device);
368 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
369 .with_entries(vec![
370 super::super::layout::GroupEntry::Own(vec![]),
371 super::super::layout::GroupEntry::Layout(extra),
372 ])
373 .build();
374
375 build_compute_raw(&device, &desc, &pool).unwrap();
376 });
377 }
378
379 #[test]
380 fn more_than_one_own_group_panics() {
381 with_device!(device, _queue, {
382 let pool = super::super::layout::GlobalLayoutPool::new();
383 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
384 .with_entries(vec![
385 super::super::layout::GroupEntry::Own(vec![]),
386 super::super::layout::GroupEntry::Own(vec![]),
387 ])
388 .build();
389
390 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
391 build_compute_raw(&device, &desc, &pool);
392 }));
393 assert!(result.is_err(), "expected a panic for more than one Own group");
394 });
395 }
396
397 #[test]
398 fn exceeding_max_bind_groups_panics() {
399 with_device!(device, _queue, {
400 let pool = super::super::layout::GlobalLayoutPool::new();
401 let groups: Vec<super::super::layout::GroupEntry> = (0..5)
403 .map(|_| {
404 super::super::layout::GroupEntry::Layout(
405 crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device),
406 )
407 })
408 .collect();
409 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER).with_entries(groups).build();
410
411 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
412 build_compute_raw(&device, &desc, &pool);
413 }));
414 assert!(result.is_err(), "expected a panic for exceeding max_bind_groups");
415 });
416 }
417}