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

djeedai / bevy_hanabi / 13738631396

08 Mar 2025 02:40PM UTC coverage: 39.99% (+0.002%) from 39.988%
13738631396

push

github

web-flow
Add `EffectAsset::prng_seed` (#427)

Add a new field to the effect asset, which determines the seed for the
GPU side PRNG used in shader expressions. This value defaults to zero,
making consecutive runs deterministic. This both gives better artistic
control (what executes at runtime is what was authored), and helps with
debugging by making repros consistent.

To restore the old behavior, assign a random value to the seed when
creating the effect asset, _.e.g_ `prng_seed = rand::random::<u32>()`.

1 of 5 new or added lines in 2 files covered. (20.0%)

1 existing line in 1 file now uncovered.

3200 of 8002 relevant lines covered (39.99%)

18.69 hits per line

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

3.45
/src/render/mod.rs
1
use std::marker::PhantomData;
2
use std::{
3
    borrow::Cow,
4
    hash::{DefaultHasher, Hash, Hasher},
5
    num::{NonZeroU32, NonZeroU64},
6
    ops::{Deref, DerefMut, Range},
7
    time::Duration,
8
};
9

10
#[cfg(feature = "2d")]
11
use bevy::core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT};
12
#[cfg(feature = "2d")]
13
use bevy::math::FloatOrd;
14
#[cfg(feature = "3d")]
15
use bevy::{
16
    core_pipeline::{
17
        core_3d::{AlphaMask3d, Opaque3d, Transparent3d, CORE_3D_DEPTH_FORMAT},
18
        prepass::OpaqueNoLightmap3dBinKey,
19
    },
20
    render::render_phase::{BinnedPhaseItem, ViewBinnedRenderPhases},
21
};
22
use bevy::{
23
    ecs::{
24
        prelude::*,
25
        system::{lifetimeless::*, SystemParam, SystemState},
26
    },
27
    log::trace,
28
    prelude::*,
29
    render::{
30
        mesh::{
31
            allocator::MeshAllocator, MeshVertexBufferLayoutRef, RenderMesh, RenderMeshBufferInfo,
32
        },
33
        render_asset::RenderAssets,
34
        render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo},
35
        render_phase::{
36
            Draw, DrawError, DrawFunctions, PhaseItemExtraIndex, SortedPhaseItem,
37
            TrackedRenderPass, ViewSortedRenderPhases,
38
        },
39
        render_resource::*,
40
        renderer::{RenderContext, RenderDevice, RenderQueue},
41
        sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity},
42
        texture::GpuImage,
43
        view::{
44
            ExtractedView, RenderVisibleEntities, ViewTarget, ViewUniform, ViewUniformOffset,
45
            ViewUniforms,
46
        },
47
        Extract,
48
    },
49
    utils::{Entry, HashMap, HashSet},
50
};
51
use bitflags::bitflags;
52
use bytemuck::{Pod, Zeroable};
53
use effect_cache::{BufferState, CachedEffect, EffectSlice};
54
use event::{CachedChildInfo, CachedEffectEvents, CachedParentInfo, CachedParentRef, GpuChildInfo};
55
use fixedbitset::FixedBitSet;
56
use naga_oil::compose::{Composer, NagaModuleDescriptor};
57

58
use crate::{
59
    asset::{DefaultMesh, EffectAsset},
60
    calc_func_id,
61
    plugin::WithCompiledParticleEffect,
62
    render::{
63
        batch::{BatchInput, EffectDrawBatch, InitAndUpdatePipelineIds},
64
        effect_cache::DispatchBufferIndices,
65
    },
66
    AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShader, EffectSimulation,
67
    EffectSpawner, ParticleLayout, PropertyLayout, SimulationCondition, TextureLayout,
68
};
69

70
mod aligned_buffer_vec;
71
mod batch;
72
mod buffer_table;
73
mod effect_cache;
74
mod event;
75
mod gpu_buffer;
76
mod property;
77
mod shader_cache;
78
mod sort;
79

80
use aligned_buffer_vec::AlignedBufferVec;
81
use batch::BatchSpawnInfo;
82
pub(crate) use batch::SortedEffectBatches;
83
use buffer_table::{BufferTable, BufferTableId};
84
pub(crate) use effect_cache::EffectCache;
85
pub(crate) use event::EventCache;
86
pub(crate) use property::{
87
    on_remove_cached_properties, prepare_property_buffers, PropertyBindGroups, PropertyCache,
88
};
89
use property::{CachedEffectProperties, PropertyBindGroupKey};
90
pub use shader_cache::ShaderCache;
91
pub(crate) use sort::SortBindGroups;
92

93
use self::batch::EffectBatch;
94

95
// Size of an indirect index (including both parts of the ping-pong buffer) in
96
// bytes.
97
const INDIRECT_INDEX_SIZE: u32 = 12;
98

99
fn calc_hash<H: Hash>(value: &H) -> u64 {
×
100
    let mut hasher = DefaultHasher::default();
×
101
    value.hash(&mut hasher);
×
102
    hasher.finish()
×
103
}
104

105
/// Source data (buffer and range inside the buffer) to create a buffer binding.
106
#[derive(Debug, Clone)]
107
pub(crate) struct BufferBindingSource {
108
    buffer: Buffer,
109
    offset: u32,
110
    size: NonZeroU32,
111
}
112

113
impl BufferBindingSource {
114
    /// Get a binding over the source data.
115
    pub fn binding(&self) -> BindingResource {
×
116
        BindingResource::Buffer(BufferBinding {
×
117
            buffer: &self.buffer,
×
118
            offset: self.offset as u64 * 4,
×
119
            size: Some(self.size.into()),
×
120
        })
121
    }
122
}
123

124
impl PartialEq for BufferBindingSource {
125
    fn eq(&self, other: &Self) -> bool {
×
126
        self.buffer.id() == other.buffer.id()
×
127
            && self.offset == other.offset
×
128
            && self.size == other.size
×
129
    }
130
}
131

132
impl<'a> From<&'a BufferBindingSource> for BufferBinding<'a> {
133
    fn from(value: &'a BufferBindingSource) -> Self {
×
134
        BufferBinding {
135
            buffer: &value.buffer,
×
136
            offset: value.offset as u64,
×
137
            size: Some(value.size.into()),
×
138
        }
139
    }
140
}
141

142
/// Simulation parameters, available to all shaders of all effects.
143
#[derive(Debug, Default, Clone, Copy, Resource)]
144
pub(crate) struct SimParams {
145
    /// Current effect system simulation time since startup, in seconds.
146
    /// This is based on the [`Time<EffectSimulation>`](EffectSimulation) clock.
147
    time: f64,
148
    /// Delta time, in seconds, since last effect system update.
149
    delta_time: f32,
150

151
    /// Current virtual time since startup, in seconds.
152
    /// This is based on the [`Time<Virtual>`](Virtual) clock.
153
    virtual_time: f64,
154
    /// Virtual delta time, in seconds, since last effect system update.
155
    virtual_delta_time: f32,
156

157
    /// Current real time since startup, in seconds.
158
    /// This is based on the [`Time<Real>`](Real) clock.
159
    real_time: f64,
160
    /// Real delta time, in seconds, since last effect system update.
161
    real_delta_time: f32,
162
}
163

164
/// GPU representation of [`SimParams`], as well as additional per-frame
165
/// effect-independent values.
166
#[repr(C)]
167
#[derive(Debug, Copy, Clone, Pod, Zeroable, ShaderType)]
168
struct GpuSimParams {
169
    /// Delta time, in seconds, since last effect system update.
170
    delta_time: f32,
171
    /// Current effect system simulation time since startup, in seconds.
172
    ///
173
    /// This is a lower-precision variant of [`SimParams::time`].
174
    time: f32,
175
    /// Virtual delta time, in seconds, since last effect system update.
176
    virtual_delta_time: f32,
177
    /// Current virtual time since startup, in seconds.
178
    ///
179
    /// This is a lower-precision variant of [`SimParams::time`].
180
    virtual_time: f32,
181
    /// Real delta time, in seconds, since last effect system update.
182
    real_delta_time: f32,
183
    /// Current real time since startup, in seconds.
184
    ///
185
    /// This is a lower-precision variant of [`SimParams::time`].
186
    real_time: f32,
187
    /// Total number of effects to update this frame. Used by the indirect
188
    /// compute pipeline to cap the compute thread to the actual number of
189
    /// effects to process.
190
    ///
191
    /// This is only used by the `vfx_indirect` compute shader.
192
    num_effects: u32,
193
}
194

195
impl Default for GpuSimParams {
196
    fn default() -> Self {
×
197
        Self {
198
            delta_time: 0.04,
199
            time: 0.0,
200
            virtual_delta_time: 0.04,
201
            virtual_time: 0.0,
202
            real_delta_time: 0.04,
203
            real_time: 0.0,
204
            num_effects: 0,
205
        }
206
    }
207
}
208

209
impl From<SimParams> for GpuSimParams {
210
    #[inline]
211
    fn from(src: SimParams) -> Self {
×
212
        Self::from(&src)
×
213
    }
214
}
215

216
impl From<&SimParams> for GpuSimParams {
217
    fn from(src: &SimParams) -> Self {
×
218
        Self {
219
            delta_time: src.delta_time,
×
220
            time: src.time as f32,
×
221
            virtual_delta_time: src.virtual_delta_time,
×
222
            virtual_time: src.virtual_time as f32,
×
223
            real_delta_time: src.real_delta_time,
×
224
            real_time: src.real_time as f32,
×
225
            ..default()
226
        }
227
    }
228
}
229

230
/// Compressed representation of a transform for GPU transfer.
231
///
232
/// The transform is stored as the three first rows of a transposed [`Mat4`],
233
/// assuming the last row is the unit row [`Vec4::W`]. The transposing ensures
234
/// that the three values are [`Vec4`] types which are naturally aligned and
235
/// without padding when used in WGSL. Without this, storing only the first
236
/// three components of each column would introduce padding, and would use the
237
/// same storage size on GPU as a full [`Mat4`].
238
#[repr(C)]
239
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
240
pub(crate) struct GpuCompressedTransform {
241
    pub x_row: [f32; 4],
242
    pub y_row: [f32; 4],
243
    pub z_row: [f32; 4],
244
}
245

246
impl From<Mat4> for GpuCompressedTransform {
247
    fn from(value: Mat4) -> Self {
×
248
        let tr = value.transpose();
×
249
        #[cfg(test)]
250
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
251
        Self {
252
            x_row: tr.x_axis.to_array(),
×
253
            y_row: tr.y_axis.to_array(),
×
254
            z_row: tr.z_axis.to_array(),
×
255
        }
256
    }
257
}
258

259
impl From<&Mat4> for GpuCompressedTransform {
260
    fn from(value: &Mat4) -> Self {
×
261
        let tr = value.transpose();
×
262
        #[cfg(test)]
263
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
264
        Self {
265
            x_row: tr.x_axis.to_array(),
×
266
            y_row: tr.y_axis.to_array(),
×
267
            z_row: tr.z_axis.to_array(),
×
268
        }
269
    }
270
}
271

272
impl GpuCompressedTransform {
273
    /// Returns the translation as represented by this transform.
274
    #[allow(dead_code)]
275
    pub fn translation(&self) -> Vec3 {
×
276
        Vec3 {
277
            x: self.x_row[3],
×
278
            y: self.y_row[3],
×
279
            z: self.z_row[3],
×
280
        }
281
    }
282
}
283

284
/// Extension trait for shader types stored in a WGSL storage buffer.
285
pub(crate) trait StorageType {
286
    /// Get the aligned size, in bytes, of this type such that it aligns to the
287
    /// given alignment, in bytes.
288
    ///
289
    /// This is mainly used to align GPU types to device requirements.
290
    fn aligned_size(alignment: u32) -> NonZeroU64;
291

292
    /// Get the WGSL padding code to append to the GPU struct to align it.
293
    ///
294
    /// This is useful if the struct needs to be bound directly with a dynamic
295
    /// bind group offset, which requires the offset to be a multiple of a GPU
296
    /// device specific alignment value.
297
    fn padding_code(alignment: u32) -> String;
298
}
299

300
impl<T: ShaderType> StorageType for T {
301
    fn aligned_size(alignment: u32) -> NonZeroU64 {
14✔
302
        NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
14✔
303
    }
304

305
    fn padding_code(alignment: u32) -> String {
6✔
306
        let aligned_size = T::aligned_size(alignment);
6✔
307
        trace!(
6✔
308
            "Aligning {} to {} bytes as device limits requires. Orignal size: {} bytes. Aligned size: {} bytes.",
×
309
            std::any::type_name::<T>(),
×
310
            alignment,
×
311
            T::min_size().get(),
×
312
            aligned_size
×
313
        );
314

315
        // We need to pad the Spawner WGSL struct based on the device padding so that we
316
        // can use it as an array element but also has a direct struct binding.
317
        if T::min_size() != aligned_size {
6✔
318
            let padding_size = aligned_size.get() - T::min_size().get();
6✔
319
            assert!(padding_size % 4 == 0);
6✔
320
            format!("padding: array<u32, {}>", padding_size / 4)
6✔
321
        } else {
322
            "".to_string()
×
323
        }
324
    }
325
}
326

327
/// GPU representation of spawner parameters.
328
#[repr(C)]
329
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
330
pub(crate) struct GpuSpawnerParams {
331
    /// Transform of the effect (origin of the emitter). This is either added to
332
    /// emitted particles at spawn time, if the effect simulated in world
333
    /// space, or to all simulated particles during rendering if the effect is
334
    /// simulated in local space.
335
    transform: GpuCompressedTransform,
336
    /// Inverse of [`transform`], stored with the same convention.
337
    ///
338
    /// [`transform`]: Self::transform
339
    inverse_transform: GpuCompressedTransform,
340
    /// Number of particles to spawn this frame.
341
    spawn: i32,
342
    /// Spawn seed, for randomized modifiers.
343
    seed: u32,
344
    /// Index of the pong (read) buffer for indirect indices, used by the render
345
    /// shader to fetch particles and render them. Only temporarily stored
346
    /// between indirect and render passes, and overwritten each frame by CPU
347
    /// upload. This is mostly a hack to transfer a value between those 2
348
    /// compute passes.
349
    render_pong: u32,
350
    /// Index of the [`GpuEffectMetadata`] for this effect.
351
    effect_metadata_index: u32,
352
}
353

354
/// GPU representation of an indirect compute dispatch input.
355
///
356
/// Note that unlike most other data structure, this doesn't need to be aligned
357
/// (except for the default 4-byte align for most GPU types) to any uniform or
358
/// storage buffer offset alignment, because the buffer storing this is only
359
/// ever used as input to indirect dispatch commands, and never bound as a
360
/// shader resource.
361
///
362
/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DispatchIndirectArgs.html.
363
#[repr(C)]
364
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
365
pub struct GpuDispatchIndirect {
366
    pub x: u32,
367
    pub y: u32,
368
    pub z: u32,
369
}
370

371
impl Default for GpuDispatchIndirect {
372
    fn default() -> Self {
×
373
        Self { x: 0, y: 1, z: 1 }
374
    }
375
}
376

377
/// Stores metadata about each particle effect.
378
///
379
/// This is written by the CPU and read by the GPU.
380
#[repr(C)]
381
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
382
pub struct GpuEffectMetadata {
383
    /// The number of vertices in the mesh, if non-indexed; if indexed, the
384
    /// number of indices in the mesh.
385
    pub vertex_or_index_count: u32,
386
    /// The number of instances to render.
387
    pub instance_count: u32,
388
    /// The first index to render, if the mesh is indexed; the offset of the
389
    /// first vertex, if the mesh is non-indexed.
390
    pub first_index_or_vertex_offset: u32,
391
    /// The offset of the first vertex, if the mesh is indexed; the first
392
    /// instance to render, if the mesh is non-indexed.
393
    pub vertex_offset_or_base_instance: i32,
394
    /// The first instance to render, if indexed; unused if non-indexed.
395
    pub base_instance: u32,
396

397
    // Additional data not part of the required draw indirect args
398
    /// Number of alive particles.
399
    pub alive_count: u32,
400
    /// Cached value of `alive_count` to cap threads in update pass.
401
    pub max_update: u32,
402
    /// Number of dead particles.
403
    pub dead_count: u32,
404
    /// Cached value of `dead_count` to cap threads in init pass.
405
    pub max_spawn: u32,
406
    /// Index of the ping buffer for particle indices. Init and update compute
407
    /// passes always write into the ping buffer and read from the pong buffer.
408
    /// The buffers are swapped (ping = 1 - ping) during the indirect dispatch.
409
    pub ping: u32,
410
    /// Index of the [`GpuSpawnerParams] struct.
411
    pub spawner_index: u32,
412
    /// Index of the [`GpuDispatchIndirect`] struct inside the global
413
    /// [`EffectsMeta::dispatch_indirect_buffer`].
414
    pub indirect_dispatch_index: u32,
415
    /// Index of the [`GpuRenderIndirect`] struct inside the global
416
    /// [`EffectsMeta::render_group_dispatch_buffer`].
417
    pub indirect_render_index: u32,
418
    /// Offset (in u32 count) of the init indirect dispatch struct inside its
419
    /// buffer. This avoids having to align those 16-byte structs to the GPU
420
    /// alignment (at least 32 bytes, even 256 bytes on some).
421
    pub init_indirect_dispatch_index: u32,
422
    /// Index of this effect into its parent's ChildInfo array
423
    /// ([`EffectChildren::effect_cache_ids`] and its associated GPU
424
    /// array). This starts at zero for the first child of each effect, and is
425
    /// only unique per parent, not globally. Only available if this effect is a
426
    /// child of another effect (i.e. if it has a parent).
427
    pub local_child_index: u32,
428
    /// For children, global index of the ChildInfo into the shared array.
429
    pub global_child_index: u32,
430
    /// For parents, base index of the their first ChildInfo into the shared
431
    /// array.
432
    pub base_child_index: u32,
433

434
    /// Particle stride, in number of u32.
435
    pub particle_stride: u32,
436
    /// Offset from the particle start to the first sort key, in number of u32.
437
    pub sort_key_offset: u32,
438
    /// Offset from the particle start to the second sort key, in number of u32.
439
    pub sort_key2_offset: u32,
440

441
    /// Atomic counter incremented each time a particle spawns. Useful for
442
    /// things like RIBBON_ID or any other use where a unique value is needed.
443
    /// The value loops back after some time, but unless some particle lives
444
    /// forever there's little chance of repetition.
445
    pub particle_counter: u32,
446
}
447

448
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
449
/// shader.
450
#[derive(Resource)]
451
pub(crate) struct DispatchIndirectPipeline {
452
    /// Layout of bind group sim_params@0.
453
    sim_params_bind_group_layout: BindGroupLayout,
454
    /// Layout of bind group effect_metadata@1.
455
    effect_metadata_bind_group_layout: BindGroupLayout,
456
    /// Layout of bind group spawner@2.
457
    spawner_bind_group_layout: BindGroupLayout,
458
    /// Layout of bind group child_infos@3.
459
    child_infos_bind_group_layout: BindGroupLayout,
460
    /// Shader when no GPU events are used (no bind group @3).
461
    indirect_shader_noevent: Handle<Shader>,
462
    /// Shader when GPU events are used (bind group @3 present).
463
    indirect_shader_events: Handle<Shader>,
464
}
465

466
impl FromWorld for DispatchIndirectPipeline {
467
    fn from_world(world: &mut World) -> Self {
×
468
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
469

470
        // Copy the indirect pipeline shaders to self, because we can't access anything
471
        // else during pipeline specialization.
472
        let (indirect_shader_noevent, indirect_shader_events) = {
×
473
            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
×
474
            (
475
                effects_meta.indirect_shader_noevent.clone(),
×
476
                effects_meta.indirect_shader_events.clone(),
×
477
            )
478
        };
479

480
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
×
481
        let render_effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
×
482
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
×
483

484
        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
485
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
×
486
        let sim_params_bind_group_layout = render_device.create_bind_group_layout(
×
487
            "hanabi:bind_group_layout:dispatch_indirect:sim_params",
488
            &[BindGroupLayoutEntry {
×
489
                binding: 0,
×
490
                visibility: ShaderStages::COMPUTE,
×
491
                ty: BindingType::Buffer {
×
492
                    ty: BufferBindingType::Uniform,
×
493
                    has_dynamic_offset: false,
×
494
                    min_binding_size: Some(GpuSimParams::min_size()),
×
495
                },
496
                count: None,
×
497
            }],
498
        );
499

500
        trace!(
×
501
            "GpuEffectMetadata: min_size={} padded_size={}",
×
502
            GpuEffectMetadata::min_size(),
×
503
            render_effect_metadata_size,
504
        );
505
        let effect_metadata_bind_group_layout = render_device.create_bind_group_layout(
×
506
            "hanabi:bind_group_layout:dispatch_indirect:effect_metadata@1",
507
            &[
×
508
                // @group(0) @binding(0) var<storage, read_write> effect_metadata_buffer :
509
                // array<u32>;
510
                BindGroupLayoutEntry {
×
511
                    binding: 0,
×
512
                    visibility: ShaderStages::COMPUTE,
×
513
                    ty: BindingType::Buffer {
×
514
                        ty: BufferBindingType::Storage { read_only: false },
×
515
                        has_dynamic_offset: false,
×
516
                        min_binding_size: Some(render_effect_metadata_size),
×
517
                    },
518
                    count: None,
×
519
                },
520
                // @group(0) @binding(2) var<storage, read_write> dispatch_indirect_buffer :
521
                // array<u32>;
522
                BindGroupLayoutEntry {
×
523
                    binding: 1,
×
524
                    visibility: ShaderStages::COMPUTE,
×
525
                    ty: BindingType::Buffer {
×
526
                        ty: BufferBindingType::Storage { read_only: false },
×
527
                        has_dynamic_offset: false,
×
528
                        min_binding_size: Some(
×
529
                            NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap(),
×
530
                        ),
531
                    },
532
                    count: None,
×
533
                },
534
            ],
535
        );
536

537
        // @group(2) @binding(0) var<storage, read_write> spawner_buffer :
538
        // array<Spawner>;
539
        let spawner_bind_group_layout = render_device.create_bind_group_layout(
×
540
            "hanabi:bind_group_layout:dispatch_indirect:spawner@2",
541
            &[BindGroupLayoutEntry {
×
542
                binding: 0,
×
543
                visibility: ShaderStages::COMPUTE,
×
544
                ty: BindingType::Buffer {
×
545
                    ty: BufferBindingType::Storage { read_only: false },
×
546
                    has_dynamic_offset: false,
×
547
                    min_binding_size: Some(spawner_min_binding_size),
×
548
                },
549
                count: None,
×
550
            }],
551
        );
552

553
        // @group(3) @binding(0) var<storage, read_write> child_info_buffer :
554
        // ChildInfoBuffer;
555
        let child_infos_bind_group_layout = render_device.create_bind_group_layout(
×
556
            "hanabi:bind_group_layout:dispatch_indirect:child_infos",
557
            &[BindGroupLayoutEntry {
×
558
                binding: 0,
×
559
                visibility: ShaderStages::COMPUTE,
×
560
                ty: BindingType::Buffer {
×
561
                    ty: BufferBindingType::Storage { read_only: false },
×
562
                    has_dynamic_offset: false,
×
563
                    min_binding_size: Some(GpuChildInfo::min_size()),
×
564
                },
565
                count: None,
×
566
            }],
567
        );
568

569
        Self {
570
            sim_params_bind_group_layout,
571
            effect_metadata_bind_group_layout,
572
            spawner_bind_group_layout,
573
            child_infos_bind_group_layout,
574
            indirect_shader_noevent,
575
            indirect_shader_events,
576
        }
577
    }
578
}
579

580
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
581
pub(crate) struct DispatchIndirectPipelineKey {
582
    /// True if any allocated effect uses GPU spawn events. In that case, the
583
    /// pipeline is specialized to clear all GPU events each frame after the
584
    /// indirect init pass consumed them to spawn particles, and before the
585
    /// update pass optionally produce more events.
586
    /// Key: HAS_GPU_SPAWN_EVENTS
587
    has_events: bool,
588
}
589

590
impl SpecializedComputePipeline for DispatchIndirectPipeline {
591
    type Key = DispatchIndirectPipelineKey;
592

593
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
594
        trace!(
×
595
            "Specializing indirect pipeline (has_events={})",
×
596
            key.has_events
597
        );
598

599
        let mut shader_defs = Vec::with_capacity(2);
×
600
        // Spawner struct needs to be defined with padding, because it's bound as an
601
        // array
602
        shader_defs.push("SPAWNER_PADDING".into());
×
603
        if key.has_events {
×
604
            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
×
605
        }
606

607
        let mut layout = Vec::with_capacity(4);
608
        layout.push(self.sim_params_bind_group_layout.clone());
609
        layout.push(self.effect_metadata_bind_group_layout.clone());
610
        layout.push(self.spawner_bind_group_layout.clone());
611
        if key.has_events {
×
612
            layout.push(self.child_infos_bind_group_layout.clone());
×
613
        }
614

615
        let label = format!(
616
            "hanabi:compute_pipeline:dispatch_indirect{}",
617
            if key.has_events {
618
                "_events"
×
619
            } else {
620
                "_noevent"
×
621
            }
622
        );
623

624
        ComputePipelineDescriptor {
625
            label: Some(label.into()),
626
            layout,
627
            shader: if key.has_events {
628
                self.indirect_shader_events.clone()
629
            } else {
630
                self.indirect_shader_noevent.clone()
631
            },
632
            shader_defs,
633
            entry_point: "main".into(),
634
            push_constant_ranges: vec![],
635
            zero_initialize_workgroup_memory: false,
636
        }
637
    }
638
}
639

640
/// Type of GPU buffer operation.
641
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
642
pub(super) enum GpuBufferOperationType {
643
    /// Clear the destination buffer to zero.
644
    ///
645
    /// The source parameters [`src_offset`] and [`src_stride`] are ignored.
646
    ///
647
    /// [`src_offset`]: crate::GpuBufferOperationArgs::src_offset
648
    /// [`src_stride`]: crate::GpuBufferOperationArgs::src_stride
649
    #[allow(dead_code)]
650
    Zero,
651
    /// Copy a source buffer into a destination buffer.
652
    ///
653
    /// The source can have a stride between each `u32` copied. The destination
654
    /// is always a contiguous buffer.
655
    #[allow(dead_code)]
656
    Copy,
657
    /// Fill the arguments for a later indirect dispatch call.
658
    ///
659
    /// This is similar to a copy, but will round up the source value to the
660
    /// number of threads per workgroup (64) before writing it into the
661
    /// destination.
662
    FillDispatchArgs,
663
    /// Fill the arguments for a later indirect dispatch call.
664
    ///
665
    /// Same as [`FillDispatchArgs`], but with a specialization for the indirect
666
    /// init pass, where we read the destination offset from the source buffer.
667
    InitFillDispatchArgs,
668
    /// Fill the arguments for a later indirect dispatch call.
669
    ///
670
    /// This is the same as [`FillDispatchArgs`], but the source element count
671
    /// is read from the fourth entry in the destination buffer directly,
672
    /// and the source buffer and source arguments are unused.
673
    #[allow(dead_code)]
674
    FillDispatchArgsSelf,
675
}
676

677
/// GPU representation of the arguments of a block operation on a buffer.
678
#[repr(C)]
679
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, Zeroable, ShaderType)]
680
pub(super) struct GpuBufferOperationArgs {
681
    /// Offset, as u32 count, where the operation starts in the source buffer.
682
    src_offset: u32,
683
    /// Stride, as u32 count, between elements in the source buffer.
684
    src_stride: u32,
685
    /// Offset, as u32 count, where the operation starts in the destination
686
    /// buffer.
687
    dst_offset: u32,
688
    /// Stride, as u32 count, between elements in the destination buffer.
689
    dst_stride: u32,
690
    /// Number of u32 elements to process for this operation.
691
    count: u32,
692
}
693

694
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
695
struct QueuedOperationBindGroupKey {
696
    src_buffer: BufferId,
697
    src_binding_size: Option<NonZeroU32>,
698
    dst_buffer: BufferId,
699
    dst_binding_size: Option<NonZeroU32>,
700
}
701

702
#[derive(Debug, Clone)]
703
struct QueuedOperation {
704
    op: GpuBufferOperationType,
705
    args_index: u32,
706
    src_buffer: Buffer,
707
    src_binding_offset: u32,
708
    src_binding_size: Option<NonZeroU32>,
709
    dst_buffer: Buffer,
710
    dst_binding_offset: u32,
711
    dst_binding_size: Option<NonZeroU32>,
712
}
713

714
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
715
    fn from(value: &QueuedOperation) -> Self {
×
716
        Self {
717
            src_buffer: value.src_buffer.id(),
×
718
            src_binding_size: value.src_binding_size,
×
719
            dst_buffer: value.dst_buffer.id(),
×
720
            dst_binding_size: value.dst_binding_size,
×
721
        }
722
    }
723
}
724

725
#[derive(Debug, Clone)]
726
struct InitFillDispatchArgs {
727
    args_index: u32,
728
    event_buffer_index: u32,
729
    event_slice: std::ops::Range<u32>,
730
}
731

732
/// Queue of GPU buffer operations for this frame.
733
#[derive(Resource)]
734
pub(super) struct GpuBufferOperationQueue {
735
    /// Arguments for the buffer operations submitted this frame.
736
    args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
737

738
    /// Unsorted temporary storage for this-frame operations, which will be
739
    /// written to [`args_buffer`] at the end of the frame after being sorted.
740
    args_buffer_unsorted: Vec<GpuBufferOperationArgs>,
741

742
    /// Queued operations.
743
    operation_queue: Vec<QueuedOperation>,
744

745
    /// Queued INIT_FILL_DISPATCH operations.
746
    init_fill_dispatch_args: Vec<InitFillDispatchArgs>,
747

748
    /// Bind groups for the queued operations.
749
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
750
}
751

752
impl FromWorld for GpuBufferOperationQueue {
753
    fn from_world(world: &mut World) -> Self {
1✔
754
        let render_device = world.get_resource::<RenderDevice>().unwrap();
1✔
755
        let align = render_device.limits().min_uniform_buffer_offset_alignment;
1✔
756
        Self::new(align)
1✔
757
    }
758
}
759

