1use crate::{
2 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 ComputeDescriptor<'a> {
27 pub label: Option<&'a str>,
30 pub shader_source: &'a str,
32 pub entry_point: Option<&'a str>,
34 pub entries: Vec<BindingEntry>,
39 pub own_group: Option<u32>,
42 pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
47}
48
49impl<'a> Default for ComputeDescriptor<'a> {
50 fn default() -> Self {
51 Self {
52 label: None,
53 shader_source: "",
54 entry_point: Some("cs_main"),
55 entries: Vec::new(),
56 own_group: Some(0),
57 extra_layouts: Vec::new(),
58 }
59 }
60}
61
62pub fn build_compute(backend: &WGPUBackend, desc: &ComputeDescriptor) -> (ComputePipeline, BindGroupLayout) {
77 build_compute_raw(&backend.device, desc)
78}
79
80pub(crate) fn build_compute_raw(
83 device: &wgpu::Device,
84 desc: &ComputeDescriptor,
85) -> (ComputePipeline, BindGroupLayout) {
86 for entry in &desc.entries {
87 if entry.kind.visibility() != ShaderStages::COMPUTE {
88 panic!(
89 "compute pass{}: entry '{}' is not visible to exactly the compute stage — \
90 compute bind group entries must be visible to exactly COMPUTE",
91 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
92 entry.name,
93 );
94 }
95 }
96
97 let layout = BindGroupLayoutBuilder::new()
98 .label(desc.label)
99 .entries(desc.entries.iter().cloned())
100 .build_raw(device);
101
102 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
103 label: desc.label,
104 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
105 });
106
107 let mut slots: Vec<super::layout::GroupLayout> = desc
108 .extra_layouts
109 .iter()
110 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
111 .collect();
112 if let Some(own_group) = desc.own_group {
113 slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
114 }
115 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
116
117 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
118 label: desc.label,
119 bind_group_layouts: &bind_group_layouts,
120 immediate_size: 0,
121 });
122
123 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
124 label: desc.label,
125 layout: Some(&pipeline_layout),
126 module: &module,
127 entry_point: desc.entry_point,
128 compilation_options: Default::default(),
129 cache: None,
130 });
131
132 (ComputePipeline(pipeline), layout)
133}
134
135pub struct GPUCompute {
138 pub pipeline: ComputePipeline,
139 layout: BindGroupLayout,
140 entries: Vec<BindingEntry>,
141}
142
143impl super::binding::BindGroupTarget for GPUCompute {
144 fn bind_group_layout(&self) -> &BindGroupLayout {
145 &self.layout
146 }
147 fn binding_entries(&self) -> &[BindingEntry] {
148 &self.entries
149 }
150}
151
152impl Asset<WGPUBackend> for GPUCompute {
153 type Source = ComputeDescriptor<'static>;
154 type Deps<'a> = ();
155
156 fn upload<'a>(source: &ComputeDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
157 let (pipeline, layout) = build_compute(backend, source);
158
159 Some(Self {
160 pipeline,
161 layout,
162 entries: source.entries.to_vec(),
163 })
164 }
165}
166
167crate::wgpu::plugin_macros::asset_plugin! {
168 ComputePlugin, GPUCompute
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::wgpu::binding::{BindingEntry, BindingKind};
179 use crate::wgpu::test_util::with_device;
180
181 const MINIMAL_COMPUTE_SHADER: &str = r#"
182 @compute @workgroup_size(1)
183 fn cs_main() {}
184 "#;
185
186 #[test]
187 fn a_fragment_visible_entry_panics_before_touching_the_device() {
188 with_device!(device, _queue, {
189 let desc = ComputeDescriptor {
190 shader_source: MINIMAL_COMPUTE_SHADER,
191 entries: vec![BindingEntry {
192 name: "bad",
193 binding: 0,
194 kind: BindingKind::sampler(ShaderStages::FRAGMENT),
195 }],
196 ..Default::default()
197 };
198 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
199 build_compute_raw(&device, &desc);
200 }));
201 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
202 });
203 }
204
205 #[test]
206 fn a_vertex_fragment_visible_entry_also_panics() {
207 with_device!(device, _queue, {
212 let desc = ComputeDescriptor {
213 shader_source: MINIMAL_COMPUTE_SHADER,
214 entries: vec![BindingEntry {
215 name: "bad",
216 binding: 0,
217 kind: BindingKind::storage_buffer_read_write(
218 ShaderStages::COMPUTE | ShaderStages::FRAGMENT,
219 ),
220 }],
221 ..Default::default()
222 };
223 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
224 build_compute_raw(&device, &desc);
225 }));
226 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
227 });
228 }
229
230 #[test]
231 fn a_compute_only_entry_builds_without_panicking() {
232 with_device!(device, _queue, {
233 let desc = ComputeDescriptor {
234 shader_source: MINIMAL_COMPUTE_SHADER,
235 entries: vec![],
236 own_group: None,
237 ..Default::default()
238 };
239 build_compute_raw(&device, &desc);
240 });
241 }
242}