• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_hanabi / 22800469877

07 Mar 2026 02:04PM UTC coverage: 57.546% (-0.2%) from 57.75%
22800469877

push

github

web-flow
Batch spawners and properties (#525)

Bind the entire spawner and property arrays in all passes, instead of a
single entry. This removes the need to pad those structures. Access them
via an offset in the effect's own metadata.

Add a new `BatchInfo` struct holding the per-batch data. This clarifies
the responsibilites between this and `EffectMetadata`, the latter
holding per-effect (not per-batch) data.

Remove the `DispatchBufferIndices` component, which was used to track
the allocated entry for indirect compute dispatch. Instead, align those
allocations 1:1 with the GPU batch info allocations, which are
re-computed each frame.

Add a prefix sum pass before the update pass, which computes the prefix
sum of alive particles after the init pass. This is used to enable
batched update compute dispatch, where the number of compute threads
maps to the total number of alive particles in the batch. In that case,
we need to find which thread updates which particle of which batch,
using that prefix sum. Note that in this change, due to other
limitations still present, each effect instance is still in its own
batch (there's effectively no batching). Enabling full batching requires
more work, notably on the sort pass for ribbons, and the GPU-based init
pass with GPU events.

Change the allocation of spawners to occur after sorting. This ensures
all effects in a same batch have sequential allocations, which enables
accessing those spawners with a simple {offset + index} strategy.

Change the render pass to use the same bind group "spawner@2" than other
passes. This binds the property buffer, although in this change the
metadata buffer is still not available, so properties can't be used yet
in the render pass. The bind groups should be reviewed anyway because,
with batching approaching, and with the current change, assumptions
about frequency of changes is now wrong, and individual bindings should
be re-grouped in more suitable frequency-based groups.

Finally, the... (continued)

193 of 404 new or added lines in 7 files covered. (47.77%)

26 existing lines in 3 files now uncovered.

4793 of 8329 relevant lines covered (57.55%)

198.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.33
/src/plugin.rs
1
use std::borrow::Cow;
2

3
#[cfg(feature = "2d")]
4
use bevy::core_pipeline::core_2d::Transparent2d;
5
#[cfg(feature = "3d")]
6
use bevy::core_pipeline::core_3d::{AlphaMask3d, Opaque3d, Transparent3d};
7
use bevy::{
8
    asset::uuid_handle,
9
    camera::visibility::VisibilitySystems,
10
    prelude::*,
11
    render::{
12
        extract_component::ExtractComponentPlugin,
13
        render_asset::prepare_assets,
14
        render_graph::RenderGraph,
15
        render_phase::DrawFunctions,
16
        render_resource::{SpecializedComputePipelines, SpecializedRenderPipelines},
17
        renderer::{RenderAdapterInfo, RenderDevice},
18
        texture::GpuImage,
19
        view::prepare_view_uniforms,
20
        Render, RenderApp, RenderSystems,
21
    },
22
    time::{time_system, TimeSystems},
23
};
24

25
#[cfg(feature = "serde")]
26
use crate::asset::EffectAssetLoader;
27
use crate::{
28
    asset::{DefaultMesh, EffectAsset},
29
    compile_effects,
30
    properties::EffectProperties,
31
    render::{
32
        allocate_effects, allocate_events, allocate_metadata, allocate_parent_child_infos,
33
        allocate_properties, batch_effects, clear_previous_frame_resizes,
34
        clear_transient_batch_inputs, extract_effect_events, extract_effects, extract_sim_params,
35
        fixup_parents, on_remove_cached_draw_indirect_args, on_remove_cached_effect,
36
        on_remove_cached_effect_events, on_remove_cached_metadata, on_remove_cached_properties,
37
        prepare_batch_inputs, prepare_bind_groups, prepare_effect_metadata, prepare_gpu_resources,
38
        prepare_indirect_pipeline, prepare_init_update_pipelines, prepare_property_buffers,
39
        propagate_ready_state, queue_effects, queue_init_fill_dispatch_ops,
40
        queue_init_indirect_workgroup_update, report_ready_state, start_stop_gpu_debug_capture,
41
        update_mesh_locations, DebugSettings, DispatchIndirectPipeline, DrawEffects,
42
        EffectAssetEvents, EffectBindGroups, EffectCache, EffectsMeta, EventCache, GpuBatchInfo,
43
        GpuBufferOperations, GpuEffectMetadata, InitFillDispatchQueue, ParticlesInitPipeline,
44
        ParticlesRenderPipeline, ParticlesUpdatePipeline, PrefixSumPipeline, PropertyBindGroups,
45
        PropertyCache, RenderDebugSettings, ShaderCache, SimParams, SortBindGroups,
46
        SortedEffectBatches, StorageType as _, UtilsPipeline, VfxSimulateDriverNode,
47
        VfxSimulateNode,
48
    },
49
    spawn::{self, Random},
50
    tick_spawners,
51
    time::effect_simulation_time_system,
52
    update_properties_from_asset, EffectSimulation, EffectVisibilityClass, ParticleEffect,
53
    SpawnerSettings, ToWgslString,
54
};
55

56
/// Source code for the `vfx_sort` compute shader.
57
pub(crate) const VFX_SORT_WGSL: Cow<'static, str> =
58
    Cow::Borrowed(include_str!("render/vfx_sort.wgsl"));
59

60
/// Labels for the Hanabi systems.
61
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)]
62
pub enum EffectSystems {
63
    /// Tick all effect instances to generate particle spawn counts.
64
    ///
65
    /// This system runs during the [`PostUpdate`] schedule. Any system which
66
    /// modifies an effect spawner should run before this set to ensure the
67
    /// spawner takes into account the newly set values during its ticking.
68
    TickSpawners,
69

70
    /// Compile the effect instances, updating the [`CompiledParticleEffect`]
71
    /// components.
72
    ///
73
    /// This system runs during the [`PostUpdate`] schedule. This is largely an
74
    /// internal task which can be ignored by most users.
75
    ///
76
    /// [`CompiledParticleEffect`]: crate::CompiledParticleEffect
77
    CompileEffects,
78

79
    /// Update the properties of the effect instance based on the declared
80
    /// properties in the [`EffectAsset`], updating the associated
81
    /// [`EffectProperties`] component.
82
    ///
83
    /// This system runs during the [`PostUpdate`] schedule, after the assets
84
    /// have been updated. Any system which modifies an [`EffectAsset`]'s
85
    /// declared properties should run before this set in order for changes to
86
    /// be taken into account in the same frame.
87
    UpdatePropertiesFromAsset,
88

89
    /// Prepare effect assets for the extracted effects.
90
    ///
91
    /// Part of Bevy's own [`RenderSystems::PrepareAssets`].
92
    PrepareEffectAssets,
93

94
    /// Queue the GPU commands for the extracted effects.
95
    ///
96
    /// Part of Bevy's own [`RenderSystems::Queue`].
97
    QueueEffects,
98

99
    /// Prepare GPU data for the queued effects.
100
    ///
101
    /// Part of Bevy's own [`RenderSystems::PrepareResources`].
102
    PrepareEffectGpuResources,
103

104
    /// Prepare the GPU bind groups once all buffers have been (re-)allocated
105
    /// and won't change this frame.
106
    ///
107
    /// Part of Bevy's own [`RenderSystems::PrepareBindGroups`].
108
    PrepareBindGroups,
109
}
110