760
impl GpuBufferOperationQueue {
761
    pub fn new(align: u32) -> Self {
1✔
762
        let args_buffer = AlignedBufferVec::new(
763
            BufferUsages::UNIFORM,
1✔
764
            Some(NonZeroU64::new(align as u64).unwrap()),
1✔
765
            Some("hanabi:buffer:gpu_operation_args".to_string()),
1✔
766
        );
767
        Self {
768
            args_buffer,
769
            args_buffer_unsorted: vec![],
1✔
770
            operation_queue: vec![],
1✔
771
            init_fill_dispatch_args: vec![],
1✔
772
            bind_groups: default(),
1✔
773
        }
774
    }
775

776
    /// Get a binding for all the entries of the arguments buffer associated
777
    /// with the given event buffer.
778
    pub fn init_args_buffer_binding(
×
779
        &self,
780
        event_buffer_index: u32,
781
    ) -> Option<(BindingResource, u32)> {
782
        // Find the slice corresponding to this event buffer. The entries are sorted by
783
        // event buffer index, so the list of entries is a contiguous slice inside the
784
        // overall buffer.
785
        let Some(start) = self
×
786
            .init_fill_dispatch_args
×
787
            .iter()
788
            .position(|ifda| ifda.event_buffer_index == event_buffer_index)
×
789
        else {
790
            trace!("Event buffer #{event_buffer_index} has no allocated operation.");
×
791
            return None;
×
792
        };
793
        let end = if let Some(end) = self
×
794
            .init_fill_dispatch_args
795
            .iter()
796
            .skip(start)
797
            .position(|ifda| ifda.event_buffer_index != event_buffer_index)
×
798
        {
799
            end
800
        } else {
801
            self.init_fill_dispatch_args.len()
×
802
        };
803
        assert!(start < end);
804
        let count = (end - start) as u32;
×
805
        trace!("Event buffer #{event_buffer_index} has {count} allocated operation(s).");
×
806

807
        self.args_buffer
×
808
            .lead_binding(count)
×
809
            .map(|binding| (binding, count))
×
810
    }
811

812
    /// Clear the queue and begin recording operations for a new frame.
813
    pub fn begin_frame(&mut self) {
2✔
814
        self.args_buffer.clear();
2✔
815
        self.args_buffer_unsorted.clear();
2✔
816
        self.operation_queue.clear();
2✔
817
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
2✔
818
        self.init_fill_dispatch_args.clear();
2✔
819
    }
820

821
    /// Enqueue a generic operation.
822
    pub fn enqueue(
×
823
        &mut self,
824
        op: GpuBufferOperationType,
825
        args: GpuBufferOperationArgs,
826
        src_buffer: Buffer,
827
        src_binding_offset: u32,
828
        src_binding_size: Option<NonZeroU32>,
829
        dst_buffer: Buffer,
830
        dst_binding_offset: u32,
831
        dst_binding_size: Option<NonZeroU32>,
832
    ) -> u32 {
833
        assert_ne!(
×
834
            op,
835
            GpuBufferOperationType::InitFillDispatchArgs,
836
            "FIXME - InitFillDispatchArgs needs enqueue_init_fill() instead"
×
837
        );
838
        trace!(
×
839
            "Queue {:?} op: args={:?} src_buffer={:?} src_binding_offset={} src_binding_size={:?} dst_buffer={:?} dst_binding_offset={} dst_binding_size={:?}",
×
840
            op,
841
            args,
842
            src_buffer,
843
            src_binding_offset,
844
            src_binding_size,
845
            dst_buffer,
846
            dst_binding_offset,
847
            dst_binding_size,
848
        );
849
        let args_index = self.args_buffer_unsorted.len() as u32;
×
850
        self.args_buffer_unsorted.push(args);
×
851
        self.operation_queue.push(QueuedOperation {
×
852
            op,
×
853
            args_index,
×
854
            src_buffer,
×
855
            src_binding_offset,
×
856
            src_binding_size,
×
857
            dst_buffer,
×
858
            dst_binding_offset,
×
859
            dst_binding_size,
×
860
        });
861
        args_index
×
862
    }
863

864
    /// Queue a new [`GpuBufferOperationType::InitFillDispatchArgs`] operation.
865
    pub fn enqueue_init_fill(
4✔
866
        &mut self,
867
        event_buffer_index: u32,
868
        event_slice: std::ops::Range<u32>,
869
        args: GpuBufferOperationArgs,
870
    ) {
871
        trace!(
4✔
872
            "Queue InitFillDispatchArgs op: ev_buffer#{} ev_slice={:?} args={:?}",
×
873
            event_buffer_index,
874
            event_slice,
875
            args
876
        );
877
        let args_index = self.args_buffer_unsorted.len() as u32;
4✔
878
        self.args_buffer_unsorted.push(args);
4✔
879
        self.init_fill_dispatch_args.push(InitFillDispatchArgs {
4✔
880
            event_buffer_index,
4✔
881
            args_index,
4✔
882
            event_slice,
4✔
883
        });
884
    }
885

886
    /// Finish recording operations for this frame, and schedule buffer writes
887
    /// to GPU.
888
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
2✔
889
        assert_eq!(
2✔
890
            self.args_buffer_unsorted.len(),
2✔
891
            self.operation_queue.len() + self.init_fill_dispatch_args.len()
2✔
892
        );
893
        assert!(self.args_buffer.is_empty());
2✔
894

895
        if self.operation_queue.is_empty() && self.init_fill_dispatch_args.is_empty() {
4✔
896
            self.args_buffer.set_content(vec![]);
×
897
        } else {
898
            let mut sorted_args =
2✔
899
                Vec::with_capacity(self.init_fill_dispatch_args.len() + self.operation_queue.len());
2✔
900

901
            // Sort the commands by buffer, so we can dispatch them in groups with a single
902
            // dispatch per buffer
903
            trace!(
2✔
904
                "Sorting {} InitFillDispatch ops...",
×
905
                self.init_fill_dispatch_args.len()
×
906
            );
907
            self.init_fill_dispatch_args
2✔
908
                .sort_unstable_by(|ifda1, ifda2| {
4✔
909
                    if ifda1.event_buffer_index != ifda2.event_buffer_index {
2✔
910
                        ifda1.event_buffer_index.cmp(&ifda2.event_buffer_index)
×
911
                    } else if ifda1.event_slice != ifda2.event_slice {
2✔
912
                        ifda1.event_slice.start.cmp(&ifda2.event_slice.start)
2✔
913
                    } else {
914
                        // Sort by source offset, which at this point contains the child_index
915
                        let arg1 = &self.args_buffer_unsorted[ifda1.args_index as usize];
×
916
                        let arg2 = &self.args_buffer_unsorted[ifda2.args_index as usize];
×
917
                        arg1.src_offset.cmp(&arg2.src_offset)
×
918
                    }
919
                });
920

921
            // Note: Do NOT sort queued operations; they migth depend on each other. It's
922
            // the caller's responsibility to ensure e.g. multiple copies can be batched
923
            // together.
924

925
            // Push entries into the final storage before GPU upload. It's a bit unfortunate
926
            // we have to make copies, but those arrays should be small.
927
            {
928
                let mut sorted_ifda = Vec::with_capacity(self.init_fill_dispatch_args.len());
929
                let mut prev_buffer = u32::MAX;
930
                for ifda in &self.init_fill_dispatch_args {
10✔
931
                    trace!("+ op: ifda={:?}", ifda);
×
932
                    if !sorted_args.is_empty() && (prev_buffer == ifda.event_buffer_index) {
6✔
933
                        let prev_idx = sorted_args.len() - 1;
2✔
934
                        let prev: &mut GpuBufferOperationArgs = &mut sorted_args[prev_idx];
2✔
935
                        let cur = &self.args_buffer_unsorted[ifda.args_index as usize];
2✔
936
                        if prev.src_stride == cur.src_stride
2✔
937
                    // at this point src_offset == child_index, and we want them to be contiguous in the source buffer so that we can increment by src_stride
938
                    && cur.src_offset == prev.src_offset + 1
2✔
939
                    && cur.dst_offset == prev.dst_offset + 1
1✔
940
                        {
941
                            prev.count += 1;
1✔
942
                            trace!("-> merged op with previous one {:?}", prev);
1✔
943
                            continue;
1✔
944
                        }
945
                    }
946
                    prev_buffer = ifda.event_buffer_index;
3✔
947
                    let sorted_args_index = sorted_args.len() as u32;
3✔
948
                    sorted_ifda.push(InitFillDispatchArgs {
3✔
949
                        event_buffer_index: ifda.event_buffer_index,
3✔
950
                        event_slice: ifda.event_slice.clone(),
3✔
951
                        args_index: sorted_args_index,
3✔
952
                    });
953
                    sorted_args.push(self.args_buffer_unsorted[ifda.args_index as usize]);
3✔
954
                }
955
                trace!("Final ops (sorted IFDAs): {:?}", sorted_ifda);
2✔
956
                self.init_fill_dispatch_args = sorted_ifda;
2✔
957
            }
958

959
            // Just copy this, we want to preserve order
960
            {
961
                for qop in &self.operation_queue {
2✔
962
                    let args_index = qop.args_index as usize;
963
                    // ensure the index returned by enqueue() is still valid for COPY ops
964
                    // FIXME - all this stuff is too brittle...
965
                    assert_eq!(args_index, sorted_args.len());
966
                    sorted_args.push(self.args_buffer_unsorted[args_index]);
×
967
                }
968
            }
969

970
            // Write CPU content for all arguments
971
            self.args_buffer.set_content(sorted_args);
2✔
972
        }
973

974
        // Upload to GPU buffer
975
        self.args_buffer.write_buffer(device, render_queue);
2✔
976
    }
977

978
    /// Create all necessary bind groups for all queued operations.
979
    pub fn create_bind_groups(
×
980
        &mut self,
981
        render_device: &RenderDevice,
982
        utils_pipeline: &UtilsPipeline,
983
    ) {
984
        trace!(
×
985
            "Creating bind groups for {} queued operations...",
×
986
            self.operation_queue.len()
×
987
        );
988
        for qop in &self.operation_queue {
×
989
            let key: QueuedOperationBindGroupKey = qop.into();
×
990
            self.bind_groups.entry(key).or_insert_with(|| {
×
991
                let src_id: NonZeroU32 = qop.src_buffer.id().into();
×
992
                let dst_id: NonZeroU32 = qop.dst_buffer.id().into();
×
993
                let label = format!("hanabi:bind_group:util_{}_{}", src_id.get(), dst_id.get());
×
994
                let bind_group_layout = match qop.op {
×
995
                    GpuBufferOperationType::FillDispatchArgs => {
996
                        utils_pipeline.bind_group_layout(qop.op, true)
×
997
                    }
998
                    _ => utils_pipeline.bind_group_layout(qop.op, false),
×
999
                };
1000
                trace!(
×
1001
                    "-> Creating new bind group '{}': src#{} ({:?}B) dst#{} ({:?}B)",
×
1002
                    label,
1003
                    src_id,
1004
                    qop.src_binding_size,
1005
                    dst_id,
1006
                    qop.dst_binding_size,
1007
                );
1008
                render_device.create_bind_group(
×
1009
                    Some(&label[..]),
×
1010
                    bind_group_layout,
×
1011
                    &[
×
1012
                        BindGroupEntry {
×
1013
                            binding: 0,
×
1014
                            resource: BindingResource::Buffer(BufferBinding {
×
1015
                                buffer: self.args_buffer.buffer().unwrap(),
×
1016
                                offset: 0,
×
1017
                                size: Some(
×
1018
                                    NonZeroU64::new(self.args_buffer.aligned_size() as u64)
×
1019
                                        .unwrap(),
×
1020
                                ),
1021
                            }),
1022
                        },
1023
                        BindGroupEntry {
×
1024
                            binding: 1,
×
1025
                            resource: BindingResource::Buffer(BufferBinding {
×
1026
                                buffer: &qop.src_buffer,
×
1027
                                offset: 0,
×
1028
                                size: qop.src_binding_size.map(Into::into),
×
1029
                            }),
1030
                        },
1031
                        BindGroupEntry {
×
1032
                            binding: 2,
×
1033
                            resource: BindingResource::Buffer(BufferBinding {
×
1034
                                buffer: &qop.dst_buffer,
×
1035
                                offset: 0,
×
1036
                                size: qop.dst_binding_size.map(Into::into),
×
1037
                            }),
1038
                        },
1039
                    ],
1040
                )
1041
            });
1042
        }
1043
    }
1044

1045
    /// Dispatch any pending [`GpuBufferOperationType::FillDispatchArgs`]
1046
    /// operation.
1047
    pub fn dispatch_fill(&self, render_context: &mut RenderContext, pipeline: &ComputePipeline) {
×
1048
        trace!(
×
1049
            "Recording GPU commands for fill dispatch operations using the {:?} pipeline...",
×
1050
            pipeline
1051
        );
1052

1053
        if self.operation_queue.is_empty() {
×
1054
            return;
×
1055
        }
1056

1057
        let mut compute_pass =
×
1058
            render_context
×
1059
                .command_encoder()
1060
                .begin_compute_pass(&ComputePassDescriptor {
×
1061
                    label: Some("hanabi:fill_dispatch"),
×
1062
                    timestamp_writes: None,
×
1063
                });
1064

1065
        compute_pass.set_pipeline(pipeline);
×
1066

1067
        for qop in &self.operation_queue {
×
1068
            trace!("qop={:?}", qop);
×
1069
            if qop.op != GpuBufferOperationType::FillDispatchArgs {
×
1070
                continue;
×
1071
            }
1072

1073
            let key: QueuedOperationBindGroupKey = qop.into();
×
1074
            if let Some(bind_group) = self.bind_groups.get(&key) {
×
1075
                let args_offset = self.args_buffer.dynamic_offset(qop.args_index as usize);
1076
                let src_offset = qop.src_binding_offset;
1077
                let dst_offset = qop.dst_binding_offset;
1078
                compute_pass.set_bind_group(0, bind_group, &[args_offset, src_offset, dst_offset]);
1079
                trace!(
1080
                    "set bind group with args_offset=+{}B src_offset=+{}B dst_offset=+{}B",
×
1081
                    args_offset,
1082
                    src_offset,
1083
                    dst_offset
1084
                );
1085
            } else {
1086
                error!("GPU fill dispatch buffer operation bind group not found for buffers src#{:?} dst#{:?}", qop.src_buffer.id(), qop.dst_buffer.id());
×
1087
                continue;
×
1088
            }
1089

1090
            // Dispatch the operations for this buffer
1091
            const WORKGROUP_SIZE: u32 = 64;
1092
            let num_ops = 1u32; // TODO - batching!
×
1093
            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
×
1094
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
1095
            trace!(
×
1096
                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
×
1097
                num_ops,
1098
                workgroup_count
1099
            );
1100
        }
1101
    }
1102

1103
    /// Dispatch any pending [`GpuBufferOperationType::InitFillDispatchArgs`]
1104
    /// operation for indirect init passes.
1105
    pub fn dispatch_init_fill(
×
1106
        &self,
1107
        render_context: &mut RenderContext,
1108
        pipeline: &ComputePipeline,
1109
        bind_groups: &EffectBindGroups,
1110
    ) {
1111
        if self.init_fill_dispatch_args.is_empty() {
×
1112
            return;
×
1113
        }
1114

1115
        trace!(
×
1116
            "Recording GPU commands for the init fill dispatch pipeline... {:?}",
×
1117
            pipeline
1118
        );
1119

1120
        let mut compute_pass =
×
1121
            render_context
×
1122
                .command_encoder()
1123
                .begin_compute_pass(&ComputePassDescriptor {
×
1124
                    label: Some("hanabi:init_fill_dispatch"),
×
1125
                    timestamp_writes: None,
×
1126
                });
1127

1128
        compute_pass.set_pipeline(pipeline);
×
1129

1130
        assert_eq!(
×
1131
            self.init_fill_dispatch_args.len() + self.operation_queue.len(),
×
1132
            self.args_buffer.content().len()
×
1133
        );
1134

1135
        for (args_index, event_buffer_index) in self
×
1136
            .init_fill_dispatch_args
×
1137
            .iter()
×
1138
            .enumerate()
×
1139
            .map(|(args_index, ifda)| (args_index as u32, ifda.event_buffer_index))
×
1140
        {
1141
            trace!(
×
1142
                "event_buffer_index={} args_index={:?}",
×
1143
                event_buffer_index,
1144
                args_index
1145
            );
1146
            if let Some(bind_group) = bind_groups.init_fill_dispatch(event_buffer_index) {
×
1147
                let dst_offset = self.args_buffer.dynamic_offset(args_index as usize);
1148
                compute_pass.set_bind_group(0, bind_group, &[]);
1149
                trace!(
1150
                    "found bind group for event buffer index #{} with dst_offset +{}B",
×
1151
                    event_buffer_index,
1152
                    dst_offset
1153
                );
1154
            } else {
1155
                warn!(
×
1156
                    "bind group not found for event buffer index #{}",
×
1157
                    event_buffer_index
1158
                );
1159
                continue;
×
1160
            }
1161

1162
            // Dispatch the operations for this buffer
1163
            const WORKGROUP_SIZE: u32 = 64;
1164
            let num_ops = 1u32;
×
1165
            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
×
1166
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
1167
            trace!(
×
1168
                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
×
1169
                num_ops,
1170
                workgroup_count
1171
            );
1172
        }
1173
    }
1174
}
1175

1176
/// Compute pipeline to run the `vfx_utils` shader.
1177
#[derive(Resource)]
1178
pub(crate) struct UtilsPipeline {
1179
    #[allow(dead_code)]
1180
    bind_group_layout: BindGroupLayout,
1181
    bind_group_layout_dyn: BindGroupLayout,
1182
    bind_group_layout_no_src: BindGroupLayout,
1183
    pipelines: [ComputePipeline; 5],
1184
}
1185

1186
impl FromWorld for UtilsPipeline {
1187
    fn from_world(world: &mut World) -> Self {
×
1188
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1189

1190
        let bind_group_layout = render_device.create_bind_group_layout(
×
1191
            "hanabi:bind_group_layout:utils",
1192
            &[
×
1193
                BindGroupLayoutEntry {
×
1194
                    binding: 0,
×
1195
                    visibility: ShaderStages::COMPUTE,
×
1196
                    ty: BindingType::Buffer {
×
1197
                        ty: BufferBindingType::Uniform,
×
1198
                        has_dynamic_offset: false,
×
1199
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
×
1200
                    },
1201
                    count: None,
×
1202
                },
1203
                BindGroupLayoutEntry {
×
1204
                    binding: 1,
×
1205
                    visibility: ShaderStages::COMPUTE,
×
1206
                    ty: BindingType::Buffer {
×
1207
                        ty: BufferBindingType::Storage { read_only: true },
×
1208
                        has_dynamic_offset: false,
×
1209
                        min_binding_size: NonZeroU64::new(4),
×
1210
                    },
1211
                    count: None,
×
1212
                },
1213
                BindGroupLayoutEntry {
×
1214
                    binding: 2,
×
1215
                    visibility: ShaderStages::COMPUTE,
×
1216
                    ty: BindingType::Buffer {
×
1217
                        ty: BufferBindingType::Storage { read_only: false },
×
1218
                        has_dynamic_offset: false,
×
1219
                        min_binding_size: NonZeroU64::new(4),
×
1220
                    },
1221
                    count: None,
×
1222
                },
1223
            ],
1224
        );
1225

1226
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
1227
            label: Some("hanabi:pipeline_layout:utils"),
×
1228
            bind_group_layouts: &[&bind_group_layout],
×
1229
            push_constant_ranges: &[],
×
1230
        });
1231

1232
        let bind_group_layout_dyn = render_device.create_bind_group_layout(
×
1233
            "hanabi:bind_group_layout:utils_dyn",
1234
            &[
×
1235
                BindGroupLayoutEntry {
×
1236
                    binding: 0,
×
1237
                    visibility: ShaderStages::COMPUTE,
×
1238
                    ty: BindingType::Buffer {
×
1239
                        ty: BufferBindingType::Uniform,
×
1240
                        has_dynamic_offset: true,
×
1241
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
×
1242
                    },
1243
                    count: None,
×
1244
                },
1245
                BindGroupLayoutEntry {
×
1246
                    binding: 1,
×
1247
                    visibility: ShaderStages::COMPUTE,
×
1248
                    ty: BindingType::Buffer {
×
1249
                        ty: BufferBindingType::Storage { read_only: true },
×
1250
                        has_dynamic_offset: true,
×
1251
                        min_binding_size: NonZeroU64::new(4),
×
1252
                    },
1253
                    count: None,
×
1254
                },
1255
                BindGroupLayoutEntry {
×
1256
                    binding: 2,
×
1257
                    visibility: ShaderStages::COMPUTE,
×
1258
                    ty: BindingType::Buffer {
×
1259
                        ty: BufferBindingType::Storage { read_only: false },
×
1260
                        has_dynamic_offset: true,
×
1261
                        min_binding_size: NonZeroU64::new(4),
×
1262
                    },
1263
                    count: None,
×
1264
                },
1265
            ],
1266
        );
1267

1268
        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
1269
            label: Some("hanabi:pipeline_layout:utils_dyn"),
×
1270
            bind_group_layouts: &[&bind_group_layout_dyn],
×
1271
            push_constant_ranges: &[],
×
1272
        });
1273

1274
        let bind_group_layout_no_src = render_device.create_bind_group_layout(
×
1275
            "hanabi:bind_group_layout:utils_no_src",
1276
            &[
×
1277
                BindGroupLayoutEntry {
×
1278
                    binding: 0,
×
1279
                    visibility: ShaderStages::COMPUTE,
×
1280
                    ty: BindingType::Buffer {
×
1281
                        ty: BufferBindingType::Uniform,
×
1282
                        has_dynamic_offset: false,
×
1283
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
×
1284
                    },
1285
                    count: None,
×
1286
                },
1287
                BindGroupLayoutEntry {
×
1288
                    binding: 2,
×
1289
                    visibility: ShaderStages::COMPUTE,
×
1290
                    ty: BindingType::Buffer {
×
1291
                        ty: BufferBindingType::Storage { read_only: false },
×
1292
                        has_dynamic_offset: false,
×
1293
                        min_binding_size: NonZeroU64::new(4),
×
1294
                    },
1295
                    count: None,
×
1296
                },
1297
            ],
1298
        );
1299

1300
        let pipeline_layout_no_src =
×
1301
            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
×
1302
                label: Some("hanabi:pipeline_layout:utils_no_src"),
×
1303
                bind_group_layouts: &[&bind_group_layout_no_src],
×
1304
                push_constant_ranges: &[],
×
1305
            });
1306

1307
        let shader_code = include_str!("vfx_utils.wgsl");
×
1308

1309
        // Resolve imports. Because we don't insert this shader into Bevy' pipeline
1310
        // cache, we don't get that part "for free", so we have to do it manually here.
1311
        let shader_source = {
×
1312
            let mut composer = Composer::default();
×
1313

1314
            let shader_defs = default();
×
1315

1316
            match composer.make_naga_module(NagaModuleDescriptor {
×
1317
                source: shader_code,
×
1318
                file_path: "vfx_utils.wgsl",
×
1319
                shader_defs,
×
1320
                ..Default::default()
×
1321
            }) {
1322
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
1323
                Err(compose_error) => panic!(
×
1324
                    "Failed to compose vfx_utils.wgsl, naga_oil returned: {}",
1325
                    compose_error.emit_to_string(&composer)
×
1326
                ),
1327
            }
1328
        };
1329

1330
        debug!("Create utils shader module:\n{}", shader_code);
×
1331
        let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
×
1332
            label: Some("hanabi:shader:utils"),
×
1333
            source: shader_source,
×
1334
        });
1335

1336
        trace!("Create vfx_utils pipelines...");
×
1337
        let dummy = std::collections::HashMap::<String, f64>::new();
×
1338
        let zero_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
×
1339
            label: Some("hanabi:compute_pipeline:zero_buffer"),
×
1340
            layout: Some(&pipeline_layout),
×
1341
            module: &shader_module,
×
1342
            entry_point: Some("zero_buffer"),
×
1343
            compilation_options: PipelineCompilationOptions {
×
1344
                constants: &dummy,
×
1345
                zero_initialize_workgroup_memory: false,
×
1346
            },
1347
            cache: None,
×
1348
        });
1349
        let copy_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
×
1350
            label: Some("hanabi:compute_pipeline:copy_buffer"),
×
1351
            layout: Some(&pipeline_layout_dyn),
×
1352
            module: &shader_module,
×
1353
            entry_point: Some("copy_buffer"),
×
1354
            compilation_options: PipelineCompilationOptions {
×
1355
                constants: &dummy,
×
1356
                zero_initialize_workgroup_memory: false,
×
1357
            },
1358
            cache: None,
×
1359
        });
1360
        let fill_dispatch_args_pipeline =
×
1361
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
×
1362
                label: Some("hanabi:compute_pipeline:fill_dispatch_args"),
×
1363
                layout: Some(&pipeline_layout_dyn),
×
1364
                module: &shader_module,
×
1365
                entry_point: Some("fill_dispatch_args"),
×
1366
                compilation_options: PipelineCompilationOptions {
×
1367
                    constants: &dummy,
×
1368
                    zero_initialize_workgroup_memory: false,
×
1369
                },
1370
                cache: None,
×
1371
            });
1372
        let init_fill_dispatch_args_pipeline =
×
1373
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
×
1374
                label: Some("hanabi:compute_pipeline:init_fill_dispatch_args"),
×
1375
                layout: Some(&pipeline_layout),
×
1376
                module: &shader_module,
×
1377
                entry_point: Some("init_fill_dispatch_args"),
×
1378
                compilation_options: PipelineCompilationOptions {
×
1379
                    constants: &dummy,
×
1380
                    zero_initialize_workgroup_memory: false,
×
1381
                },
1382
                cache: None,
×
1383
            });
1384
        let fill_dispatch_args_self_pipeline =
×
1385
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
×
1386
                label: Some("hanabi:compute_pipeline:fill_dispatch_args_self"),
×
1387
                layout: Some(&pipeline_layout_no_src),
×
1388
                module: &shader_module,
×
1389
                entry_point: Some("fill_dispatch_args_self"),
×
1390
                compilation_options: PipelineCompilationOptions {
×
1391
                    constants: &dummy,
×
1392
                    zero_initialize_workgroup_memory: false,
×
1393
                },
1394
                cache: None,
×
1395
            });
1396

1397
        Self {
1398
            bind_group_layout,
1399
            bind_group_layout_dyn,
1400
            bind_group_layout_no_src,
1401
            pipelines: [
×
1402
                zero_pipeline,
1403
                copy_pipeline,
1404
                fill_dispatch_args_pipeline,
1405
                init_fill_dispatch_args_pipeline,
1406
                fill_dispatch_args_self_pipeline,
1407
            ],
1408
        }
1409
    }
1410
}
1411

1412
impl UtilsPipeline {
1413
    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
×
1414
        match op {
×
1415
            GpuBufferOperationType::Zero => &self.pipelines[0],
×
1416
            GpuBufferOperationType::Copy => &self.pipelines[1],
×
1417
            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
×
1418
            GpuBufferOperationType::InitFillDispatchArgs => &self.pipelines[3],
×
1419
            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[4],
×
1420
        }
1421
    }
1422

1423
    fn bind_group_layout(
×
1424
        &self,
1425
        op: GpuBufferOperationType,
1426
        with_dynamic_offsets: bool,
1427
    ) -> &BindGroupLayout {
1428
        if op == GpuBufferOperationType::FillDispatchArgsSelf {
×
1429
            assert!(
×
1430
                !with_dynamic_offsets,
×
1431
                "FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
×
1432
            );
1433
            &self.bind_group_layout_no_src
×
1434
        } else if with_dynamic_offsets {
×
1435
            &self.bind_group_layout_dyn
×
1436
        } else {
1437
            &self.bind_group_layout
×
1438
        }
1439
    }
1440
}
1441

1442
#[derive(Resource)]
1443
pub(crate) struct ParticlesInitPipeline {
1444
    sim_params_layout: BindGroupLayout,
1445

1446
    // Temporary values passed to specialize()
1447
    // https://github.com/bevyengine/bevy/issues/17132
1448
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1449
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1450
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1451
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1452
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1453
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1454
}
1455

1456
impl FromWorld for ParticlesInitPipeline {
1457
    fn from_world(world: &mut World) -> Self {
×
1458
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1459

1460
        let sim_params_layout = render_device.create_bind_group_layout(
×
1461
            "hanabi:bind_group_layout:update_sim_params",
1462
            // @group(0) @binding(0) var<uniform> sim_params: SimParams;
1463
            &[BindGroupLayoutEntry {
×
1464
                binding: 0,
×
1465
                visibility: ShaderStages::COMPUTE,
×
1466
                ty: BindingType::Buffer {
×
1467
                    ty: BufferBindingType::Uniform,
×
1468
                    has_dynamic_offset: false,
×
1469
                    min_binding_size: Some(GpuSimParams::min_size()),
×
1470
                },
1471
                count: None,
×
1472
            }],
1473
        );
1474

1475
        Self {
1476
            sim_params_layout,
1477
            temp_particle_bind_group_layout: None,
1478
            temp_spawner_bind_group_layout: None,
1479
            temp_metadata_bind_group_layout: None,
1480
        }
1481
    }
1482
}
1483

1484
bitflags! {
1485
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1486
    pub struct ParticleInitPipelineKeyFlags: u8 {
1487
        //const CLONE = (1u8 << 0); // DEPRECATED
1488
        const ATTRIBUTE_PREV = (1u8 << 1);
1489
        const ATTRIBUTE_NEXT = (1u8 << 2);
1490
        const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
1491
    }
1492
}
1493

1494
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1495
pub(crate) struct ParticleInitPipelineKey {
1496
    /// Compute shader, with snippets applied, but not preprocessed yet.
1497
    shader: Handle<Shader>,
1498
    /// Minimum binding size in bytes for the particle layout buffer.
1499
    particle_layout_min_binding_size: NonZeroU32,
1500
    /// Minimum binding size in bytes for the particle layout buffer of the
1501
    /// parent effect, if any.
1502
    /// Key: READ_PARENT_PARTICLE
1503
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1504
    /// Pipeline flags.
1505
    flags: ParticleInitPipelineKeyFlags,
1506
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1507
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1508
    particle_bind_group_layout_id: BindGroupLayoutId,
1509
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1510
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1511
    spawner_bind_group_layout_id: BindGroupLayoutId,
1512
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1513
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1514
    metadata_bind_group_layout_id: BindGroupLayoutId,
1515
}
1516

1517
impl SpecializedComputePipeline for ParticlesInitPipeline {
1518
    type Key = ParticleInitPipelineKey;
1519

1520
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
1521
        // We use the hash to correlate the key content with the GPU resource name
1522
        let hash = calc_hash(&key);
×
1523
        trace!("Specializing init pipeline {hash:016X} with key {key:?}");
×
1524

1525
        let mut shader_defs = Vec::with_capacity(4);
×
1526
        if key
×
1527
            .flags
×
1528
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
×
1529
        {
1530
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1531
        }
1532
        if key
1533
            .flags
1534
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
1535
        {
1536
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1537
        }
1538
        let consume_gpu_spawn_events = key
1539
            .flags
1540
            .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
1541
        if consume_gpu_spawn_events {
×
1542
            shader_defs.push("CONSUME_GPU_SPAWN_EVENTS".into());
×
1543
        }
1544
        // FIXME - for now this needs to keep in sync with consume_gpu_spawn_events
1545
        if key.parent_particle_layout_min_binding_size.is_some() {
1546
            assert!(consume_gpu_spawn_events);
×
1547
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1548
        } else {
1549
            assert!(!consume_gpu_spawn_events);
×
1550
        }
1551

1552
        // This should always be valid when specialize() is called, by design. This is
1553
        // how we pass the value to specialize() to work around the lack of access to
1554
        // external data.
1555
        // https://github.com/bevyengine/bevy/issues/17132
1556
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
×
1557
        assert_eq!(
×
1558
            particle_bind_group_layout.id(),
×
1559
            key.particle_bind_group_layout_id
×
1560
        );
1561
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
×
1562
        assert_eq!(
×
1563
            spawner_bind_group_layout.id(),
×
1564
            key.spawner_bind_group_layout_id
×
1565
        );
1566
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
×
1567
        assert_eq!(
×
1568
            metadata_bind_group_layout.id(),
×
1569
            key.metadata_bind_group_layout_id
×
1570
        );
1571

1572
        let label = format!("hanabi:pipeline:init_{hash:016X}");
×
1573
        trace!(
×
1574
            "-> creating pipeline '{}' with shader defs:{}",
×
1575
            label,
×
1576
            shader_defs
×
1577
                .iter()
×
1578
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
1579
        );
1580

1581
        ComputePipelineDescriptor {
1582
            label: Some(label.into()),
×
1583
            layout: vec![
×
1584
                self.sim_params_layout.clone(),
1585
                particle_bind_group_layout.clone(),
1586
                spawner_bind_group_layout.clone(),
1587
                metadata_bind_group_layout.clone(),
1588
            ],
1589
            shader: key.shader,
×
1590
            shader_defs,
1591
            entry_point: "main".into(),
×
1592
            push_constant_ranges: vec![],
×
1593
            zero_initialize_workgroup_memory: false,
1594
        }
1595
    }
