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, Clone)]
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 .add_systems(ExtractSchedule, extract_compute_frame)
220 .add_systems(
221 RenderStartup,
222 pipeline::init_compute_pipelines.after(ShaderSystems::CompileLayouts),
223 )
224 .add_systems(
225 Render,
226 pipeline::prepare_compute_run
227 .in_set(RenderSystems::PrepareBindGroups)
228 .after(ShaderSystems::FinalizeBindGroups),
229 );
230
231 render_app.add_systems(
233 RenderGraph,
234 node::compute_pass
235 .in_set(ShaderPassSystems::Compute)
236 .in_set(RenderGraphSystems::Render)
237 .before(camera_driver),
238 );
239 }
240}
241
242fn workgroup_count(threads: UVec3, size: UVec3) -> Result<Option<UVec3>, String> {
245 if size.cmpeq(UVec3::ZERO).any() {
246 return Err(format!("invalid workgroup size {size}"));
247 }
248 let count = UVec3::new(
249 threads.x.div_ceil(size.x),
250 threads.y.div_ceil(size.y),
251 threads.z.div_ceil(size.z),
252 );
253 check_count(count)
254}
255
256fn check_count(count: UVec3) -> Result<Option<UVec3>, String> {
257 if count.max_element() > MAX_WORKGROUPS_PER_DIMENSION {
258 return Err(format!(
259 "dispatch of {count} workgroups exceeds the limit of {MAX_WORKGROUPS_PER_DIMENSION} per dimension"
260 ));
261 }
262 if count.cmpeq(UVec3::ZERO).any() {
263 return Ok(None);
264 }
265 Ok(Some(count))
266}
267
268fn plan_dispatches(
271 config: Res<ComputeConfig>,
272 reflection: Res<ComputeReflection>,
273 lengths: Res<GpuBufferLengths>,
274 windows: Query<&Window, With<PrimaryWindow>>,
275 mut passes: ResMut<ComputePasses>,
276 mut frame: ResMut<ComputeFrame>,
277 mut reported: Local<HashSet<(usize, String)>>,
278) {
279 let window_size = windows
280 .single()
281 .ok()
282 .map(|window| UVec3::new(window.physical_width(), window.physical_height(), 1));
283
284 let mut report = |index: usize, message: String| {
285 if reported.insert((index, message.clone())) {
286 warn!(
287 "compute pass `{}` skipped: {message}",
288 config.passes[index].entry
289 );
290 }
291 };
292
293 frame.counts.clear();
294 for (index, pass) in config.passes.iter().enumerate() {
295 let reflected_size = || -> Result<Option<UVec3>, String> {
296 let Some(sizes) = &reflection.sizes else {
297 return Ok(None);
299 };
300 match sizes.get(pass.entry) {
301 Some(WorkgroupSize::Fixed(size)) => Ok(Some(*size)),
302 Some(WorkgroupSize::Overridden) => Err(
303 "its @workgroup_size uses override constants; use `Workgroups::exact`".into(),
304 ),
305 None => Err("no compute entry point with this name".into()),
306 }
307 };
308
309 let count = match &pass.workgroups.0 {
310 WorkgroupsKind::Exact(count) => check_count(*count),
311 kind => reflected_size().and_then(|size| {
312 let Some(size) = size else { return Ok(None) };
313 let threads = match kind {
314 WorkgroupsKind::Window => window_size.unwrap_or(UVec3::ZERO),
315 WorkgroupsKind::Buffer { tag, tag_name } => {
316 let Some(&len) = lengths.0.get(tag) else {
317 return Err(format!(
318 "no GPU buffer is registered with tag `{tag_name}`"
319 ));
320 };
321 if size.y != 1 || size.z != 1 {
322 return Err(format!(
323 "`Workgroups::over` dispatches along x only, but the workgroup size is {size}; use @workgroup_size(N)"
324 ));
325 }
326 UVec3::new(len, 1, 1)
327 }
328 WorkgroupsKind::Threads(threads) => *threads,
329 WorkgroupsKind::Exact(_) => unreachable!(),
330 };
331 workgroup_count(threads, size)
332 }),
333 };
334
335 frame.counts.push(count.unwrap_or_else(|message| {
336 report(index, message);
337 None
338 }));
339 }
340
341 frame.reruns.clear();
342 for entry in passes.reruns.drain(..) {
343 let matching: Vec<usize> = config
344 .passes
345 .iter()
346 .enumerate()
347 .filter(|(_, pass)| pass.init && pass.entry == entry)
348 .map(|(index, _)| index)
349 .collect();
350 if matching.is_empty() {
351 warn!("ComputePasses::rerun: `{entry}` is not an init pass");
352 }
353 frame.reruns.extend(matching);
354 }
355}
356
357fn extract_compute_frame(mut commands: Commands, frame: Extract<Res<ComputeFrame>>) {
358 commands.insert_resource(frame.clone());
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn rounds_thread_counts_up() {
367 assert_eq!(
368 workgroup_count(UVec3::new(500, 1, 1), UVec3::new(64, 1, 1)),
369 Ok(Some(UVec3::new(8, 1, 1)))
370 );
371 assert_eq!(
372 workgroup_count(UVec3::new(1920, 1080, 1), UVec3::new(8, 8, 1)),
373 Ok(Some(UVec3::new(240, 135, 1)))
374 );
375 }
376
377 #[test]
378 fn skips_empty_dispatches() {
379 assert_eq!(
380 workgroup_count(UVec3::new(0, 0, 1), UVec3::new(8, 8, 1)),
381 Ok(None)
382 );
383 }
384
385 #[test]
386 fn rejects_oversized_dispatches() {
387 assert!(workgroup_count(UVec3::new(65536 * 64 + 1, 1, 1), UVec3::new(64, 1, 1)).is_err());
388 assert!(check_count(UVec3::new(65536, 1, 1)).is_err());
389 }
390
391 #[test]
392 fn rejects_zero_workgroup_size() {
393 assert!(workgroup_count(UVec3::ONE, UVec3::new(0, 1, 1)).is_err());
394 }
395}