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 entries: Vec<BindingEntry>,
41 own_group: Option<u32>,
44 extra_layouts: Vec<super::layout::OwnedGroupLayout>,
49}
50
51impl Default for Compute {
52 fn default() -> Self {
53 Self {
54 label: None,
55 shader_source: "",
56 entry_point: Some("cs_main"),
57 entries: Vec::new(),
58 own_group: Some(0),
59 extra_layouts: Vec::new(),
60 }
61 }
62}
63
64impl Compute {
65 pub fn new(shader_source: &'static str) -> Self {
68 Self { shader_source, ..Self::default() }
69 }
70
71 pub fn label(mut self, label: &'static str) -> Self {
72 self.label = Some(label);
73 self
74 }
75
76 pub fn entry_point(mut self, entry: &'static str) -> Self {
77 self.entry_point = Some(entry);
78 self
79 }
80
81 pub fn entries(mut self, entries: Vec<BindingEntry>) -> Self {
82 self.entries = entries;
83 self
84 }
85
86 pub fn own_group(mut self, group: u32) -> Self {
87 self.own_group = Some(group);
88 self
89 }
90
91 pub fn no_own_group(mut self) -> Self {
96 self.own_group = None;
97 self
98 }
99
100 pub fn extra_layouts(mut self, layouts: Vec<super::layout::OwnedGroupLayout>) -> Self {
101 self.extra_layouts = layouts;
102 self
103 }
104
105 fn validate(&self) {
110 if self.entries.is_empty() && self.extra_layouts.is_empty() {
111 tracing::warn!(
112 "Compute{}: no bind group entries at all — this pass can't read or write \
113 anything; consider calling .entries(...) or .extra_layouts(...)",
114 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
115 );
116 }
117 }
118
119 pub fn build(self) -> Self {
121 self.validate();
122 self
123 }
124
125 pub fn build_asset(self, name: &str, assets: &mut Assets<Self>) -> Handle<Self> {
128 self.validate();
129 assets.insert(name, self)
130 }
131}
132
133pub fn build_compute(backend: &WGPUBackend, desc: &Compute) -> (ComputePipeline, BindGroupLayout) {
148 build_compute_raw(&backend.device, desc)
149}
150
151pub(crate) fn build_compute_raw(
154 device: &wgpu::Device,
155 desc: &Compute,
156) -> (ComputePipeline, BindGroupLayout) {
157 for entry in &desc.entries {
158 if entry.kind.visibility() != ShaderStages::COMPUTE {
159 panic!(
160 "compute pass{}: entry '{}' is not visible to exactly the compute stage — \
161 compute bind group entries must be visible to exactly COMPUTE",
162 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
163 entry.name,
164 );
165 }
166 }
167
168 let layout = BindGroupLayoutBuilder::new()
169 .label(desc.label)
170 .entries(desc.entries.iter().cloned())
171 .build_raw(device);
172
173 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
174 label: desc.label,
175 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
176 });
177
178 let mut slots: Vec<super::layout::GroupLayout> = desc
179 .extra_layouts
180 .iter()
181 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
182 .collect();
183 if let Some(own_group) = desc.own_group {
184 slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
185 }
186 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
187
188 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
189 label: desc.label,
190 bind_group_layouts: &bind_group_layouts,
191 immediate_size: 0,
192 });
193
194 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
195 label: desc.label,
196 layout: Some(&pipeline_layout),
197 module: &module,
198 entry_point: desc.entry_point,
199 compilation_options: Default::default(),
200 cache: None,
201 });
202
203 (ComputePipeline(pipeline), layout)
204}
205
206pub struct GPUCompute {
209 pub pipeline: ComputePipeline,
210 layout: BindGroupLayout,
211 entries: Vec<BindingEntry>,
212}
213
214impl super::binding::BindGroupTarget for GPUCompute {
215 fn bind_group_layout(&self) -> &BindGroupLayout {
216 &self.layout
217 }
218 fn binding_entries(&self) -> &[BindingEntry] {
219 &self.entries
220 }
221}
222
223impl Asset<WGPUBackend> for GPUCompute {
224 type Source = Compute;
225 type Deps<'a> = ();
226
227 fn upload<'a>(source: &Compute, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
228 let (pipeline, layout) = build_compute(backend, source);
229
230 Some(Self {
231 pipeline,
232 layout,
233 entries: source.entries.to_vec(),
234 })
235 }
236}
237
238crate::wgpu::plugin_macros::asset_plugin! {
239 ComputePlugin, GPUCompute
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::wgpu::binding::{BindingEntry, BindingKind};
250 use crate::wgpu::test_util::with_device;
251
252 const MINIMAL_COMPUTE_SHADER: &str = r#"
253 @compute @workgroup_size(1)
254 fn cs_main() {}
255 "#;
256
257 #[test]
258 fn a_fragment_visible_entry_panics_before_touching_the_device() {
259 with_device!(device, _queue, {
260 let desc = Compute {
261 shader_source: MINIMAL_COMPUTE_SHADER,
262 entries: vec![BindingEntry {
263 name: "bad",
264 binding: 0,
265 kind: BindingKind::sampler(ShaderStages::FRAGMENT),
266 }],
267 ..Default::default()
268 };
269 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
270 build_compute_raw(&device, &desc);
271 }));
272 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
273 });
274 }
275
276 #[test]
277 fn a_vertex_fragment_visible_entry_also_panics() {
278 with_device!(device, _queue, {
283 let desc = Compute {
284 shader_source: MINIMAL_COMPUTE_SHADER,
285 entries: vec![BindingEntry {
286 name: "bad",
287 binding: 0,
288 kind: BindingKind::storage_buffer_read_write(
289 ShaderStages::COMPUTE | ShaderStages::FRAGMENT,
290 ),
291 }],
292 ..Default::default()
293 };
294 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
295 build_compute_raw(&device, &desc);
296 }));
297 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
298 });
299 }
300
301 #[test]
302 fn a_compute_only_entry_builds_without_panicking() {
303 with_device!(device, _queue, {
304 let desc = Compute {
305 shader_source: MINIMAL_COMPUTE_SHADER,
306 entries: vec![],
307 own_group: None,
308 ..Default::default()
309 };
310 build_compute_raw(&device, &desc);
311 });
312 }
313}