1596
}
1597

1598
#[derive(Resource)]
1599
pub(crate) struct ParticlesUpdatePipeline {
1600
    sim_params_layout: BindGroupLayout,
1601

1602
    // Temporary values passed to specialize()
1603
    // https://github.com/bevyengine/bevy/issues/17132
1604
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1605
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1606
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1607
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1608
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1609
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1610
}
1611

1612
impl FromWorld for ParticlesUpdatePipeline {
1613
    fn from_world(world: &mut World) -> Self {
×
1614
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1615

1616
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
×
1617
        let sim_params_layout = render_device.create_bind_group_layout(
×
1618
            "hanabi:bind_group_layout:update:particle",
1619
            &[BindGroupLayoutEntry {
×
1620
                binding: 0,
×
1621
                visibility: ShaderStages::COMPUTE,
×
1622
                ty: BindingType::Buffer {
×
1623
                    ty: BufferBindingType::Uniform,
×
1624
                    has_dynamic_offset: false,
×
1625
                    min_binding_size: Some(GpuSimParams::min_size()),
×
1626
                },
1627
                count: None,
×
1628
            }],
1629
        );
1630

1631
        Self {
1632
            sim_params_layout,
1633
            temp_particle_bind_group_layout: None,
1634
            temp_spawner_bind_group_layout: None,
1635
            temp_metadata_bind_group_layout: None,
1636
        }
1637
    }
1638
}
1639

1640
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1641
pub(crate) struct ParticleUpdatePipelineKey {
1642
    /// Compute shader, with snippets applied, but not preprocessed yet.
1643
    shader: Handle<Shader>,
1644
    /// Particle layout.
1645
    particle_layout: ParticleLayout,
1646
    /// Minimum binding size in bytes for the particle layout buffer of the
1647
    /// parent effect, if any.
1648
    /// Key: READ_PARENT_PARTICLE
1649
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1650
    /// Key: EMITS_GPU_SPAWN_EVENTS
1651
    num_event_buffers: u32,
1652
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1653
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1654
    particle_bind_group_layout_id: BindGroupLayoutId,
1655
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1656
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1657
    spawner_bind_group_layout_id: BindGroupLayoutId,
1658
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1659
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1660
    metadata_bind_group_layout_id: BindGroupLayoutId,
1661
}
1662

1663
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1664
    type Key = ParticleUpdatePipelineKey;
1665

1666
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
×
1667
        // We use the hash to correlate the key content with the GPU resource name
1668
        let hash = calc_hash(&key);
×
1669
        trace!("Specializing update pipeline {hash:016X} with key {key:?}");
×
1670

1671
        let mut shader_defs = Vec::with_capacity(6);
×
1672
        shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
×
1673
        // ChildInfo needs atomic event_count because all threads append to the event
1674
        // buffer(s) in parallel.
1675
        shader_defs.push("CHILD_INFO_IS_ATOMIC".into());
×
1676
        if key.particle_layout.contains(Attribute::PREV) {
×
1677
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1678
        }
1679
        if key.particle_layout.contains(Attribute::NEXT) {
×
1680
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1681
        }
1682
        if key.parent_particle_layout_min_binding_size.is_some() {
×
1683
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1684
        }
1685
        if key.num_event_buffers > 0 {
×
1686
            shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
×
1687
        }
1688

1689
        // This should always be valid when specialize() is called, by design. This is
1690
        // how we pass the value to specialize() to work around the lack of access to
1691
        // external data.
1692
        // https://github.com/bevyengine/bevy/issues/17132
1693
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
1694
        assert_eq!(
1695
            particle_bind_group_layout.id(),
1696
            key.particle_bind_group_layout_id
1697
        );
1698
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
×
1699
        assert_eq!(
×
1700
            spawner_bind_group_layout.id(),
×
1701
            key.spawner_bind_group_layout_id
×
1702
        );
1703
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
×
1704
        assert_eq!(
×
1705
            metadata_bind_group_layout.id(),
×
1706
            key.metadata_bind_group_layout_id
×
1707
        );
1708

1709
        let hash = calc_func_id(&key);
×
1710
        let label = format!("hanabi:pipeline:update_{hash:016X}");
×
1711
        trace!(
×
1712
            "-> creating pipeline '{}' with shader defs:{}",
×
1713
            label,
×
1714
            shader_defs
×
1715
                .iter()
×
1716
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
1717
        );
1718

1719
        ComputePipelineDescriptor {
1720
            label: Some(label.into()),
×
1721
            layout: vec![
×
1722
                self.sim_params_layout.clone(),
1723
                particle_bind_group_layout.clone(),
1724
                spawner_bind_group_layout.clone(),
1725
                metadata_bind_group_layout.clone(),
1726
            ],
1727
            shader: key.shader,
×
1728
            shader_defs,
1729
            entry_point: "main".into(),
×
1730
            push_constant_ranges: Vec::new(),
×
1731
            zero_initialize_workgroup_memory: false,
1732
        }
1733
    }
1734
}
1735

1736
#[derive(Resource)]
1737
pub(crate) struct ParticlesRenderPipeline {
1738
    render_device: RenderDevice,
1739
    view_layout: BindGroupLayout,
1740
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
1741
}
1742

1743
impl ParticlesRenderPipeline {
1744
    /// Cache a material, creating its bind group layout based on the texture
1745
    /// layout.
1746
    pub fn cache_material(&mut self, layout: &TextureLayout) {
×
1747
        if layout.layout.is_empty() {
×
1748
            return;
×
1749
        }
1750

1751
        // FIXME - no current stable API to insert an entry into a HashMap only if it
1752
        // doesn't exist, and without having to build a key (as opposed to a reference).
1753
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
1754
        // cached (which should be the common case).
1755
        if self.material_layouts.contains_key(layout) {
×
1756
            return;
×
1757
        }
1758

1759
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
×
1760
        let mut index = 0;
×
1761
        for _slot in &layout.layout {
×
1762
            entries.push(BindGroupLayoutEntry {
×
1763
                binding: index,
×
1764
                visibility: ShaderStages::FRAGMENT,
×
1765
                ty: BindingType::Texture {
×
1766
                    multisampled: false,
×
1767
                    sample_type: TextureSampleType::Float { filterable: true },
×
1768
                    view_dimension: TextureViewDimension::D2,
×
1769
                },
1770
                count: None,
×
1771
            });
1772
            entries.push(BindGroupLayoutEntry {
×
1773
                binding: index + 1,
×
1774
                visibility: ShaderStages::FRAGMENT,
×
1775
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
×
1776
                count: None,
×
1777
            });
1778
            index += 2;
×
1779
        }
1780
        debug!(
1781
            "Creating material bind group with {} entries [{:?}] for layout {:?}",
×
1782
            entries.len(),
×
1783
            entries,
1784
            layout
1785
        );
1786
        let material_bind_group_layout = self
×
1787
            .render_device
×
1788
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
×
1789

1790
        self.material_layouts
×
1791
            .insert(layout.clone(), material_bind_group_layout);
×
1792
    }
1793

1794
    /// Retrieve a bind group layout for a cached material.
1795
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
×
1796
        // Prevent a hash and lookup for the trivial case of an empty layout
1797
        if layout.layout.is_empty() {
×
1798
            return None;
×
1799
        }
1800

1801
        self.material_layouts.get(layout)
×
1802
    }
1803
}
1804

1805
impl FromWorld for ParticlesRenderPipeline {
1806
    fn from_world(world: &mut World) -> Self {
×
1807
        let render_device = world.get_resource::<RenderDevice>().unwrap();
×
1808

1809
        let view_layout = render_device.create_bind_group_layout(
×
1810
            "hanabi:bind_group_layout:render:view@0",
1811
            &[
×
1812
                // @group(0) @binding(0) var<uniform> view: View;
1813
                BindGroupLayoutEntry {
×
1814
                    binding: 0,
×
1815
                    visibility: ShaderStages::VERTEX_FRAGMENT,
×
1816
                    ty: BindingType::Buffer {
×
1817
                        ty: BufferBindingType::Uniform,
×
1818
                        has_dynamic_offset: true,
×
1819
                        min_binding_size: Some(ViewUniform::min_size()),
×
1820
                    },
1821
                    count: None,
×
1822
                },
1823
                // @group(0) @binding(1) var<uniform> sim_params : SimParams;
1824
                BindGroupLayoutEntry {
×
1825
                    binding: 1,
×
1826
                    visibility: ShaderStages::VERTEX_FRAGMENT,
×
1827
                    ty: BindingType::Buffer {
×
1828
                        ty: BufferBindingType::Uniform,
×
1829
                        has_dynamic_offset: false,
×
1830
                        min_binding_size: Some(GpuSimParams::min_size()),
×
1831
                    },
1832
                    count: None,
×
1833
                },
1834
            ],
1835
        );
1836

1837
        Self {
1838
            render_device: render_device.clone(),
×
1839
            view_layout,
1840
            material_layouts: default(),
×
1841
        }
1842
    }
1843
}
1844

1845
#[cfg(all(feature = "2d", feature = "3d"))]
1846
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1847
enum PipelineMode {
1848
    Camera2d,
1849
    Camera3d,
1850
}
1851

1852
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1853
pub(crate) struct ParticleRenderPipelineKey {
1854
    /// Render shader, with snippets applied, but not preprocessed yet.
1855
    shader: Handle<Shader>,
1856
    /// Particle layout.
1857
    particle_layout: ParticleLayout,
1858
    mesh_layout: Option<MeshVertexBufferLayoutRef>,
1859
    /// Texture layout.
1860
    texture_layout: TextureLayout,
1861
    /// Key: LOCAL_SPACE_SIMULATION
1862
    /// The effect is simulated in local space, and during rendering all
1863
    /// particles are transformed by the effect's [`GlobalTransform`].
1864
    local_space_simulation: bool,
1865
    /// Key: USE_ALPHA_MASK, OPAQUE
1866
    /// The particle's alpha masking behavior.
1867
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
1868
    /// The effect needs Alpha blend.
1869
    alpha_mode: AlphaMode,
1870
    /// Key: FLIPBOOK
1871
    /// The effect is rendered with flipbook texture animation based on the
1872
    /// sprite index of each particle.
1873
    flipbook: bool,
1874
    /// Key: NEEDS_UV
1875
    /// The effect needs UVs.
1876
    needs_uv: bool,
1877
    /// Key: NEEDS_NORMAL
1878
    /// The effect needs normals.
1879
    needs_normal: bool,
1880
    /// Key: RIBBONS
1881
    /// The effect has ribbons.
1882
    ribbons: bool,
1883
    /// For dual-mode configurations only, the actual mode of the current render
1884
    /// pipeline. Otherwise the mode is implicitly determined by the active
1885
    /// feature.
1886
    #[cfg(all(feature = "2d", feature = "3d"))]
1887
    pipeline_mode: PipelineMode,
1888
    /// MSAA sample count.
1889
    msaa_samples: u32,
1890
    /// Is the camera using an HDR render target?
1891
    hdr: bool,
1892
}
1893

1894
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1895
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1896
    #[default]
1897
    Blend,
1898
    /// Key: USE_ALPHA_MASK
1899
    /// The effect is rendered with alpha masking.
1900
    AlphaMask,
1901
    /// Key: OPAQUE
1902
    /// The effect is rendered fully-opaquely.
1903
    Opaque,
1904
}
1905

1906
impl Default for ParticleRenderPipelineKey {
1907
    fn default() -> Self {
×
1908
        Self {
1909
            shader: Handle::default(),
×
1910
            particle_layout: ParticleLayout::empty(),
×
1911
            mesh_layout: None,
1912
            texture_layout: default(),
×
1913
            local_space_simulation: false,
1914
            alpha_mask: default(),
×
1915
            alpha_mode: AlphaMode::Blend,
1916
            flipbook: false,
1917
            needs_uv: false,
1918
            needs_normal: false,
1919
            ribbons: false,
1920
            #[cfg(all(feature = "2d", feature = "3d"))]
1921
            pipeline_mode: PipelineMode::Camera3d,
1922
            msaa_samples: Msaa::default().samples(),
×
1923
            hdr: false,
1924
        }
1925
    }
1926
}
1927

1928
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1929
    type Key = ParticleRenderPipelineKey;
1930

1931
    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
×
1932
        trace!("Specializing render pipeline for key: {key:?}");
×
1933

1934
        trace!("Creating layout for bind group particle@1 of render pass");
×
1935
        let alignment = self
×
1936
            .render_device
×
1937
            .limits()
×
1938
            .min_storage_buffer_offset_alignment;
×
1939
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(alignment);
×
1940
        let entries = [
×
1941
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
1942
            BindGroupLayoutEntry {
×
1943
                binding: 0,
×
1944
                visibility: ShaderStages::VERTEX,
×
1945
                ty: BindingType::Buffer {
×
1946
                    ty: BufferBindingType::Storage { read_only: true },
×
1947
                    has_dynamic_offset: false,
×
1948
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
×
1949
                },
1950
                count: None,
×
1951
            },
1952
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
1953
            BindGroupLayoutEntry {
×
1954
                binding: 1,
×
1955
                visibility: ShaderStages::VERTEX,
×
1956
                ty: BindingType::Buffer {
×
1957
                    ty: BufferBindingType::Storage { read_only: true },
×
1958
                    has_dynamic_offset: false,
×
1959
                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
×
1960
                },
1961
                count: None,
×
1962
            },
1963
            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
1964
            BindGroupLayoutEntry {
×
1965
                binding: 2,
×
1966
                visibility: ShaderStages::VERTEX,
×
1967
                ty: BindingType::Buffer {
×
1968
                    ty: BufferBindingType::Storage { read_only: true },
×
1969
                    has_dynamic_offset: true,
×
1970
                    min_binding_size: Some(spawner_min_binding_size),
×
1971
                },
1972
                count: None,
×
1973
            },
1974
        ];
1975
        let particle_bind_group_layout = self
×
1976
            .render_device
×
1977
            .create_bind_group_layout("hanabi:bind_group_layout:render:particle@1", &entries[..]);
×
1978

1979
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
×
1980
        let mut shader_defs = vec![];
×
1981

1982
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
×
1983
            mesh_layout
×
1984
                .0
×
1985
                .get_layout(&[
×
1986
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
×
1987
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
×
1988
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
×
1989
                ])
1990
                .ok()
×
1991
        });
1992

1993
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
×
1994
            layout.push(material_bind_group_layout.clone());
1995
            // //  @location(1) vertex_uv: vec2<f32>
1996
            // vertex_buffer_layout.attributes.push(VertexAttribute {
1997
            //     format: VertexFormat::Float32x2,
1998
            //     offset: 12,
1999
            //     shader_location: 1,
2000
            // });
2001
            // vertex_buffer_layout.array_stride += 8;
2002
        }
2003

2004
        // Key: LOCAL_SPACE_SIMULATION
2005
        if key.local_space_simulation {
×
2006
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2007
        }
2008

2009
        match key.alpha_mask {
2010
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
×
2011
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2012
                // Key: USE_ALPHA_MASK
2013
                shader_defs.push("USE_ALPHA_MASK".into())
×
2014
            }
2015
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2016
                // Key: OPAQUE
2017
                shader_defs.push("OPAQUE".into())
×
2018
            }
2019
        }
2020

2021
        // Key: FLIPBOOK
2022
        if key.flipbook {
×
2023
            shader_defs.push("FLIPBOOK".into());
×
2024
        }
2025

2026
        // Key: NEEDS_UV
2027
        if key.needs_uv {
×
2028
            shader_defs.push("NEEDS_UV".into());
×
2029
        }
2030

2031
        // Key: NEEDS_NORMAL
2032
        if key.needs_normal {
×
2033
            shader_defs.push("NEEDS_NORMAL".into());
×
2034
        }
2035

2036
        // Key: RIBBONS
2037
        if key.ribbons {
×
2038
            shader_defs.push("RIBBONS".into());
×
2039
        }
2040

2041
        #[cfg(feature = "2d")]
2042
        let depth_stencil_2d = DepthStencilState {
2043
            format: CORE_2D_DEPTH_FORMAT,
2044
            // Use depth buffer with alpha-masked particles, not with transparent ones
2045
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2046
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2047
            depth_compare: CompareFunction::GreaterEqual,
2048
            stencil: StencilState::default(),
2049
            bias: DepthBiasState::default(),
2050
        };
2051

2052
        #[cfg(feature = "3d")]
2053
        let depth_stencil_3d = DepthStencilState {
2054
            format: CORE_3D_DEPTH_FORMAT,
2055
            // Use depth buffer with alpha-masked or opaque particles, not
2056
            // with transparent ones
2057
            depth_write_enabled: matches!(
×
2058
                key.alpha_mask,
2059
                ParticleRenderAlphaMaskPipelineKey::AlphaMask
2060
                    | ParticleRenderAlphaMaskPipelineKey::Opaque
2061
            ),
2062
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2063
            depth_compare: CompareFunction::GreaterEqual,
2064
            stencil: StencilState::default(),
2065
            bias: DepthBiasState::default(),
2066
        };
2067

2068
        #[cfg(all(feature = "2d", feature = "3d"))]
2069
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2070
        #[cfg(all(feature = "2d", feature = "3d"))]
2071
        let depth_stencil = match key.pipeline_mode {
×
2072
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2073
            PipelineMode::Camera3d => Some(depth_stencil_3d),
×
2074
        };
2075

2076
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2077
        let depth_stencil = Some(depth_stencil_2d);
2078

2079
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2080
        let depth_stencil = Some(depth_stencil_3d);
2081

2082
        let format = if key.hdr {
×
2083
            ViewTarget::TEXTURE_FORMAT_HDR
×
2084
        } else {
2085
            TextureFormat::bevy_default()
×
2086
        };
2087

2088
        let hash = calc_func_id(&key);
×
2089
        let label = format!("hanabi:pipeline:render_{hash:016X}");
×
2090
        trace!(
×
2091
            "-> creating pipeline '{}' with shader defs:{}",
×
2092
            label,
×
2093
            shader_defs
×
2094
                .iter()
×
2095
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
2096
        );
2097

2098
        RenderPipelineDescriptor {
2099
            label: Some(label.into()),
×
2100
            vertex: VertexState {
×
2101
                shader: key.shader.clone(),
2102
                entry_point: "vertex".into(),
2103
                shader_defs: shader_defs.clone(),
2104
                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
2105
            },
2106
            fragment: Some(FragmentState {
×
2107
                shader: key.shader,
2108
                shader_defs,
2109
                entry_point: "fragment".into(),
2110
                targets: vec![Some(ColorTargetState {
2111
                    format,
2112
                    blend: Some(key.alpha_mode.into()),
2113
                    write_mask: ColorWrites::ALL,
2114
                })],
2115
            }),
2116
            layout,
2117
            primitive: PrimitiveState {
×
2118
                front_face: FrontFace::Ccw,
2119
                cull_mode: None,
2120
                unclipped_depth: false,
2121
                polygon_mode: PolygonMode::Fill,
2122
                conservative: false,
2123
                topology: PrimitiveTopology::TriangleList,
2124
                strip_index_format: None,
2125
            },
2126
            depth_stencil,
2127
            multisample: MultisampleState {
×
2128
                count: key.msaa_samples,
2129
                mask: !0,
2130
                alpha_to_coverage_enabled: false,
2131
            },
2132
            push_constant_ranges: Vec::new(),
×
2133
            zero_initialize_workgroup_memory: false,
2134
        }
2135
    }
2136
}
2137

2138
/// A single effect instance extracted from a [`ParticleEffect`] as a
2139
/// render world item.
2140
///
2141
/// [`ParticleEffect`]: crate::ParticleEffect
2142
#[derive(Debug)]
2143
pub(crate) struct ExtractedEffect {
2144
    /// Main world entity owning the [`CompiledParticleEffect`] this effect was
2145
    /// extracted from. Mainly used for visibility.
2146
    pub main_entity: MainEntity,
2147
    /// Render world entity, if any, where the [`CachedEffect`] component
2148
    /// caching this extracted effect resides. If this component was never
2149
    /// cached in the render world, this is `None`. In that case a new
2150
    /// [`CachedEffect`] will be spawned automatically.
2151
    pub render_entity: RenderEntity,
2152
    /// Handle to the effect asset this instance is based on.
2153
    /// The handle is weak to prevent refcount cycles and gracefully handle
2154
    /// assets unloaded or destroyed after a draw call has been submitted.
2155
    pub handle: Handle<EffectAsset>,
2156
    /// Particle layout for the effect.
2157
    #[allow(dead_code)]
2158
    pub particle_layout: ParticleLayout,
2159
    /// Property layout for the effect.
2160
    pub property_layout: PropertyLayout,
2161
    /// Values of properties written in a binary blob according to
2162
    /// [`property_layout`].
2163
    ///
2164
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2165
    /// `None` if nothing needs to be done for this frame.
2166
    ///
2167
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2168
    pub property_data: Option<Vec<u8>>,
2169
    /// Number of particles to spawn this frame.
2170
    ///
2171
    /// This is ignored if the effect is a child effect consuming GPU spawn
2172
    /// events.
2173
    pub spawn_count: u32,
2174
    /// PRNG seed.
2175
    pub prng_seed: u32,
2176
    /// Global transform of the effect origin.
2177
    pub transform: GlobalTransform,
2178
    /// Layout flags.
2179
    pub layout_flags: LayoutFlags,
2180
    /// Texture layout.
2181
    pub texture_layout: TextureLayout,
2182
    /// Textures.
2183
    pub textures: Vec<Handle<Image>>,
2184
    /// Alpha mode.
2185
    pub alpha_mode: AlphaMode,
2186
    /// Effect shaders.
2187
    pub effect_shaders: EffectShader,
2188
    /// For 2D rendering, the Z coordinate used as the sort key. Ignored for 3D
2189
    /// rendering.
2190
    #[cfg(feature = "2d")]
2191
    pub z_sort_key_2d: FloatOrd,
2192
}
2193

2194
pub struct AddedEffectParent {
2195
    pub entity: MainEntity,
2196
    pub layout: ParticleLayout,
2197
    /// GPU spawn event count to allocate for this effect.
2198
    pub event_count: u32,
2199
}
2200

2201
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
2202
/// GPU allocation.
2203
///
2204
/// [`ParticleEffect`]: crate::ParticleEffect
2205
pub struct AddedEffect {
2206
    /// Entity with a newly-added [`ParticleEffect`] component.
2207
    ///
2208
    /// [`ParticleEffect`]: crate::ParticleEffect
2209
    pub entity: MainEntity,
2210
    #[allow(dead_code)]
2211
    pub render_entity: RenderEntity,
2212
    /// Capacity, in number of particles, of the effect.
2213
    pub capacity: u32,
2214
    /// Resolved particle mesh, either the one provided by the user or the
2215
    /// default one. This should always be valid.
2216
    pub mesh: Handle<Mesh>,
2217
    /// Parent effect, if any.
2218
    pub parent: Option<AddedEffectParent>,
2219
    /// Layout of particle attributes.
2220
    pub particle_layout: ParticleLayout,
2221
    /// Layout of properties for the effect, if properties are used at all, or
2222
    /// an empty layout.
2223
    pub property_layout: PropertyLayout,
2224
    /// Effect flags.
2225
    pub layout_flags: LayoutFlags,
2226
    /// Handle of the effect asset.
2227
    pub handle: Handle<EffectAsset>,
2228
}
2229

2230
/// Collection of all extracted effects for this frame, inserted into the
2231
/// render world as a render resource.
2232
#[derive(Default, Resource)]
2233
pub(crate) struct ExtractedEffects {
2234
    /// Extracted effects this frame.
2235
    pub effects: Vec<ExtractedEffect>,
2236
    /// Newly added effects without a GPU allocation yet.
2237
    pub added_effects: Vec<AddedEffect>,
2238
}
2239

2240
#[derive(Default, Resource)]
2241
pub(crate) struct EffectAssetEvents {
2242
    pub images: Vec<AssetEvent<Image>>,
2243
}
2244

2245
/// System extracting all the asset events for the [`Image`] assets to enable
2246
/// dynamic update of images bound to any effect.
2247
///
2248
/// This system runs in parallel of [`extract_effects`].
2249
pub(crate) fn extract_effect_events(
×
2250
    mut events: ResMut<EffectAssetEvents>,
2251
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2252
) {
2253
    #[cfg(feature = "trace")]
2254
    let _span = bevy::utils::tracing::info_span!("extract_effect_events").entered();
×
2255
    trace!("extract_effect_events()");
×
2256

2257
    let EffectAssetEvents { ref mut images } = *events;
×
2258
    *images = image_events.read().copied().collect();
×
2259
}
2260

2261
/// Debugging settings.
2262
///
2263
/// Settings used to debug Hanabi. These have no effect on the actual behavior
2264
/// of Hanabi, but may affect its performance.
2265
///
2266
/// # Example
2267
///
2268
/// ```
2269
/// # use bevy::prelude::*;
2270
/// # use bevy_hanabi::*;
2271
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2272
///     // Each time a new effect is spawned, capture 2 frames
2273
///     debug_settings.start_capture_on_new_effect = true;
2274
///     debug_settings.capture_frame_count = 2;
2275
/// }
2276
/// ```
2277
#[derive(Debug, Default, Clone, Copy, Resource)]
2278
pub struct DebugSettings {
2279
    /// Enable automatically starting a GPU debugger capture as soon as this
2280
    /// frame starts rendering (extract phase).
2281
    ///
2282
    /// Enable this feature to automatically capture one or more GPU frames when
2283
    /// the [`extract_effects`] system runs next. This instructs any attached
2284
    /// GPU debugger to start a capture; this has no effect if no debugger
2285
    /// is attached.
2286
    ///
2287
    /// If a capture is already on-going this has no effect; the on-going
2288
    /// capture needs to be terminated first. Note however that a capture can
2289
    /// stop and another start in the same frame.
2290
    ///
2291
    /// This value is not reset automatically. If you set this to `true`, you
2292
    /// should set it back to `false` on next frame to avoid capturing forever.
2293
    pub start_capture_this_frame: bool,
2294

2295
    /// Enable automatically starting a GPU debugger capture when one or more
2296
    /// effects are spawned.
2297
    ///
2298
    /// Enable this feature to automatically capture one or more GPU frames when
2299
    /// a new effect is spawned (as detected by ECS change detection). This
2300
    /// instructs any attached GPU debugger to start a capture; this has no
2301
    /// effect if no debugger is attached.
2302
    pub start_capture_on_new_effect: bool,
2303

2304
    /// Number of frames to capture with a GPU debugger.
2305
    ///
2306
    /// By default this value is zero, and a GPU debugger capture runs for a
2307
    /// single frame. If a non-zero frame count is specified here, the capture
2308
    /// will instead stop once the specified number of frames has been recorded.
2309
    ///
2310
    /// You should avoid setting this to a value too large, to prevent the
2311
    /// capture size from getting out of control. A typical value is 1 to 3
2312
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2313
    /// debuggers or graphics APIs might further limit this value on their own,
2314
    /// so there's no guarantee the graphics API will honor this value.
2315
    pub capture_frame_count: u32,
2316
}
2317

2318
#[derive(Debug, Default, Clone, Copy, Resource)]
2319
pub(crate) struct RenderDebugSettings {
2320
    /// Is a GPU debugger capture on-going?
2321
    is_capturing: bool,
2322
    /// Start time of any on-going GPU debugger capture.
2323
    capture_start: Duration,
2324
    /// Number of frames captured so far for on-going GPU debugger capture.
2325
    captured_frames: u32,
2326
}
2327

2328
/// System extracting data for rendering of all active [`ParticleEffect`]
2329
/// components.
2330
///
2331
/// Extract rendering data for all [`ParticleEffect`] components in the world
2332
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
2333
/// the data into a new [`ExtractedEffect`] instance added to the
2334
/// [`ExtractedEffects`] resource.
2335
///
2336
/// This system runs in parallel of [`extract_effect_events`].
2337
///
2338
/// If any GPU debug capture is configured to start or stop in
2339
/// [`DebugSettings`], they do so at the beginning of this system. This ensures
2340
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2341
/// from Bevy itself, if another Bevy system runs before this one).
2342
///
2343
/// [`ParticleEffect`]: crate::ParticleEffect
2344
pub(crate) fn extract_effects(
×
2345
    real_time: Extract<Res<Time<Real>>>,
2346
    virtual_time: Extract<Res<Time<Virtual>>>,
2347
    time: Extract<Res<Time<EffectSimulation>>>,
2348
    effects: Extract<Res<Assets<EffectAsset>>>,
2349
    q_added_effects: Extract<
2350
        Query<
2351
            (Entity, &RenderEntity, &CompiledParticleEffect),
2352
            (Added<CompiledParticleEffect>, With<GlobalTransform>),
2353
        >,
2354
    >,
2355
    q_effects: Extract<
2356
        Query<(
2357
            Entity,
2358
            &RenderEntity,
2359
            Option<&InheritedVisibility>,
2360
            Option<&ViewVisibility>,
2361
            &EffectSpawner,
2362
            &CompiledParticleEffect,
2363
            Option<Ref<EffectProperties>>,
2364
            &GlobalTransform,
2365
        )>,
2366
    >,
2367
    render_device: Res<RenderDevice>,
2368
    debug_settings: Extract<Res<DebugSettings>>,
2369
    default_mesh: Extract<Res<DefaultMesh>>,
2370
    mut sim_params: ResMut<SimParams>,
2371
    mut extracted_effects: ResMut<ExtractedEffects>,
2372
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2373
) {
2374
    #[cfg(feature = "trace")]
2375
    let _span = bevy::utils::tracing::info_span!("extract_effects").entered();
×
2376
    trace!("extract_effects()");
×
2377

2378
    // Manage GPU debug capture
2379
    if render_debug_settings.is_capturing {
×
2380
        render_debug_settings.captured_frames += 1;
×
2381

2382
        // Stop any pending capture if needed
2383
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2384
            render_device.wgpu_device().stop_capture();
×
2385
            render_debug_settings.is_capturing = false;
×
2386
            warn!(
×
2387
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2388
                render_debug_settings.captured_frames,
×
2389
                real_time.elapsed().as_secs_f64()
×
2390
            );
2391
        }
2392
    }
2393
    if !render_debug_settings.is_capturing {
×
2394
        // If no pending capture, consider starting a new one
2395
        if debug_settings.start_capture_this_frame
×
2396
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty())
×
2397
        {
2398
            render_device.wgpu_device().start_capture();
×
2399
            render_debug_settings.is_capturing = true;
×
2400
            render_debug_settings.capture_start = real_time.elapsed();
×
2401
            render_debug_settings.captured_frames = 0;
×
2402
            warn!(
×
2403
                "Started GPU debug capture at t={}s.",
×
2404
                render_debug_settings.capture_start.as_secs_f64()
×
2405
            );
2406
        }