111
pub mod main_graph {
112
    pub mod node {
113
        use bevy::render::render_graph::RenderLabel;
114

115
        /// Label for the simulation driver node running the simulation graph.
116
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RenderLabel)]
117
        pub struct HanabiDriverNode;
118
    }
119
}
120

121
pub mod simulate_graph {
122
    use bevy::render::render_graph::RenderSubGraph;
123

124
    /// Name of the simulation sub-graph.
125
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RenderSubGraph)]
126
    pub struct HanabiSimulateGraph;
127

128
    pub mod node {
129
        use bevy::render::render_graph::RenderLabel;
130

131
        /// Label for the simulation node (init and update compute passes;
132
        /// view-independent).
133
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, RenderLabel)]
134
        pub struct HanabiSimulateNode;
135
    }
136
}
137

138
const HANABI_COMMON_TEMPLATE_HANDLE: Handle<Shader> =
139
    uuid_handle!("626E7AD3-4E54-487E-B796-9A90E34CC1EC");
140

141
/// Plugin to add systems related to Hanabi.
142
#[derive(Debug, Clone, Copy)]
143
pub struct HanabiPlugin;
144

145
impl HanabiPlugin {
146
    /// Create the `vfx_common.wgsl` shader with proper alignment.
147
    ///
148
    /// This creates a new [`Shader`] from the `vfx_common.wgsl` template file,
149
    /// by applying the given alignment for storage buffers. This produces a
150
    /// shader ready for the specific GPU device associated with that
151
    /// alignment.
152
    pub(crate) fn make_common_shader(min_storage_buffer_offset_alignment: u32) -> Shader {
6✔
153
        let batch_info_padding_code =
6✔
154
            GpuBatchInfo::padding_code(min_storage_buffer_offset_alignment);
12✔
155
        let effect_metadata_padding_code =
6✔
156
            GpuEffectMetadata::padding_code(min_storage_buffer_offset_alignment);
12✔
157
        let render_effect_indirect_size =
6✔
158
            GpuEffectMetadata::aligned_size(min_storage_buffer_offset_alignment);
12✔
159
        let effect_metadata_stride_code =
6✔
160
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
12✔
161
        let common_code = include_str!("render/vfx_common.wgsl")
30✔
162
            .replace("{{BATCH_INFO_PADDING}}", &batch_info_padding_code)
18✔
163
            .replace("{{EFFECT_METADATA_PADDING}}", &effect_metadata_padding_code)
12✔
164
            .replace("{{EFFECT_METADATA_STRIDE}}", &effect_metadata_stride_code);
6✔
165
        Shader::from_wgsl(
166
            common_code,
6✔
167
            std::path::Path::new(file!())
18✔
168
                .parent()
12✔
169
                .unwrap()
12✔
170
                .join(format!(
6✔
171
                    "render/vfx_common_{}.wgsl",
6✔
172
                    min_storage_buffer_offset_alignment
6✔
173
                ))
174
                .to_string_lossy(),
6✔
175
        )
176
    }
177

178
    /// Create the `vfx_indirect.wgsl` shader with proper alignment.
179
    ///
180
    /// This creates a new [`Shader`] from the `vfx_indirect.wgsl` template
181
    /// file, by applying the given alignment for storage buffers. This
182
    /// produces a shader ready for the specific GPU device associated with
183
    /// that alignment.
184
    pub(crate) fn make_indirect_shader(
6✔
185
        min_storage_buffer_offset_alignment: u32,
186
        has_events: bool,
187
    ) -> Shader {
188
        let render_effect_indirect_size =
6✔
189
            GpuEffectMetadata::aligned_size(min_storage_buffer_offset_alignment);
12✔
190
        let render_effect_indirect_stride_code =
6✔
191
            (render_effect_indirect_size.get() as u32).to_wgsl_string();
12✔
192
        let indirect_code = include_str!("render/vfx_indirect.wgsl").replace(
18✔
193
            "{{EFFECT_METADATA_STRIDE}}",
194
            &render_effect_indirect_stride_code,
6✔
195
        );
196
        Shader::from_wgsl(
197
            indirect_code,
6✔
198
            std::path::Path::new(file!())
12✔
199
                .parent()
6✔
200
                .unwrap()
6✔
201
                .join(format!(
12✔
202
                    "render/vfx_indirect_{}_{}.wgsl",
203
                    min_storage_buffer_offset_alignment,
204
                    if has_events { "events" } else { "noevent" },
12✔
205
                ))
206
                .to_string_lossy(),
6✔
207
        )
208
    }
209

210
    /// Create the `vfx_prefix_sum.wgsl` shader.
211
    ///
212
    /// This creates a new [`Shader`] from the `vfx_prefix_sum.wgsl` template
213
    /// file.
214
    pub(crate) fn make_prefix_sum_shader() -> Shader {
3✔
215
        let prefix_sum_code = include_str!("render/vfx_prefix_sum.wgsl");
6✔
216
        Shader::from_wgsl(
217
            prefix_sum_code,
3✔
218
            std::path::Path::new(file!())
6✔
219
                .parent()
3✔
220
                .unwrap()
3✔
221
                .join("render/vfx_prefix_sum.wgsl")
3✔
222
                .to_string_lossy(),
3✔
223
        )
224
    }
225
}
226

