1use crate::{
2 assets::{handle::Handle, storage::Assets, upload::Asset},
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 label(mut self, label: &'static str) -> Self {
69 self.label = Some(label);
70 self
71 }
72
73 pub fn entry_point(mut self, entry: &'static str) -> Self {
74 self.entry_point = Some(entry);
75 self
76 }
77
78 pub fn 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 .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 .label(desc.label)
182 .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 Asset<WGPUBackend> for GPUCompute {
234 type Source = Compute;
235 type Deps<'a> = crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>;
236
237 fn upload<'a>(
238 source: &Compute,
239 backend: &WGPUBackend,
240 pool: &crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>,
241 ) -> Option<Self> {
242 let (pipeline, layout) = build_compute(backend, source, pool)?;
243 let entries =
244 super::layout::find_own_entries(source.label, super::layout::PipelineKind::Compute, &source.groups)
245 .to_vec();
246
247 Some(Self { pipeline, layout, entries })
248 }
249}
250
251crate::wgpu::plugin_macros::asset_plugin! {
252 ComputePlugin, GPUCompute
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::wgpu::binding::{BindingEntry, BindingKind};
263 use crate::wgpu::test_util::with_device;
264
265 const MINIMAL_COMPUTE_SHADER: &str = r#"
266 @compute @workgroup_size(1)
267 fn cs_main() {}
268 "#;
269
270 #[test]
271 fn a_fragment_visible_own_entry_panics_before_touching_the_device() {
272 with_device!(device, _queue, {
273 let pool = super::super::layout::GlobalLayoutPool::new();
274 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
275 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
276 name: "bad",
277 binding: 0,
278 kind: BindingKind::sampler(ShaderStages::FRAGMENT),
279 }])])
280 .build();
281 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
282 build_compute_raw(&device, &desc, &pool);
283 }));
284 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
285 });
286 }
287
288 #[test]
289 fn a_vertex_fragment_visible_own_entry_also_panics() {
290 with_device!(device, _queue, {
295 let pool = super::super::layout::GlobalLayoutPool::new();
296 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
297 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
298 name: "bad",
299 binding: 0,
300 kind: BindingKind::storage_buffer_read_write(
301 ShaderStages::COMPUTE | ShaderStages::FRAGMENT,
302 ),
303 }])])
304 .build();
305 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
306 build_compute_raw(&device, &desc, &pool);
307 }));
308 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
309 });
310 }
311
312 #[test]
313 fn no_entries_at_all_builds_without_panicking() {
314 with_device!(device, _queue, {
315 let pool = super::super::layout::GlobalLayoutPool::new();
316 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER).build();
317 build_compute_raw(&device, &desc, &pool).unwrap();
318 });
319 }
320
321 #[test]
322 fn a_layout_pulled_from_the_global_pool_ends_up_in_the_pipeline_layout() {
323 with_device!(device, _queue, {
324 let mut pool = super::super::layout::GlobalLayoutPool::new();
325 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
326
327 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
328 .entries(vec![super::super::layout::GroupEntry::Layout(pool.get("camera").unwrap())])
329 .build();
330
331 build_compute_raw(&device, &desc, &pool).unwrap();
332 });
333 }
334
335 #[test]
336 fn a_global_entry_resolves_from_the_pool_at_build_time() {
337 with_device!(device, _queue, {
338 let mut pool = super::super::layout::GlobalLayoutPool::new();
339 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
340
341 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
342 .entries(vec![super::super::layout::GroupEntry::Global("camera")])
343 .build();
344
345 build_compute_raw(&device, &desc, &pool).unwrap();
346 });
347 }
348
349 #[test]
350 fn a_global_entry_not_yet_registered_returns_none_instead_of_panicking() {
351 with_device!(device, _queue, {
352 let pool = super::super::layout::GlobalLayoutPool::new(); let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
354 .entries(vec![super::super::layout::GroupEntry::Global("camera")])
355 .build();
356
357 assert!(build_compute_raw(&device, &desc, &pool).is_none());
358 });
359 }
360
361 #[test]
362 fn own_and_layout_groups_are_ordered_by_position() {
363 with_device!(device, _queue, {
364 let pool = super::super::layout::GlobalLayoutPool::new();
365 let extra = crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device);
366 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
367 .entries(vec![
368 super::super::layout::GroupEntry::Own(vec![]),
369 super::super::layout::GroupEntry::Layout(extra),
370 ])
371 .build();
372
373 build_compute_raw(&device, &desc, &pool).unwrap();
374 });
375 }
376
377 #[test]
378 fn more_than_one_own_group_panics() {
379 with_device!(device, _queue, {
380 let pool = super::super::layout::GlobalLayoutPool::new();
381 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER)
382 .entries(vec![
383 super::super::layout::GroupEntry::Own(vec![]),
384 super::super::layout::GroupEntry::Own(vec![]),
385 ])
386 .build();
387
388 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
389 build_compute_raw(&device, &desc, &pool);
390 }));
391 assert!(result.is_err(), "expected a panic for more than one Own group");
392 });
393 }
394
395 #[test]
396 fn exceeding_max_bind_groups_panics() {
397 with_device!(device, _queue, {
398 let pool = super::super::layout::GlobalLayoutPool::new();
399 let groups: Vec<super::super::layout::GroupEntry> = (0..5)
401 .map(|_| {
402 super::super::layout::GroupEntry::Layout(
403 crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device),
404 )
405 })
406 .collect();
407 let desc = ComputeBuilder::new(MINIMAL_COMPUTE_SHADER).entries(groups).build();
408
409 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
410 build_compute_raw(&device, &desc, &pool);
411 }));
412 assert!(result.is_err(), "expected a panic for exceeding max_bind_groups");
413 });
414 }
415}