2407
    }
2408

2409
    // Save simulation params into render world
2410
    sim_params.time = time.elapsed_secs_f64();
×
2411
    sim_params.delta_time = time.delta_secs();
×
2412
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
×
2413
    sim_params.virtual_delta_time = virtual_time.delta_secs();
×
2414
    sim_params.real_time = real_time.elapsed_secs_f64();
×
2415
    sim_params.real_delta_time = real_time.delta_secs();
×
2416

2417
    // Collect added effects for later GPU data allocation
2418
    extracted_effects.added_effects = q_added_effects
×
2419
        .iter()
×
2420
        .filter_map(|(entity, render_entity, compiled_effect)| {
×
2421
            let handle = compiled_effect.asset.clone_weak();
×
2422
            let asset = effects.get(&compiled_effect.asset)?;
×
2423
            let particle_layout = asset.particle_layout();
2424
            assert!(
2425
                particle_layout.size() > 0,
2426
                "Invalid empty particle layout for effect '{}' on entity {:?} (render entity {:?}). Did you forget to add some modifier to the asset?",
×
2427
                asset.name,
×
2428
                entity,
×
2429
                render_entity.id(),
×
2430
            );
2431
            let property_layout = asset.property_layout();
×
2432
            let mesh = compiled_effect
×
2433
                .mesh
×
2434
                .clone()
×
2435
                .unwrap_or(default_mesh.0.clone());
×
2436

2437
            trace!(
×
2438
                "Found new effect: entity {:?} | render entity {:?} | capacity {:?} | particle_layout {:?} | \
×
2439
                 property_layout {:?} | layout_flags {:?} | mesh {:?}",
×
2440
                 entity,
×
2441
                 render_entity.id(),
×
2442
                 asset.capacity(),
×
2443
                 particle_layout,
2444
                 property_layout,
2445
                 compiled_effect.layout_flags,
2446
                 mesh);
2447

2448
            // FIXME - fixed 256 events per child (per frame) for now... this neatly avoids any issue with alignment 32/256 byte storage buffer align for bind groups
2449
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2450
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
×
2451
                entity: entity.into(),
×
2452
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2453
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2454
            });
2455

2456
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
×
2457
            Some(AddedEffect {
×
2458
                entity: MainEntity::from(entity),
×
2459
                render_entity: *render_entity,
×
2460
                capacity: asset.capacity(),
×
2461
                mesh,
×
2462
                parent,
×
2463
                particle_layout,
×
2464
                property_layout,
×
2465
                layout_flags: compiled_effect.layout_flags,
×
2466
                handle,
×
2467
            })
2468
        })
2469
        .collect();
2470

2471
    // Loop over all existing effects to extract them
2472
    extracted_effects.effects.clear();
2473
    for (
2474
        main_entity,
×
2475
        render_entity,
×
2476
        maybe_inherited_visibility,
×
2477
        maybe_view_visibility,
×
2478
        effect_spawner,
×
2479
        compiled_effect,
×
2480
        maybe_properties,
×
2481
        transform,
×
2482
    ) in q_effects.iter()
2483
    {
2484
        // Check if shaders are configured
2485
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
×
2486
            continue;
×
2487
        };
2488

2489
        // Check if hidden, unless always simulated
2490
        if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
2491
            && !maybe_inherited_visibility
×
2492
                .map(|cv| cv.get())
×
2493
                .unwrap_or(true)
×
2494
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
2495
        {
2496
            continue;
×
2497
        }
2498

2499
        // Check if asset is available, otherwise silently ignore
2500
        let Some(asset) = effects.get(&compiled_effect.asset) else {
×
2501
            trace!(
×
2502
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
2503
                main_entity
2504
            );
2505
            continue;
×
2506
        };
2507

2508
        // Resolve the render entity of the parent, if any
2509
        let _parent = if let Some(main_entity) = compiled_effect.parent {
×
2510
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2511
                error!(
×
2512
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2513
                    main_entity
2514
                );
2515
                continue;
×
2516
            };
2517
            Some(*render_entity)
2518
        } else {
2519
            None
×
2520
        };
2521

2522
        #[cfg(feature = "2d")]
2523
        let z_sort_key_2d = compiled_effect.z_layer_2d;
2524

2525
        let property_layout = asset.property_layout();
2526
        let property_data = if let Some(properties) = maybe_properties {
×
2527
            // Note: must check that property layout is not empty, because the
2528
            // EffectProperties component is marked as changed when added but contains an
2529
            // empty Vec if there's no property, which would later raise an error if we
2530
            // don't return None here.
2531
            if properties.is_changed() && !property_layout.is_empty() {
×
2532
                trace!("Detected property change, re-serializing...");
×
2533
                Some(properties.serialize(&property_layout))
×
2534
            } else {
2535
                None
×
2536
            }
2537
        } else {
2538
            None
×
2539
        };
2540

2541
        let texture_layout = asset.module().texture_layout();
2542
        let layout_flags = compiled_effect.layout_flags;
2543
        // let mesh = compiled_effect
2544
        //     .mesh
2545
        //     .clone()
2546
        //     .unwrap_or(default_mesh.0.clone());
2547
        let alpha_mode = compiled_effect.alpha_mode;
2548

2549
        trace!(
2550
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
×
2551
            asset.name,
×
2552
            main_entity,
×
2553
            render_entity.id(),
×
2554
            texture_layout.layout.len(),
×
2555
            compiled_effect.textures.len(),
×
2556
            layout_flags,
2557
        );
2558

2559
        extracted_effects.effects.push(ExtractedEffect {
×
2560
            render_entity: *render_entity,
×
2561
            main_entity: main_entity.into(),
×
2562
            handle: compiled_effect.asset.clone_weak(),
×
2563
            particle_layout: asset.particle_layout().clone(),
×
2564
            property_layout,
×
2565
            property_data,
×
NEW
2566
            spawn_count: effect_spawner.spawn_count,
×
NEW
2567
            prng_seed: compiled_effect.prng_seed,
×
2568
            transform: *transform,
×
2569
            layout_flags,
×
2570
            texture_layout,
×
2571
            textures: compiled_effect.textures.clone(),
×
2572
            alpha_mode,
×
2573
            effect_shaders: effect_shaders.clone(),
×
2574
            #[cfg(feature = "2d")]
×
2575
            z_sort_key_2d,
×
2576
        });
2577
    }
2578
}
2579

2580
/// Various GPU limits and aligned sizes computed once and cached.
2581
struct GpuLimits {
2582
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2583
    ///
2584
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2585
    storage_buffer_align: NonZeroU32,
2586

2587
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2588
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2589
    ///
2590
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2591
    effect_metadata_aligned_size: NonZeroU32,
2592
}
2593

2594
impl GpuLimits {
2595
    pub fn from_device(render_device: &RenderDevice) -> Self {
1✔
2596
        let storage_buffer_align =
1✔
2597
            render_device.limits().min_storage_buffer_offset_alignment as u64;
1✔
2598

2599
        let effect_metadata_aligned_size = NonZeroU32::new(
2600
            GpuEffectMetadata::min_size()
1✔
2601
                .get()
1✔
2602
                .next_multiple_of(storage_buffer_align) as u32,
1✔
2603
        )
2604
        .unwrap();
2605

2606
        trace!(
1✔
2607
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
×
2608
            storage_buffer_align,
×
2609
            GpuEffectMetadata::min_size().get(),
×
2610
            effect_metadata_aligned_size.get(),
×
2611
        );
2612

2613
        Self {
2614
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
1✔
2615
            effect_metadata_aligned_size,
2616
        }
2617
    }
2618

2619
    /// Byte alignment for any storage buffer binding.
2620
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
×
2621
        self.storage_buffer_align
×
2622
    }
2623

2624
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2625
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2626
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2627
    }
2628

2629
    /// Byte alignment for [`GpuEffectMetadata`].
2630
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
×
2631
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
×
2632
    }
2633
}
2634

2635
/// Global resource containing the GPU data to draw all the particle effects in
2636
/// all views.
2637
///
2638
/// The resource is populated by [`prepare_effects()`] with all the effects to
2639
/// render for the current frame, for all views in the frame, and consumed by
2640
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2641
/// effects.
2642
#[derive(Resource)]
2643
pub struct EffectsMeta {
2644
    /// Bind group for the camera view, containing the camera projection and
2645
    /// other uniform values related to the camera.
2646
    view_bind_group: Option<BindGroup>,
2647
    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2648
    /// like the current time and frame delta time.
2649
    indirect_sim_params_bind_group: Option<BindGroup>,
2650
    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2651
    /// compute dispatch and render buffers.
2652
    indirect_metadata_bind_group: Option<BindGroup>,
2653
    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2654
    indirect_spawner_bind_group: Option<BindGroup>,
2655
    /// Global shared GPU uniform buffer storing the simulation parameters,
2656
    /// uploaded each frame from CPU to GPU.
2657
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2658
    /// Global shared GPU buffer storing the various spawner parameter structs
2659
    /// for the active effect instances.
2660
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2661
    /// Global shared GPU buffer storing the various indirect dispatch structs
2662
    /// for the indirect dispatch of the Update pass.
2663
    update_dispatch_indirect_buffer: BufferTable<GpuDispatchIndirect>,
2664
    /// Global shared GPU buffer storing the various `EffectMetadata`
2665
    /// structs for the active effect instances.
2666
    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2667
    /// Various GPU limits and aligned sizes lazily allocated and cached for
2668
    /// convenience.
2669
    gpu_limits: GpuLimits,
2670
    indirect_shader_noevent: Handle<Shader>,
2671
    indirect_shader_events: Handle<Shader>,
2672
    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2673
    /// -noevent and -events variants).
2674
    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2675
    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2676
    /// is either the -noevent or -events variant depending on whether there's
2677
    /// any child effect with GPU events currently active.
2678
    active_indirect_pipeline_id: CachedComputePipelineId,
2679
}
2680

2681
impl EffectsMeta {
2682
    pub fn new(
×
2683
        device: RenderDevice,
2684
        indirect_shader_noevent: Handle<Shader>,
2685
        indirect_shader_events: Handle<Shader>,
2686
    ) -> Self {
2687
        let gpu_limits = GpuLimits::from_device(&device);
×
2688

2689
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2690
        // be addressed individually by the computer shaders.
2691
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
×
2692
        trace!(
×
2693
            "Aligning storage buffers to {} bytes as device limits requires.",
×
2694
            item_align
2695
        );
2696

2697
        Self {
2698
            view_bind_group: None,
2699
            indirect_sim_params_bind_group: None,
2700
            indirect_metadata_bind_group: None,
2701
            indirect_spawner_bind_group: None,
2702
            sim_params_uniforms: UniformBuffer::default(),
×
2703
            spawner_buffer: AlignedBufferVec::new(
×
2704
                BufferUsages::STORAGE,
2705
                NonZeroU64::new(item_align),
2706
                Some("hanabi:buffer:spawner".to_string()),
2707
            ),
2708
            update_dispatch_indirect_buffer: BufferTable::new(
×
2709
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2710
                // Indirect dispatch args don't need to be aligned
2711
                None,
2712
                Some("hanabi:buffer:update_dispatch_indirect".to_string()),
2713
            ),
2714
            effect_metadata_buffer: BufferTable::new(
×
2715
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2716
                NonZeroU64::new(item_align),
2717
                Some("hanabi:buffer:effect_metadata".to_string()),
2718
            ),
2719
            gpu_limits,
2720
            indirect_shader_noevent,
2721
            indirect_shader_events,
2722
            indirect_pipeline_ids: [
×
2723
                CachedComputePipelineId::INVALID,
2724
                CachedComputePipelineId::INVALID,
2725
            ],
2726
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2727
        }
2728
    }
2729

2730
    /// Allocate internal resources for newly spawned effects.
2731
    ///
2732
    /// After this system ran, all valid extracted effects from the main world
2733
    /// have a corresponding entity with a [`CachedEffect`] component in the
2734
    /// render world. An extracted effect is considered valid if it passed some
2735
    /// basic checks, like having a valid mesh. Note however that the main
2736
    /// world's entity might still be missing its [`RenderEntity`]
2737
    /// reference, since we cannot yet write into the main world.
2738
    pub fn add_effects(
×
2739
        &mut self,
2740
        mut commands: Commands,
2741
        mut added_effects: Vec<AddedEffect>,
2742
        render_device: &RenderDevice,
2743
        render_queue: &RenderQueue,
2744
        mesh_allocator: &MeshAllocator,
2745
        render_meshes: &RenderAssets<RenderMesh>,
2746
        effect_bind_groups: &mut ResMut<EffectBindGroups>,
2747
        effect_cache: &mut ResMut<EffectCache>,
2748
        property_cache: &mut ResMut<PropertyCache>,
2749
        event_cache: &mut ResMut<EventCache>,
2750
    ) {
2751
        // FIXME - We delete a buffer above, and have a chance to immediatly re-create
2752
        // it below. We should keep the GPU buffer around until the end of this method.
2753
        // On the other hand, we should also be careful that allocated buffers need to
2754
        // be tightly packed because 'vfx_indirect.wgsl' index them by buffer index in
2755
        // order, so doesn't support offset.
2756

2757
        trace!("Adding {} newly spawned effects", added_effects.len());
×
2758
        for added_effect in added_effects.drain(..) {
×
2759
            trace!("+ added effect: capacity={}", added_effect.capacity);
×
2760

2761
            // Allocate an indirect dispatch arguments struct for this instance
2762
            let update_dispatch_indirect_buffer_table_id = self
×
2763
                .update_dispatch_indirect_buffer
×
2764
                .insert(GpuDispatchIndirect::default());
×
2765

2766
            // Allocate per-effect metadata. Note that we run after Bevy has allocated
2767
            // meshes, so we already know the buffer and position of the particle mesh, and
2768
            // can fill the indirect args with it.
2769
            let (gpu_effect_metadata, cached_mesh) = {
×
2770
                // FIXME - this is too soon because prepare_assets::<RenderMesh>() didn't
2771
                // necessarily run. we should defer CachedMesh until later,
2772
                // as we don't really need it here anyway. use Added<CachedEffect> to detect
2773
                // newly added effects later in the render frame? note also that
2774
                // we use cmd.get(entity).insert() so technically the CachedEffect _could_
2775
                // already exist... maybe should only do the bare minimum here
2776
                // (insert into caches) and not update components eagerly? not sure...
2777

2778
                let Some(render_mesh) = render_meshes.get(added_effect.mesh.id()) else {
×
2779
                    warn!(
×
2780
                        "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
2781
                        added_effect.entity, added_effect.mesh
2782
                    );
2783
                    continue;
×
2784
                };
2785
                let Some(mesh_vertex_buffer_slice) =
×
2786
                    mesh_allocator.mesh_vertex_slice(&added_effect.mesh.id())
2787
                else {
2788
                    trace!(
×
2789
                        "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
2790
                        added_effect.entity,
2791
                        added_effect.mesh
2792
                    );
2793
                    continue;
×
2794
                };
2795
                let mesh_index_buffer_slice =
2796
                    mesh_allocator.mesh_index_slice(&added_effect.mesh.id());
2797
                let indexed = if let RenderMeshBufferInfo::Indexed { index_format, .. } =
×
2798
                    render_mesh.buffer_info
2799
                {
2800
                    if let Some(ref slice) = mesh_index_buffer_slice {
×
2801
                        Some(MeshIndexSlice {
2802
                            format: index_format,
2803
                            buffer: slice.buffer.clone(),
2804
                            range: slice.range.clone(),
2805
                        })
2806
                    } else {
2807
                        trace!(
×
2808
                            "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
2809
                            added_effect.entity,
2810
                            added_effect.mesh
2811
                        );
2812
                        continue;
×
2813
                    }
2814
                } else {
2815
                    None
×
2816
                };
2817

2818
                (
2819
                    match &mesh_index_buffer_slice {
2820
                        // Indexed mesh rendering
2821
                        Some(mesh_index_buffer_slice) => {
×
2822
                            let ret = GpuEffectMetadata {
×
2823
                                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
×
2824
                                instance_count: 0,
×
2825
                                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
×
2826
                                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start
×
2827
                                    as i32,
×
2828
                                base_instance: 0,
×
2829
                                alive_count: 0,
×
2830
                                max_update: 0,
×
2831
                                dead_count: added_effect.capacity,
×
2832
                                max_spawn: added_effect.capacity,
×
2833
                                ..default()
×
2834
                            };
2835
                            trace!("+ Effect[indexed]: {:?}", ret);
×
2836
                            ret
×
2837
                        }
2838
                        // Non-indexed mesh rendering
2839
                        None => {
2840
                            let ret = GpuEffectMetadata {
×
2841
                                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
2842
                                instance_count: 0,
×
2843
                                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
2844
                                vertex_offset_or_base_instance: 0,
×
2845
                                base_instance: 0,
×
2846
                                alive_count: 0,
×
2847
                                max_update: 0,
×
2848
                                dead_count: added_effect.capacity,
×
2849
                                max_spawn: added_effect.capacity,
×
2850
                                ..default()
×
2851
                            };
2852
                            trace!("+ Effect[non-indexed]: {:?}", ret);
×
2853
                            ret
×
2854
                        }
2855
                    },
2856
                    CachedMesh {
2857
                        mesh: added_effect.mesh.id(),
2858
                        buffer: mesh_vertex_buffer_slice.buffer.clone(),
2859
                        range: mesh_vertex_buffer_slice.range.clone(),
2860
                        indexed,
2861
                    },
2862
                )
2863
            };
2864
            let effect_metadata_buffer_table_id =
2865
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2866
            let dispatch_buffer_indices = DispatchBufferIndices {
2867
                update_dispatch_indirect_buffer_table_id,
2868
                effect_metadata_buffer_table_id,
2869
            };
2870

2871
            // Insert the effect into the cache. This will allocate all the necessary
2872
            // mandatory GPU resources as needed.
2873
            let cached_effect = effect_cache.insert(
2874
                added_effect.handle,
2875
                added_effect.capacity,
2876
                &added_effect.particle_layout,
2877
                added_effect.layout_flags,
2878
            );
2879
            let mut cmd = commands.entity(added_effect.render_entity.id());
2880
            cmd.insert((
2881
                added_effect.entity,
2882
                cached_effect,
2883
                dispatch_buffer_indices,
2884
                cached_mesh,
2885
            ));
2886

2887
            // Allocate storage for properties if needed
2888
            if !added_effect.property_layout.is_empty() {
×
2889
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
×
2890
                cmd.insert(cached_effect_properties);
×
2891
            } else {
2892
                cmd.remove::<CachedEffectProperties>();
×
2893
            }
2894

2895
            // Allocate storage for the reference to the parent effect if needed. Note that
2896
            // we cannot yet allocate the complete parent info (CachedChildInfo) because it
2897
            // depends on the list of children, which we can't resolve until all
2898
            // effects have been added/removed this frame. This will be done later in
2899
            // resolve_parents().
2900
            if let Some(parent) = added_effect.parent.as_ref() {
×
2901
                let cached_parent: CachedParentRef = CachedParentRef {
2902
                    entity: parent.entity,
2903
                };
2904
                cmd.insert(cached_parent);
2905
                trace!("+ new effect declares parent entity {:?}", parent.entity);
×
2906
            } else {
2907
                cmd.remove::<CachedParentRef>();
×
2908
                trace!("+ new effect declares no parent");
×
2909
            }
2910

2911
            // Allocate storage for GPU spawn events if needed
2912
            if let Some(parent) = added_effect.parent.as_ref() {
×
2913
                let cached_events = event_cache.allocate(parent.event_count);
2914
                cmd.insert(cached_events);
2915
            } else {
2916
                cmd.remove::<CachedEffectEvents>();
×
2917
            }
2918

2919
            // Ensure the particle@1 bind group layout exists for the given configuration of
2920
            // particle layout and (optionally) parent particle layout.
2921
            {
2922
                let parent_min_binding_size = added_effect
2923
                    .parent
2924
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2925
                effect_cache.ensure_particle_bind_group_layout(
2926
                    added_effect.particle_layout.min_binding_size32(),
2927
                    parent_min_binding_size,
2928
                );
2929
            }
2930

2931
            // Ensure the metadata@3 bind group layout exists for init pass.
2932
            {
2933
                let consume_gpu_spawn_events = added_effect
2934
                    .layout_flags
2935
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2936
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2937
            }
2938

2939
            // We cannot yet determine the layout of the metadata@3 bind group for the
2940
            // update pass, because it depends on the number of children, and
2941
            // this is encoded indirectly via the number of child effects
2942
            // pointing to this parent, and only calculated later in
2943
            // resolve_parents().
2944

2945
            trace!(
2946
                "+ added effect entity {:?}: main_entity={:?} \
×
2947
                first_update_group_dispatch_buffer_index={} \
×
2948
                render_effect_dispatch_buffer_id={}",
×
2949
                added_effect.render_entity,
2950
                added_effect.entity,
2951
                update_dispatch_indirect_buffer_table_id.0,
2952
                effect_metadata_buffer_table_id.0
2953
            );
2954
        }
2955

2956
        // Once all changes are applied, immediately schedule any GPU buffer
2957
        // (re)allocation based on the new buffer size. The actual GPU buffer content
2958
        // will be written later.
2959
        if self
×
2960
            .update_dispatch_indirect_buffer
×
2961
            .allocate_gpu(render_device, render_queue)
×
2962
        {
2963
            // All those bind groups use the buffer so need to be re-created
2964
            trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
×
2965
            effect_bind_groups.particle_buffers.clear();
×
2966
        }
2967
    }
2968

2969
    pub fn allocate_spawner(
×
2970
        &mut self,
2971
        global_transform: &GlobalTransform,
2972
        spawn_count: u32,
2973
        prng_seed: u32,
2974
        effect_metadata_buffer_table_id: BufferTableId,
2975
    ) -> u32 {
2976
        let spawner_base = self.spawner_buffer.len() as u32;
×
2977
        let transform = global_transform.compute_matrix().into();
×
2978
        let inverse_transform = Mat4::from(
2979
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2980
            // efficient than inversing the Mat4.
2981
            global_transform.affine().inverse(),
×
2982
        )
2983
        .into();
2984
        let spawner_params = GpuSpawnerParams {
2985
            transform,
2986
            inverse_transform,
2987
            spawn: spawn_count as i32,
×
2988
            seed: prng_seed,
2989
            effect_metadata_index: effect_metadata_buffer_table_id.0,
×
2990
            ..default()
2991
        };
2992
        trace!("spawner params = {:?}", spawner_params);
×
2993
        self.spawner_buffer.push(spawner_params);
×
2994
        spawner_base
×
2995
    }
2996
}
2997

2998
bitflags! {
2999
    /// Effect flags.
3000
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3001
    pub struct LayoutFlags: u32 {
3002
        /// No flags.
3003
        const NONE = 0;
3004
        // DEPRECATED - The effect uses an image texture.
3005
        //const PARTICLE_TEXTURE = (1 << 0);
3006
        /// The effect is simulated in local space.
3007
        const LOCAL_SPACE_SIMULATION = (1 << 2);
3008
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
3009
        const USE_ALPHA_MASK = (1 << 3);
3010
        /// The effect is rendered with flipbook texture animation based on the
3011
        /// [`Attribute::SPRITE_INDEX`] of each particle.
3012
        const FLIPBOOK = (1 << 4);
3013
        /// The effect needs UVs.
3014
        const NEEDS_UV = (1 << 5);
3015
        /// The effect has ribbons.
3016
        const RIBBONS = (1 << 6);
3017
        /// The effects needs normals.
3018
        const NEEDS_NORMAL = (1 << 7);
3019
        /// The effect is fully-opaque.
3020
        const OPAQUE = (1 << 8);
3021
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
3022
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
3023
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
3024
        /// a single CPU spawn count.
3025
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
3026
        /// The (init or update) shader needs access to its parent particle. This allows
3027
        /// a particle init or update pass to read the data of a parent particle, for
3028
        /// example to inherit some of the attributes.
3029
        const READ_PARENT_PARTICLE = (1 << 11);
3030
    }
3031
}
3032

3033
impl Default for LayoutFlags {
3034
    fn default() -> Self {
1✔
3035
        Self::NONE
1✔
3036
    }
3037
}
3038

3039
/// Observer raised when the [`CachedEffect`] component is removed, which
3040
/// indicates that the effect instance was despawned.
3041
pub(crate) fn on_remove_cached_effect(
×
3042
    trigger: Trigger<OnRemove, CachedEffect>,
3043
    query: Query<(
3044
        Entity,
3045
        MainEntity,
3046
        &CachedEffect,
3047
        &DispatchBufferIndices,
3048
        Option<&CachedEffectProperties>,
3049
        Option<&CachedParentInfo>,
3050
        Option<&CachedEffectEvents>,
3051
    )>,
3052
    mut effect_cache: ResMut<EffectCache>,
3053
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3054
    mut effects_meta: ResMut<EffectsMeta>,
3055
    mut event_cache: ResMut<EventCache>,
3056
) {
3057
    #[cfg(feature = "trace")]
3058
    let _span = bevy::utils::tracing::info_span!("on_remove_cached_effect").entered();
×
3059

3060
    // FIXME - review this Observer pattern; this triggers for each event one by
3061
    // one, which could kill performance if many effects are removed.
3062

3063
    // Fecth the components of the effect being destroyed. Note that the despawn
3064
    // command above is not yet applied, so this query should always succeed.
3065
    let Ok((
3066
        render_entity,
×
3067
        main_entity,
×
3068
        cached_effect,
×
3069
        dispatch_buffer_indices,
×
3070
        _opt_props,
×
3071
        _opt_parent,
×
3072
        opt_cached_effect_events,
×
3073
    )) = query.get(trigger.entity())
3074
    else {
3075
        return;
×
3076
    };
3077

3078
    // Dealllocate the effect slice in the event buffer, if any.
3079
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
3080
        match event_cache.free(cached_effect_events) {
3081
            Err(err) => {
×
3082
                error!("Error while freeing effect event slice: {err:?}");
×
3083
            }
3084
            Ok(buffer_state) => {
×
3085
                if buffer_state != BufferState::Used {
×
3086
                    // Clear bind groups associated with the old buffer
3087
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
3088
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3089
                }
3090
            }
3091
        }
3092
    }
3093

3094
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3095
    // last slice, also deallocate the GPU buffer itself.
3096
    trace!(
×
3097
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
×
3098
        render_entity,
3099
        main_entity,
3100
    );
3101
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
×
3102
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3103
        // do.
3104
        return;
×
3105
    };
3106

3107
    // Clear bind groups associated with the removed buffer
3108
    trace!(
×
3109
        "=> GPU buffer #{} gone, destroying its bind groups...",
×
3110
        cached_effect.buffer_index
3111
    );
3112
    effect_bind_groups
×
3113
        .particle_buffers
×
3114
        .remove(&cached_effect.buffer_index);
×
3115
    effects_meta
×
3116
        .update_dispatch_indirect_buffer
×
3117
        .remove(dispatch_buffer_indices.update_dispatch_indirect_buffer_table_id);
×
3118
    effects_meta
×
3119
        .effect_metadata_buffer
×
3120
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
×
3121
}
3122

3123
/// Update the [`CachedEffect`] component for any newly allocated effect.
3124
///
3125
/// After this system ran, and its commands are applied, all valid extracted
3126
/// effects have a corresponding entity in the render world, with a
3127
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3128
pub(crate) fn add_effects(
×
3129
    render_device: Res<RenderDevice>,
3130
    render_queue: Res<RenderQueue>,
3131
    mesh_allocator: Res<MeshAllocator>,
3132
    render_meshes: Res<RenderAssets<RenderMesh>>,
3133
    commands: Commands,
3134
    mut effects_meta: ResMut<EffectsMeta>,
3135
    mut effect_cache: ResMut<EffectCache>,
3136
    mut property_cache: ResMut<PropertyCache>,
3137
    mut event_cache: ResMut<EventCache>,
3138
    mut extracted_effects: ResMut<ExtractedEffects>,
3139
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3140
    mut sort_bind_groups: ResMut<SortBindGroups>,
3141
) {
3142
    #[cfg(feature = "trace")]
3143
    let _span = bevy::utils::tracing::info_span!("add_effects").entered();
×
3144
    trace!("add_effects");
×
3145

3146
    // Clear last frame's buffer resizes which may have occured during last frame,
3147
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3148
    // the first point at which we can do that where we're not blocking the main
3149
    // world (so, excluding the extract system).
3150
    effects_meta
×
3151
        .update_dispatch_indirect_buffer
×
3152
        .clear_previous_frame_resizes();
3153
    effects_meta
×
3154
        .effect_metadata_buffer
×
3155
        .clear_previous_frame_resizes();
3156
    sort_bind_groups.clear_previous_frame_resizes();
×
3157

3158
    // Allocate new effects
3159
    effects_meta.add_effects(
×
3160
        commands,
×
3161
        std::mem::take(&mut extracted_effects.added_effects),
×
3162
        &render_device,
×
3163
        &render_queue,
×
3164
        &mesh_allocator,
×
3165
        &render_meshes,
×
3166
        &mut effect_bind_groups,
×
3167
        &mut effect_cache,
×
3168
        &mut property_cache,
×
3169
        &mut event_cache,
×
3170
    );
3171

3172
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3173
    // because EffectBuffer already contains a reference to the
3174
    // RenderDevice, so has done so internally. This is not ideal
3175
    // design-wise, but works.
3176
}
3177

3178
/// Check if two lists of entities are equal.
3179
fn is_child_list_changed(
×
3180
    parent_entity: Entity,
3181
    old: impl ExactSizeIterator<Item = Entity>,
3182
    new: impl ExactSizeIterator<Item = Entity>,
3183
) -> bool {
3184
    if old.len() != new.len() {
×
3185
        trace!(
×
3186
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3187
            parent_entity,
×
3188
            old.len(),
×
3189
            new.len()
×
3190
        );
3191
        return true;
×
3192
    }
3193

3194
    // TODO - this value is arbitrary
3195
    if old.len() >= 16 {
×
3196
        // For large-ish lists, use a hash set.
3197
        let old = HashSet::from_iter(old);
×
3198
        let new = HashSet::from_iter(new);
×
3199
        if old != new {
×
3200
            trace!(
×
3201
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3202
            );
3203
            true
×
3204
        } else {
3205
            false
×
3206
        }
3207
    } else {
3208
        // For small lists, just use a linear array and sort it
3209
        let mut old = old.collect::<Vec<_>>();
×
3210
        let mut new = new.collect::<Vec<_>>();
×
3211
        old.sort_unstable();
×
3212
        new.sort_unstable();
×
3213
        if old != new {
×
3214
            trace!(
×
3215
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3216
            );
3217
            true
×
3218
        } else {
3219
            false
×
3220
        }
3221
    }
3222
}
3223

3224
/// Resolve parents and children, updating their [`CachedParent`] and
3225
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3226
/// slice for all children of each parent.
3227
pub(crate) fn resolve_parents(
×
3228
    mut commands: Commands,
3229
    q_child_effects: Query<
3230
        (
3231
            Entity,
3232
            &CachedParentRef,
3233
            &CachedEffectEvents,
3234
            Option<&CachedChildInfo>,
3235
        ),
3236
        With<CachedEffect>,
3237
    >,
3238
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3239
    effect_cache: Res<EffectCache>,
3240
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3241
    mut event_cache: ResMut<EventCache>,
3242
    mut children_from_parent: Local<
3243
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3244
    >,
3245
) {
3246
    #[cfg(feature = "trace")]
3247
    let _span = bevy::utils::tracing::info_span!("resolve_parents").entered();
×
3248
    let num_parent_effects = q_parent_effects.iter().len();
3249
    trace!("resolve_parents: num_parents={num_parent_effects}");
×
3250

3251
    // Build map of render entity from main entity for all cached effects.
3252
    let render_from_main_entity = q_cached_effects
×
3253
        .iter()
3254
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
×
3255
        .collect::<HashMap<_, _>>();
3256

3257
    // Group child effects by parent, building a list of children for each parent,
3258
    // solely based on the declaration each child makes of its parent. This doesn't
3259
    // mean yet that the parent exists.
3260
    if children_from_parent.capacity() < num_parent_effects {
×
3261
        let extra = num_parent_effects - children_from_parent.capacity();
×
3262
        children_from_parent.reserve(extra);
×
3263
    }
3264
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3265
        q_child_effects.iter()
3266
    {
3267
        // Resolve the parent reference into the render world
3268
        let parent_main_entity = cached_parent_ref.entity;
×
3269
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3270
            warn!(
×
3271
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3272
                parent_main_entity, child_entity
3273
            );
3274
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3275
            continue;
×
3276
        };
3277
        let parent_entity = *parent_entity;
3278

3279
        // Resolve the parent
3280
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3281
            // Since we failed to resolve, remove this component so the next systems ignore
3282
            // this effect.
3283
            warn!(
×
3284
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3285
                parent_entity, child_entity
3286
            );
3287
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3288
            continue;
×
3289
        };
3290
        let Some(parent_buffer_binding_source) = effect_cache
×
3291
            .get_buffer(parent_cached_effect.buffer_index)
3292
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3293
        else {
3294
            // Since we failed to resolve, remove this component so the next systems ignore
3295
            // this effect.
3296
            warn!(
×
3297
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3298
                parent_cached_effect.buffer_index, child_entity
3299
            );
3300
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3301
            continue;
×
3302
        };
3303

3304
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3305
        else {
3306
            // Since we failed to resolve, remove this component so the next systems ignore
3307
            // this effect.
3308
            warn!(
×
3309
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3310
                cached_effect_events.buffer_index, child_entity
3311
            );
3312
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3313
            continue;
×
3314
        };
3315
        let child_buffer_binding_source = BufferBindingSource {
3316
            buffer: child_event_buffer.clone(),
3317
            offset: cached_effect_events.range.start,
3318
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3319
        };
3320

3321
        // Push the child entity into the children list
3322
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3323
        let local_child_index = child_vec.len() as u32;
3324
        child_vec.push((child_entity, child_buffer_binding_source));
3325
        child_infos.push(GpuChildInfo {
3326
            event_count: 0,
3327
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3328
        });
3329

3330
        // Check if child info changed. Avoid overwriting if no change.
3331
        if let Some(old_cached_child_info) = cached_child_info {
×
3332
            if parent_entity == old_cached_child_info.parent
3333
                && parent_cached_effect.slice.particle_layout
×
3334
                    == old_cached_child_info.parent_particle_layout
×
3335
                && parent_buffer_binding_source
×
3336
                    == old_cached_child_info.parent_buffer_binding_source
×
3337
                // Note: if local child index didn't change, then keep global one too for now. Chances are the parent didn't change, but anyway we can't know for now without inspecting all its children.
3338
                && local_child_index == old_cached_child_info.local_child_index
×
3339
                && cached_effect_events.init_indirect_dispatch_index
×
3340
                    == old_cached_child_info.init_indirect_dispatch_index
×
3341
            {
3342
                trace!(
×
3343
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3344
                    child_entity
3345
                );
3346
                continue;
×
3347
            }
3348
        }
3349

3350
        // Allocate (or overwrite, if already existing) the child info, now that the
3351
        // parent is resolved.
3352
        let cached_child_info = CachedChildInfo {
3353
            parent: parent_entity,
3354
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3355
            parent_buffer_binding_source,
3356
            local_child_index,
3357
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3358
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3359
        };
3360
        commands.entity(child_entity).insert(cached_child_info);
×
3361
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3362
    }
3363

3364
    // Once all parents are resolved, diff all children of already-cached parents,
3365
    // and re-allocate their GpuChildInfo if needed.
3366
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
×
3367
        // Fetch the newly extracted list of children
3368
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3369
        else {
3370
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3371
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3372
            continue;
×
3373
        };
3374

3375
        // Check if any child changed compared to the existing CachedChildren component
3376
        if !is_child_list_changed(
3377
            parent_entity,
3378
            cached_parent_info
3379
                .children
3380
                .iter()
3381
                .map(|(entity, _)| *entity),
×
3382
            children.iter().map(|(entity, _)| *entity),
×
3383
        ) {
3384
            continue;
×
3385
        }
3386

3387
        event_cache.reallocate_child_infos(
×
3388
            parent_entity,
×
3389
            children,
×
3390
            &child_infos[..],
×
3391
            cached_parent_info.deref_mut(),
×
3392
        );
3393
    }
3394

3395
    // Once this is done, the children hash map contains all entries which don't
3396
    // already have a CachedParentInfo component. That is, all entities which are
3397
    // new parents.
3398
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
×
3399
        let cached_parent_info =
×
3400
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
×
3401
        commands.entity(parent_entity).insert(cached_parent_info);
×
3402
    }
3403

3404
    // // Once all changes are applied, immediately schedule any GPU buffer
3405
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3406
    // content // will be written later.
3407
    // if event_cache
3408
    //     .child_infos()
3409
    //     .allocate_gpu(render_device, render_queue)
3410
    // {
3411
    //     // All those bind groups use the buffer so need to be re-created
3412
    //     effect_bind_groups.particle_buffers.clear();
3413
    // }
3414
}
3415

3416
pub fn fixup_parents(
×
3417
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3418
    mut q_children: Query<&mut CachedChildInfo>,
3419
) {
3420
    #[cfg(feature = "trace")]
3421
    let _span = bevy::utils::tracing::info_span!("fixup_parents").entered();
×
3422
    trace!("fixup_parents");
×
3423

3424
    // Once all parents are (re-)allocated, fix up the global index of all
3425
    // children if the parent base index changed.
3426
    trace!(
×
3427
        "Updating the global index of children of parent effects whose child list just changed..."
×
3428
    );
3429
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
×
3430
        let base_index =
×
3431
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
×
3432
        trace!(
×
3433
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3434
            cached_parent_info.children.len(),
×
3435
            parent_entity,
3436
            base_index
3437
        );
3438
        for (child_entity, _) in &cached_parent_info.children {
×
3439
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3440
                continue;
×
3441
            };
3442
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
3443
            trace!(
3444
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3445
                child_entity,
×
3446
                parent_entity,
×
3447
                cached_child_info.local_child_index,
×
3448
                cached_child_info.global_child_index
×
3449
            );
3450
        }
3451
    }
3452
}
3453

3454
// TEMP - Mark all cached effects as invalid for this frame until another system
3455
// explicitly marks them as valid. Otherwise we early out in some parts, and
3456
// reuse by mistake the previous frame's extraction.
3457
pub fn clear_all_effects(
×
3458
    mut commands: Commands,
3459
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3460
) {
3461
    for entity in &mut q_cached_effects {
×
3462
        if let Some(mut cmd) = commands.get_entity(entity) {
×
3463
            cmd.remove::<BatchInput>();
×
3464
        }
3465
    }
3466
}
3467

3468
/// Indexed mesh metadata for [`CachedMesh`].
3469
#[derive(Debug, Clone)]
3470
#[allow(dead_code)]
3471
pub(crate) struct MeshIndexSlice {
3472
    /// Index format.
3473
    pub format: IndexFormat,
3474
    /// GPU buffer containing the indices.
3475
    pub buffer: Buffer,
3476
    /// Range inside [`Self::buffer`] where the indices are.
3477
    pub range: Range<u32>,
3478
}
3479

3480
/// Render world cached mesh infos for a single effect instance.
3481
#[derive(Debug, Clone, Component)]
3482
pub(crate) struct CachedMesh {
3483
    /// Asset of the effect mesh to draw.
3484
    pub mesh: AssetId<Mesh>,
3485
    /// GPU buffer storing the [`mesh`] of the effect.
3486
    pub buffer: Buffer,
3487
    /// Range slice inside the GPU buffer for the effect mesh.
3488
    pub range: Range<u32>,
3489
    /// Indexed rendering metadata.
3490
    #[allow(unused)]
3491
    pub indexed: Option<MeshIndexSlice>,
3492
}
3493

3494
/// Render world cached properties info for a single effect instance.
3495
#[allow(unused)]
3496
#[derive(Debug, Component)]
3497
pub(crate) struct CachedProperties {
3498
    /// Layout of the effect properties.
3499
    pub layout: PropertyLayout,
3500
    /// Index of the buffer in the [`EffectCache`].
3501
    pub buffer_index: u32,
3502
    /// Offset in bytes inside the buffer.
3503
    pub offset: u32,
3504
    /// Binding size in bytes of the property struct.
3505
    pub binding_size: u32,
3506
}
3507

3508
#[derive(SystemParam)]
3509
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3510
    sim_params: Res<'w, SimParams>,
3511
    render_device: Res<'w, RenderDevice>,
3512
    render_queue: Res<'w, RenderQueue>,
3513
    #[system_param(ignore)]
3514
    marker: PhantomData<&'s usize>,
3515
}
3516

3517
#[derive(SystemParam)]
3518
pub struct PipelineSystemParams<'w, 's> {
3519
    pipeline_cache: Res<'w, PipelineCache>,
3520
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3521
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3522
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3523
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3524
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3525
    specialized_indirect_pipelines:
3526
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3527
    #[system_param(ignore)]
3528
    marker: PhantomData<&'s usize>,
3529
}
3530

3531
pub(crate) fn prepare_effects(
×
3532
    mut commands: Commands,
3533
    read_only_params: PrepareEffectsReadOnlyParams,
3534
    mut pipelines: PipelineSystemParams,
3535
    mut property_cache: ResMut<PropertyCache>,
3536
    event_cache: Res<EventCache>,
3537
    mut effect_cache: ResMut<EffectCache>,
3538
    mut effects_meta: ResMut<EffectsMeta>,
3539
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3540
    mut extracted_effects: ResMut<ExtractedEffects>,
3541
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3542
    q_cached_effects: Query<(
3543
        MainEntity,
3544
        &CachedEffect,
3545
        Ref<CachedMesh>,
3546
        &DispatchBufferIndices,
3547
        Option<&CachedEffectProperties>,
3548
        Option<&CachedParentInfo>,
3549
        Option<&CachedChildInfo>,
3550
        Option<&CachedEffectEvents>,
3551
    )>,
3552
    q_debug_all_entities: Query<MainEntity>,
3553
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperationQueue>,
3554
    mut sort_bind_groups: ResMut<SortBindGroups>,
3555
) {
3556
    #[cfg(feature = "trace")]
3557
    let _span = bevy::utils::tracing::info_span!("prepare_effects").entered();
×
3558
    trace!("prepare_effects");
×
3559

3560
    // Workaround for too many params in system (TODO: refactor to split work?)
3561
    let sim_params = read_only_params.sim_params.into_inner();
×
3562
    let render_device = read_only_params.render_device.into_inner();
×
3563
    let render_queue = read_only_params.render_queue.into_inner();
×
3564
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
×
3565
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
×
3566
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
×
3567
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
×
3568

3569
    // // sort first by z and then by handle. this ensures that, when possible,
3570
    // batches span multiple z layers // batches won't span z-layers if there is
3571
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3572
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3573
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3574
    //         other => other,
3575
    //     }
3576
    // });
3577

3578
    // Ensure the indirect pipelines are created
3579
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
×
3580
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
×
3581
            pipeline_cache,
×
3582
            &pipelines.indirect_pipeline,
×
3583
            DispatchIndirectPipelineKey { has_events: false },
×
3584
        );
3585
    }
3586
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
×
3587
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
×
3588
            pipeline_cache,
×
3589
            &pipelines.indirect_pipeline,
×
3590
            DispatchIndirectPipelineKey { has_events: true },
×
3591
        );
3592
    }
3593
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
×
3594
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3595
    } else {
3596
        // If this is the first time we insert an event buffer, we need to switch the
3597
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3598
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3599
        // more effect using GPU spawn events, we can deallocate.
3600
        let was_empty =
×
3601
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
×
3602
        let is_empty = event_cache.child_infos().is_empty();
×
3603
        if was_empty && !is_empty {
×
3604
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3605
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3606
        } else if is_empty && !was_empty {
×
3607
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3608
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3609
        }
3610
    }
3611

3612
    gpu_buffer_operation_queue.begin_frame();
×
3613

3614
    // Clear per-instance buffers, which are filled below and re-uploaded each frame
3615
    effects_meta.spawner_buffer.clear();
×
3616

3617
    // Build batcher inputs from extracted effects, updating all cached components
3618
    // for each effect on the fly.
3619
    let effects = std::mem::take(&mut extracted_effects.effects);
×
3620
    let extracted_effect_count = effects.len();
×
3621
    let mut prepared_effect_count = 0;
×
3622
    for extracted_effect in effects.into_iter() {
×
3623
        // Skip effects not cached. Since we're iterating over the extracted effects
3624
        // instead of the cached ones, it might happen we didn't cache some effect on
3625
        // purpose because they failed earlier validations.
3626
        // FIXME - extract into ECS directly so we don't have to do that?
3627
        let Ok((
3628
            main_entity,
×
3629
            cached_effect,
×
3630
            cached_mesh,
×
3631
            dispatch_buffer_indices,
×
3632
            cached_effect_properties,
×
3633
            cached_parent_info,
×
3634
            cached_child_info,
×
3635
            cached_effect_events,
×
3636
        )) = q_cached_effects.get(extracted_effect.render_entity.id())
×
3637
        else {
3638
            warn!(
×
3639
                "Unknown render entity {:?} for extracted effect.",
×
3640
                extracted_effect.render_entity.id()
×
3641
            );
3642
            if let Ok(main_entity) = q_debug_all_entities.get(extracted_effect.render_entity.id()) {
×
3643
                info!(
3644
                    "Render entity {:?} exists with main entity {:?}, some component missing!",
×
3645
                    extracted_effect.render_entity.id(),
×
3646
                    main_entity
3647
                );
3648
            } else {
3649
                info!(
×
3650
                    "Render entity {:?} does not exists with a MainEntity.",
×
3651
                    extracted_effect.render_entity.id()
×
3652
                );
3653
            }
3654
            continue;
×
3655
        };
3656

3657
        let effect_slice = EffectSlice {
3658
            slice: cached_effect.slice.range(),
3659
            buffer_index: cached_effect.buffer_index,
3660
            particle_layout: cached_effect.slice.particle_layout.clone(),
3661
        };
3662

3663
        let has_event_buffer = cached_child_info.is_some();
3664
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3665
        // should be Option<T>, not T)
3666
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3667
            None
×
3668
        } else {
3669
            Some(extracted_effect.property_layout.min_binding_size())
×
3670
        };
3671

3672
        // Schedule some GPU buffer operation to update the number of workgroups to
3673
        // dispatch during the indirect init pass of this effect based on the number of
3674
        // GPU spawn events written in its buffer.
3675
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3676
            (cached_effect_events, cached_child_info)
3677
        {
3678
            debug_assert_eq!(
×
3679
                GpuChildInfo::min_size().get() % 4,
×
3680
                0,
3681
                "Invalid GpuChildInfo alignment."
×
3682
            );
3683

3684
            // Resolve parent entry
3685
            let Ok((_, _, _, _, _, cached_parent_info, _, _)) =
×
3686
                q_cached_effects.get(cached_child_info.parent)
×
3687
            else {
3688
                continue;
×
3689
            };
3690
            let Some(cached_parent_info) = cached_parent_info else {
×
3691
                error!("Effect {:?} indicates its parent is {:?}, but that parent effect is missing a CachedParentInfo component. This is a bug.", extracted_effect.render_entity.id(), cached_child_info.parent);
×
3692
                continue;
×
3693
            };
3694

3695
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3696
            let child_info_size_u32 = GpuChildInfo::min_size().get() as u32 / 4;
3697
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3698
            let global_child_index = cached_child_info.global_child_index;
×
3699

3700
            // Schedule a fill dispatch
3701
            let event_buffer_index = cached_effect_events.buffer_index;
×
3702
            let event_slice = cached_effect_events.range.clone();
×
3703
            trace!(
×
3704
                "queue_init_fill(): event_buffer_index={} event_slice={:?} src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3705
                event_buffer_index,
3706
                event_slice,
3707
                global_child_index,
3708
                init_indirect_dispatch_index,
3709
            );
3710
            gpu_buffer_operation_queue.enqueue_init_fill(
×
3711
                event_buffer_index,
×
3712
                event_slice,
×
3713
                GpuBufferOperationArgs {
×
3714
                    src_offset: global_child_index,
×
3715
                    src_stride: child_info_size_u32,
×
3716
                    dst_offset: init_indirect_dispatch_index,
×
3717
                    dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
3718
                    count: 1, // FIXME - should be a batch here!!
×
3719
                },
3720
            );
3721
        }
3722

3723
        // Create init pipeline key flags.
3724
        let init_pipeline_key_flags = {
×
3725
            let mut flags = ParticleInitPipelineKeyFlags::empty();
×
3726
            flags.set(
×
3727
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
3728
                effect_slice.particle_layout.contains(Attribute::PREV),
×
3729
            );
3730
            flags.set(
×
3731
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
3732
                effect_slice.particle_layout.contains(Attribute::NEXT),
×
3733
            );
3734
            flags.set(
×
3735
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
×
3736
                has_event_buffer,
×
3737
            );
3738
            flags
×
3739
        };
3740

3741
        // This should always exist by the time we reach this point, because we should
3742
        // have inserted any property in the cache, which would have allocated the
3743
        // proper bind group layout (or the default no-property one).
3744
        let spawner_bind_group_layout = property_cache
×
3745
            .bind_group_layout(property_layout_min_binding_size)
×
3746
            .unwrap_or_else(|| {
×
3747
                panic!(
×
3748
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
×
3749
                    property_layout_min_binding_size,
×
3750
                )
3751
            });
3752
        trace!(
3753
            "Retrieved spawner@2 bind group layout {:?} for property binding size {:?}.",
×
3754
            spawner_bind_group_layout.id(),
×
3755
            property_layout_min_binding_size
3756
        );
3757

3758
        // Fetch the bind group layouts from the cache
3759
        trace!("cached_child_info={:?}", cached_child_info);
×
3760
        let (parent_particle_layout_min_binding_size, parent_buffer_index) =
×
3761
            if let Some(cached_child) = cached_child_info.as_ref() {
×
3762
                let Ok((_, parent_cached_effect, _, _, _, _, _, _)) =
×
3763
                    q_cached_effects.get(cached_child.parent)
3764
                else {
3765
                    // At this point we should have discarded invalid effects with a missing parent,
3766
                    // so if the parent is not found this is a bug.
3767
                    error!(
×
3768
                        "Effect main_entity {:?}: parent render entity {:?} not found.",
×
3769
                        main_entity, cached_child.parent
3770
                    );
3771
                    continue;
×
3772
                };
3773
                (
3774
                    Some(
3775
                        parent_cached_effect
3776
                            .slice
3777
                            .particle_layout
3778
                            .min_binding_size32(),
3779
                    ),
3780
                    Some(parent_cached_effect.buffer_index),
3781
                )
3782
            } else {
3783
                (None, None)
×
3784
            };
3785
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
×
3786
            effect_slice.particle_layout.min_binding_size32(),
3787
            parent_particle_layout_min_binding_size,
3788
        ) else {
3789
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", 
×
3790
            effect_slice.particle_layout.min_binding_size32(), parent_particle_layout_min_binding_size);
×
3791
            continue;
×
3792
        };
3793
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3794
        trace!(
3795
            "Retrieved particle@1 bind group layout {:?} for particle binding size {:?} and parent binding size {:?}.",
×
3796
            particle_bind_group_layout.id(),
×
3797
            effect_slice.particle_layout.min_binding_size32(),
×
3798
            parent_particle_layout_min_binding_size,
3799
        );
3800

3801
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
×
3802
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
×
3803

3804
        // Specialize the init pipeline based on the effect.
3805
        let init_pipeline_id = {
×
3806
            let consume_gpu_spawn_events = init_pipeline_key_flags
3807
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3808

3809
            // Fetch the metadata@3 bind group layout from the cache
3810
            let metadata_bind_group_layout = effect_cache
3811
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3812
                .unwrap()
3813
                .clone();
3814

3815
            // https://github.com/bevyengine/bevy/issues/17132
3816
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3817
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3818
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3819
            pipelines.init_pipeline.temp_particle_bind_group_layout =
3820
                Some(particle_bind_group_layout.clone());
3821
            pipelines.init_pipeline.temp_spawner_bind_group_layout =
3822
                Some(spawner_bind_group_layout.clone());
3823
            pipelines.init_pipeline.temp_metadata_bind_group_layout =
3824
                Some(metadata_bind_group_layout);
3825
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3826
                pipeline_cache,
3827
                &pipelines.init_pipeline,
3828
                ParticleInitPipelineKey {
3829
                    shader: extracted_effect.effect_shaders.init.clone(),
3830
                    particle_layout_min_binding_size,
3831
                    parent_particle_layout_min_binding_size,
3832
                    flags: init_pipeline_key_flags,
3833
                    particle_bind_group_layout_id,
3834
                    spawner_bind_group_layout_id,
3835
                    metadata_bind_group_layout_id,
3836
                },
3837
            );
3838
            // keep things tidy; this is just a hack, should not persist
3839
            pipelines.init_pipeline.temp_particle_bind_group_layout = None;
3840
            pipelines.init_pipeline.temp_spawner_bind_group_layout = None;
3841
            pipelines.init_pipeline.temp_metadata_bind_group_layout = None;
3842
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
3843

3844
            init_pipeline_id
3845
        };
3846

3847
        let update_pipeline_id = {
×
3848
            let num_event_buffers = cached_parent_info
3849
                .map(|p| p.children.len() as u32)
×
3850
                .unwrap_or_default();
3851

3852
            // FIXME: currently don't hava a way to determine when this is needed, because
3853
            // we know the number of children per parent only after resolving
3854
            // all parents, but by that point we forgot if this is a newly added
3855
            // effect or not. So since we need to re-ensure for all effects, not
3856
            // only new ones, might as well do here...
3857
            effect_cache.ensure_metadata_update_bind_group_layout(num_event_buffers);
3858

3859
            // Fetch the bind group layouts from the cache
3860
            let metadata_bind_group_layout = effect_cache
3861
                .metadata_update_bind_group_layout(num_event_buffers)
3862
                .unwrap()
3863
                .clone();
3864

3865
            // https://github.com/bevyengine/bevy/issues/17132
3866
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3867
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3868
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3869
            pipelines.update_pipeline.temp_particle_bind_group_layout =
3870
                Some(particle_bind_group_layout);
3871
            pipelines.update_pipeline.temp_spawner_bind_group_layout =
3872
                Some(spawner_bind_group_layout);
3873
            pipelines.update_pipeline.temp_metadata_bind_group_layout =
3874
                Some(metadata_bind_group_layout);
3875
            let update_pipeline_id = specialized_update_pipelines.specialize(
3876
                pipeline_cache,
3877
                &pipelines.update_pipeline,
3878
                ParticleUpdatePipelineKey {
3879
                    shader: extracted_effect.effect_shaders.update.clone(),
3880
                    particle_layout: effect_slice.particle_layout.clone(),
3881
                    parent_particle_layout_min_binding_size,
3882
                    num_event_buffers,
3883
                    particle_bind_group_layout_id,
3884
                    spawner_bind_group_layout_id,
3885
                    metadata_bind_group_layout_id,
3886
                },
3887
            );
3888
            // keep things tidy; this is just a hack, should not persist
3889
            pipelines.update_pipeline.temp_particle_bind_group_layout = None;
3890
            pipelines.update_pipeline.temp_spawner_bind_group_layout = None;
3891
            pipelines.update_pipeline.temp_metadata_bind_group_layout = None;
3892
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
3893

3894
            update_pipeline_id
3895
        };
3896

3897
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3898
            init: init_pipeline_id,
3899
            update: update_pipeline_id,
3900
        };
3901

3902
        // For ribbons, which need particle sorting, create a bind group layout for
3903
        // sorting the effect, based on its particle layout.
3904
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
3905
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
3906
                pipeline_cache,
×
3907
                &extracted_effect.particle_layout,
×
3908
            ) {
3909
                error!(
3910
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
3911
                    err
3912
                );
3913
                continue;
×
3914
            }
3915
        }
3916

3917
        // Output some debug info
3918
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
×
3919
        trace!(
×
3920
            "update_shader = {:?}",
×
3921
            extracted_effect.effect_shaders.update
3922
        );
3923
        trace!(
×
3924
            "render_shader = {:?}",
×
3925
            extracted_effect.effect_shaders.render
3926
        );
3927
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
3928
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
3929
        #[cfg(feature = "2d")]
3930
        {
3931
            trace!("z_sort_key_2d = {:?}", extracted_effect.z_sort_key_2d);
×
3932
        }
3933

3934
        let spawner_index = effects_meta.allocate_spawner(
×
3935
            &extracted_effect.transform,
×
NEW
3936
            extracted_effect.spawn_count,
×
NEW
3937
            extracted_effect.prng_seed,
×
UNCOV
3938
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
3939
        );
3940

3941
        trace!(
×
3942
            "Updating cached effect at entity {:?}...",
×
3943
            extracted_effect.render_entity.id()
×
3944
        );
3945
        let mut cmd = commands.entity(extracted_effect.render_entity.id());
×
3946
        cmd.insert(BatchInput {
×
3947
            handle: extracted_effect.handle,
×
3948
            entity: extracted_effect.render_entity.id(),
×
3949
            main_entity: extracted_effect.main_entity,
×
3950
            effect_slice,
×
3951
            init_and_update_pipeline_ids,
×
3952
            parent_buffer_index,
×
3953
            event_buffer_index: cached_effect_events.map(|cee| cee.buffer_index),
×
3954
            child_effects: cached_parent_info
3955
                .map(|cp| cp.children.clone())
×
3956
                .unwrap_or_default(),
3957
            layout_flags: extracted_effect.layout_flags,
3958
            texture_layout: extracted_effect.texture_layout.clone(),
3959
            textures: extracted_effect.textures.clone(),
3960
            alpha_mode: extracted_effect.alpha_mode,
3961
            particle_layout: extracted_effect.particle_layout.clone(),
3962
            shaders: extracted_effect.effect_shaders,
3963
            spawner_base: spawner_index,
3964
            spawn_count: extracted_effect.spawn_count,
3965
            #[cfg(feature = "3d")]
3966
            position: extracted_effect.transform.translation(),
3967
            init_indirect_dispatch_index: cached_child_info
3968
                .map(|cc| cc.init_indirect_dispatch_index),
×
3969
            #[cfg(feature = "2d")]
3970
            z_sort_key_2d: extracted_effect.z_sort_key_2d,
3971
        });
3972

3973
        // Update properties
3974
        if let Some(cached_effect_properties) = cached_effect_properties {
×
3975
            // Because the component is persisted, it may be there from a previous version
3976
            // of the asset. And add_remove_effects() only add new instances or remove old
3977
            // ones, but doesn't update existing ones. Check if it needs to be removed.
3978
            // FIXME - Dedupe with add_remove_effect(), we shouldn't have 2 codepaths doing
3979
            // the same thing at 2 different times.
3980
            if extracted_effect.property_layout.is_empty() {
3981
                trace!(
×
3982
                    "Render entity {:?} had CachedEffectProperties component, but newly extracted property layout is empty. Removing component...",
×
3983
                    extracted_effect.render_entity.id(),
×
3984
                );
3985
                cmd.remove::<CachedEffectProperties>();
×
3986
                // Also remove the other one. FIXME - dedupe those two...
3987
                cmd.remove::<CachedProperties>();
×
3988

3989
                if extracted_effect.property_data.is_some() {
×
3990
                    warn!(
×
3991
                        "Effect on entity {:?} doesn't declare any property in its Module, but some property values were provided. Those values will be discarded.",
×
3992
                        extracted_effect.main_entity.id(),
×
3993
                    );
3994
                }
3995
            } else {
3996
                // Insert a new component or overwrite the existing one
3997
                cmd.insert(CachedProperties {
×
3998
                    layout: extracted_effect.property_layout.clone(),
×
3999
                    buffer_index: cached_effect_properties.buffer_index,
×
4000
                    offset: cached_effect_properties.range.start,
×
4001
                    binding_size: cached_effect_properties.range.len() as u32,
×
4002
                });
4003

4004
                // Write properties for this effect if they were modified.
4005
                // FIXME - This doesn't work with batching!
4006
                if let Some(property_data) = &extracted_effect.property_data {
×
4007
                    trace!(
4008
                    "Properties changed; (re-)uploading to GPU... New data: {} bytes. Capacity: {} bytes.",
×
4009
                    property_data.len(),
×
4010
                    cached_effect_properties.range.len(),
×
4011
                );
4012
                    if property_data.len() <= cached_effect_properties.range.len() {
×
4013
                        let property_buffer = property_cache.buffers_mut()
×
4014
                            [cached_effect_properties.buffer_index as usize]
×
4015
                            .as_mut()
4016
                            .unwrap();
4017
                        property_buffer.write(cached_effect_properties.range.start, property_data);
×
4018
                    } else {
4019
                        error!(
×
4020
                            "Cannot upload properties: existing property slice in property buffer #{} is too small ({} bytes) for the new data ({} bytes).",
×
4021
                            cached_effect_properties.buffer_index,
×
4022
                            cached_effect_properties.range.len(),
×
4023
                            property_data.len()
×
4024
                        );
4025
                    }
4026
                }
4027
            }
4028
        } else {
4029
            // No property on the effect; remove the component
4030
            trace!(
×
4031
                "No CachedEffectProperties on render entity {:?}, remove any CachedProperties component too.",
×
4032
                extracted_effect.render_entity.id()
×
4033
            );
4034
            cmd.remove::<CachedProperties>();
×
4035
        }
4036

4037
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4038
        // update its GpuEffectMetadata with all those infos.
4039
        // FIXME - should do this only when the below changes (not only the mesh), via
4040
        // some invalidation mechanism and ECS change detection.
