1use crate::{
2 assets::{handle::Handle, storage::Assets, upload::{Asset, AssetSource}},
3 ecs::resources::Read,
4 graphics::{
5 pipeline::{
6 binding::{BindGroupLayout, BindGroupLayoutBuilder, BindingKind},
7 buffers::{BindGroup, Buffer, DynamicBuffer},
8 cubemap::Cubemap,
9 layout::{
10 ComputePipelineCache, ComputePipelineKey, GlobalLayoutPool, GroupEntry, OwnEntriesBuilder, PipelineKind,
11 assemble_group_layouts, find_own_entries,
12 },
13 params::{BindGroupParams, BindingValue, build_bind_group},
14 samplers::{GlobalSamplers, SamplerKind},
15 texture_array::TextureArray,
16 texture_view::TextureView,
17 textures::Texture,
18 },
19 render::Backend,
20 types::flags::ShaderStages,
21 },
22};
23
24pub use pebble_derive::ComputeParams;
25
26#[derive(Clone)]
31pub struct ComputePipeline(wgpu::ComputePipeline);
32
33impl ComputePipeline {
34 pub(crate) fn raw(&self) -> &wgpu::ComputePipeline {
35 &self.0
36 }
37}
38
39pub struct Compute {
46 label: Option<&'static str>,
47 shader_source: &'static str,
48 entry_point: Option<&'static str>,
49 own_entries: OwnEntriesBuilder,
50 extra_groups: Vec<GroupEntry>,
51 params: BindGroupParams,
52}
53
54impl Default for Compute {
55 fn default() -> Self {
56 Self {
57 label: None,
58 shader_source: "",
59 entry_point: Some("cs_main"),
60 own_entries: OwnEntriesBuilder::new(),
61 extra_groups: Vec::new(),
62 params: BindGroupParams::new(),
63 }
64 }
65}
66
67impl Compute {
68 pub fn new(shader_source: &'static str) -> Self {
69 Self { shader_source, ..Self::default() }
70 }
71
72 pub fn with_label(mut self, label: &'static str) -> Self {
73 self.label = Some(label);
74 self
75 }
76
77 pub fn with_entry_point(mut self, entry: &'static str) -> Self {
78 self.entry_point = Some(entry);
79 self
80 }
81
82 pub fn with_entry(mut self, name: &'static str, kind: BindingKind) -> Self {
88 self.own_entries = self.own_entries.with_entry(name, kind);
89 self
90 }
91
92 pub fn with_entry_at(mut self, name: &'static str, binding: u32, kind: BindingKind) -> Self {
95 self.own_entries = self.own_entries.with_entry_at(name, binding, kind);
96 self
97 }
98
99 pub fn with_extra_group(mut self, group: GroupEntry) -> Self {
104 self.extra_groups.push(group);
105 self
106 }
107
108 pub fn with_texture(mut self, name: &'static str, handle: Handle<Texture>) -> Self {
110 self.params = self.params.with_texture(name, handle);
111 self
112 }
113
114 pub fn with_texture_array(mut self, name: &'static str, handle: Handle<TextureArray>) -> Self {
116 self.params = self.params.with_texture_array(name, handle);
117 self
118 }
119
120 pub fn with_cubemap(mut self, name: &'static str, handle: Handle<Cubemap>) -> Self {
122 self.params = self.params.with_cubemap(name, handle);
123 self
124 }
125
126 pub fn with_texture_view(mut self, name: &'static str, view: TextureView) -> Self {
133 self.params = self.params.with_texture_view(name, view);
134 self
135 }
136
137 pub fn with_sampler(mut self, name: &'static str, kind: SamplerKind) -> Self {
139 self.params = self.params.with_sampler(name, kind);
140 self
141 }
142
143 pub fn with_uniform(mut self, name: &'static str, data: Vec<u8>) -> Self {
145 self.params = self.params.with_uniform(name, data);
146 self
147 }
148
149 pub fn with_storage(mut self, name: &'static str, data: Vec<u8>) -> Self {
151 self.params = self.params.with_storage(name, data);
152 self
153 }
154
155 pub fn with_uniform_value<T>(mut self, name: &'static str, value: &T) -> Self
160 where
161 T: encase::ShaderType + encase::internal::WriteInto,
162 {
163 self.params = self.params.with_uniform_value(name, value);
164 self
165 }
166
167 pub fn with_storage_value<T>(mut self, name: &'static str, value: &T) -> Self
172 where
173 T: encase::ShaderType + encase::internal::WriteInto,
174 {
175 self.params = self.params.with_storage_value(name, value);
176 self
177 }
178
179 pub fn texture(mut self, name: &'static str, handle: Handle<Texture>) -> Self {
187 self.own_entries = self.own_entries.with_entry(name, BindingKind::texture_2d(ShaderStages::COMPUTE));
188 self.with_texture(name, handle)
189 }
190
191 pub fn texture_array(mut self, name: &'static str, handle: Handle<TextureArray>) -> Self {
193 self.own_entries = self.own_entries.with_entry(name, BindingKind::texture_2d_array(ShaderStages::COMPUTE));
194 self.with_texture_array(name, handle)
195 }
196
197 pub fn cubemap(mut self, name: &'static str, handle: Handle<Cubemap>) -> Self {
199 self.own_entries = self.own_entries.with_entry(name, BindingKind::texture_cubemap(ShaderStages::COMPUTE));
200 self.with_cubemap(name, handle)
201 }
202
203 pub fn sampler(mut self, name: &'static str, kind: SamplerKind) -> Self {
205 self.own_entries = self.own_entries.with_entry(name, BindingKind::sampler(ShaderStages::COMPUTE));
206 self.with_sampler(name, kind)
207 }
208
209 pub fn uniform(mut self, name: &'static str, data: Vec<u8>) -> Self {
211 self.own_entries = self.own_entries.with_entry(name, BindingKind::uniform_buffer(ShaderStages::COMPUTE));
212 self.with_uniform(name, data)
213 }
214
215 pub fn storage(mut self, name: &'static str, data: Vec<u8>) -> Self {
220 self.own_entries = self.own_entries.with_entry(name, BindingKind::storage_buffer_read_write(ShaderStages::COMPUTE));
221 self.with_storage(name, data)
222 }
223
224 pub fn uniform_value<T>(mut self, name: &'static str, value: &T) -> Self
227 where
228 T: encase::ShaderType + encase::internal::WriteInto,
229 {
230 self.own_entries = self.own_entries.with_entry(name, BindingKind::uniform_buffer(ShaderStages::COMPUTE));
231 self.with_uniform_value(name, value)
232 }
233
234 pub fn storage_value<T>(mut self, name: &'static str, value: &T) -> Self
237 where
238 T: encase::ShaderType + encase::internal::WriteInto,
239 {
240 self.own_entries = self.own_entries.with_entry(name, BindingKind::storage_buffer_read_write(ShaderStages::COMPUTE));
241 self.with_storage_value(name, value)
242 }
243
244 pub fn with_buffer(mut self, name: &'static str, buffer: Buffer) -> Self {
249 self.params = self.params.with_buffer(name, buffer);
250 self
251 }
252
253 pub fn with_dynamic_buffer(mut self, name: &'static str, buffer: DynamicBuffer) -> Self {
256 self.params = self.params.with_dynamic_buffer(name, buffer);
257 self
258 }
259
260 pub fn with_param(mut self, name: &'static str, entry: BindingValue) -> Self {
261 self.params = self.params.with_param(name, entry);
262 self
263 }
264
265 fn groups(&self) -> Vec<GroupEntry> {
269 std::iter::once(GroupEntry::Own(self.own_entries.entries().to_vec()))
270 .chain(self.extra_groups.iter().cloned())
271 .collect()
272 }
273
274 fn validate(&self) {
275 if self.own_entries.entries().is_empty() && self.extra_groups.is_empty() {
276 tracing::warn!(
277 "Compute{}: no bind groups at all — this pass can't read or write \
278 anything; consider calling .texture(...)/.buffer(...)/etc.",
279 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
280 );
281 }
282 if self.params.is_empty() {
283 tracing::warn!(
284 "Compute{}: no bind group params — this pass won't bind anything against \
285 its own entries; did you forget to chain .with_texture(...)/.with_buffer(...)/etc.?",
286 self.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
287 );
288 }
289 }
290
291 pub fn build_asset(self, name: &str, assets: &mut Assets<Compute>) -> Handle<Compute> {
292 self.validate();
293 assets.insert(name, self)
294 }
295}
296
297pub fn build_compute(backend: &Backend, desc: &Compute, pool: &GlobalLayoutPool) -> Option<(ComputePipeline, BindGroupLayout)> {
302 let groups = desc.groups();
303 let own_entries = find_own_entries(desc.label, PipelineKind::Compute, &groups);
304 for entry in own_entries {
305 if entry.kind.visibility() != ShaderStages::COMPUTE {
306 panic!(
307 "compute pass{}: entry '{}' is not visible to exactly the compute stage — \
308 compute bind group entries must be visible to exactly COMPUTE",
309 desc.label.map(|l| format!(" '{l}'")).unwrap_or_default(),
310 entry.name,
311 );
312 }
313 }
314
315 let layout = BindGroupLayoutBuilder::new()
316 .with_label(desc.label)
317 .with_entries(own_entries.iter().cloned())
318 .build(backend);
319
320 let device = &backend.device;
321 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
322 label: desc.label,
323 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
324 });
325
326 let bind_group_layouts = assemble_group_layouts(
327 desc.label,
328 &groups,
329 &layout,
330 pool,
331 device.limits().max_bind_groups,
332 )?;
333
334 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
335 label: desc.label,
336 bind_group_layouts: &bind_group_layouts,
337 immediate_size: 0,
338 });
339
340 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
341 label: desc.label,
342 layout: Some(&pipeline_layout),
343 module: &module,
344 entry_point: desc.entry_point,
345 compilation_options: Default::default(),
346 cache: None,
347 });
348
349 Some((ComputePipeline(pipeline), layout))
350}
351
352pub struct GPUCompute {
356 pub pipeline: ComputePipeline,
357 pub bind_group: BindGroup,
358 buffers: Vec<(&'static str, Buffer)>,
359 dynamic_buffers: Vec<(&'static str, DynamicBuffer)>,
360}
361
362impl GPUCompute {
363 pub fn update(&self, name: &str, data: &[u8]) {
366 match self.buffer(name) {
367 Some(buf) => buf.write(data),
368 None => tracing::warn!(
369 "GPUCompute::update: no bound buffer named '{name}' — check for a typo \
370 against this pass's own .with_uniform(...)/.with_storage(...) entries"
371 ),
372 }
373 }
374
375 pub fn update_value<T>(&self, name: &str, value: &T)
379 where
380 T: encase::ShaderType + encase::internal::WriteInto,
381 {
382 let mut buffer = encase::UniformBuffer::new(Vec::new());
383 buffer
384 .write(value)
385 .expect("encase: failed to write value — this shouldn't happen for a #[derive(ShaderType)] struct");
386 self.update(name, &buffer.into_inner());
387 }
388
389 pub fn buffer(&self, name: &str) -> Option<&Buffer> {
390 self.buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
391 }
392
393 pub fn dynamic_buffer(&self, name: &str) -> Option<&DynamicBuffer> {
397 self.dynamic_buffers.iter().find(|(n, _)| *n == name).map(|(_, buf)| buf)
398 }
399}
400
401impl AssetSource for Compute {
402 type Processed = GPUCompute;
403}
404
405impl Asset<Backend> for Compute {
406 type Deps<'a> = (
407 Read<'a, GlobalLayoutPool>,
408 Read<'a, ComputePipelineCache>,
409 Read<'a, Assets<Texture>>,
410 Read<'a, Assets<TextureArray>>,
411 Read<'a, Assets<Cubemap>>,
412 Read<'a, GlobalSamplers>,
413 );
414
415 fn upload<'a>(&self, backend: &Backend, deps: &Self::Deps<'a>) -> Option<GPUCompute> {
416 let (layout_pool, pipeline_cache, textures, texture_arrays, cubemaps, samplers) = deps;
417
418 let groups = self.groups();
419 let key = ComputePipelineKey::new(self.shader_source, self.entry_point, &groups);
420 let (pipeline, layout) = pipeline_cache.get_or_compile(key, || build_compute(backend, self, layout_pool))?;
421 let entries = find_own_entries(self.label, PipelineKind::Compute, &groups);
422
423 let built = build_bind_group(backend, &self.params, &layout, entries, textures, texture_arrays, cubemaps, samplers)?;
424
425 Some(GPUCompute {
426 pipeline,
427 bind_group: built.bind_group,
428 buffers: built.buffers,
429 dynamic_buffers: built.dynamic_buffers,
430 })
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[derive(ComputeParams)]
439 struct TestParams {
440 #[storage(0)]
441 data: f32,
442 #[texture(1)]
443 tex: Handle<Texture>,
444 }
445
446 #[test]
447 fn compute_params_derive_uses_compute_visibility_and_read_write_storage() {
448 let params = TestParams { data: 1.0, tex: Handle::default() };
449 let compute = params.into_compute(Compute::new("shader"));
450
451 assert!(!compute.params.is_empty());
452
453 let groups = compute.groups();
454 assert_eq!(groups.len(), 1);
455 let GroupEntry::Own(entries) = &groups[0] else { panic!("group 0 should be Own") };
456 assert_eq!(entries.len(), 2);
457 assert_eq!(entries[0].binding, 0);
458 assert!(entries[0].kind.visibility() == ShaderStages::COMPUTE);
459 assert_eq!(entries[1].binding, 1);
460 assert!(entries[1].kind.visibility() == ShaderStages::COMPUTE);
461 }
462}