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(backend: &WGPUBackend, desc: &Compute) -> (ComputePipeline, BindGroupLayout) {
134 build_compute_raw(&backend.device, desc)
135}
136
137pub(crate) fn build_compute_raw(
140 device: &wgpu::Device,
141 desc: &Compute,
142) -> (ComputePipeline, BindGroupLayout) {
143 let own_entries =
144 super::layout::find_own_entries(desc.label, super::layout::PipelineKind::Compute, &desc.groups);
145 for entry in own_entries {
146 if entry.kind.visibility() != ShaderStages::COMPUTE {
147 panic!(
148 "compute pass{}: entry '{}' is not visible to exactly the compute stage — \
149 compute bind group entries must be visible to exactly COMPUTE",
150 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
151 entry.name,
152 );
153 }
154 }
155
156 let layout = BindGroupLayoutBuilder::new()
157 .label(desc.label)
158 .entries(own_entries.iter().cloned())
159 .build_raw(device);
160
161 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
162 label: desc.label,
163 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
164 });
165
166 let bind_group_layouts = super::layout::assemble_group_layouts(
167 desc.label,
168 &desc.groups,
169 &layout,
170 device.limits().max_bind_groups,
171 );
172
173 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
174 label: desc.label,
175 bind_group_layouts: &bind_group_layouts,
176 immediate_size: 0,
177 });
178
179 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
180 label: desc.label,
181 layout: Some(&pipeline_layout),
182 module: &module,
183 entry_point: desc.entry_point,
184 compilation_options: Default::default(),
185 cache: None,
186 });
187
188 (ComputePipeline(pipeline), layout)
189}
190
191pub struct GPUCompute {
194 pub pipeline: ComputePipeline,
195 layout: BindGroupLayout,
196 entries: Vec<BindingEntry>,
197}
198
199impl super::binding::BindGroupTarget for GPUCompute {
200 fn bind_group_layout(&self) -> &BindGroupLayout {
201 &self.layout
202 }
203 fn binding_entries(&self) -> &[BindingEntry] {
204 &self.entries
205 }
206}
207
208impl Asset<WGPUBackend> for GPUCompute {
209 type Source = Compute;
210 type Deps<'a> = ();
211
212 fn upload<'a>(source: &Compute, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
213 let (pipeline, layout) = build_compute(backend, source);
214 let entries =
215 super::layout::find_own_entries(source.label, super::layout::PipelineKind::Compute, &source.groups)
216 .to_vec();
217
218 Some(Self { pipeline, layout, entries })
219 }
220}
221
222crate::wgpu::plugin_macros::asset_plugin! {
223 ComputePlugin, GPUCompute
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::wgpu::binding::{BindingEntry, BindingKind};
234 use crate::wgpu::test_util::with_device;
235
236 const MINIMAL_COMPUTE_SHADER: &str = r#"
237 @compute @workgroup_size(1)
238 fn cs_main() {}
239 "#;
240
241 #[test]
242 fn a_fragment_visible_own_entry_panics_before_touching_the_device() {
243 with_device!(device, _queue, {
244 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
245 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
246 name: "bad",
247 binding: 0,
248 kind: BindingKind::sampler(ShaderStages::FRAGMENT),
249 }])])
250 .build();
251 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
252 build_compute_raw(&device, &desc);
253 }));
254 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
255 });
256 }
257
258 #[test]
259 fn a_vertex_fragment_visible_own_entry_also_panics() {
260 with_device!(device, _queue, {
265 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
266 .entries(vec![super::super::layout::GroupEntry::Own(vec![BindingEntry {
267 name: "bad",
268 binding: 0,
269 kind: BindingKind::storage_buffer_read_write(
270 ShaderStages::COMPUTE | ShaderStages::FRAGMENT,
271 ),
272 }])])
273 .build();
274 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
275 build_compute_raw(&device, &desc);
276 }));
277 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
278 });
279 }
280
281 #[test]
282 fn no_entries_at_all_builds_without_panicking() {
283 with_device!(device, _queue, {
284 let desc = Compute::new(MINIMAL_COMPUTE_SHADER).build();
285 build_compute_raw(&device, &desc);
286 });
287 }
288
289 #[test]
290 fn a_layout_pulled_from_the_global_pool_ends_up_in_the_pipeline_layout() {
291 with_device!(device, _queue, {
292 let mut pool = super::super::layout::GlobalLayoutPool::new();
293 pool.register("camera", crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device));
294
295 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
296 .entries(vec![super::super::layout::GroupEntry::Layout(pool.get("camera").unwrap())])
297 .build();
298
299 build_compute_raw(&device, &desc);
300 });
301 }
302
303 #[test]
304 fn own_and_layout_groups_are_ordered_by_position() {
305 with_device!(device, _queue, {
306 let extra = crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device);
307 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
308 .entries(vec![
309 super::super::layout::GroupEntry::Own(vec![]),
310 super::super::layout::GroupEntry::Layout(extra),
311 ])
312 .build();
313
314 build_compute_raw(&device, &desc);
315 });
316 }
317
318 #[test]
319 fn more_than_one_own_group_panics() {
320 with_device!(device, _queue, {
321 let desc = Compute::new(MINIMAL_COMPUTE_SHADER)
322 .entries(vec![
323 super::super::layout::GroupEntry::Own(vec![]),
324 super::super::layout::GroupEntry::Own(vec![]),
325 ])
326 .build();
327
328 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
329 build_compute_raw(&device, &desc);
330 }));
331 assert!(result.is_err(), "expected a panic for more than one Own group");
332 });
333 }
334
335 #[test]
336 fn exceeding_max_bind_groups_panics() {
337 with_device!(device, _queue, {
338 let groups: Vec<super::super::layout::GroupEntry> = (0..5)
340 .map(|_| {
341 super::super::layout::GroupEntry::Layout(
342 crate::wgpu::binding::BindGroupLayoutBuilder::new().build_raw(&device),
343 )
344 })
345 .collect();
346 let desc = Compute::new(MINIMAL_COMPUTE_SHADER).entries(groups).build();
347
348 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
349 build_compute_raw(&device, &desc);
350 }));
351 assert!(result.is_err(), "expected a panic for exceeding max_bind_groups");
352 });
353 }
354}