4041
        if cached_mesh.is_changed() {
×
4042
            let capacity = cached_effect.slice.len();
×
4043

4044
            // Global and local indices of this effect as a child of another (parent) effect
4045
            let (global_child_index, local_child_index) = cached_child_info
×
4046
                .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4047
                .unwrap_or_default();
4048

4049
            // Base index of all children of this (parent) effect
4050
            let base_child_index = cached_parent_info
×
4051
                .map(|cpi| {
×
4052
                    debug_assert_eq!(
×
4053
                        cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4054
                        0
4055
                    );
4056
                    cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4057
                })
4058
                .unwrap_or_default();
4059

4060
            let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
×
4061
            let sort_key_offset = extracted_effect
×
4062
                .particle_layout
×
4063
                .offset(Attribute::RIBBON_ID)
×
4064
                .unwrap_or(0)
×
4065
                / 4;
×
4066
            let sort_key2_offset = extracted_effect
×
4067
                .particle_layout
×
4068
                .offset(Attribute::AGE)
×
4069
                .unwrap_or(0)
×
4070
                / 4;
×
4071

4072
            let mut gpu_effect_metadata = GpuEffectMetadata {
4073
                instance_count: 0,
4074
                base_instance: 0,
4075
                alive_count: 0,
4076
                max_update: 0,
4077
                dead_count: capacity,
4078
                max_spawn: capacity,
4079
                ping: 0,
4080
                spawner_index,
4081
                indirect_dispatch_index: dispatch_buffer_indices
×
4082
                    .update_dispatch_indirect_buffer_table_id
4083
                    .0,
4084
                // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4085
                indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
×
4086
                init_indirect_dispatch_index: cached_effect_events
×
4087
                    .map(|cee| cee.init_indirect_dispatch_index)
4088
                    .unwrap_or_default(),
4089
                local_child_index,
4090
                global_child_index,
4091
                base_child_index,
4092
                particle_stride,
4093
                sort_key_offset,
4094
                sort_key2_offset,
4095
                ..default()
4096
            };
4097
            if let Some(indexed) = &cached_mesh.indexed {
×
4098
                gpu_effect_metadata.vertex_or_index_count = indexed.range.len() as u32;
4099
                gpu_effect_metadata.first_index_or_vertex_offset = indexed.range.start;
4100
                gpu_effect_metadata.vertex_offset_or_base_instance = cached_mesh.range.start as i32;
4101
            } else {
4102
                gpu_effect_metadata.vertex_or_index_count = cached_mesh.range.len() as u32;
×
4103
                gpu_effect_metadata.first_index_or_vertex_offset = cached_mesh.range.start;
×
4104
                gpu_effect_metadata.vertex_offset_or_base_instance = 0;
×
4105
            };
4106
            assert!(dispatch_buffer_indices
×
4107
                .effect_metadata_buffer_table_id
×
4108
                .is_valid());
×
4109
            effects_meta.effect_metadata_buffer.update(
×
4110
                dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
4111
                gpu_effect_metadata,
×
4112
            );
4113

4114
            warn!(
×
4115
                "Updated metadata entry {} for effect {:?}, this will reset it.",
×
4116
                dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4117
            );
4118
        }
4119

4120
        prepared_effect_count += 1;
×
4121
    }
4122
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
×
4123

4124
    // Once all EffectMetadata values are written, schedule a GPU upload
4125
    if effects_meta
×
4126
        .effect_metadata_buffer
×
4127
        .allocate_gpu(render_device, render_queue)
×
4128
    {
4129
        // All those bind groups use the buffer so need to be re-created
4130
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
×
4131
        effects_meta.indirect_metadata_bind_group = None;
×
4132
        effect_bind_groups.init_metadata_bind_groups.clear();
×
4133
        effect_bind_groups.update_metadata_bind_groups.clear();
×
4134
    }
4135

4136
    // Write the entire spawner buffer for this frame, for all effects combined
4137
    assert_eq!(
×
4138
        prepared_effect_count,
×
4139
        effects_meta.spawner_buffer.len() as u32
×
4140
    );
4141
    if effects_meta
×
4142
        .spawner_buffer
×
4143
        .write_buffer(render_device, render_queue)
×
4144
    {
4145
        // All property bind groups use the spawner buffer, which was reallocate
4146
        property_bind_groups.clear(true);
×
4147
        effects_meta.indirect_spawner_bind_group = None;
×
4148
    }
4149

4150
    // Update simulation parameters
4151
    effects_meta.sim_params_uniforms.set(sim_params.into());
×
4152
    {
4153
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
4154
        gpu_sim_params.num_effects = prepared_effect_count;
×
4155

4156
        trace!(
×
4157
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
4158
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
×
4159
            gpu_sim_params.time,
4160
            gpu_sim_params.delta_time,
4161
            gpu_sim_params.virtual_time,
4162
            gpu_sim_params.virtual_delta_time,
4163
            gpu_sim_params.real_time,
4164
            gpu_sim_params.real_delta_time,
4165
            gpu_sim_params.num_effects,
4166
        );
4167
    }
4168
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
×
4169
    effects_meta
4170
        .sim_params_uniforms
4171
        .write_buffer(render_device, render_queue);
4172
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
×
4173
        // Buffer changed, invalidate bind groups
4174
        effects_meta.indirect_sim_params_bind_group = None;
×
4175
    }
4176
}
4177

4178
pub(crate) fn batch_effects(
×
4179
    mut commands: Commands,
4180
    render_device: Res<RenderDevice>,
4181
    render_queue: Res<RenderQueue>,
4182
    effects_meta: Res<EffectsMeta>,
4183
    mut sort_bind_groups: ResMut<SortBindGroups>,
4184
    mut q_cached_effects: Query<(
4185
        Entity,
4186
        &CachedMesh,
4187
        Option<&CachedEffectEvents>,
4188
        Option<&CachedChildInfo>,
4189
        Option<&CachedProperties>,
4190
        &mut DispatchBufferIndices,
4191
        &mut BatchInput,
4192
    )>,
4193
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4194
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperationQueue>,
4195
) {
4196
    trace!("batch_effects");
×
4197

4198
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4199
    // inside that buffer. This is critical for batching to work, because
4200
    // batching effects is based on compatible items, which implies same GPU
4201
    // buffer and continuous slice ranges (the next slice start must be equal to
4202
    // the previous start end, without gap). EffectSlice already contains both
4203
    // information, and the proper ordering implementation.
4204
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4205

4206
    // For now we re-create that buffer each frame. Since there's no CPU -> GPU
4207
    // transfer, this is pretty cheap in practice.
4208
    sort_bind_groups.clear_indirect_dispatch_buffer();
×
4209

4210
    // Loop on all extracted effects in order, and try to batch them together to
4211
    // reduce draw calls. -- currently does nothing, batching was broken and never
4212
    // fixed.
4213
    // FIXME - This is in ECS order, if we re-add the sorting above we need a
4214
    // different order here!
4215
    trace!("Batching {} effects...", q_cached_effects.iter().len());
×
4216
    sorted_effect_batches.clear();
×
4217
    for (
4218
        entity,
×
4219
        cached_mesh,
×
4220
        cached_effect_events,
×
4221
        cached_child_info,
×
4222
        cached_properties,
×
4223
        dispatch_buffer_indices,
×
4224
        mut input,
×
4225
    ) in &mut q_cached_effects
×
4226
    {
4227
        // Detect if this cached effect was not updated this frame by a new extracted
4228
        // effect. This happens when e.g. the effect is invisible and not simulated, or
4229
        // some error prevented it from being extracted. We use the pipeline IDs vector
4230
        // as a marker, because each frame we move it out of the CachedGroup
4231
        // component during batching, so if empty this means a new one was not created
4232
        // this frame.
4233
        // if input.init_and_update_pipeline_ids.is_empty() {
4234
        //     trace!(
4235
        //         "Skipped cached effect on render entity {:?}: not extracted this
4236
        // frame.",         entity
4237
        //     );
4238
        //     continue;
4239
        // }
4240

4241
        #[cfg(feature = "2d")]
4242
        let z_sort_key_2d = input.z_sort_key_2d;
4243

4244
        #[cfg(feature = "3d")]
4245
        let translation_3d = input.position;
4246

4247
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4248
        // most of the data needed to drive rendering. However this doesn't drive
4249
        // rendering; this is just storage.
4250
        let mut effect_batch = EffectBatch::from_input(
4251
            cached_mesh,
4252
            cached_effect_events,
4253
            cached_child_info,
4254
            &mut input,
4255
            *dispatch_buffer_indices.as_ref(),
4256
            cached_properties.map(|cp| PropertyBindGroupKey {
×
4257
                buffer_index: cp.buffer_index,
×
4258
                binding_size: cp.binding_size,
×
4259
            }),
4260
            cached_properties.map(|cp| cp.offset),
×
4261
        );
4262

4263
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4264
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4265
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4266
        // ribbon).
4267
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
4268
            // This buffer is allocated in prepare_effects(), so should always be available
4269
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4270
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4271
                continue;
×
4272
            };
4273

4274
            // Allocate a GpuDispatchIndirect entry
4275
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4276
            effect_batch.sort_fill_indirect_dispatch_index =
4277
                Some(sort_fill_indirect_dispatch_index);
4278

4279
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4280
            // compute a number of workgroups to dispatch based on that particle count, and
4281
            // store the result into a GpuDispatchIndirect struct which will be used to
4282
            // dispatch the fill-sort pass.
4283
            {
4284
                let src_buffer = effect_metadata_buffer.clone();
4285
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4286
                    effect_batch
4287
                        .dispatch_buffer_indices
4288
                        .effect_metadata_buffer_table_id,
4289
                );
4290
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4291
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4292
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4293
                    continue;
×
4294
                };
4295
                let dst_buffer = dst_buffer.clone();
4296
                let dst_binding_offset = sort_bind_groups
4297
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index);
4298
                let dst_binding_size = NonZeroU32::new(12).unwrap();
4299
                trace!(
4300
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4301
                    src_buffer.id(),
×
4302
                    src_binding_offset,
×
4303
                    src_binding_size.get(),
×
4304
                    dst_buffer.id(),
×
4305
                    dst_binding_offset,
×
4306
                    dst_binding_size.get(),
×
4307
                );
4308
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
×
4309
                debug_assert_eq!(
×
4310
                    src_offset, 5,
4311
                    "GpuEffectMetadata changed, update this assert."
×
4312
                );
4313
                gpu_buffer_operation_queue.enqueue(
×
4314
                    GpuBufferOperationType::FillDispatchArgs,
×
4315
                    GpuBufferOperationArgs {
×
4316
                        src_offset,
×
4317
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
×
4318
                        dst_offset: 0,
×
4319
                        dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
4320
                        count: 1,
×
4321
                    },
4322
                    src_buffer,
×
4323
                    src_binding_offset,
×
4324
                    Some(src_binding_size),
×
4325
                    dst_buffer,
×
4326
                    dst_binding_offset,
×
4327
                    Some(dst_binding_size),
×
4328
                );
4329
            }
4330
        }
4331

4332
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4333
        trace!(
×
4334
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4335
            effect_batch_index,
4336
            entity,
4337
        );
4338

4339
        // Spawn an EffectDrawBatch, to actually drive rendering.
4340
        commands
×
4341
            .spawn(EffectDrawBatch {
×
4342
                effect_batch_index,
×
4343
                #[cfg(feature = "2d")]
×
4344
                z_sort_key_2d,
×
4345
                #[cfg(feature = "3d")]
×
4346
                translation_3d,
×
4347
            })
4348
            .insert(TemporaryRenderEntity);
×
4349
    }
4350

4351
    // Once all GPU operations for this frame are enqueued, upload them to GPU
4352
    gpu_buffer_operation_queue.end_frame(&render_device, &render_queue);
×
4353

4354
    sorted_effect_batches.sort();
×
4355
}
4356

4357
/// Per-buffer bind groups for a GPU effect buffer.
4358
///
4359
/// This contains all bind groups specific to a single [`EffectBuffer`].
4360
///
4361
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4362
pub(crate) struct BufferBindGroups {
4363
    /// Bind group for the render shader.
4364
    ///
4365
    /// ```wgsl
4366
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4367
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4368
    /// @binding(2) var<storage, read> spawner : Spawner;
4369
    /// ```
4370
    render: BindGroup,
4371
    // /// Bind group for filling the indirect dispatch arguments of any child init
4372
    // /// pass.
4373
    // ///
4374
    // /// This bind group is optional; it's only created if the current effect has
4375
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4376
    // /// (although normally the event buffer is not created if there's no
4377
    // /// children).
4378
    // ///
4379
    // /// The source buffer is always the current effect's event buffer. The
4380
    // /// destination buffer is the global shared buffer for indirect fill args
4381
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4382
    // /// args contains the data to index the relevant part of the global shared
4383
    // /// buffer for this effect buffer; it may contain multiple entries in case
4384
    // /// multiple effects are batched inside the current effect buffer.
4385
    // ///
4386
    // /// ```wgsl
4387
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4388
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4389
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4390
    // /// ```
4391
    // init_fill_dispatch: Option<BindGroup>,
4392
}
4393

4394
/// Combination of a texture layout and the bound textures.
4395
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4396
struct Material {
4397
    layout: TextureLayout,
4398
    textures: Vec<AssetId<Image>>,
4399
}
4400

4401
impl Material {
4402
    /// Get the bind group entries to create a bind group.
4403
    pub fn make_entries<'a>(
×
4404
        &self,
4405
        gpu_images: &'a RenderAssets<GpuImage>,
4406
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4407
        if self.textures.is_empty() {
×
4408
            return Ok(vec![]);
×
4409
        }
4410

4411
        let entries: Vec<BindGroupEntry<'a>> = self
×
4412
            .textures
×
4413
            .iter()
4414
            .enumerate()
4415
            .flat_map(|(index, id)| {
×
4416
                let base_binding = index as u32 * 2;
×
4417
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4418
                    vec![
×
4419
                        BindGroupEntry {
×
4420
                            binding: base_binding,
×
4421
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4422
                        },
4423
                        BindGroupEntry {
×
4424
                            binding: base_binding + 1,
×
4425
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4426
                        },
4427
                    ]
4428
                } else {
4429
                    vec![]
×
4430
                }
4431
            })
4432
            .collect();
4433
        if entries.len() == self.textures.len() * 2 {
×
4434
            return Ok(entries);
×
4435
        }
4436
        Err(())
×
4437
    }
4438
}
4439

4440
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4441
struct BindingKey {
4442
    pub buffer_id: BufferId,
4443
    pub offset: u32,
4444
    pub size: NonZeroU32,
4445
}
4446

4447
impl<'a> From<BufferSlice<'a>> for BindingKey {
4448
    fn from(value: BufferSlice<'a>) -> Self {
×
4449
        Self {
4450
            buffer_id: value.buffer.id(),
×
4451
            offset: value.offset,
×
4452
            size: value.size,
×
4453
        }
4454
    }
4455
}
4456

4457
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4458
    fn from(value: &BufferSlice<'a>) -> Self {
×
4459
        Self {
4460
            buffer_id: value.buffer.id(),
×
4461
            offset: value.offset,
×
4462
            size: value.size,
×
4463
        }
4464
    }
4465
}
4466

4467
impl From<&BufferBindingSource> for BindingKey {
4468
    fn from(value: &BufferBindingSource) -> Self {
×
4469
        Self {
4470
            buffer_id: value.buffer.id(),
×
4471
            offset: value.offset,
×
4472
            size: value.size,
×
4473
        }
4474
    }
4475
}
4476

4477
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4478
struct ConsumeEventKey {
4479
    child_infos_buffer_id: BufferId,
4480
    events: BindingKey,
4481
}
4482

4483
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4484
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4485
        Self {
4486
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4487
            events: value.events.into(),
×
4488
        }
4489
    }
4490
}
4491

4492
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4493
struct InitMetadataBindGroupKey {
4494
    pub buffer_index: u32,
4495
    pub effect_metadata_buffer: BufferId,
4496
    pub effect_metadata_offset: u32,
4497
    pub consume_event_key: Option<ConsumeEventKey>,
4498
}
4499

4500
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4501
struct UpdateMetadataBindGroupKey {
4502
    pub buffer_index: u32,
4503
    pub effect_metadata_buffer: BufferId,
4504
    pub effect_metadata_offset: u32,
4505
    pub child_info_buffer_id: Option<BufferId>,
4506
    pub event_buffers_keys: Vec<BindingKey>,
4507
}
4508

4509
struct CachedBindGroup<K: Eq> {
4510
    /// Key the bind group was created from. Each time the key changes, the bind
4511
    /// group should be re-created.
4512
    key: K,
4513
    /// Bind group created from the key.
4514
    bind_group: BindGroup,
4515
}
4516

4517
#[derive(Debug, Clone, Copy)]
4518
struct BufferSlice<'a> {
4519
    pub buffer: &'a Buffer,
4520
    pub offset: u32,
4521
    pub size: NonZeroU32,
4522
}
4523

4524
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4525
    fn from(value: BufferSlice<'a>) -> Self {
×
4526
        Self {
4527
            buffer: value.buffer,
×
4528
            offset: value.offset.into(),
×
4529
            size: Some(value.size.into()),
×
4530
        }
4531
    }
4532
}
4533

4534
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4535
    fn from(value: &BufferSlice<'a>) -> Self {
×
4536
        Self {
4537
            buffer: value.buffer,
×
4538
            offset: value.offset.into(),
×
4539
            size: Some(value.size.into()),
×
4540
        }
4541
    }
4542
}
4543

4544
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4545
    fn from(value: &'a BufferBindingSource) -> Self {
×
4546
        Self {
4547
            buffer: &value.buffer,
×
4548
            offset: value.offset,
×
4549
            size: value.size,
×
4550
        }
4551
    }
4552
}
4553

4554
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4555
/// the init pass consumes GPU events as a mechanism to spawn particles.
4556
struct ConsumeEventBuffers<'a> {
4557
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4558
    /// This is dynamically indexed inside the shader.
4559
    child_infos_buffer: &'a Buffer,
4560
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4561
    events: BufferSlice<'a>,
4562
}
4563

4564
#[derive(Default, Resource)]
4565
pub struct EffectBindGroups {
4566
    /// Map from buffer index to the bind groups shared among all effects that
4567
    /// use that buffer.
4568
    particle_buffers: HashMap<u32, BufferBindGroups>,
4569
    /// Map of bind groups for image assets used as particle textures.
4570
    images: HashMap<AssetId<Image>, BindGroup>,
4571
    /// Map from buffer index to its metadata bind group (group 3) for the init
4572
    /// pass.
4573
    // FIXME - doesn't work with batching; this should be the instance ID
4574
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4575
    /// Map from buffer index to its metadata bind group (group 3) for the
4576
    /// update pass.
4577
    // FIXME - doesn't work with batching; this should be the instance ID
4578
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4579
    /// Map from an effect material to its bind group.
4580
    material_bind_groups: HashMap<Material, BindGroup>,
4581
    /// Map from an event buffer index to the bind group @0 for the init fill
4582
    /// pass in charge of filling all its init dispatches.
4583
    init_fill_dispatch: HashMap<u32, BindGroup>,
4584
}
4585

4586
impl EffectBindGroups {
4587
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4588
        self.particle_buffers
×
4589
            .get(&buffer_index)
×
4590
            .map(|bg| &bg.render)
×
4591
    }
4592

4593
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4594
    /// needed.
4595
    pub(self) fn get_or_create_init_metadata(
×
4596
        &mut self,
4597
        effect_batch: &EffectBatch,
4598
        gpu_limits: &GpuLimits,
4599
        render_device: &RenderDevice,
4600
        layout: &BindGroupLayout,
4601
        effect_metadata_buffer: &Buffer,
4602
        consume_event_buffers: Option<ConsumeEventBuffers>,
4603
    ) -> Result<&BindGroup, ()> {
4604
        let DispatchBufferIndices {
×
4605
            effect_metadata_buffer_table_id,
×
4606
            ..
×
4607
        } = &effect_batch.dispatch_buffer_indices;
×
4608

4609
        let effect_metadata_offset =
×
4610
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
×
4611
        let key = InitMetadataBindGroupKey {
4612
            buffer_index: effect_batch.buffer_index,
×
4613
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4614
            effect_metadata_offset,
4615
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
×
4616
        };
4617

4618
        let make_entry = || {
×
4619
            let mut entries = Vec::with_capacity(3);
×
4620
            entries.push(
×
4621
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4622
                BindGroupEntry {
×
4623
                    binding: 0,
×
4624
                    resource: BindingResource::Buffer(BufferBinding {
×
4625
                        buffer: effect_metadata_buffer,
×
4626
                        offset: key.effect_metadata_offset as u64,
×
4627
                        size: Some(gpu_limits.effect_metadata_size()),
×
4628
                    }),
4629
                },
4630
            );
4631
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
×
4632
                entries.push(
4633
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4634
                    // ChildInfoBuffer;
4635
                    BindGroupEntry {
4636
                        binding: 1,
4637
                        resource: BindingResource::Buffer(BufferBinding {
4638
                            buffer: consume_event_buffers.child_infos_buffer,
4639
                            offset: 0,
4640
                            size: None,
4641
                        }),
4642
                    },
4643
                );
4644
                entries.push(
4645
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4646
                    BindGroupEntry {
4647
                        binding: 2,
4648
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4649
                    },
4650
                );
4651
            }
4652

4653
            let bind_group = render_device.create_bind_group(
×
4654
                "hanabi:bind_group:init:metadata@3",
4655
                layout,
×
4656
                &entries[..],
×
4657
            );
4658

4659
            trace!(
×
4660
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
×
4661
                    effect_batch.buffer_index,
4662
                    effect_metadata_buffer_table_id.0,
4663
                );
4664

4665
            bind_group
×
4666
        };
4667

4668
        Ok(&self
×
4669
            .init_metadata_bind_groups
×
4670
            .entry(effect_batch.buffer_index)
×
4671
            .and_modify(|cbg| {
×
4672
                if cbg.key != key {
×
4673
                    trace!(
×
4674
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4675
                        cbg.key,
4676
                        key
4677
                    );
4678
                    cbg.key = key;
×
4679
                    cbg.bind_group = make_entry();
×
4680
                }
4681
            })
4682
            .or_insert_with(|| {
×
4683
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
×
4684
                CachedBindGroup {
×
4685
                    key,
×
4686
                    bind_group: make_entry(),
×
4687
                }
4688
            })
4689
            .bind_group)
×
4690
    }
4691

4692
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4693
    /// needed.
4694
    pub(self) fn get_or_create_update_metadata(
×
4695
        &mut self,
4696
        effect_batch: &EffectBatch,
4697
        gpu_limits: &GpuLimits,
4698
        render_device: &RenderDevice,
4699
        layout: &BindGroupLayout,
4700
        effect_metadata_buffer: &Buffer,
4701
        child_info_buffer: Option<&Buffer>,
4702
        event_buffers: &[(Entity, BufferBindingSource)],
4703
    ) -> Result<&BindGroup, ()> {
4704
        let DispatchBufferIndices {
×
4705
            effect_metadata_buffer_table_id,
×
4706
            ..
×
4707
        } = &effect_batch.dispatch_buffer_indices;
×
4708

4709
        // Check arguments consistency
4710
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
×
4711
        let emits_gpu_spawn_events = !event_buffers.is_empty();
×
4712
        let child_info_buffer_id = if emits_gpu_spawn_events {
×
4713
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4714
        } else {
4715
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4716
            // if relevant, that is if the effect emits GPU spawn events.
4717
            None
×
4718
        };
4719
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
×
4720

4721
        let event_buffers_keys = event_buffers
×
4722
            .iter()
4723
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4724
            .collect::<Vec<_>>();
4725

4726
        let key = UpdateMetadataBindGroupKey {
4727
            buffer_index: effect_batch.buffer_index,
×
4728
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4729
            effect_metadata_offset: gpu_limits
×
4730
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4731
                as u32,
4732
            child_info_buffer_id,
4733
            event_buffers_keys,
4734
        };
4735

4736
        let make_entry = || {
×
4737
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
×
4738
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4739
            // EffectMetadata;
4740
            entries.push(BindGroupEntry {
×
4741
                binding: 0,
×
4742
                resource: BindingResource::Buffer(BufferBinding {
×
4743
                    buffer: effect_metadata_buffer,
×
4744
                    offset: key.effect_metadata_offset as u64,
×
4745
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
×
4746
                }),
4747
            });
4748
            if emits_gpu_spawn_events {
×
4749
                let child_info_buffer = child_info_buffer.unwrap();
×
4750

4751
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4752
                // ChildInfoBuffer;
4753
                entries.push(BindGroupEntry {
×
4754
                    binding: 1,
×
4755
                    resource: BindingResource::Buffer(BufferBinding {
×
4756
                        buffer: child_info_buffer,
×
4757
                        offset: 0,
×
4758
                        size: None,
×
4759
                    }),
4760
                });
4761

4762
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4763
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4764
                    // EventBuffer;
4765
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4766
                    // then moved to counting in bytes, so now need some conversion. Need to review
4767
                    // all of this...
4768
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
×
4769
                    buffer_binding.offset *= 4;
×
4770
                    buffer_binding.size = buffer_binding
×
4771
                        .size
×
4772
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4773
                    entries.push(BindGroupEntry {
×
4774
                        binding: 2 + index as u32,
×
4775
                        resource: BindingResource::Buffer(buffer_binding),
×
4776
                    });
4777
                }
4778
            }
4779

4780
            let bind_group = render_device.create_bind_group(
×
4781
                "hanabi:bind_group:update:metadata@3",
4782
                layout,
×
4783
                &entries[..],
×
4784
            );
4785

4786
            trace!(
×
4787
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
×
4788
                effect_batch.buffer_index,
4789
                effect_metadata_buffer_table_id.0,
4790
            );
4791

4792
            bind_group
×
4793
        };
4794

4795
        Ok(&self
×
4796
            .update_metadata_bind_groups
×
4797
            .entry(effect_batch.buffer_index)
×
4798
            .and_modify(|cbg| {
×
4799
                if cbg.key != key {
×
4800
                    trace!(
×
4801
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4802
                        cbg.key,
4803
                        key
4804
                    );
4805
                    cbg.key = key.clone();
×
4806
                    cbg.bind_group = make_entry();
×
4807
                }
4808
            })
4809
            .or_insert_with(|| {
×
4810
                trace!(
×
4811
                    "Inserting new bind group for update metadata@3 with key={:?}",
×
4812
                    key
4813
                );
4814
                CachedBindGroup {
×
4815
                    key: key.clone(),
×
4816
                    bind_group: make_entry(),
×
4817
                }
4818
            })
4819
            .bind_group)
×
4820
    }
4821

4822
    pub fn init_fill_dispatch(&self, event_buffer_index: u32) -> Option<&BindGroup> {
×
4823
        self.init_fill_dispatch.get(&event_buffer_index)
×
4824
    }
4825
}
4826

4827
#[derive(SystemParam)]
4828
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4829
    #[cfg(feature = "2d")]
4830
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4831
    #[cfg(feature = "3d")]
4832
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4833
    #[cfg(feature = "3d")]
4834
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4835
    #[cfg(feature = "3d")]
4836
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4837
    #[system_param(ignore)]
4838
    marker: PhantomData<&'s usize>,
4839
}
4840

4841
fn emit_sorted_draw<T, F>(
×
4842
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
4843
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
4844
    view_entities: &mut FixedBitSet,
4845
    sorted_effect_batches: &SortedEffectBatches,
4846
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4847
    render_pipeline: &mut ParticlesRenderPipeline,
4848
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4849
    render_meshes: &RenderAssets<RenderMesh>,
4850
    pipeline_cache: &PipelineCache,
4851
    make_phase_item: F,
4852
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4853
) where
4854
    T: SortedPhaseItem,
4855
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
4856
{
4857
    trace!("emit_sorted_draw() {} views", views.iter().len());
×
4858

4859
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
4860
        trace!(
×
4861
            "Process new sorted view with {} visible particle effect entities",
×
4862
            visible_entities.len::<WithCompiledParticleEffect>()
×
4863
        );
4864

4865
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4866
            continue;
×
4867
        };
4868

4869
        {
4870
            #[cfg(feature = "trace")]
4871
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4872

4873
            view_entities.clear();
×
4874
            view_entities.extend(
×
4875
                visible_entities
×
4876
                    .iter::<WithCompiledParticleEffect>()
×
4877
                    .map(|e| e.1.index() as usize),
×
4878
            );
4879
        }
4880

4881
        // For each view, loop over all the effect batches to determine if the effect
4882
        // needs to be rendered for that view, and enqueue a view-dependent
4883
        // batch if so.
4884
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
4885
            #[cfg(feature = "trace")]
4886
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
4887

4888
            trace!(
×
4889
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4890
                draw_entity,
×
4891
                draw_batch.effect_batch_index,
×
4892
            );
4893

4894
            // Get the EffectBatches this EffectDrawBatch is part of.
4895
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4896
            else {
×
4897
                continue;
×
4898
            };
4899

4900
            trace!(
×
4901
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4902
                effect_batch.buffer_index,
×
4903
                effect_batch.spawner_base,
×
4904
                effect_batch.layout_flags,
×
4905
            );
4906

4907
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4908
            if effect_batch
×
4909
                .layout_flags
×
4910
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4911
            {
4912
                trace!("Non-transparent batch. Skipped.");
×
4913
                continue;
×
4914
            }
4915

4916
            // Check if batch contains any entity visible in the current view. Otherwise we
4917
            // can skip the entire batch. Note: This is O(n^2) but (unlike
4918
            // the Sprite renderer this is inspired from) we don't expect more than
4919
            // a handful of particle effect instances, so would rather not pay the memory
4920
            // cost of a FixedBitSet for the sake of an arguable speed-up.
4921
            // TODO - Profile to confirm.
4922
            #[cfg(feature = "trace")]
4923
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
4924
            let has_visible_entity = effect_batch
×
4925
                .entities
×
4926
                .iter()
4927
                .any(|index| view_entities.contains(*index as usize));
×
4928
            if !has_visible_entity {
×
4929
                trace!("No visible entity for view, not emitting any draw call.");
×
4930
                continue;
×
4931
            }
4932
            #[cfg(feature = "trace")]
4933
            _span_check_vis.exit();
×
4934

4935
            // Create and cache the bind group layout for this texture layout
4936
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4937

4938
            // FIXME - We draw the entire batch, but part of it may not be visible in this
4939
            // view! We should re-batch for the current view specifically!
4940

4941
            let local_space_simulation = effect_batch
×
4942
                .layout_flags
×
4943
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
4944
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
4945
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
4946
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
4947
            let needs_normal = effect_batch
×
4948
                .layout_flags
×
4949
                .contains(LayoutFlags::NEEDS_NORMAL);
×
4950
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
4951
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
4952

4953
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4954
            // re-querying here...?
4955
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
×
4956
                trace!("Batch has no render mesh, skipped.");
×
4957
                continue;
×
4958
            };
4959
            let mesh_layout = render_mesh.layout.clone();
×
4960

4961
            // Specialize the render pipeline based on the effect batch
4962
            trace!(
×
4963
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
4964
                effect_batch.render_shader,
×
4965
                image_count,
×
4966
                alpha_mask,
×
4967
                flipbook,
×
4968
                view.hdr
×
4969
            );
4970

4971
            // Add a draw pass for the effect batch
4972
            trace!("Emitting individual draw for batch");
×
4973

4974
            let alpha_mode = effect_batch.alpha_mode;
×
4975

4976
            #[cfg(feature = "trace")]
4977
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
4978
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
4979
                pipeline_cache,
×
4980
                render_pipeline,
×
4981
                ParticleRenderPipelineKey {
×
4982
                    shader: effect_batch.render_shader.clone(),
×
4983
                    mesh_layout: Some(mesh_layout),
×
4984
                    particle_layout: effect_batch.particle_layout.clone(),
×
4985
                    texture_layout: effect_batch.texture_layout.clone(),
×
4986
                    local_space_simulation,
×
4987
                    alpha_mask,
×
4988
                    alpha_mode,
×
4989
                    flipbook,
×
4990
                    needs_uv,
×
4991
                    needs_normal,
×
4992
                    ribbons,
×
4993
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
4994
                    pipeline_mode,
×
4995
                    msaa_samples: msaa.samples(),
×
4996
                    hdr: view.hdr,
×
4997
                },