227
impl Plugin for HanabiPlugin {
228
    fn build(&self, app: &mut App) {
3✔
229
        // Register asset
230
        app.init_asset::<EffectAsset>()
18✔
231
            .insert_resource(Random(spawn::new_rng()))
15✔
232
            .add_plugins(ExtractComponentPlugin::<EffectVisibilityClass>::default())
12✔
233
            .init_resource::<DefaultMesh>()
234
            .init_resource::<ShaderCache>()
235
            .init_resource::<DebugSettings>()
236
            .init_resource::<Time<EffectSimulation>>()
237
            .configure_sets(
238
                PostUpdate,
12✔
239
                (
240
                    EffectSystems::TickSpawners
12✔
241
                        // This checks the visibility to skip work, so needs to run after
242
                        // ComputedVisibility was updated.
243
                        .after(VisibilitySystems::VisibilityPropagate),
12✔
244
                    EffectSystems::CompileEffects,
9✔
245
                ),
246
            )
247
            .configure_sets(
248
                PreUpdate,
9✔
249
                EffectSystems::UpdatePropertiesFromAsset.after(bevy::asset::AssetTrackingSystems),
9✔
250
            )
251
            .add_systems(
252
                First,
6✔
253
                effect_simulation_time_system
3✔
254
                    .after(time_system)
6✔
255
                    .in_set(TimeSystems),
3✔
256
            )
257
            .add_systems(
258
                PostUpdate,
3✔
259
                (
260
                    tick_spawners.in_set(EffectSystems::TickSpawners),
9✔
261
                    compile_effects.in_set(EffectSystems::CompileEffects),
9✔
262
                    update_properties_from_asset.in_set(EffectSystems::UpdatePropertiesFromAsset),
3✔
263
                ),
264
            );
265

266
        #[cfg(feature = "serde")]
267
        app.init_asset_loader::<EffectAssetLoader>();
6✔
268

269
        // Register types with reflection
270
        app.register_type::<EffectAsset>()
3✔
271
            .register_type::<ParticleEffect>()
272
            .register_type::<EffectProperties>()
273
            .register_type::<SpawnerSettings>()
274
            .register_type::<Time<EffectSimulation>>();
275
    }
276

277
    fn finish(&self, app: &mut App) {
3✔
278
        let render_device = app
9✔
279
            .sub_app(RenderApp)
3✔
280
            .world()
281
            .resource::<RenderDevice>()
282
            .clone();
283

284
        let adapter_name = app
6✔
285
            .world()
286
            .get_resource::<RenderAdapterInfo>()
287
            .map(|ai| &ai.name[..])
6✔
288
            .unwrap_or("<unknown>");
289

290
        // Check device limits
291
        let limits = render_device.limits();
9✔
292
        if limits.max_bind_groups < 4 {
3✔
293
            error!("Hanabi requires a GPU device supporting at least 4 bind groups (Limits::max_bind_groups).\n  Current adapter: {}\n  Supported bind groups: {}", adapter_name, limits.max_bind_groups);
×
294
            return;
×
295
        } else {
296
            info!("Initializing Hanabi for GPU adapter {}", adapter_name);
3✔
297
        }
298

299
        // Insert the properly aligned `vfx_common.wgsl` shader into Assets<Shader>, so
300
        // that the automated Bevy shader processing finds it as an import. This is used
301
        // for init/update/render shaders (but not the indirect one).
302
        {
303
            let common_shader = HanabiPlugin::make_common_shader(
304
                render_device.limits().min_storage_buffer_offset_alignment,
×
305
            );
306
            let mut assets = app.world_mut().resource_mut::<Assets<Shader>>();
×
307
            assets
×
308
                .insert(&HANABI_COMMON_TEMPLATE_HANDLE, common_shader)
×
309
                .unwrap();
310
        }
311

312
        // Insert the two variants of the properly aligned `vfx_indirect.wgsl` shaders
313
        // into Assets<Shader>.
314
        let (
315
            indirect_shader_noevent,
×
316
            indirect_shader_events,
×
NEW
317
            prefix_sum_shader,
×
318
            sort_fill_shader,
×
319
            sort_shader,
×
320
            sort_copy_shader,
×
321
        ) = {
×
322
            let align = render_device.limits().min_storage_buffer_offset_alignment;
×
323
            let indirect_shader_noevent = HanabiPlugin::make_indirect_shader(align, false);
×
324
            let indirect_shader_events = HanabiPlugin::make_indirect_shader(align, true);
×
NEW
325
            let prefix_sum_shader = HanabiPlugin::make_prefix_sum_shader();
×
326
            let sort_fill_shader = Shader::from_wgsl(
327
                include_str!("render/vfx_sort_fill.wgsl"),
×
328
                std::path::Path::new(file!())
×
329
                    .parent()
×
330
                    .unwrap()
×
331
                    .join("render/vfx_sort_fill.wgsl")
×
332
                    .to_string_lossy(),
×
333
            );
334
            let sort_shader = Shader::from_wgsl(
NEW
335
                VFX_SORT_WGSL,
×
336
                std::path::Path::new(file!())
×
337
                    .parent()
×
338
                    .unwrap()
×
339
                    .join("render/vfx_sort.wgsl")
×
340
                    .to_string_lossy(),
×
341
            );
342
            let sort_copy_shader = Shader::from_wgsl(
343
                include_str!("render/vfx_sort_copy.wgsl"),
×
344
                std::path::Path::new(file!())
×
345
                    .parent()
×
346
                    .unwrap()
×
347
                    .join("render/vfx_sort_copy.wgsl")
×
348
                    .to_string_lossy(),
×
349
            );
350

351
            let mut assets = app.world_mut().resource_mut::<Assets<Shader>>();
×
352
            let indirect_shader_noevent = assets.add(indirect_shader_noevent);
×
353
            let indirect_shader_events = assets.add(indirect_shader_events);
×
NEW
354
            let prefix_sum_shader = assets.add(prefix_sum_shader);
×
355
            let sort_fill_shader = assets.add(sort_fill_shader);
×
356
            let sort_shader = assets.add(sort_shader);
×
357
            let sort_copy_shader = assets.add(sort_copy_shader);
×
358

359
            (
360
                indirect_shader_noevent,
×
361
                indirect_shader_events,
×
NEW
362
                prefix_sum_shader,
×
363
                sort_fill_shader,
×
364
                sort_shader,
×
365
                sort_copy_shader,
×
366
            )
367
        };
368

369
        let effects_meta = EffectsMeta::new(
370
            render_device.clone(),
×
371
            indirect_shader_noevent,
×
372
            indirect_shader_events,
×
NEW
373
            prefix_sum_shader,
×
374
        );
375

376
        let effect_cache = EffectCache::new(render_device.clone());
×
377
        let property_cache = PropertyCache::new(render_device.clone());
×
378
        let event_cache = EventCache::new(render_device);
×
379

380
        let render_app = app.sub_app_mut(RenderApp);
×
381
        let sort_bind_groups = SortBindGroups::new(
382
            render_app.world_mut(),
×
383
            sort_fill_shader,
×
384
            sort_shader,
×
385
            sort_copy_shader,
×
386
        );
387

388
        // Register the custom render pipeline
389
        render_app
×
390
            .insert_resource(effects_meta)
×
391
            .insert_resource(effect_cache)
×
392
            .insert_resource(property_cache)
×
393
            .insert_resource(event_cache)
×
394
            .init_resource::<RenderDebugSettings>()
395
            .init_resource::<EffectBindGroups>()
396
            .init_resource::<PropertyBindGroups>()
397
            .init_resource::<InitFillDispatchQueue>()
398
            .insert_resource(sort_bind_groups)
×
399
            .init_resource::<UtilsPipeline>()
400
            .init_resource::<GpuBufferOperations>()
401
            .init_resource::<PrefixSumPipeline>()
402
            .init_resource::<DispatchIndirectPipeline>()
403
            .init_resource::<SpecializedComputePipelines<DispatchIndirectPipeline>>()
404
            .init_resource::<ParticlesInitPipeline>()
405
            .init_resource::<SpecializedComputePipelines<ParticlesInitPipeline>>()
406
            .init_resource::<ParticlesInitPipeline>()
407
            .init_resource::<SpecializedComputePipelines<ParticlesInitPipeline>>()
408
            .init_resource::<ParticlesUpdatePipeline>()
409
            .init_resource::<SpecializedComputePipelines<ParticlesUpdatePipeline>>()
410
            .init_resource::<ParticlesRenderPipeline>()
411
            .init_resource::<SpecializedRenderPipelines<ParticlesRenderPipeline>>()
412
            .init_resource::<EffectAssetEvents>()
413
            .init_resource::<SimParams>()
414
            .init_resource::<SortedEffectBatches>()
415
            .configure_sets(
416
                Render,
×
417
                (
418
                    EffectSystems::PrepareEffectAssets.in_set(RenderSystems::PrepareAssets),
×
419
                    EffectSystems::QueueEffects.in_set(RenderSystems::Queue),
×
420
                    EffectSystems::PrepareEffectGpuResources
×
421
                        .in_set(RenderSystems::PrepareResources),
×
422
                    EffectSystems::PrepareBindGroups.in_set(RenderSystems::PrepareBindGroups),
×
423
                ),
424
            )
425
            .edit_schedule(ExtractSchedule, |schedule| {
3✔
426
                schedule.add_systems((
9✔
427
                    start_stop_gpu_debug_capture,
3✔
428
                    report_ready_state.before(extract_effects),
3✔
429
                    extract_effects,
3✔
430
                    extract_sim_params,
3✔
431
                    extract_effect_events,
3✔
432
                ));
433
            })
434
            .add_systems(
435
                Render,
×
436
                (
437
                    (
438
                        // Do all clears from previous frame; they can run in parallel as they
439
                        // clear different resources.
440
                        (clear_transient_batch_inputs, clear_previous_frame_resizes),
×
441
                        // Allocate GPU resources depending only on the extracted data; they can
442
                        // run in parallel as they touch different components.
443
                        (
444
                            // Allocate GPU storage for the effect particles
445
                            allocate_effects,
×
446
                            // Allocate GPU storage for GPU events (for child effects)
447
                            allocate_events,
×
448
                            // Allocate GPU storage for properties
449
                            allocate_properties,
×
450
                            // Update draw indirect args if Bevy relocated a render mesh
451
                            update_mesh_locations
×
452
                                // Need Bevy to have allocated the mesh in the MeshAllocator
453
                                .after(bevy::render::mesh::allocator::allocate_and_free_meshes)
×
454
                                // Need Bevy to have prepared the RenderMesh to read it
455
                                .after(prepare_assets::<bevy::render::mesh::RenderMesh>),
×
456
                            // Allocate GPU effect metadata
457
                            allocate_metadata,
×
458
                        ),
459
                        // Allocate parent and child infos. Those need all effects allocated and
460
                        // all parents resolved first, as well as event buffers allocated.
461
                        allocate_parent_child_infos
×
462
                            // Need the effects allocated to fetch the parent's slab ID
463
                            .after(allocate_effects)
×
464
                            // Need the events allocated to fetch the event buffer of children
465
                            .after(allocate_events),
×
466
                        fixup_parents
×
467
                            // Second pass fixup after allocate_parent_child_infos()
468
                            .after(allocate_parent_child_infos),
×
469
                        // Prepare pipelines; they can run in parallel as they touch different
470
                        // resources.
471
                        (
472
                            // Resolve the init and update pipelines, queue them if needed, and
473
                            // check their state to determine if the
474
                            // effect can be used this frame.
475
                            prepare_init_update_pipelines
×
476
                                // Need the bind group layout for the effect itself, which depends
477
                                // on the particle layout.
478
                                .after(allocate_effects)
×
479
                                // Need the bind group layout for properties, which depends on the
480
                                // property layout.
481
                                .after(allocate_properties)
×
482
                                // Need the number of event buffers to bind
483
                                .after(fixup_parents),
×
484
                            // Prepare the indirect pipeline depending on whether there's any child
485
                            // info.
486
                            prepare_indirect_pipeline
×
487
                                // Need to know if any GPU event using effect is active or not
488
                                .after(allocate_events),
×
489
                        ),
490
                        propagate_ready_state
×
491
                            // Need the ready state of parents, which depends on the init/update
492
                            // pipeline states
493
                            .after(prepare_init_update_pipelines),
×
494
                        prepare_batch_inputs,
×
495
                        batch_effects,
×
496
                    )
497
                        // TODO: remove this chain() once all system dependencies are setup
498
                        // correctly above.
499
                        .chain()
×
500
                        .in_set(EffectSystems::PrepareEffectAssets),
×
501
                    // Once batched, queue the effects/batches which are ready to be
502
                    // updated/rendered this frame.
503
                    queue_effects
×
504
                        .in_set(EffectSystems::QueueEffects)
×
505
                        .after(batch_effects),
×
506
                    // Queue the dispatch ops to fill the indirect dispatch args of the init pass
507
                    // of child effects.
508
                    queue_init_indirect_workgroup_update
×
509
                        .in_set(EffectSystems::QueueEffects)
×
510
                        .after(batch_effects)
×
511
                        .after(fixup_parents),
×
512
                    prepare_gpu_resources
×
513
                        .in_set(EffectSystems::PrepareEffectGpuResources)
×
514
                        // This creates the bind group for the view
515
                        .after(prepare_view_uniforms)
×
516
                        // Upload and optionally resize the draw indirect args buffer
517
                        .after(update_mesh_locations)
×
518
                        // Bind groups depend on buffers being re-/allocated
519
                        .before(prepare_bind_groups),
×
520
                    prepare_property_buffers
×
521
                        .in_set(EffectSystems::PrepareEffectGpuResources)
×
522
                        .before(prepare_bind_groups),
×
523
                    prepare_effect_metadata
×
524
                        .in_set(EffectSystems::PrepareEffectGpuResources)
×
525
                        // Need DispatchBufferIndices to be allocated
526
                        .after(allocate_effects)
×
527
                        // Need the draw indirect args to be allocated
528
                        .after(update_mesh_locations)
×
529
                        // Need the local/global/base child index
530
                        .after(fixup_parents)
×
531
                        // Need the indirect dispatch args index for GPU event based init pass
532
                        .after(allocate_events)
×
533
                        // Need the properties block offset in GPU slab
NEW
534
                        .after(allocate_properties)
×
535
                        // This may invalidate some bind groups when resizing the metadata buffer
536
                        .before(prepare_bind_groups),
×
537
                    queue_init_fill_dispatch_ops
×
538
                        .in_set(EffectSystems::PrepareEffectGpuResources)
×
539
                        .after(prepare_gpu_resources)
×
540
                        .before(prepare_bind_groups),
×
541
                    // Prepare the bind groups
542
                    prepare_bind_groups
×
543
                        .in_set(EffectSystems::PrepareBindGroups)
×
544
                        .after(queue_effects)
×
545
                        .after(prepare_assets::<GpuImage>),
×
546
                ),
547
            );
548

549
        // Register observers to deallocate GPU resources
550
        {
551
            let world = render_app.world_mut();
×
552
            world.add_observer(on_remove_cached_effect);
×
553
            world.add_observer(on_remove_cached_metadata);
×
554
            world.add_observer(on_remove_cached_draw_indirect_args);
×
555
            world.add_observer(on_remove_cached_effect_events);
×
556
            world.add_observer(on_remove_cached_properties);
×
557
        }
558

559
        // Register the draw function for drawing the particles. This will be called
560
        // during the main 2D/3D pass, at the Transparent2d/3d phase, after the
561
        // opaque objects have been rendered (or, rather, commands for those
562
        // have been recorded).
563
        #[cfg(feature = "2d")]
564
        {
565
            let draw_particles = DrawEffects::new(render_app.world_mut());
×
566
            render_app
×
567
                .world()
568
                .get_resource::<DrawFunctions<Transparent2d>>()
569
                .unwrap()
570
                .write()
571
                .add(draw_particles);
×
572
        }
573
        #[cfg(feature = "3d")]
574
        {
575
            let draw_particles = DrawEffects::new(render_app.world_mut());
×
576
            render_app
×
577
                .world()
578
                .get_resource::<DrawFunctions<Transparent3d>>()
579
                .unwrap()
580
                .write()
581
                .add(draw_particles);
×
582

583
            let draw_particles = DrawEffects::new(render_app.world_mut());
×
584
            render_app
×
585
                .world()
586
                .get_resource::<DrawFunctions<AlphaMask3d>>()
587
                .unwrap()
588
                .write()
589
                .add(draw_particles);
×
590

591
            let draw_particles = DrawEffects::new(render_app.world_mut());
×
592
            render_app
×
593
                .world()
594
                .get_resource::<DrawFunctions<Opaque3d>>()
595
                .unwrap()
596
                .write()
597
                .add(draw_particles);
×
598
        }
599

600
        // Add the simulation sub-graph. This render graph runs once per frame no matter
601
        // how many cameras/views are active (view-independent).
602
        let mut simulate_graph = RenderGraph::default();
×
603
        let simulate_node = VfxSimulateNode::new(render_app.world_mut());
×
604
        simulate_graph.add_node(simulate_graph::node::HanabiSimulateNode, simulate_node);
×
605
        let mut graph = render_app
×
606
            .world_mut()
607
            .get_resource_mut::<RenderGraph>()
608
            .unwrap();
609
        graph.add_sub_graph(simulate_graph::HanabiSimulateGraph, simulate_graph);
×
610

611
        // Add the simulation driver node which executes the simulation sub-graph. It
612
        // runs before the camera driver, since rendering needs to access simulated
613
        // particles.
614
        graph.add_node(main_graph::node::HanabiDriverNode, VfxSimulateDriverNode {});
×
615
        graph.add_node_edge(
×
616
            main_graph::node::HanabiDriverNode,
×
617
            bevy::render::graph::CameraDriverLabel,
×
618
        );
619
    }
620
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc