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
41impl Default for Compute {
42 fn default() -> Self {
43 Self {
44 label: None,
45 shader_source: "",
46 entry_point: Some("cs_main"),
47 groups: Vec::new(),
48 }
49 }
50}
51
52impl Compute {
53 pub fn new(shader_source: &'static str) -> Self {
56 Self { shader_source, ..Self::default() }
57 }
58
59 pub fn label(mut self, label: &'static str) -> Self {
60 self.label = Some(label);
61 self
62 }
63
64 pub fn entry_point(mut self, entry: &'static str) -> Self {
65 self.entry_point = Some(entry);
66 self
67 }
68
69 pub fn entries(mut self, groups: Vec<super::layout::GroupEntry>) -> Self {
88 self.groups = groups;
89 self
90 }
91
92 fn validate(&self) {
96 if self.groups.is_empty() {
97 tracing::warn!(
98 "Compute{}: no bind groups at all — this pass can't read or write anything; \
99 consider calling .entries(...)",
100 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
101 );
102 }
103 }
104
105 pub fn build(self) -> Self {
107 self.validate();
108 self
109 }
110
111 pub fn build_asset(self, name: &str, assets: &mut Assets<Self>) -> Handle<Self> {
114 self.validate();
115 assets.insert(name, self)
116 }
117}
118
119pub fn build_compute(
139 backend: &WGPUBackend,
140 desc: &Compute,
141 pool: &super::layout::GlobalLayoutPool,
142) -> Option<(ComputePipeline, BindGroupLayout)> {
143 build_compute_raw(&backend.device, desc, pool)
144}
145
146pub(crate) fn build_compute_raw(
149 device: &wgpu::Device,
150 desc: &Compute,
151 pool: &super::layout::GlobalLayoutPool,
152) -> Option<(ComputePipeline, BindGroupLayout)> {
153 let own_entries =
154 super::layout::find_own_entries(desc.label, super::layout::PipelineKind::Compute, &desc.groups);
155 for entry in own_entries {
156 if entry.kind.visibility() != ShaderStages::COMPUTE {
157 panic!(
158 "compute pass{}: entry '{}' is not visible to exactly the compute stage — \
159 compute bind group entries must be visible to exactly COMPUTE",
160 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
161 entry.name,
162 );
163 }
164 }
165
166 let layout = BindGroupLayoutBuilder::new()
167 .label(desc.label)
168 .entries(own_entries.iter().cloned())
169 .build_raw(device);
170
171 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
172 label: desc.label,
173 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
174 });
175
176 let bind_group_layouts = super::layout::assemble_group_layouts(
177 desc.label,
178 &desc.groups,
179 &layout,
180 pool,
181 device.limits().max_bind_groups,
182 )?;
183
184 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
185 label: desc.label,
186 bind_group_layouts: &bind_group_layouts,
187 immediate_size: 0,
188 });
189
190 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
191 label: desc.label,
192 layout: Some(&pipeline_layout),
193 module: &module,
194 entry_point: desc.entry_point,
195 compilation_options: Default::default(),
196 cache: None,
197 });
198
199 Some((ComputePipeline(pipeline), layout))
200}
201
202pub struct GPUCompute {
205 pub pipeline: ComputePipeline,
206 layout: BindGroupLayout,
207 entries: Vec<BindingEntry>,
208}
209
210impl super::binding::BindGroupTarget for GPUCompute {
211 fn bind_group_layout(&self) -> &BindGroupLayout {
212 &self.layout
213 }
214 fn binding_entries(&self) -> &[BindingEntry] {
215 &self.entries
216 }
217}
218
219impl Asset<WGPUBackend> for GPUCompute {
220 type Source = Compute;
221 type Deps<'a> = crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>;
222
223 fn upload<'a>(
224 source: &Compute,
225 backend: &WGPUBackend,
226 pool: &crate::ecs::system::Res<'a, super::layout::GlobalLayoutPool>,
227 ) -> Option<Self> {
228 let (pipeline, layout) = build_compute(backend, source, pool)?;
229 let entries =
230 super::layout::find_own_entries(source.label, super::layout::PipelineKind::Compute, &source.groups)
231 .to_vec();
232
233 Some(Self { pipeline, layout, entries })
234 }
235}
236
237crate::wgpu::plugin_macros::asset_plugin! {
238 ComputePlugin, GPUCompute
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::wgpu::binding::{BindingEntry, BindingKind};
249 use crate::wgpu::test_util::with_device;
250
251 const MINIMAL_COMPUTE_SHADER: &str = r#"
252 @compute @workgroup_size(1)
253 fn cs_main() {}
254 "#;
255
256 #[test]
257 fn a_fragment_visible_own_entry_panics_before_touching_the_device() {
258 with_device!(device, _queue, {
259 let pool = super::super::layout::GlobalLayoutPool::new();
260 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
261 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
262 name: "bad",
263 binding: 0,
264 kind: BindingKind::sampler(ShaderStages::FRAGMENT),
265 }])])
266 .build();
267 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
268 build_compute_raw(&device, &desc, &pool);
269 }));
270 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
271 });
272 }
273
274 #[test]
275 fn a_vertex_fragment_visible_own_entry_also_panics() {
276 with_device!(device, _queue, {
281 let pool = super::super::layout::GlobalLayoutPool::new();
282 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
283 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
284 name: "bad",
285 binding: 0,
286 kind: BindingKind::storage_buffer_read_write(
287 ShaderStages::COMPUTE | ShaderStages::FRAGMENT,
288 ),
289 }])])
290 .build();
291 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
292 build_compute_raw(&device, &desc, &pool);
293 }));
294 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
295 });
296 }
297
298 #[test]
299 fn no_entries_at_all_builds_without_panicking() {
300 with_device!(device, _queue, {
301 let pool = super::super::layout::GlobalLayoutPool::new();
302 let desc = Compute::new(MINIMAL_COMPUTE_SHADER).build();
303 build_compute_raw(&device, &desc, &pool).unwrap();
304 });
305 }
306
307 #[test]
308 fn a_layout_pulled_from_the_global_pool_ends_up_in_the_pipeline_layout() {
309 with_device!(device, _queue, {
310 let mut pool = super::super::layout::GlobalLayoutPool::new();
311 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
312
313 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
314 .entries(vec![super::super::layout::GroupEntry::Layout(pool.get("camera").unwrap())])
315 .build();
316
317 build_compute_raw(&device, &desc, &pool).unwrap();
318 });
319 }
320
321 #[test]
322 fn a_global_entry_resolves_from_the_pool_at_build_time() {
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 = Compute::new(MINIMAL_COMPUTE_SHADER)
328 .entries(vec![super::super::layout::GroupEntry::Global("camera")])
329 .build();
330
331 build_compute_raw(&device, &desc, &pool).unwrap();
332 });
333 }
334
335 #[test]
336 fn a_global_entry_not_yet_registered_returns_none_instead_of_panicking() {
337 with_device!(device, _queue, {
338 let pool = super::super::layout::GlobalLayoutPool::new(); let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
340 .entries(vec![super::super::layout::GroupEntry::Global("camera")])
341 .build();
342
343 assert!(build_compute_raw(&device, &desc, &pool).is_none());
344 });
345 }
346
347 #[test]
348 fn own_and_layout_groups_are_ordered_by_position() {
349 with_device!(device, _queue, {
350 let pool = super::super::layout::GlobalLayoutPool::new();
351 let extra = crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device);
352 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
353 .entries(vec![
354 super::super::layout::GroupEntry::Own(vec![]),
355 super::super::layout::GroupEntry::Layout(extra),
356 ])
357 .build();
358
359 build_compute_raw(&device, &desc, &pool).unwrap();
360 });
361 }
362
363 #[test]
364 fn more_than_one_own_group_panics() {
365 with_device!(device, _queue, {
366 let pool = super::super::layout::GlobalLayoutPool::new();
367 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
368 .entries(vec![
369 super::super::layout::GroupEntry::Own(vec![]),
370 super::super::layout::GroupEntry::Own(vec![]),
371 ])
372 .build();
373
374 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
375 build_compute_raw(&device, &desc, &pool);
376 }));
377 assert!(result.is_err(), "expected a panic for more than one Own group");
378 });
379 }
380
381 #[test]
382 fn exceeding_max_bind_groups_panics() {
383 with_device!(device, _queue, {
384 let pool = super::super::layout::GlobalLayoutPool::new();
385 let groups: Vec<super::super::layout::GroupEntry> = (0..5)
387 .map(|_| {
388 super::super::layout::GroupEntry::Layout(
389 crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device),
390 )
391 })
392 .collect();
393 let desc = Compute::new(MINIMAL_COMPUTE_SHADER).entries(groups).build();
394
395 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
396 build_compute_raw(&device, &desc, &pool);
397 }));
398 assert!(result.is_err(), "expected a panic for exceeding max_bind_groups");
399 });
400 }
401}