4998
            );
4999
            #[cfg(feature = "trace")]
5000
            _span_specialize.exit();
×
5001

5002
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5003
            trace!(
×
5004
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5005
                spawner_base={} handle={:?}",
×
5006
                draw_entity,
×
5007
                effect_batch.buffer_index,
×
5008
                effect_batch.spawner_base,
×
5009
                effect_batch.handle
×
5010
            );
5011
            render_phase.add(make_phase_item(
×
5012
                render_pipeline_id,
×
5013
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5014
                draw_batch,
×
5015
                view,
×
5016
            ));
5017
        }
5018
    }
5019
}
5020

5021
#[cfg(feature = "3d")]
5022
fn emit_binned_draw<T, F>(
×
5023
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
5024
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5025
    view_entities: &mut FixedBitSet,
5026
    sorted_effect_batches: &SortedEffectBatches,
5027
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5028
    render_pipeline: &mut ParticlesRenderPipeline,
5029
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5030
    pipeline_cache: &PipelineCache,
5031
    render_meshes: &RenderAssets<RenderMesh>,
5032
    make_bin_key: F,
5033
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5034
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5035
) where
5036
    T: BinnedPhaseItem,
5037
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BinKey,
5038
{
5039
    use bevy::render::render_phase::BinnedRenderPhaseType;
5040

5041
    trace!("emit_binned_draw() {} views", views.iter().len());
×
5042

5043
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
5044
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
×
5045

5046
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
5047
            continue;
×
5048
        };
5049

5050
        {
5051
            #[cfg(feature = "trace")]
5052
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
5053

5054
            view_entities.clear();
×
5055
            view_entities.extend(
×
5056
                visible_entities
×
5057
                    .iter::<WithCompiledParticleEffect>()
×
5058
                    .map(|e| e.1.index() as usize),
×
5059
            );
5060
        }
5061

5062
        // For each view, loop over all the effect batches to determine if the effect
5063
        // needs to be rendered for that view, and enqueue a view-dependent
5064
        // batch if so.
5065
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
5066
            #[cfg(feature = "trace")]
5067
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
5068

5069
            trace!(
×
5070
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5071
                draw_entity,
×
5072
                draw_batch.effect_batch_index,
×
5073
            );
5074

5075
            // Get the EffectBatches this EffectDrawBatch is part of.
5076
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5077
            else {
×
5078
                continue;
×
5079
            };
5080

5081
            trace!(
×
5082
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5083
                effect_batch.buffer_index,
×
5084
                effect_batch.spawner_base,
×
5085
                effect_batch.layout_flags,
×
5086
            );
5087

5088
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5089
                trace!(
×
5090
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5091
                    effect_batch.layout_flags,
×
5092
                    alpha_mask
×
5093
                );
5094
                continue;
×
5095
            }
5096

5097
            // Check if batch contains any entity visible in the current view. Otherwise we
5098
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5099
            // the Sprite renderer this is inspired from) we don't expect more than
5100
            // a handful of particle effect instances, so would rather not pay the memory
5101
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5102
            // TODO - Profile to confirm.
5103
            #[cfg(feature = "trace")]
5104
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
5105
            let has_visible_entity = effect_batch
×
5106
                .entities
×
5107
                .iter()
5108
                .any(|index| view_entities.contains(*index as usize));
×
5109
            if !has_visible_entity {
×
5110
                trace!("No visible entity for view, not emitting any draw call.");
×
5111
                continue;
×
5112
            }
5113
            #[cfg(feature = "trace")]
5114
            _span_check_vis.exit();
×
5115

5116
            // Create and cache the bind group layout for this texture layout
5117
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5118

5119
            // FIXME - We draw the entire batch, but part of it may not be visible in this
5120
            // view! We should re-batch for the current view specifically!
5121

5122
            let local_space_simulation = effect_batch
×
5123
                .layout_flags
×
5124
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5125
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5126
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5127
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5128
            let needs_normal = effect_batch
×
5129
                .layout_flags
×
5130
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5131
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5132
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5133
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5134

5135
            // Specialize the render pipeline based on the effect batch
5136
            trace!(
×
5137
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5138
                effect_batch.render_shader,
×
5139
                image_count,
×
5140
                alpha_mask,
×
5141
                flipbook,
×
5142
                view.hdr
×
5143
            );
5144

5145
            // Add a draw pass for the effect batch
5146
            trace!("Emitting individual draw for batch");
×
5147

5148
            let alpha_mode = effect_batch.alpha_mode;
×
5149

5150
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5151
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5152
                continue;
×
5153
            };
5154

5155
            #[cfg(feature = "trace")]
5156
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
5157
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5158
                pipeline_cache,
×
5159
                render_pipeline,
×
5160
                ParticleRenderPipelineKey {
×
5161
                    shader: effect_batch.render_shader.clone(),
×
5162
                    mesh_layout: Some(mesh_layout),
×
5163
                    particle_layout: effect_batch.particle_layout.clone(),
×
5164
                    texture_layout: effect_batch.texture_layout.clone(),
×
5165
                    local_space_simulation,
×
5166
                    alpha_mask,
×
5167
                    alpha_mode,
×
5168
                    flipbook,
×
5169
                    needs_uv,
×
5170
                    needs_normal,
×
5171
                    ribbons,
×
5172
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5173
                    pipeline_mode,
×
5174
                    msaa_samples: msaa.samples(),
×
5175
                    hdr: view.hdr,
×
5176
                },
5177
            );
5178
            #[cfg(feature = "trace")]
5179
            _span_specialize.exit();
×
5180

5181
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5182
            trace!(
×
5183
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5184
                spawner_base={} handle={:?}",
×
5185
                draw_entity,
×
5186
                effect_batch.buffer_index,
×
5187
                effect_batch.spawner_base,
×
5188
                effect_batch.handle
×
5189
            );
5190
            render_phase.add(
×
5191
                make_bin_key(render_pipeline_id, draw_batch, view),
×
5192
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5193
                BinnedRenderPhaseType::NonMesh,
×
5194
            );
5195
        }
5196
    }
5197
}
5198

5199
#[allow(clippy::too_many_arguments)]
5200
pub(crate) fn queue_effects(
×
5201
    views: Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
5202
    effects_meta: Res<EffectsMeta>,
5203
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5204
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5205
    pipeline_cache: Res<PipelineCache>,
5206
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5207
    sorted_effect_batches: Res<SortedEffectBatches>,
5208
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5209
    events: Res<EffectAssetEvents>,
5210
    render_meshes: Res<RenderAssets<RenderMesh>>,
5211
    read_params: QueueEffectsReadOnlyParams,
5212
    mut view_entities: Local<FixedBitSet>,
5213
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5214
        ViewSortedRenderPhases<Transparent2d>,
5215
    >,
5216
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5217
        ViewSortedRenderPhases<Transparent3d>,
5218
    >,
5219
    #[cfg(feature = "3d")] mut alpha_mask_3d_render_phases: ResMut<
5220
        ViewBinnedRenderPhases<AlphaMask3d>,
5221
    >,
5222
) {
5223
    #[cfg(feature = "trace")]
5224
    let _span = bevy::utils::tracing::info_span!("hanabi:queue_effects").entered();
×
5225

5226
    trace!("queue_effects");
×
5227

5228
    // If an image has changed, the GpuImage has (probably) changed
5229
    for event in &events.images {
×
5230
        match event {
×
5231
            AssetEvent::Added { .. } => None,
×
5232
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5233
            AssetEvent::Unused { .. } => None,
×
5234
            AssetEvent::Modified { id } => {
×
5235
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5236
                effect_bind_groups.images.remove(id)
×
5237
            }
5238
            AssetEvent::Removed { id } => {
×
5239
                trace!("Destroy bind group of removed image asset {:?}", id);
×
5240
                effect_bind_groups.images.remove(id)
×
5241
            }
5242
        };
5243
    }
5244

5245
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5246
        // No spawners are active
5247
        return;
×
5248
    }
5249

5250
    // Loop over all 2D cameras/views that need to render effects
5251
    #[cfg(feature = "2d")]
5252
    {
5253
        #[cfg(feature = "trace")]
5254
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
×
5255

5256
        let draw_effects_function_2d = read_params
5257
            .draw_functions_2d
5258
            .read()
5259
            .get_id::<DrawEffects>()
5260
            .unwrap();
5261

5262
        // Effects with full alpha blending
5263
        if !views.is_empty() {
5264
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
5265
            emit_sorted_draw(
5266
                &views,
×
5267
                &mut transparent_2d_render_phases,
×
5268
                &mut view_entities,
×
5269
                &sorted_effect_batches,
×
5270
                &effect_draw_batches,
×
5271
                &mut render_pipeline,
×
5272
                specialized_render_pipelines.reborrow(),
×
5273
                &render_meshes,
×
5274
                &pipeline_cache,
×
5275
                |id, entity, draw_batch, _view| Transparent2d {
×
5276
                    sort_key: draw_batch.z_sort_key_2d,
×
5277
                    entity,
×
5278
                    pipeline: id,
×
5279
                    draw_function: draw_effects_function_2d,
×
5280
                    batch_range: 0..1,
×
5281
                    extra_index: PhaseItemExtraIndex::NONE,
×
5282
                },
5283
                #[cfg(feature = "3d")]
5284
                PipelineMode::Camera2d,
5285
            );
5286
        }
5287
    }
5288

5289
    // Loop over all 3D cameras/views that need to render effects
5290
    #[cfg(feature = "3d")]
5291
    {
5292
        #[cfg(feature = "trace")]
5293
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
×
5294

5295
        // Effects with full alpha blending
5296
        if !views.is_empty() {
5297
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5298

5299
            let draw_effects_function_3d = read_params
×
5300
                .draw_functions_3d
×
5301
                .read()
5302
                .get_id::<DrawEffects>()
5303
                .unwrap();
5304

5305
            emit_sorted_draw(
5306
                &views,
×
5307
                &mut transparent_3d_render_phases,
×
5308
                &mut view_entities,
×
5309
                &sorted_effect_batches,
×
5310
                &effect_draw_batches,
×
5311
                &mut render_pipeline,
×
5312
                specialized_render_pipelines.reborrow(),
×
5313
                &render_meshes,
×
5314
                &pipeline_cache,
×
5315
                |id, entity, batch, view| Transparent3d {
×
5316
                    draw_function: draw_effects_function_3d,
×
5317
                    pipeline: id,
×
5318
                    entity,
×
5319
                    distance: view
×
5320
                        .rangefinder3d()
×
5321
                        .distance_translation(&batch.translation_3d),
×
5322
                    batch_range: 0..1,
×
5323
                    extra_index: PhaseItemExtraIndex::NONE,
×
5324
                },
5325
                #[cfg(feature = "2d")]
5326
                PipelineMode::Camera3d,
5327
            );
5328
        }
5329

5330
        // Effects with alpha mask
5331
        if !views.is_empty() {
×
5332
            #[cfg(feature = "trace")]
5333
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
5334

5335
            trace!("Emit effect draw calls for alpha masked 3D views...");
×
5336

5337
            let draw_effects_function_alpha_mask = read_params
×
5338
                .draw_functions_alpha_mask
×
5339
                .read()
5340
                .get_id::<DrawEffects>()
5341
                .unwrap();
5342

5343
            emit_binned_draw(
5344
                &views,
×
5345
                &mut alpha_mask_3d_render_phases,
×
5346
                &mut view_entities,
×
5347
                &sorted_effect_batches,
×
5348
                &effect_draw_batches,
×
5349
                &mut render_pipeline,
×
5350
                specialized_render_pipelines.reborrow(),
×
5351
                &pipeline_cache,
×
5352
                &render_meshes,
×
5353
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5354
                    pipeline: id,
×
5355
                    draw_function: draw_effects_function_alpha_mask,
×
5356
                    asset_id: AssetId::<Image>::default().untyped(),
×
5357
                    material_bind_group_id: None,
×
5358
                    // },
5359
                    // distance: view
5360
                    //     .rangefinder3d()
5361
                    //     .distance_translation(&batch.translation_3d),
5362
                    // batch_range: 0..1,
5363
                    // extra_index: PhaseItemExtraIndex::NONE,
5364
                },
5365
                #[cfg(feature = "2d")]
5366
                PipelineMode::Camera3d,
5367
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5368
            );
5369
        }
5370

5371
        // Opaque particles
5372
        if !views.is_empty() {
×
5373
            #[cfg(feature = "trace")]
5374
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
5375

5376
            trace!("Emit effect draw calls for opaque 3D views...");
×
5377

5378
            let draw_effects_function_opaque = read_params
×
5379
                .draw_functions_opaque
×
5380
                .read()
5381
                .get_id::<DrawEffects>()
5382
                .unwrap();
5383

5384
            emit_binned_draw(
5385
                &views,
×
5386
                &mut alpha_mask_3d_render_phases,
×
5387
                &mut view_entities,
×
5388
                &sorted_effect_batches,
×
5389
                &effect_draw_batches,
×
5390
                &mut render_pipeline,
×
5391
                specialized_render_pipelines.reborrow(),
×
5392
                &pipeline_cache,
×
5393
                &render_meshes,
×
5394
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5395
                    pipeline: id,
×
5396
                    draw_function: draw_effects_function_opaque,
×
5397
                    asset_id: AssetId::<Image>::default().untyped(),
×
5398
                    material_bind_group_id: None,
×
5399
                    // },
5400
                    // distance: view
5401
                    //     .rangefinder3d()
5402
                    //     .distance_translation(&batch.translation_3d),
5403
                    // batch_range: 0..1,
5404
                    // extra_index: PhaseItemExtraIndex::NONE,
5405
                },
5406
                #[cfg(feature = "2d")]
5407
                PipelineMode::Camera3d,
5408
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5409
            );
5410
        }
5411
    }
5412
}
5413

5414
/// Prepare GPU resources for effect rendering.
5415
///
5416
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5417
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5418
/// access to the current camera view.
5419
pub(crate) fn prepare_gpu_resources(
×
5420
    mut effects_meta: ResMut<EffectsMeta>,
5421
    //mut effect_cache: ResMut<EffectCache>,
5422
    mut event_cache: ResMut<EventCache>,
5423
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5424
    mut sort_bind_groups: ResMut<SortBindGroups>,
5425
    render_device: Res<RenderDevice>,
5426
    render_queue: Res<RenderQueue>,
5427
    view_uniforms: Res<ViewUniforms>,
5428
    render_pipeline: Res<ParticlesRenderPipeline>,
5429
) {
5430
    // Get the binding for the ViewUniform, the uniform data structure containing
5431
    // the Camera data for the current view. If not available, we cannot render
5432
    // anything.
5433
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
×
5434
        return;
×
5435
    };
5436

5437
    // Create the bind group for the camera/view parameters
5438
    // FIXME - Not here!
5439
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5440
        "hanabi:bind_group_camera_view",
5441
        &render_pipeline.view_layout,
5442
        &[
5443
            BindGroupEntry {
5444
                binding: 0,
5445
                resource: view_binding,
5446
            },
5447
            BindGroupEntry {
5448
                binding: 1,
5449
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5450
            },
5451
        ],
5452
    ));
5453

5454
    // Re-/allocate any GPU buffer if needed
5455
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5456
    // effect_bind_groups);
5457
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5458
    sort_bind_groups.prepare_buffers(&render_device);
5459
}
5460

5461
pub(crate) fn prepare_bind_groups(
×
5462
    mut effects_meta: ResMut<EffectsMeta>,
5463
    mut effect_cache: ResMut<EffectCache>,
5464
    mut event_cache: ResMut<EventCache>,
5465
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5466
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5467
    mut sort_bind_groups: ResMut<SortBindGroups>,
5468
    property_cache: Res<PropertyCache>,
5469
    sorted_effect_batched: Res<SortedEffectBatches>,
5470
    render_device: Res<RenderDevice>,
5471
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5472
    utils_pipeline: Res<UtilsPipeline>,
5473
    update_pipeline: Res<ParticlesUpdatePipeline>,
5474
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5475
    gpu_images: Res<RenderAssets<GpuImage>>,
5476
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperationQueue>,
5477
) {
5478
    // We can't simulate nor render anything without at least the spawner buffer
5479
    if effects_meta.spawner_buffer.is_empty() {
×
5480
        return;
×
5481
    }
5482
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5483
        return;
×
5484
    };
5485

5486
    // Ensure child_infos@3 bind group for the indirect pass is available if needed.
5487
    // This returns `None` if the buffer is not ready, either because it's not
5488
    // created yet or because it's not needed (no child effect).
5489
    event_cache.ensure_indirect_child_info_buffer_bind_group(&render_device);
5490

5491
    {
5492
        #[cfg(feature = "trace")]
5493
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
×
5494

5495
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5496
        // loop below. Also allows earlying out before doing any work in case some
5497
        // buffer is missing.
5498
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5499
            return;
×
5500
        };
5501

5502
        // Create the sim_params@0 bind group for the global simulation parameters,
5503
        // which is shared by the init and update passes.
5504
        if effects_meta.indirect_sim_params_bind_group.is_none() {
×
5505
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
×
5506
                "hanabi:bind_group:vfx_indirect:sim_params@0",
×
5507
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
×
5508
                &[BindGroupEntry {
×
5509
                    binding: 0,
×
5510
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
5511
                }],
5512
            ));
5513
        }
5514

5515
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5516
        // effects at once
5517
        effects_meta.indirect_metadata_bind_group = match (
×
5518
            effects_meta.effect_metadata_buffer.buffer(),
5519
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5520
        ) {
5521
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
×
5522
                // Base bind group for indirect pass
5523
                Some(render_device.create_bind_group(
×
5524
                    "hanabi:bind_group:vfx_indirect:metadata@1",
×
5525
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
×
5526
                    &[
×
5527
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5528
                        BindGroupEntry {
×
5529
                            binding: 0,
×
5530
                            resource: BindingResource::Buffer(BufferBinding {
×
5531
                                buffer: effect_metadata_buffer,
×
5532
                                offset: 0,
×
5533
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
×
5534
                            }),
5535
                        },
5536
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5537
                        BindGroupEntry {
×
5538
                            binding: 1,
×
5539
                            resource: BindingResource::Buffer(BufferBinding {
×
5540
                                buffer: dispatch_indirect_buffer,
×
5541
                                offset: 0,
×
5542
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
5543
                            }),
5544
                        },
5545
                    ],
5546
                ))
5547
            }
5548

5549
            // Some buffer is not yet available, can't create the bind group
5550
            _ => None,
×
5551
        };
5552

5553
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5554
        // effects at once
5555
        if effects_meta.indirect_spawner_bind_group.is_none() {
×
5556
            let bind_group = render_device.create_bind_group(
×
5557
                "hanabi:bind_group:vfx_indirect:spawner@2",
5558
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
×
5559
                &[
×
5560
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5561
                    BindGroupEntry {
×
5562
                        binding: 0,
×
5563
                        resource: BindingResource::Buffer(BufferBinding {
×
5564
                            buffer: &spawner_buffer,
×
5565
                            offset: 0,
×
5566
                            size: None,
×
5567
                        }),
5568
                    },
5569
                ],
5570
            );
5571

5572
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5573
        }
5574
    }
5575

5576
    // Create the per-buffer bind groups
5577
    trace!("Create per-buffer bind groups...");
×
5578
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
×
5579
        #[cfg(feature = "trace")]
5580
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
×
5581

5582
        let Some(effect_buffer) = effect_buffer else {
×
5583
            trace!(
×
5584
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5585
                buffer_index
5586
            );
5587
            continue;
×
5588
        };
5589

5590
        // Ensure all effects in this batch have a bind group for the entire buffer of
5591
        // the group, since the update phase runs on an entire group/buffer at once,
5592
        // with all the effect instances in it batched together.
5593
        trace!("effect particle buffer_index=#{}", buffer_index);
×
5594
        effect_bind_groups
×
5595
            .particle_buffers
×
5596
            .entry(buffer_index as u32)
×
5597
            .or_insert_with(|| {
×
5598
                // Bind group particle@1 for render pass
5599
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
×
5600
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
×
5601
                    render_device.limits().min_storage_buffer_offset_alignment,
×
5602
                );
5603
                let entries = [
×
5604
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5605
                    BindGroupEntry {
×
5606
                        binding: 0,
×
5607
                        resource: effect_buffer.max_binding(),
×
5608
                    },
5609
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5610
                    BindGroupEntry {
×
5611
                        binding: 1,
×
5612
                        resource: effect_buffer.indirect_index_max_binding(),
×
5613
                    },
5614
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5615
                    BindGroupEntry {
×
5616
                        binding: 2,
×
5617
                        resource: BindingResource::Buffer(BufferBinding {
×
5618
                            buffer: &spawner_buffer,
×
5619
                            offset: 0,
×
5620
                            size: Some(spawner_min_binding_size),
×
5621
                        }),
5622
                    },
5623
                ];
5624
                let render = render_device.create_bind_group(
×
5625
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
×
5626
                    effect_buffer.render_particles_buffer_layout(),
×
5627
                    &entries[..],
×
5628
                );
5629

5630
                BufferBindGroups { render }
×
5631
            });
5632
    }
5633

5634
    // Create bind groups for queued GPU buffer operations
5635
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
×
5636

5637
    // Create the per-event-buffer bind groups
5638
    for (event_buffer_index, event_buffer) in event_cache.buffers().iter().enumerate() {
×
5639
        if event_buffer.is_none() {
×
5640
            trace!(
×
5641
                "Event buffer index #{event_buffer_index} has no allocated EventBuffer, skipped.",
×
5642
            );
5643
            continue;
×
5644
        }
5645
        let event_buffer_index = event_buffer_index as u32;
×
5646

5647
        // Check if the entry is missing
5648
        let entry = effect_bind_groups
×
5649
            .init_fill_dispatch
×
5650
            .entry(event_buffer_index);
×
5651
        if matches!(entry, Entry::Vacant(_)) {
×
5652
            trace!(
×
5653
                "Event buffer #{} missing a bind group @0 for init fill args. Trying to create now...",
×
5654
                event_buffer_index
5655
            );
5656

5657
            // Check if the binding is available to create the bind group and fill the entry
5658
            let Some((args_binding, args_count)) =
×
5659
                gpu_buffer_operation_queue.init_args_buffer_binding(event_buffer_index)
×
5660
            else {
5661
                continue;
×
5662
            };
5663

5664
            let Some(source_binding_resource) = event_cache.child_infos().max_binding() else {
×
5665
                warn!("Event buffer #{event_buffer_index} has {args_count} operations pending, but the effect cache has no child_infos binding for the source buffer. Discarding event operations for this frame. This will result in particles not spawning.");
×
5666
                continue;
×
5667
            };
5668

5669
            let Some(target_binding_resource) =
×
5670
                event_cache.init_indirect_dispatch_binding_resource()
5671
            else {
5672
                warn!("Event buffer #{event_buffer_index} has {args_count} operations pending, but the effect cache has no init_indirect_dispatch_binding_resource for the target buffer. Discarding event operations for this frame. This will result in particles not spawning.");
×
5673
                continue;
×
5674
            };
5675

5676
            // Actually create the new bind group entry
5677
            entry.insert(render_device.create_bind_group(
5678
                &format!("hanabi:bind_group:init_fill_dispatch@0:event{event_buffer_index}")[..],
5679
                &utils_pipeline.bind_group_layout,
5680
                &[
5681
                    // @group(0) @binding(0) var<uniform> args : BufferOperationArgs
5682
                    BindGroupEntry {
5683
                        binding: 0,
5684
                        resource: args_binding,
5685
                    },
5686
                    // @group(0) @binding(1) var<storage, read> src_buffer : array<u32>
5687
                    BindGroupEntry {
5688
                        binding: 1,
5689
                        resource: source_binding_resource,
5690
                    },
5691
                    // @group(0) @binding(2) var<storage, read_write> dst_buffer :
5692
                    // array<u32>
5693
                    BindGroupEntry {
5694
                        binding: 2,
5695
                        resource: target_binding_resource,
5696
                    },
5697
                ],
5698
            ));
5699
            trace!(
5700
                "Created new bind group for init fill args of event buffer #{}",
×
5701
                event_buffer_index
5702
            );
5703
        }
5704
    }
5705

5706
    // Create the per-effect bind groups
5707
    let spawner_buffer_binding_size =
×
5708
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
×
5709
    for effect_batch in sorted_effect_batched.iter() {
×
5710
        #[cfg(feature = "trace")]
5711
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
5712

5713
        // Create the property bind group @2 if needed
5714
        if let Some(property_key) = &effect_batch.property_key {
×
5715
            if let Err(err) = property_bind_groups.ensure_exists(
×
5716
                property_key,
5717
                &property_cache,
5718
                &spawner_buffer,
5719
                spawner_buffer_binding_size,
5720
                &render_device,
5721
            ) {
5722
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5723
                continue;
×
5724
            }
5725
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
×
5726
            &property_cache,
×
5727
            &spawner_buffer,
×
5728
            spawner_buffer_binding_size,
×
5729
            &render_device,
×
5730
        ) {
5731
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5732
            continue;
×
5733
        }
5734

5735
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5736
        // simulate particles.
5737
        if effect_cache
×
5738
            .create_particle_sim_bind_group(
5739
                effect_batch.buffer_index,
×
5740
                &render_device,
×
5741
                effect_batch.particle_layout.min_binding_size32(),
×
5742
                effect_batch.parent_min_binding_size,
×
5743
                effect_batch.parent_binding_source.as_ref(),
×
5744
            )
5745
            .is_err()
5746
        {
5747
            error!("No particle buffer allocated for effect batch.");
×
5748
            continue;
×
5749
        }
5750

5751
        // Bind group @3 of init pass
5752
        // FIXME - this is instance-dependent, not buffer-dependent
5753
        {
5754
            let consume_gpu_spawn_events = effect_batch
×
5755
                .layout_flags
×
5756
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
5757
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
×
5758
                effect_batch.spawn_info
5759
            {
5760
                assert!(consume_gpu_spawn_events);
×
5761
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
5762
                Some(ConsumeEventBuffers {
×
5763
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
5764
                    events: BufferSlice {
×
5765
                        buffer: event_cache
×
5766
                            .get_buffer(cached_effect_events.buffer_index)
×
5767
                            .unwrap(),
×
5768
                        // Note: event range is in u32 count, not bytes
5769
                        offset: cached_effect_events.range.start * 4,
×
5770
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5771
                    },
5772
                })
5773
            } else {
5774
                assert!(!consume_gpu_spawn_events);
×
5775
                None
×
5776
            };
5777
            let Some(init_metadata_layout) =
×
5778
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
5779
            else {
5780
                continue;
×
5781
            };
5782
            if effect_bind_groups
5783
                .get_or_create_init_metadata(
5784
                    effect_batch,
5785
                    &effects_meta.gpu_limits,
5786
                    &render_device,
5787
                    init_metadata_layout,
5788
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5789
                    consume_event_buffers,
5790
                )
5791
                .is_err()
5792
            {
5793
                continue;
×
5794
            }
5795
        }
5796

5797
        // Bind group @3 of update pass
5798
        // FIXME - this is instance-dependent, not buffer-dependent#
5799
        {
5800
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
5801

5802
            let Some(update_metadata_layout) =
×
5803
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5804
            else {
5805
                continue;
×
5806
            };
5807
            if effect_bind_groups
5808
                .get_or_create_update_metadata(
5809
                    effect_batch,
5810
                    &effects_meta.gpu_limits,
5811
                    &render_device,
5812
                    update_metadata_layout,
5813
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5814
                    event_cache.child_infos_buffer(),
5815
                    &effect_batch.child_event_buffers[..],
5816
                )
5817
                .is_err()
5818
            {
5819
                continue;
×
5820
            }
5821
        }
5822

5823
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5824
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5825

5826
            // Bind group @0 of sort-fill pass
5827
            let particle_buffer = effect_buffer.particle_buffer();
×
5828
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5829
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
5830
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
5831
                &effect_batch.particle_layout,
×
5832
                particle_buffer,
×
5833
                indirect_index_buffer,
×
5834
                effect_metadata_buffer,
×
5835
            ) {
5836
                error!(
5837
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
5838
                    err
5839
                );
5840
                continue;
×
5841
            }
5842

5843
            // Bind group @0 of sort-copy pass
5844
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5845
            if let Err(err) = sort_bind_groups
×
5846
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5847
            {
5848
                error!(
5849
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5850
                    err
5851
                );
5852
                continue;
×
5853
            }
5854
        }
5855

5856
        // Ensure the particle texture(s) are available as GPU resources and that a bind
5857
        // group for them exists
5858
        // FIXME fix this insert+get below
5859
        if !effect_batch.texture_layout.layout.is_empty() {
×
5860
            // This should always be available, as this is cached into the render pipeline
5861
            // just before we start specializing it.
5862
            let Some(material_bind_group_layout) =
×
5863
                render_pipeline.get_material(&effect_batch.texture_layout)
×
5864
            else {
5865
                error!(
×
5866
                    "Failed to find material bind group layout for buffer #{}",
×
5867
                    effect_batch.buffer_index
5868
                );
5869
                continue;
×
5870
            };
5871

5872
            // TODO = move
5873
            let material = Material {
5874
                layout: effect_batch.texture_layout.clone(),
5875
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5876
            };
5877
            assert_eq!(material.layout.layout.len(), material.textures.len());
5878

5879
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5880
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5881
                trace!(
×
5882
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5883
                    material
5884
                );
5885
                continue;
×
5886
            };
5887

5888
            effect_bind_groups
5889
                .material_bind_groups
5890
                .entry(material.clone())
5891
                .or_insert_with(|| {
×
5892
                    debug!("Creating material bind group for material {:?}", material);
×
5893
                    render_device.create_bind_group(
×
5894
                        &format!(
×
5895
                            "hanabi:material_bind_group_{}",
×
5896
                            material.layout.layout.len()
×
5897
                        )[..],
×
5898
                        material_bind_group_layout,
×
5899
                        &bind_group_entries[..],
×
5900
                    )
5901
                });
5902
        }
5903
    }
5904
}
5905

5906
type DrawEffectsSystemState = SystemState<(
5907
    SRes<EffectsMeta>,
5908
    SRes<EffectBindGroups>,
5909
    SRes<PipelineCache>,
5910
    SRes<RenderAssets<RenderMesh>>,
5911
    SRes<MeshAllocator>,
5912
    SQuery<Read<ViewUniformOffset>>,
5913
    SRes<SortedEffectBatches>,
5914
    SQuery<Read<EffectDrawBatch>>,
5915
)>;
5916

5917
/// Draw function for rendering all active effects for the current frame.
5918
///
5919
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5920
/// and the [`Transparent3d`] phase of the main 3D pass.
5921
pub(crate) struct DrawEffects {
5922
    params: DrawEffectsSystemState,
5923
}
5924

5925
impl DrawEffects {
5926
    pub fn new(world: &mut World) -> Self {
×
5927
        Self {
5928
            params: SystemState::new(world),
×
5929
        }
5930
    }
5931
}
5932

5933
/// Draw all particles of a single effect in view, in 2D or 3D.
5934
///
5935
/// FIXME: use pipeline ID to look up which group index it is.
5936
fn draw<'w>(
×
5937
    world: &'w World,
