1use crate::{
2 assets::upload::Asset,
3 wgpu::{backend::WGPUBackend, binding::{BindGroupLayoutBuilder, BindingEntry}},
4};
5
6pub struct ComputeDescriptor<'a> {
9 pub label: Option<&'a str>,
12 pub shader_source: &'a str,
14 pub entry_point: Option<&'a str>,
16 pub entries: Vec<BindingEntry>,
21 pub own_group: Option<u32>,
24 pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
29}
30
31impl<'a> Default for ComputeDescriptor<'a> {
32 fn default() -> Self {
33 Self {
34 label: None,
35 shader_source: "",
36 entry_point: Some("cs_main"),
37 entries: Vec::new(),
38 own_group: Some(0),
39 extra_layouts: Vec::new(),
40 }
41 }
42}
43
44pub fn build_compute(
59 device: &wgpu::Device,
60 desc: &ComputeDescriptor,
61) -> (wgpu::ComputePipeline, wgpu::BindGroupLayout) {
62 for entry in &desc.entries {
63 if entry.kind.visibility() != wgpu::ShaderStages::COMPUTE {
64 panic!(
65 "compute pass{}: entry '{}' has visibility {:?} — compute bind group entries \
66 must be visible to exactly the compute stage",
67 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
68 entry.name,
69 entry.kind.visibility()
70 );
71 }
72 }
73
74 let layout = BindGroupLayoutBuilder::new()
75 .label(desc.label)
76 .entries(desc.entries.iter().cloned())
77 .build(device);
78
79 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
80 label: desc.label,
81 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
82 });
83
84 let mut slots: Vec<super::layout::GroupLayout> = desc
85 .extra_layouts
86 .iter()
87 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
88 .collect();
89 if let Some(own_group) = desc.own_group {
90 slots.push(super::layout::GroupLayout { group: own_group, layout: &layout });
91 }
92 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
93
94 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
95 label: desc.label,
96 bind_group_layouts: &bind_group_layouts,
97 immediate_size: 0,
98 });
99
100 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
101 label: desc.label,
102 layout: Some(&pipeline_layout),
103 module: &module,
104 entry_point: desc.entry_point,
105 compilation_options: Default::default(),
106 cache: None,
107 });
108
109 (pipeline, layout)
110}
111
112pub struct GPUCompute {
115 pub pipeline: wgpu::ComputePipeline,
116 pub layout: wgpu::BindGroupLayout,
117 pub entries: Vec<BindingEntry>,
118}
119
120impl super::binding::BindGroupTarget for GPUCompute {
121 fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
122 &self.layout
123 }
124 fn binding_entries(&self) -> &[BindingEntry] {
125 &self.entries
126 }
127}
128
129impl Asset<WGPUBackend> for GPUCompute {
130 type Source = ComputeDescriptor<'static>;
131 type Deps<'a> = ();
132
133 fn upload<'a>(source: &ComputeDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
134 let (pipeline, layout) = build_compute(&backend.device, source);
135
136 Some(Self {
137 pipeline,
138 layout,
139 entries: source.entries.to_vec(),
140 })
141 }
142}
143
144crate::wgpu::plugin_macros::asset_plugin! {
145 ComputePlugin, GPUCompute
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::wgpu::binding::{BindingEntry, BindingKind};
156 use crate::wgpu::test_util::with_device;
157
158 const MINIMAL_COMPUTE_SHADER: &str = r#"
159 @compute @workgroup_size(1)
160 fn cs_main() {}
161 "#;
162
163 #[test]
164 fn a_fragment_visible_entry_panics_before_touching_the_device() {
165 with_device!(device, _queue, {
166 let desc = ComputeDescriptor {
167 shader_source: MINIMAL_COMPUTE_SHADER,
168 entries: vec![BindingEntry {
169 name: "bad",
170 binding: 0,
171 kind: BindingKind::sampler(wgpu::ShaderStages::FRAGMENT),
172 }],
173 ..Default::default()
174 };
175 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
176 build_compute(&device, &desc);
177 }));
178 assert!(result.is_err(), "expected a panic for a non-COMPUTE-visible compute entry");
179 });
180 }
181
182 #[test]
183 fn a_vertex_fragment_visible_entry_also_panics() {
184 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::storage_buffer_read_write(
195 wgpu::ShaderStages::COMPUTE | wgpu::ShaderStages::FRAGMENT,
196 ),
197 }],
198 ..Default::default()
199 };
200 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
201 build_compute(&device, &desc);
202 }));
203 assert!(result.is_err(), "expected a panic for a COMPUTE | FRAGMENT compute entry");
204 });
205 }
206
207 #[test]
208 fn a_compute_only_entry_builds_without_panicking() {
209 with_device!(device, _queue, {
210 let desc = ComputeDescriptor {
211 shader_source: MINIMAL_COMPUTE_SHADER,
212 entries: vec![],
213 own_group: None,
214 ..Default::default()
215 };
216 build_compute(&device, &desc);
217 });
218 }
219}