bevy_shader_plugin/compute/
mod.rs1mod node;
2mod pipeline;
3mod reflect;
4
5use std::{
6 any::{TypeId, type_name},
7 collections::HashSet,
8};
9
10use bevy::{
11 core_pipeline::schedule::camera_driver,
12 prelude::*,
13 render::{
14 Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems,
15 renderer::{RenderGraph, RenderGraphSystems},
16 },
17 shader::Shader,
18 window::PrimaryWindow,
19};
20
21use crate::{ShaderPassSystems, ShaderSystems, add_core_plugin, gpu_buffer::GpuBufferLengths};
22
23use reflect::{ComputeReflection, WorkgroupSize};
24
25const MAX_WORKGROUPS_PER_DIMENSION: u32 = 65535;
27
28#[derive(Clone, Debug)]
34pub struct Workgroups(WorkgroupsKind);
35
36#[derive(Clone, Debug)]
37enum WorkgroupsKind {
38 Window,
39 Buffer { tag: TypeId, tag_name: &'static str },
40 Threads(UVec3),
41 Exact(UVec3),
42}
43
44impl Workgroups {
45 pub fn window() -> Self {
48 Self(WorkgroupsKind::Window)
49 }
50
51 pub fn over<Tag: 'static>() -> Self {
55 Self(WorkgroupsKind::Buffer {
56 tag: TypeId::of::<Tag>(),
57 tag_name: type_name::<Tag>(),
58 })
59 }
60
61 pub fn threads(threads: UVec3) -> Self {
63 Self(WorkgroupsKind::Threads(threads))
64 }
65
66 pub fn exact(workgroups: UVec3) -> Self {
69 Self(WorkgroupsKind::Exact(workgroups))
70 }
71}
72
73#[derive(Clone, Debug)]
75pub(crate) struct PassConfig {
76 pub entry: &'static str,
77 pub workgroups: Workgroups,
78 pub init: bool,
80}
81
82pub struct ComputeShaderPlugin {
107 pub shader_path: &'static str,
109 passes: Vec<PassConfig>,
110}
111
112impl ComputeShaderPlugin {
113 pub fn new(shader_path: &'static str) -> Self {
116 Self {
117 shader_path,
118 passes: Vec::new(),
119 }
120 }
121
122 pub fn pass(mut self, entry: &'static str, workgroups: Workgroups) -> Self {
126 self.passes.push(PassConfig {
127 entry,
128 workgroups,
129 init: false,
130 });
131 self
132 }
133
134 pub fn init_pass(mut self, entry: &'static str, workgroups: Workgroups) -> Self {
138 self.passes.push(PassConfig {
139 entry,
140 workgroups,
141 init: true,
142 });
143 self
144 }
145}
146
147#[derive(Resource, Default)]
157pub struct ComputePasses {
158 reruns: Vec<String>,
159}
160
161impl ComputePasses {
162 pub fn rerun(&mut self, entry: &str) {
164 self.reruns.push(entry.to_owned());
165 }
166}
167
168#[derive(Resource)]
170pub(crate) struct ComputeConfig {
171 pub shader_path: &'static str,
172 pub shader: Handle<Shader>,
173 pub passes: Vec<PassConfig>,
174}
175
176#[derive(Resource, Default)]
178pub(crate) struct ComputeFrame {
179 pub counts: Vec<Option<UVec3>>,
182 pub reruns: Vec<usize>,
184}
185
186impl Plugin for ComputeShaderPlugin {
187 fn build(&self, app: &mut App) {
188 add_core_plugin(app);
189
190 let shader = app.world().resource::<AssetServer>().load(self.shader_path);
191
192 app.init_resource::<GpuBufferLengths>()
193 .init_resource::<ComputePasses>()
194 .init_resource::<ComputeReflection>()
195 .init_resource::<ComputeFrame>()
196 .insert_resource(ComputeConfig {
197 shader_path: self.shader_path,
198 shader,
199 passes: self.passes.clone(),
200 })
201 .add_systems(
202 PostUpdate,
203 (reflect::reflect_workgroup_sizes, plan_dispatches).chain(),
204 );
205
206 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
207 return;
208 };
209
210 render_app
211 .insert_resource(pipeline::ComputeRenderConfig {
212 shader_path: self.shader_path,
213 passes: self.passes.clone(),
214 })
215 .insert_resource(pipeline::ComputeRunState {
216 init_done: vec![false; self.passes.len()],
217 })
218 .init_resource::<pipeline::ComputeRunList>()
219 .init_resource::<ComputeFrame>()
220 .add_systems(ExtractSchedule, extract_compute_frame)
221 .add_systems(
222 RenderStartup,
223 pipeline::init_compute_pipelines.after(ShaderSystems::CompileLayouts),
224 )
225 .add_systems(
226 Render,
227 pipeline::prepare_compute_run
228 .in_set(RenderSystems::PrepareBindGroups)
229 .after(ShaderSystems::FinalizeBindGroups),
230 );
231
232 render_app.add_systems(
234 RenderGraph,
235 node::compute_pass
236 .in_set(ShaderPassSystems::Compute)
237 .in_set(RenderGraphSystems::Render)
238 .before(camera_driver),
239 );
240 }
241}
242
243fn workgroup_count(threads: UVec3, size: UVec3) -> Result<Option<UVec3>, String> {
246 if size.cmpeq(UVec3::ZERO).any() {
247 return Err(format!("invalid workgroup size {size}"));
248 }
249 let count = UVec3::new(
250 threads.x.div_ceil(size.x),
251 threads.y.div_ceil(size.y),
252 threads.z.div_ceil(size.z),
253 );
254 check_count(count)
255}
256
257fn check_count(count: UVec3) -> Result<Option<UVec3>, String> {
258 if count.max_element() > MAX_WORKGROUPS_PER_DIMENSION {
259 return Err(format!(
260 "dispatch of {count} workgroups exceeds the limit of {MAX_WORKGROUPS_PER_DIMENSION} per dimension"
261 ));
262 }
263 if count.cmpeq(UVec3::ZERO).any() {
264 return Ok(None);
265 }
266 Ok(Some(count))
267}
268
269fn plan_dispatches(
272 config: Res<ComputeConfig>,
273 reflection: Res<ComputeReflection>,
274 lengths: Res<GpuBufferLengths>,
275 windows: Query<&Window, With<PrimaryWindow>>,
276 mut passes: ResMut<ComputePasses>,
277 mut frame: ResMut<ComputeFrame>,
278 mut reported: Local<HashSet<(usize, String)>>,
279) {
280 let window_size = windows
281 .single()
282 .ok()
283 .map(|window| UVec3::new(window.physical_width(), window.physical_height(), 1));
284
285 let mut report = |index: usize, message: String| {
286 if reported.insert((index, message.clone())) {
287 warn!(
288 "compute pass `{}` skipped: {message}",
289 config.passes[index].entry
290 );
291 }
292 };
293
294 frame.counts.clear();
295 for (index, pass) in config.passes.iter().enumerate() {
296 let reflected_size = || -> Result<Option<UVec3>, String> {
297 let Some(sizes) = &reflection.sizes else {
298 return Ok(None);
300 };
301 match sizes.get(pass.entry) {
302 Some(WorkgroupSize::Fixed(size)) => Ok(Some(*size)),
303 Some(WorkgroupSize::Overridden) => Err(
304 "its @workgroup_size uses override constants; use `Workgroups::exact`".into(),
305 ),
306 None => Err("no compute entry point with this name".into()),
307 }
308 };
309
310 let count = match &pass.workgroups.0 {
311 WorkgroupsKind::Exact(count) => check_count(*count),
312 kind => reflected_size().and_then(|size| {
313 let Some(size) = size else { return Ok(None) };
314 let threads = match kind {
315 WorkgroupsKind::Window => window_size.unwrap_or(UVec3::ZERO),
316 WorkgroupsKind::Buffer { tag, tag_name } => {
317 let Some(&len) = lengths.0.get(tag) else {
318 return Err(format!(
319 "no GPU buffer is registered with tag `{tag_name}`"
320 ));
321 };
322 if size.y != 1 || size.z != 1 {
323 return Err(format!(
324 "`Workgroups::over` dispatches along x only, but the workgroup size is {size}; use @workgroup_size(N)"
325 ));
326 }
327 UVec3::new(len, 1, 1)
328 }
329 WorkgroupsKind::Threads(threads) => *threads,
330 WorkgroupsKind::Exact(_) => unreachable!(),
331 };
332 workgroup_count(threads, size)
333 }),
334 };
335
336 frame.counts.push(count.unwrap_or_else(|message| {
337 report(index, message);
338 None
339 }));
340 }
341
342 frame.reruns.clear();
343 for entry in passes.reruns.drain(..) {
344 let matching: Vec<usize> = config
345 .passes
346 .iter()
347 .enumerate()
348 .filter(|(_, pass)| pass.init && pass.entry == entry)
349 .map(|(index, _)| index)
350 .collect();
351 if matching.is_empty() {
352 warn!("ComputePasses::rerun: `{entry}` is not an init pass");
353 }
354 frame.reruns.extend(matching);
355 }
356}
357
358fn extract_compute_frame(
360 frame: Extract<Res<ComputeFrame>>,
361 mut render_frame: ResMut<ComputeFrame>,
362) {
363 render_frame.counts.clone_from(&frame.counts);
364 render_frame.reruns.clone_from(&frame.reruns);
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn rounds_thread_counts_up() {
373 assert_eq!(
374 workgroup_count(UVec3::new(500, 1, 1), UVec3::new(64, 1, 1)),
375 Ok(Some(UVec3::new(8, 1, 1)))
376 );
377 assert_eq!(
378 workgroup_count(UVec3::new(1920, 1080, 1), UVec3::new(8, 8, 1)),
379 Ok(Some(UVec3::new(240, 135, 1)))
380 );
381 }
382
383 #[test]
384 fn skips_empty_dispatches() {
385 assert_eq!(
386 workgroup_count(UVec3::new(0, 0, 1), UVec3::new(8, 8, 1)),
387 Ok(None)
388 );
389 }
390
391 #[test]
392 fn rejects_oversized_dispatches() {
393 assert!(workgroup_count(UVec3::new(65536 * 64 + 1, 1, 1), UVec3::new(64, 1, 1)).is_err());
394 assert!(check_count(UVec3::new(65536, 1, 1)).is_err());
395 }
396
397 #[test]
398 fn rejects_zero_workgroup_size() {
399 assert!(workgroup_count(UVec3::ONE, UVec3::new(0, 1, 1)).is_err());
400 }
401}