5938
    pass: &mut TrackedRenderPass<'w>,
5939
    view: Entity,
5940
    entity: (Entity, MainEntity),
5941
    pipeline_id: CachedRenderPipelineId,
5942
    params: &mut DrawEffectsSystemState,
5943
) {
5944
    let (
×
5945
        effects_meta,
×
5946
        effect_bind_groups,
×
5947
        pipeline_cache,
×
5948
        meshes,
×
5949
        mesh_allocator,
×
5950
        views,
×
5951
        sorted_effect_batches,
×
5952
        effect_draw_batches,
×
5953
    ) = params.get(world);
×
5954
    let view_uniform = views.get(view).unwrap();
×
5955
    let effects_meta = effects_meta.into_inner();
×
5956
    let effect_bind_groups = effect_bind_groups.into_inner();
×
5957
    let meshes = meshes.into_inner();
×
5958
    let mesh_allocator = mesh_allocator.into_inner();
×
5959
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
×
5960
    let effect_batch = sorted_effect_batches
×
5961
        .get(effect_draw_batch.effect_batch_index)
×
5962
        .unwrap();
5963

5964
    let gpu_limits = &effects_meta.gpu_limits;
×
5965

5966
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5967
        return;
×
5968
    };
5969

5970
    trace!("render pass");
×
5971

5972
    pass.set_render_pipeline(pipeline);
×
5973

5974
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5975
        return;
×
5976
    };
5977
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5978
        return;
×
5979
    };
5980

5981
    // Vertex buffer containing the particle model to draw. Generally a quad.
5982
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5983
    // "base_vertex" in the indirect struct...
5984
    assert_eq!(effect_batch.mesh_buffer_id, vertex_buffer_slice.buffer.id());
×
5985
    assert_eq!(effect_batch.mesh_slice, vertex_buffer_slice.range);
×
5986
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5987

5988
    // View properties (camera matrix, etc.)
5989
    pass.set_bind_group(
×
5990
        0,
5991
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5992
        &[view_uniform.offset],
×
5993
    );
5994

5995
    // Particles buffer
5996
    let spawner_base = effect_batch.spawner_base;
×
5997
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5998
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5999
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
6000
    pass.set_bind_group(
×
6001
        1,
6002
        effect_bind_groups
×
6003
            .particle_render(effect_batch.buffer_index)
×
6004
            .unwrap(),
×
6005
        &[spawner_offset],
×
6006
    );
6007

6008
    // Particle texture
6009
    // TODO = move
6010
    let material = Material {
6011
        layout: effect_batch.texture_layout.clone(),
×
6012
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6013
    };
6014
    if !effect_batch.texture_layout.layout.is_empty() {
×
6015
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6016
            pass.set_bind_group(2, bind_group, &[]);
×
6017
        } else {
6018
            // Texture(s) not ready; skip this drawing for now
6019
            trace!(
×
6020
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
6021
                effect_batch.buffer_index,
×
6022
            );
6023
            return;
×
6024
        }
6025
    }
6026

6027
    let effect_metadata_index = effect_batch
×
6028
        .dispatch_buffer_indices
×
6029
        .effect_metadata_buffer_table_id
×
6030
        .0;
×
6031
    let effect_metadata_offset =
×
6032
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
6033
    trace!(
×
6034
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
6035
            (effect_metadata_index={}, offset={}B).",
×
6036
        effect_batch.slice.len(),
×
6037
        render_mesh.vertex_count,
×
6038
        effect_batch.buffer_index,
×
6039
        effect_metadata_index,
×
6040
        effect_metadata_offset,
×
6041
    );
6042

6043
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
6044
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
6045
        trace!(
×
6046
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6047
            effect_batch.buffer_index,
×
6048
        );
6049
        return;
×
6050
    };
6051

6052
    match render_mesh.buffer_info {
×
6053
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
6054
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
6055
            else {
×
6056
                return;
×
6057
            };
6058

6059
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6060
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6061
        }
6062
        RenderMeshBufferInfo::NonIndexed => {
×
6063
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6064
        }
6065
    }
6066
}
6067

6068
#[cfg(feature = "2d")]
6069
impl Draw<Transparent2d> for DrawEffects {
6070
    fn draw<'w>(
×
6071
        &mut self,
6072
        world: &'w World,
6073
        pass: &mut TrackedRenderPass<'w>,
6074
        view: Entity,
6075
        item: &Transparent2d,
6076
    ) -> Result<(), DrawError> {
6077
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6078
        draw(
6079
            world,
×
6080
            pass,
×
6081
            view,
×
6082
            item.entity,
×
6083
            item.pipeline,
×
6084
            &mut self.params,
×
6085
        );
6086
        Ok(())
×
6087
    }
6088
}
6089

6090
#[cfg(feature = "3d")]
6091
impl Draw<Transparent3d> for DrawEffects {
6092
    fn draw<'w>(
×
6093
        &mut self,
6094
        world: &'w World,
6095
        pass: &mut TrackedRenderPass<'w>,
6096
        view: Entity,
6097
        item: &Transparent3d,
6098
    ) -> Result<(), DrawError> {
6099
        trace!("Draw<Transparent3d>: view={:?}", view);
×
6100
        draw(
6101
            world,
×
6102
            pass,
×
6103
            view,
×
6104
            item.entity,
×
6105
            item.pipeline,
×
6106
            &mut self.params,
×
6107
        );
6108
        Ok(())
×
6109
    }
6110
}
6111

6112
#[cfg(feature = "3d")]
6113
impl Draw<AlphaMask3d> for DrawEffects {
6114
    fn draw<'w>(
×
6115
        &mut self,
6116
        world: &'w World,
6117
        pass: &mut TrackedRenderPass<'w>,
6118
        view: Entity,
6119
        item: &AlphaMask3d,
6120
    ) -> Result<(), DrawError> {
6121
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6122
        draw(
6123
            world,
×
6124
            pass,
×
6125
            view,
×
6126
            item.representative_entity,
×
6127
            item.key.pipeline,
×
6128
            &mut self.params,
×
6129
        );
6130
        Ok(())
×
6131
    }
6132
}
6133

6134
#[cfg(feature = "3d")]
6135
impl Draw<Opaque3d> for DrawEffects {
6136
    fn draw<'w>(
×
6137
        &mut self,
6138
        world: &'w World,
6139
        pass: &mut TrackedRenderPass<'w>,
6140
        view: Entity,
6141
        item: &Opaque3d,
6142
    ) -> Result<(), DrawError> {
6143
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6144
        draw(
6145
            world,
×
6146
            pass,
×
6147
            view,
×
6148
            item.representative_entity,
×
6149
            item.key.pipeline,
×
6150
            &mut self.params,
×
6151
        );
6152
        Ok(())
×
6153
    }
6154
}
6155

6156
/// Render node to run the simulation sub-graph once per frame.
6157
///
6158
/// This node doesn't simulate anything by itself, but instead schedules the
6159
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6160
/// actual simulation.
6161
///
6162
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6163
/// renders all the views, such that rendered views have access to the
6164
/// just-simulated particles to render them.
6165
///
6166
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6167
pub(crate) struct VfxSimulateDriverNode;
6168

6169
impl Node for VfxSimulateDriverNode {
6170
    fn run(
×
6171
        &self,
6172
        graph: &mut RenderGraphContext,
6173
        _render_context: &mut RenderContext,
6174
        _world: &World,
6175
    ) -> Result<(), NodeRunError> {
6176
        graph.run_sub_graph(
×
6177
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
6178
            vec![],
×
6179
            None,
×
6180
        )?;
6181
        Ok(())
×
6182
    }
6183
}
6184

6185
#[derive(Debug, Clone, PartialEq, Eq)]
6186
enum HanabiPipelineId {
6187
    Invalid,
6188
    Cached(CachedComputePipelineId),
6189
}
6190

6191
pub(crate) struct HanabiComputePass<'a> {
6192
    /// Pipeline cache to fetch cached compute pipelines by ID.
6193
    pipeline_cache: &'a PipelineCache,
6194
    /// WGPU compute pass.
6195
    compute_pass: ComputePass<'a>,
6196
    /// Current pipeline (cached).
6197
    pipeline_id: HanabiPipelineId,
6198
}
6199

6200
impl<'a> Deref for HanabiComputePass<'a> {
6201
    type Target = ComputePass<'a>;
6202

6203
    fn deref(&self) -> &Self::Target {
×
6204
        &self.compute_pass
×
6205
    }
6206
}
6207

6208
impl DerefMut for HanabiComputePass<'_> {
6209
    fn deref_mut(&mut self) -> &mut Self::Target {
×
6210
        &mut self.compute_pass
×
6211
    }
6212
}
6213

6214
impl<'a> HanabiComputePass<'a> {
6215
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
×
6216
        Self {
6217
            pipeline_cache,
6218
            compute_pass,
6219
            pipeline_id: HanabiPipelineId::Invalid,
6220
        }
6221
    }
6222

6223
    pub fn set_cached_compute_pipeline(
×
6224
        &mut self,
6225
        pipeline_id: CachedComputePipelineId,
6226
    ) -> Result<(), NodeRunError> {
6227
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
6228
            return Ok(());
×
6229
        }
6230
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
6231
            if let CachedPipelineState::Err(err) =
×
6232
                self.pipeline_cache.get_compute_pipeline_state(pipeline_id)
×
6233
            {
6234
                error!(
×
6235
                    "Failed to find compute pipeline #{}: {:?}",
×
6236
                    pipeline_id.id(),
×
6237
                    err
×
6238
                );
6239
            }
6240
            // FIXME - Bevy doesn't allow returning custom errors here...
6241
            return Ok(());
×
6242
        };
6243
        self.compute_pass.set_pipeline(pipeline);
×
6244
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6245
        Ok(())
×
6246
    }
6247
}
6248

6249
/// Render node to run the simulation of all effects once per frame.
6250
///
6251
/// Runs inside the simulation sub-graph, looping over all extracted effect
6252
/// batches to simulate them.
6253
pub(crate) struct VfxSimulateNode {}
6254

6255
impl VfxSimulateNode {
6256
    /// Create a new node for simulating the effects of the given world.
6257
    pub fn new(_world: &mut World) -> Self {
×
6258
        Self {}
6259
    }
6260

6261
    /// Begin a new compute pass and return a wrapper with extra
6262
    /// functionalities.
6263
    pub fn begin_compute_pass<'encoder>(
×
6264
        &self,
6265
        label: &str,
6266
        pipeline_cache: &'encoder PipelineCache,
6267
        render_context: &'encoder mut RenderContext,
6268
    ) -> HanabiComputePass<'encoder> {
6269
        let compute_pass =
×
6270
            render_context
×
6271
                .command_encoder()
6272
                .begin_compute_pass(&ComputePassDescriptor {
×
6273
                    label: Some(label),
×
6274
                    timestamp_writes: None,
×
6275
                });
6276
        HanabiComputePass::new(pipeline_cache, compute_pass)
×
6277
    }
6278
}
6279

6280
impl Node for VfxSimulateNode {
6281
    fn input(&self) -> Vec<SlotInfo> {
×
6282
        vec![]
×
6283
    }
6284

6285
    fn update(&mut self, _world: &mut World) {}
×
6286

6287
    fn run(
×
6288
        &self,
6289
        _graph: &mut RenderGraphContext,
6290
        render_context: &mut RenderContext,
6291
        world: &World,
6292
    ) -> Result<(), NodeRunError> {
6293
        trace!("VfxSimulateNode::run()");
×
6294

6295
        let pipeline_cache = world.resource::<PipelineCache>();
×
6296
        let effects_meta = world.resource::<EffectsMeta>();
×
6297
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
6298
        let property_bind_groups = world.resource::<PropertyBindGroups>();
×
6299
        let sort_bind_groups = world.resource::<SortBindGroups>();
×
6300
        let utils_pipeline = world.resource::<UtilsPipeline>();
×
6301
        let effect_cache = world.resource::<EffectCache>();
×
6302
        let event_cache = world.resource::<EventCache>();
×
6303
        let gpu_buffer_operation_queue = world.resource::<GpuBufferOperationQueue>();
×
6304
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
×
6305

6306
        // Make sure to schedule any buffer copy before accessing their content later in
6307
        // the GPU commands below.
6308
        {
6309
            let command_encoder = render_context.command_encoder();
×
6310
            effects_meta
×
6311
                .update_dispatch_indirect_buffer
×
6312
                .write_buffer(command_encoder);
×
6313
            effects_meta
×
6314
                .effect_metadata_buffer
×
6315
                .write_buffer(command_encoder);
×
6316
            sort_bind_groups.write_buffers(command_encoder);
×
6317
        }
6318

6319
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6320
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6321
        // the update pass of their parent effect during the previous frame.
6322
        gpu_buffer_operation_queue.dispatch_init_fill(
×
6323
            render_context,
×
6324
            utils_pipeline.get_pipeline(GpuBufferOperationType::InitFillDispatchArgs),
×
6325
            effect_bind_groups,
×
6326
        );
6327

6328
        // If there's no batch, there's nothing more to do. Avoid continuing because
6329
        // some GPU resources are missing, which is expected when there's no effect but
6330
        // is an error (and will log warnings/errors) otherwise.
6331
        if sorted_effect_batches.is_empty() {
×
6332
            return Ok(());
×
6333
        }
6334

6335
        // Compute init pass
6336
        {
6337
            trace!("init: loop over effect batches...");
×
6338

6339
            let mut compute_pass =
×
6340
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
×
6341

6342
            // Bind group simparams@0 is common to everything, only set once per init pass
6343
            compute_pass.set_bind_group(
×
6344
                0,
6345
                effects_meta
×
6346
                    .indirect_sim_params_bind_group
×
6347
                    .as_ref()
×
6348
                    .unwrap(),
×
6349
                &[],
×
6350
            );
6351

6352
            // Dispatch init compute jobs for all batches
6353
            for effect_batch in sorted_effect_batches.iter() {
×
6354
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6355
                // batch. Note that this hopefully should have been skipped earlier.
6356
                {
6357
                    let use_indirect_dispatch = effect_batch
×
6358
                        .layout_flags
×
6359
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6360
                    match effect_batch.spawn_info {
×
6361
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
×
6362
                            assert!(!use_indirect_dispatch);
×
6363
                            if total_spawn_count == 0 {
×
6364
                                continue;
×
6365
                            }
6366
                        }
6367
                        BatchSpawnInfo::GpuSpawner { .. } => {
6368
                            assert!(use_indirect_dispatch);
×
6369
                        }
6370
                    }
6371
                }
6372

6373
                // Fetch bind group particle@1
6374
                let Some(particle_bind_group) =
×
6375
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6376
                else {
6377
                    error!(
×
6378
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6379
                        effect_batch.buffer_index
6380
                    );
6381
                    continue;
×
6382
                };
6383

6384
                // Fetch bind group metadata@3
6385
                let Some(metadata_bind_group) = effect_bind_groups
×
6386
                    .init_metadata_bind_groups
6387
                    .get(&effect_batch.buffer_index)
6388
                else {
6389
                    error!(
×
6390
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6391
                        effect_batch.buffer_index
6392
                    );
6393
                    continue;
×
6394
                };
6395

6396
                if compute_pass
6397
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6398
                    .is_err()
6399
                {
6400
                    continue;
×
6401
                }
6402

6403
                // Compute dynamic offsets
6404
                let spawner_index = effect_batch.spawner_base;
×
6405
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6406
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6407
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6408
                let property_offset = effect_batch.property_offset;
×
6409

6410
                // Setup init pass
6411
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6412
                let offsets = if let Some(property_offset) = property_offset {
×
6413
                    vec![spawner_offset, property_offset]
6414
                } else {
6415
                    vec![spawner_offset]
×
6416
                };
6417
                compute_pass.set_bind_group(
6418
                    2,
6419
                    property_bind_groups
6420
                        .get(effect_batch.property_key.as_ref())
6421
                        .unwrap(),
6422
                    &offsets[..],
6423
                );
6424
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6425

6426
                // Dispatch init job
6427
                match effect_batch.spawn_info {
6428
                    // Indirect dispatch via GPU spawn events
6429
                    BatchSpawnInfo::GpuSpawner {
6430
                        init_indirect_dispatch_index,
×
6431
                        ..
×
6432
                    } => {
×
6433
                        assert!(effect_batch
×
6434
                            .layout_flags
×
6435
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6436

6437
                        // Note: the indirect offset of a dispatch workgroup only needs
6438
                        // 4-byte alignment
6439
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6440
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6441

6442
                        trace!(
×
6443
                            "record commands for indirect init pipeline of effect {:?} \
×
6444
                                init_indirect_dispatch_index={} \
×
6445
                                indirect_offset={} \
×
6446
                                spawner_base={} \
×
6447
                                spawner_offset={} \
×
6448
                                property_key={:?}...",
×
6449
                            effect_batch.handle,
6450
                            init_indirect_dispatch_index,
6451
                            indirect_offset,
6452
                            spawner_index,
6453
                            spawner_offset,
6454
                            effect_batch.property_key,
6455
                        );
6456

6457
                        compute_pass.dispatch_workgroups_indirect(
×
6458
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6459
                            indirect_offset,
×
6460
                        );
6461
                    }
6462

6463
                    // Direct dispatch via CPU spawn count
6464
                    BatchSpawnInfo::CpuSpawner {
6465
                        total_spawn_count: spawn_count,
×
6466
                    } => {
×
6467
                        assert!(!effect_batch
×
6468
                            .layout_flags
×
6469
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6470

6471
                        const WORKGROUP_SIZE: u32 = 64;
6472
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
×
6473

6474
                        trace!(
×
6475
                            "record commands for init pipeline of effect {:?} \
×
6476
                                (spawn {} particles => {} workgroups) spawner_base={} \
×
6477
                                spawner_offset={} \
×
6478
                                property_key={:?}...",
×
6479
                            effect_batch.handle,
6480
                            spawn_count,
6481
                            workgroup_count,
6482
                            spawner_index,
6483
                            spawner_offset,
6484
                            effect_batch.property_key,
6485
                        );
6486

6487
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
6488
                    }
6489
                }
6490

6491
                trace!("init compute dispatched");
×
6492
            }
6493
        }
6494

6495
        // Compute indirect dispatch pass
6496
        if effects_meta.spawner_buffer.buffer().is_some()
×
6497
            && !effects_meta.spawner_buffer.is_empty()
×
6498
            && effects_meta.indirect_metadata_bind_group.is_some()
×
6499
            && effects_meta.indirect_sim_params_bind_group.is_some()
×
6500
        {
6501
            // Only start a compute pass if there's an effect; makes things clearer in
6502
            // debugger.
6503
            let mut compute_pass =
×
6504
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
×
6505

6506
            // Dispatch indirect dispatch compute job
6507
            trace!("record commands for indirect dispatch pipeline...");
×
6508

6509
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
×
6510
            if has_gpu_spawn_events {
×
6511
                if let Some(indirect_child_info_buffer_bind_group) =
×
6512
                    event_cache.indirect_child_info_buffer_bind_group()
×
6513
                {
6514
                    assert!(has_gpu_spawn_events);
6515
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6516
                } else {
6517
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6518
                    render_context
×
6519
                        .command_encoder()
6520
                        .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6521
                    // FIXME - Bevy doesn't allow returning custom errors here...
6522
                    return Ok(());
×
6523
                }
6524
            }
6525

6526
            compute_pass.set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)?;
×
6527

6528
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6529
            // the size exluding gaps!");
6530
            const WORKGROUP_SIZE: u32 = 64;
6531
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6532
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
×
6533
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
×
6534

6535
            // Setup vfx_indirect pass
6536
            compute_pass.set_bind_group(
×
6537
                0,
6538
                effects_meta
×
6539
                    .indirect_sim_params_bind_group
×
6540
                    .as_ref()
×
6541
                    .unwrap(),
×
6542
                &[],
×
6543
            );
6544
            compute_pass.set_bind_group(
×
6545
                1,
6546
                // FIXME - got some unwrap() panic here, investigate... possibly race
6547
                // condition!
6548
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
×
6549
                &[],
×
6550
            );
6551
            compute_pass.set_bind_group(
×
6552
                2,
6553
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
×
6554
                &[],
×
6555
            );
6556
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
6557
            trace!(
×
6558
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
×
6559
                total_effect_count,
6560
                workgroup_count
6561
            );
6562
        }
6563

6564
        // Compute update pass
6565
        {
6566
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
×
6567
            else {
6568
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6569
                render_context
×
6570
                    .command_encoder()
6571
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6572
                // FIXME - Bevy doesn't allow returning custom errors here...
6573
                return Ok(());
×
6574
            };
6575

6576
            let mut compute_pass =
6577
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6578

6579
            // Bind group simparams@0 is common to everything, only set once per update pass
6580
            compute_pass.set_bind_group(
6581
                0,
6582
                effects_meta
6583
                    .indirect_sim_params_bind_group
6584
                    .as_ref()
6585
                    .unwrap(),
6586
                &[],
6587
            );
6588

6589
            // Dispatch update compute jobs
6590
            for effect_batch in sorted_effect_batches.iter() {
×
6591
                // Fetch bind group particle@1
6592
                let Some(particle_bind_group) =
×
6593
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6594
                else {
6595
                    error!(
×
6596
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6597
                        effect_batch.buffer_index
6598
                    );
6599
                    continue;
×
6600
                };
6601

6602
                // Fetch bind group metadata@3
6603
                let Some(metadata_bind_group) = effect_bind_groups
×
6604
                    .update_metadata_bind_groups
6605
                    .get(&effect_batch.buffer_index)
6606
                else {
6607
                    error!(
×
6608
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6609
                        effect_batch.buffer_index
6610
                    );
6611
                    continue;
×
6612
                };
6613

6614
                // Fetch compute pipeline
6615
                if compute_pass
6616
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6617
                    .is_err()
6618
                {
6619
                    continue;
×
6620
                }
6621

6622
                // Compute dynamic offsets
6623
                let spawner_index = effect_batch.spawner_base;
×
6624
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6625
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6626
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6627
                let property_offset = effect_batch.property_offset;
×
6628

6629
                trace!(
×
6630
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6631
                    effect_batch.handle,
6632
                    spawner_index,
6633
                );
6634

6635
                // Setup update pass
6636
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6637
                let offsets = if let Some(property_offset) = property_offset {
×
6638
                    vec![spawner_offset, property_offset]
6639
                } else {
6640
                    vec![spawner_offset]
×
6641
                };
6642
                compute_pass.set_bind_group(
6643
                    2,
6644
                    property_bind_groups
6645
                        .get(effect_batch.property_key.as_ref())
6646
                        .unwrap(),
6647
                    &offsets[..],
6648
                );
6649
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6650

6651
                // Dispatch update job
6652
                let dispatch_indirect_buffer_table_id = effect_batch
6653
                    .dispatch_buffer_indices
6654
                    .update_dispatch_indirect_buffer_table_id;
6655
                let dispatch_indirect_offset = dispatch_indirect_buffer_table_id.0 * 12;
6656
                trace!(
6657
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
×
6658
                    indirect_buffer,
6659
                    dispatch_indirect_offset,
6660
                );
6661
                compute_pass
×
6662
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
×
6663

6664
                trace!("update compute dispatched");
×
6665
            }
6666
        }
6667

6668
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6669
        // batch of particles which needs sorting, based on the actual number of alive
6670
        // particles in the batch after their update in the compute update pass. Since
6671
        // particles may die during update, this may be different from the number of
6672
        // particles updated.
6673
        gpu_buffer_operation_queue.dispatch_fill(
×
6674
            render_context,
×
6675
            utils_pipeline.get_pipeline(GpuBufferOperationType::FillDispatchArgs),
×
6676
        );
6677

6678
        // Compute sort pass
6679
        {
6680
            let mut compute_pass =
×
6681
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
×
6682

6683
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6684
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
×
6685

6686
            // Loop on batches and find those which need sorting
6687
            for effect_batch in sorted_effect_batches.iter() {
×
6688
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6689
                    continue;
×
6690
                }
6691
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6692
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6693

6694
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6695
                    warn!("Missing sort-fill effect buffer.");
×
6696
                    continue;
×
6697
                };
6698

6699
                // Fill the sort buffer with the key-value pairs to sort
6700
                {
6701
                    compute_pass.push_debug_group("hanabi:sort_fill");
6702

6703
                    // Fetch compute pipeline
6704
                    let Some(pipeline_id) =
×
6705
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6706
                    else {
6707
                        warn!("Missing sort-fill pipeline.");
×
6708
                        continue;
×
6709
                    };
6710
                    compute_pass.set_cached_compute_pipeline(pipeline_id)?;
×
6711

6712
                    // Bind group sort_fill@0
6713
                    let particle_buffer = effect_buffer.particle_buffer();
×
6714
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6715
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6716
                        particle_buffer.id(),
6717
                        indirect_index_buffer.id(),
6718
                        effect_metadata_buffer.id(),
6719
                    ) else {
6720
                        warn!("Missing sort-fill bind group.");
×
6721
                        continue;
×
6722
                    };
6723
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6724
                    let indirect_index_offset =
6725
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6726
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6727
                        effect_batch
6728
                            .dispatch_buffer_indices
6729
                            .effect_metadata_buffer_table_id
6730
                            .0,
6731
                    ) as u32;
6732
                    compute_pass.set_bind_group(
6733
                        0,
6734
                        bind_group,
6735
                        &[
6736
                            particle_offset,
6737
                            indirect_index_offset,
6738
                            effect_metadata_offset,
6739
                        ],
6740
                    );
6741

6742
                    let indirect_dispatch_index = *effect_batch
6743
                        .sort_fill_indirect_dispatch_index
6744
                        .as_ref()
6745
                        .unwrap();
6746
                    let indirect_offset =
6747
                        sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6748
                    compute_pass
6749
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6750
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6751

6752
                    compute_pass.pop_debug_group();
×
6753
                }
6754

6755
                // Do the actual sort
6756
                {
6757
                    compute_pass.push_debug_group("hanabi:sort");
×
6758

6759
                    compute_pass
×
6760
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())?;
×
6761
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6762
                    let indirect_offset =
×
6763
                        sort_bind_groups.get_sort_indirect_dispatch_byte_offset() as u64;
×
6764
                    compute_pass.dispatch_workgroups_indirect(indirect_buffer, indirect_offset);
×
6765
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6766

6767
                    compute_pass.pop_debug_group();
×
6768
                }
6769

6770
                // Copy the sorted particle indices back into the indirect index buffer, where
6771
                // the render pass will read them.
6772
                {
6773
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
×
6774

6775
                    // Fetch compute pipeline
6776
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
×
6777
                    compute_pass.set_cached_compute_pipeline(pipeline_id)?;
×
6778

6779
                    // Bind group sort_copy@0
6780
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6781
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6782
                        indirect_index_buffer.id(),
6783
                        effect_metadata_buffer.id(),
6784
                    ) else {
6785
                        warn!("Missing sort-copy bind group.");
×
6786
                        continue;
×
6787
                    };
6788
                    let indirect_index_offset = effect_batch.slice.start;
6789
                    let effect_metadata_offset =
6790
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6791
                            effect_batch
6792
                                .dispatch_buffer_indices
6793
                                .effect_metadata_buffer_table_id,
6794
                        );
6795
                    compute_pass.set_bind_group(
6796
                        0,
6797
                        bind_group,
6798
                        &[indirect_index_offset, effect_metadata_offset],
6799
                    );
6800

6801
                    // Note: we can reuse the same indirect buffer as for copying key-value pairs,
6802
                    // since we're copying the same number of particles indices than we sorted.
6803
                    let indirect_dispatch_index = *effect_batch
6804
                        .sort_fill_indirect_dispatch_index
6805
                        .as_ref()
6806
                        .unwrap();
6807
                    let indirect_offset =
6808
                        sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6809
                    compute_pass
6810
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6811
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6812

6813
                    compute_pass.pop_debug_group();
×
6814
                }
6815
            }
6816
        }
6817

6818
        Ok(())
×
6819
    }
6820
}
6821

6822
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6823
    fn from(layout_flags: LayoutFlags) -> Self {
×
6824
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
6825
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6826
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
6827
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6828
        } else {
6829
            ParticleRenderAlphaMaskPipelineKey::Blend
×
6830
        }
6831
    }
6832
}
6833

6834
#[cfg(test)]
6835
mod tests {
6836
    use super::*;
6837

6838
    #[test]
6839
    fn layout_flags() {
6840
        let flags = LayoutFlags::default();
6841
        assert_eq!(flags, LayoutFlags::NONE);
6842
    }
6843

6844
    #[cfg(feature = "gpu_tests")]
6845
    #[test]
6846
    fn gpu_limits() {
6847
        use crate::test_utils::MockRenderer;
6848

6849
        let renderer = MockRenderer::new();
6850
        let device = renderer.device();
6851
        let limits = GpuLimits::from_device(&device);
6852

6853
        // assert!(limits.storage_buffer_align().get() >= 1);
6854
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6855
    }
6856

6857
    #[cfg(feature = "gpu_tests")]
6858
    #[test]
6859
    fn gpu_ops_queue() {
6860
        use crate::test_utils::MockRenderer;
6861

6862
        let renderer = MockRenderer::new();
6863
        let device = renderer.device();
6864
        let render_queue = renderer.queue();
6865

6866
        let mut world = World::new();
6867
        world.insert_resource(device.clone());
6868
        let mut queue = GpuBufferOperationQueue::from_world(&mut world);
6869

6870
        // Two consecutive ops can be merged if in order. This includes having
6871
        // contiguous slices both in source and destination.
6872
        queue.begin_frame();
6873
        queue.enqueue_init_fill(
6874
            0,
6875
            0..200,
6876
            GpuBufferOperationArgs {
6877
                src_offset: 0,
6878
                src_stride: 2,
6879
                dst_offset: 0,
6880
                dst_stride: 0,
6881
                count: 1,
6882
            },
6883
        );
6884
        queue.enqueue_init_fill(
6885
            0,
6886
            200..300,
6887
            GpuBufferOperationArgs {
6888
                src_offset: 1,
6889
                src_stride: 2,
6890
                dst_offset: 1,
6891
                dst_stride: 0,
6892
                count: 1,
6893
            },
6894
        );
6895
        queue.end_frame(&device, &render_queue);
6896
        assert_eq!(queue.init_fill_dispatch_args.len(), 1);
6897
        assert_eq!(queue.args_buffer.content().len(), 1);
6898

6899
        // However if out of order, they remain distinct. Here the source offsets are
6900
        // inverted.
6901
        queue.begin_frame();
6902
        queue.enqueue_init_fill(
6903
            0,
6904
            0..200,
6905
            GpuBufferOperationArgs {
6906
                src_offset: 1,
6907
                src_stride: 2,
6908
                dst_offset: 0,
6909
                dst_stride: 0,
6910
                count: 1,
6911
            },
6912
        );
6913
        queue.enqueue_init_fill(
6914
            0,
6915
            200..300,
6916
            GpuBufferOperationArgs {
6917
                src_offset: 0,
6918
                src_stride: 2,
6919
                dst_offset: 1,
6920
                dst_stride: 0,
6921
                count: 1,
6922
            },
6923
        );
6924
        queue.end_frame(&device, &render_queue);
6925
        assert_eq!(queue.init_fill_dispatch_args.len(), 2);
6926
        assert_eq!(queue.args_buffer.content().len(), 2);
6927
    }
6928
}
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