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

djeedai / bevy_hanabi / 14348959599

09 Apr 2025 04:25AM UTC coverage: 39.38% (-0.7%) from 40.116%
14348959599

Pull #444

github

web-flow
Merge 5cbb94515 into 027286d2a
Pull Request #444: Make the number of particles to emit in a GPU event an expression instead of a constant.

0 of 3 new or added lines in 1 file covered. (0.0%)

139 existing lines in 8 files now uncovered.

3022 of 7674 relevant lines covered (39.38%)

17.34 hits per line

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

3.56
/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
    /// Unused. TODO remove.
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

UNCOV
607
        let mut layout = Vec::with_capacity(4);
×
UNCOV
608
        layout.push(self.sim_params_bind_group_layout.clone());
×
UNCOV
609
        layout.push(self.effect_metadata_bind_group_layout.clone());
×
UNCOV
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

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

624
        ComputePipelineDescriptor {
UNCOV
625
            label: Some(label.into()),
×
626
            layout,
UNCOV
627
            shader: if key.has_events {
×
628
                self.indirect_shader_events.clone()
629
            } else {
630
                self.indirect_shader_noevent.clone()
631
            },
632
            shader_defs,
UNCOV
633
            entry_point: "main".into(),
×
UNCOV
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
908
                .sort_unstable_by(|ifda1, ifda2| {
2✔
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) {
2✔
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
                }
UNCOV
955
                trace!("Final ops (sorted IFDAs): {:?}", sorted_ifda);
×
956
                self.init_fill_dispatch_args = sorted_ifda;
957
            }
958

959
            // Just copy this, we want to preserve order
960
            {
961
                for qop in &self.operation_queue {
2✔
UNCOV
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...
UNCOV
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
            }) {
UNCOV
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
        }
UNCOV
1532
        if key
×
UNCOV
1533
            .flags
×
UNCOV
1534
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
×
1535
        {
1536
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1537
        }
UNCOV
1538
        let consume_gpu_spawn_events = key
×
UNCOV
1539
            .flags
×
UNCOV
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
UNCOV
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
UNCOV
1693
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
×
UNCOV
1694
        assert_eq!(
×
UNCOV
1695
            particle_bind_group_layout.id(),
×
UNCOV
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
        }
1996

1997
        // Key: LOCAL_SPACE_SIMULATION
1998
        if key.local_space_simulation {
×
1999
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2000
        }
2001

UNCOV
2002
        match key.alpha_mask {
×
2003
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
×
2004
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2005
                // Key: USE_ALPHA_MASK
2006
                shader_defs.push("USE_ALPHA_MASK".into())
×
2007
            }
2008
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2009
                // Key: OPAQUE
2010
                shader_defs.push("OPAQUE".into())
×
2011
            }
2012
        }
2013

2014
        // Key: FLIPBOOK
2015
        if key.flipbook {
×
2016
            shader_defs.push("FLIPBOOK".into());
×
2017
        }
2018

2019
        // Key: NEEDS_UV
2020
        if key.needs_uv {
×
2021
            shader_defs.push("NEEDS_UV".into());
×
2022
        }
2023

2024
        // Key: NEEDS_NORMAL
2025
        if key.needs_normal {
×
2026
            shader_defs.push("NEEDS_NORMAL".into());
×
2027
        }
2028

2029
        // Key: RIBBONS
2030
        if key.ribbons {
×
2031
            shader_defs.push("RIBBONS".into());
×
2032
        }
2033

2034
        #[cfg(feature = "2d")]
2035
        let depth_stencil_2d = DepthStencilState {
2036
            format: CORE_2D_DEPTH_FORMAT,
2037
            // Use depth buffer with alpha-masked particles, not with transparent ones
2038
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2039
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2040
            depth_compare: CompareFunction::GreaterEqual,
UNCOV
2041
            stencil: StencilState::default(),
×
UNCOV
2042
            bias: DepthBiasState::default(),
×
2043
        };
2044

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

2061
        #[cfg(all(feature = "2d", feature = "3d"))]
UNCOV
2062
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
×
2063
        #[cfg(all(feature = "2d", feature = "3d"))]
2064
        let depth_stencil = match key.pipeline_mode {
×
2065
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2066
            PipelineMode::Camera3d => Some(depth_stencil_3d),
×
2067
        };
2068

2069
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2070
        let depth_stencil = Some(depth_stencil_2d);
2071

2072
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2073
        let depth_stencil = Some(depth_stencil_3d);
2074

2075
        let format = if key.hdr {
×
2076
            ViewTarget::TEXTURE_FORMAT_HDR
×
2077
        } else {
2078
            TextureFormat::bevy_default()
×
2079
        };
2080

2081
        let hash = calc_func_id(&key);
×
2082
        let label = format!("hanabi:pipeline:render_{hash:016X}");
×
2083
        trace!(
×
2084
            "-> creating pipeline '{}' with shader defs:{}",
×
2085
            label,
×
2086
            shader_defs
×
2087
                .iter()
×
2088
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
×
2089
        );
2090

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

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

2183
pub struct AddedEffectParent {
2184
    pub entity: MainEntity,
2185
    pub layout: ParticleLayout,
2186
    /// GPU spawn event count to allocate for this effect.
2187
    pub event_count: u32,
2188
}
2189

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

2219
/// Collection of all extracted effects for this frame, inserted into the
2220
/// render world as a render resource.
2221
#[derive(Default, Resource)]
2222
pub(crate) struct ExtractedEffects {
2223
    /// Extracted effects this frame.
2224
    pub effects: Vec<ExtractedEffect>,
2225
    /// Newly added effects without a GPU allocation yet.
2226
    pub added_effects: Vec<AddedEffect>,
2227
}
2228

2229
#[derive(Default, Resource)]
2230
pub(crate) struct EffectAssetEvents {
2231
    pub images: Vec<AssetEvent<Image>>,
2232
}
2233

2234
/// System extracting all the asset events for the [`Image`] assets to enable
2235
/// dynamic update of images bound to any effect.
2236
///
2237
/// This system runs in parallel of [`extract_effects`].
2238
pub(crate) fn extract_effect_events(
×
2239
    mut events: ResMut<EffectAssetEvents>,
2240
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2241
) {
2242
    #[cfg(feature = "trace")]
2243
    let _span = bevy::utils::tracing::info_span!("extract_effect_events").entered();
×
2244
    trace!("extract_effect_events()");
×
2245

2246
    let EffectAssetEvents { ref mut images } = *events;
×
2247
    *images = image_events.read().copied().collect();
×
2248
}
2249

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

2284
    /// Enable automatically starting a GPU debugger capture when one or more
2285
    /// effects are spawned.
2286
    ///
2287
    /// Enable this feature to automatically capture one or more GPU frames when
2288
    /// a new effect is spawned (as detected by ECS change detection). This
2289
    /// instructs any attached GPU debugger to start a capture; this has no
2290
    /// effect if no debugger is attached.
2291
    pub start_capture_on_new_effect: bool,
2292

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

2307
#[derive(Debug, Default, Clone, Copy, Resource)]
2308
pub(crate) struct RenderDebugSettings {
2309
    /// Is a GPU debugger capture on-going?
2310
    is_capturing: bool,
2311
    /// Start time of any on-going GPU debugger capture.
2312
    capture_start: Duration,
2313
    /// Number of frames captured so far for on-going GPU debugger capture.
2314
    captured_frames: u32,
2315
}
2316

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

2367
    // Manage GPU debug capture
2368
    if render_debug_settings.is_capturing {
×
2369
        render_debug_settings.captured_frames += 1;
×
2370

2371
        // Stop any pending capture if needed
2372
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2373
            render_device.wgpu_device().stop_capture();
×
2374
            render_debug_settings.is_capturing = false;
×
2375
            warn!(
×
2376
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2377
                render_debug_settings.captured_frames,
×
2378
                real_time.elapsed().as_secs_f64()
×
2379
            );
2380
        }
2381
    }
2382
    if !render_debug_settings.is_capturing {
×
2383
        // If no pending capture, consider starting a new one
2384
        if debug_settings.start_capture_this_frame
×
2385
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty())
×
2386
        {
2387
            render_device.wgpu_device().start_capture();
×
2388
            render_debug_settings.is_capturing = true;
×
2389
            render_debug_settings.capture_start = real_time.elapsed();
×
2390
            render_debug_settings.captured_frames = 0;
×
2391
            warn!(
×
2392
                "Started GPU debug capture at t={}s.",
×
2393
                render_debug_settings.capture_start.as_secs_f64()
×
2394
            );
2395
        }
2396
    }
2397

2398
    // Save simulation params into render world
2399
    sim_params.time = time.elapsed_secs_f64();
×
2400
    sim_params.delta_time = time.delta_secs();
×
2401
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
×
2402
    sim_params.virtual_delta_time = virtual_time.delta_secs();
×
2403
    sim_params.real_time = real_time.elapsed_secs_f64();
×
2404
    sim_params.real_delta_time = real_time.delta_secs();
×
2405

2406
    // Collect added effects for later GPU data allocation
2407
    extracted_effects.added_effects = q_added_effects
×
2408
        .iter()
×
2409
        .filter_map(|(entity, render_entity, compiled_effect)| {
×
2410
            let handle = compiled_effect.asset.clone_weak();
×
2411
            let asset = effects.get(&compiled_effect.asset)?;
×
2412
            let particle_layout = asset.particle_layout();
2413
            assert!(
2414
                particle_layout.size() > 0,
2415
                "Invalid empty particle layout for effect '{}' on entity {:?} (render entity {:?}). Did you forget to add some modifier to the asset?",
×
2416
                asset.name,
×
2417
                entity,
×
2418
                render_entity.id(),
×
2419
            );
2420
            let property_layout = asset.property_layout();
×
2421
            let mesh = compiled_effect
×
2422
                .mesh
×
2423
                .clone()
×
2424
                .unwrap_or(default_mesh.0.clone());
×
2425

2426
            trace!(
×
2427
                "Found new effect: entity {:?} | render entity {:?} | capacity {:?} | particle_layout {:?} | \
×
2428
                 property_layout {:?} | layout_flags {:?} | mesh {:?}",
×
2429
                 entity,
×
2430
                 render_entity.id(),
×
2431
                 asset.capacity(),
×
2432
                 particle_layout,
2433
                 property_layout,
2434
                 compiled_effect.layout_flags,
2435
                 mesh);
2436

2437
            // 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
2438
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2439
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
×
2440
                entity: entity.into(),
×
2441
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2442
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2443
            });
2444

2445
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
×
2446
            Some(AddedEffect {
×
2447
                entity: MainEntity::from(entity),
×
2448
                render_entity: *render_entity,
×
2449
                capacity: asset.capacity(),
×
2450
                mesh,
×
2451
                parent,
×
2452
                particle_layout,
×
2453
                property_layout,
×
2454
                layout_flags: compiled_effect.layout_flags,
×
2455
                handle,
×
2456
            })
2457
        })
UNCOV
2458
        .collect();
×
2459

2460
    // Loop over all existing effects to extract them
UNCOV
2461
    extracted_effects.effects.clear();
×
2462
    for (
2463
        main_entity,
×
2464
        render_entity,
×
2465
        maybe_inherited_visibility,
×
2466
        maybe_view_visibility,
×
2467
        effect_spawner,
×
2468
        compiled_effect,
×
2469
        maybe_properties,
×
2470
        transform,
×
UNCOV
2471
    ) in q_effects.iter()
×
2472
    {
2473
        // Check if shaders are configured
2474
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
×
2475
            continue;
×
2476
        };
2477

2478
        // Check if hidden, unless always simulated
2479
        if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
2480
            && !maybe_inherited_visibility
×
2481
                .map(|cv| cv.get())
×
2482
                .unwrap_or(true)
×
2483
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
2484
        {
2485
            continue;
×
2486
        }
2487

2488
        // Check if asset is available, otherwise silently ignore
2489
        let Some(asset) = effects.get(&compiled_effect.asset) else {
×
2490
            trace!(
×
2491
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
2492
                main_entity
2493
            );
2494
            continue;
×
2495
        };
2496

2497
        // Resolve the render entity of the parent, if any
2498
        let _parent = if let Some(main_entity) = compiled_effect.parent {
×
2499
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2500
                error!(
×
2501
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2502
                    main_entity
2503
                );
2504
                continue;
×
2505
            };
2506
            Some(*render_entity)
2507
        } else {
2508
            None
×
2509
        };
2510

2511
        let property_layout = asset.property_layout();
2512
        let property_data = if let Some(properties) = maybe_properties {
×
2513
            // Note: must check that property layout is not empty, because the
2514
            // EffectProperties component is marked as changed when added but contains an
2515
            // empty Vec if there's no property, which would later raise an error if we
2516
            // don't return None here.
2517
            if properties.is_changed() && !property_layout.is_empty() {
×
2518
                trace!("Detected property change, re-serializing...");
×
2519
                Some(properties.serialize(&property_layout))
2520
            } else {
2521
                None
×
2522
            }
2523
        } else {
2524
            None
×
2525
        };
2526

2527
        let texture_layout = asset.module().texture_layout();
2528
        let layout_flags = compiled_effect.layout_flags;
2529
        // let mesh = compiled_effect
2530
        //     .mesh
2531
        //     .clone()
2532
        //     .unwrap_or(default_mesh.0.clone());
2533
        let alpha_mode = compiled_effect.alpha_mode;
2534

2535
        trace!(
2536
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
×
2537
            asset.name,
×
2538
            main_entity,
×
2539
            render_entity.id(),
×
2540
            texture_layout.layout.len(),
×
2541
            compiled_effect.textures.len(),
×
2542
            layout_flags,
2543
        );
2544

2545
        extracted_effects.effects.push(ExtractedEffect {
2546
            render_entity: *render_entity,
2547
            main_entity: main_entity.into(),
2548
            handle: compiled_effect.asset.clone_weak(),
2549
            particle_layout: asset.particle_layout().clone(),
2550
            property_layout,
2551
            property_data,
2552
            spawn_count: effect_spawner.spawn_count,
2553
            prng_seed: compiled_effect.prng_seed,
2554
            transform: *transform,
2555
            layout_flags,
2556
            texture_layout,
2557
            textures: compiled_effect.textures.clone(),
2558
            alpha_mode,
2559
            effect_shaders: effect_shaders.clone(),
2560
        });
2561
    }
2562
}
2563

2564
/// Various GPU limits and aligned sizes computed once and cached.
2565
struct GpuLimits {
2566
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2567
    ///
2568
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2569
    storage_buffer_align: NonZeroU32,
2570

2571
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2572
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2573
    ///
2574
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2575
    effect_metadata_aligned_size: NonZeroU32,
2576
}
2577

2578
impl GpuLimits {
2579
    pub fn from_device(render_device: &RenderDevice) -> Self {
1✔
2580
        let storage_buffer_align =
1✔
2581
            render_device.limits().min_storage_buffer_offset_alignment as u64;
1✔
2582

2583
        let effect_metadata_aligned_size = NonZeroU32::new(
2584
            GpuEffectMetadata::min_size()
1✔
2585
                .get()
1✔
2586
                .next_multiple_of(storage_buffer_align) as u32,
1✔
2587
        )
2588
        .unwrap();
2589

2590
        trace!(
1✔
2591
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
×
2592
            storage_buffer_align,
×
2593
            GpuEffectMetadata::min_size().get(),
×
2594
            effect_metadata_aligned_size.get(),
×
2595
        );
2596

2597
        Self {
2598
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
1✔
2599
            effect_metadata_aligned_size,
2600
        }
2601
    }
2602

2603
    /// Byte alignment for any storage buffer binding.
2604
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
×
2605
        self.storage_buffer_align
×
2606
    }
2607

2608
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2609
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2610
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2611
    }
2612

2613
    /// Byte alignment for [`GpuEffectMetadata`].
2614
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
×
2615
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
×
2616
    }
2617
}
2618

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

2665
impl EffectsMeta {
2666
    pub fn new(
×
2667
        device: RenderDevice,
2668
        indirect_shader_noevent: Handle<Shader>,
2669
        indirect_shader_events: Handle<Shader>,
2670
    ) -> Self {
2671
        let gpu_limits = GpuLimits::from_device(&device);
×
2672

2673
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2674
        // be addressed individually by the computer shaders.
2675
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
×
2676
        trace!(
×
2677
            "Aligning storage buffers to {} bytes as device limits requires.",
×
2678
            item_align
2679
        );
2680

2681
        Self {
2682
            view_bind_group: None,
2683
            indirect_sim_params_bind_group: None,
2684
            indirect_metadata_bind_group: None,
2685
            indirect_spawner_bind_group: None,
2686
            sim_params_uniforms: UniformBuffer::default(),
×
2687
            spawner_buffer: AlignedBufferVec::new(
×
2688
                BufferUsages::STORAGE,
2689
                NonZeroU64::new(item_align),
2690
                Some("hanabi:buffer:spawner".to_string()),
2691
            ),
2692
            update_dispatch_indirect_buffer: BufferTable::new(
×
2693
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2694
                // Indirect dispatch args don't need to be aligned
2695
                None,
2696
                Some("hanabi:buffer:update_dispatch_indirect".to_string()),
2697
            ),
2698
            effect_metadata_buffer: BufferTable::new(
×
2699
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2700
                NonZeroU64::new(item_align),
2701
                Some("hanabi:buffer:effect_metadata".to_string()),
2702
            ),
2703
            gpu_limits,
2704
            indirect_shader_noevent,
2705
            indirect_shader_events,
2706
            indirect_pipeline_ids: [
×
2707
                CachedComputePipelineId::INVALID,
2708
                CachedComputePipelineId::INVALID,
2709
            ],
2710
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2711
        }
2712
    }
2713

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

2741
        trace!("Adding {} newly spawned effects", added_effects.len());
×
2742
        for added_effect in added_effects.drain(..) {
×
2743
            trace!("+ added effect: capacity={}", added_effect.capacity);
×
2744

2745
            // Allocate an indirect dispatch arguments struct for this instance
2746
            let update_dispatch_indirect_buffer_table_id = self
2747
                .update_dispatch_indirect_buffer
2748
                .insert(GpuDispatchIndirect::default());
2749

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

2762
                let Some(render_mesh) = render_meshes.get(added_effect.mesh.id()) else {
×
2763
                    warn!(
×
2764
                        "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
2765
                        added_effect.entity, added_effect.mesh
2766
                    );
2767
                    continue;
×
2768
                };
2769
                let Some(mesh_vertex_buffer_slice) =
×
2770
                    mesh_allocator.mesh_vertex_slice(&added_effect.mesh.id())
2771
                else {
2772
                    trace!(
×
2773
                        "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
2774
                        added_effect.entity,
2775
                        added_effect.mesh
2776
                    );
2777
                    continue;
×
2778
                };
2779
                let mesh_index_buffer_slice =
2780
                    mesh_allocator.mesh_index_slice(&added_effect.mesh.id());
2781
                let indexed = if let RenderMeshBufferInfo::Indexed { index_format, .. } =
×
2782
                    render_mesh.buffer_info
2783
                {
2784
                    if let Some(ref slice) = mesh_index_buffer_slice {
×
2785
                        Some(MeshIndexSlice {
2786
                            format: index_format,
2787
                            buffer: slice.buffer.clone(),
2788
                            range: slice.range.clone(),
2789
                        })
2790
                    } else {
2791
                        trace!(
×
2792
                            "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
2793
                            added_effect.entity,
2794
                            added_effect.mesh
2795
                        );
2796
                        continue;
×
2797
                    }
2798
                } else {
2799
                    None
×
2800
                };
2801

2802
                (
2803
                    match &mesh_index_buffer_slice {
2804
                        // Indexed mesh rendering
2805
                        Some(mesh_index_buffer_slice) => {
×
2806
                            let ret = GpuEffectMetadata {
×
2807
                                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
×
2808
                                instance_count: 0,
×
2809
                                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
×
2810
                                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start
×
2811
                                    as i32,
×
2812
                                base_instance: 0,
×
2813
                                alive_count: 0,
×
2814
                                max_update: 0,
×
2815
                                dead_count: added_effect.capacity,
×
2816
                                max_spawn: added_effect.capacity,
×
2817
                                ..default()
×
2818
                            };
2819
                            trace!("+ Effect[indexed]: {:?}", ret);
×
2820
                            ret
×
2821
                        }
2822
                        // Non-indexed mesh rendering
2823
                        None => {
2824
                            let ret = GpuEffectMetadata {
×
2825
                                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
2826
                                instance_count: 0,
×
2827
                                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
2828
                                vertex_offset_or_base_instance: 0,
×
2829
                                base_instance: 0,
×
2830
                                alive_count: 0,
×
2831
                                max_update: 0,
×
2832
                                dead_count: added_effect.capacity,
×
2833
                                max_spawn: added_effect.capacity,
×
2834
                                ..default()
×
2835
                            };
2836
                            trace!("+ Effect[non-indexed]: {:?}", ret);
×
2837
                            ret
2838
                        }
2839
                    },
2840
                    CachedMesh {
2841
                        mesh: added_effect.mesh.id(),
2842
                        buffer: mesh_vertex_buffer_slice.buffer.clone(),
2843
                        range: mesh_vertex_buffer_slice.range.clone(),
2844
                        indexed,
2845
                    },
2846
                )
2847
            };
2848
            let effect_metadata_buffer_table_id =
2849
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2850
            let dispatch_buffer_indices = DispatchBufferIndices {
2851
                update_dispatch_indirect_buffer_table_id,
2852
                effect_metadata_buffer_table_id,
2853
            };
2854

2855
            // Insert the effect into the cache. This will allocate all the necessary
2856
            // mandatory GPU resources as needed.
2857
            let cached_effect = effect_cache.insert(
2858
                added_effect.handle,
2859
                added_effect.capacity,
2860
                &added_effect.particle_layout,
2861
                added_effect.layout_flags,
2862
            );
2863
            let mut cmd = commands.entity(added_effect.render_entity.id());
2864
            cmd.insert((
2865
                added_effect.entity,
2866
                cached_effect,
2867
                dispatch_buffer_indices,
2868
                cached_mesh,
2869
            ));
2870

2871
            // Allocate storage for properties if needed
2872
            if !added_effect.property_layout.is_empty() {
×
2873
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
×
2874
                cmd.insert(cached_effect_properties);
×
2875
            } else {
2876
                cmd.remove::<CachedEffectProperties>();
×
2877
            }
2878

2879
            // Allocate storage for the reference to the parent effect if needed. Note that
2880
            // we cannot yet allocate the complete parent info (CachedChildInfo) because it
2881
            // depends on the list of children, which we can't resolve until all
2882
            // effects have been added/removed this frame. This will be done later in
2883
            // resolve_parents().
2884
            if let Some(parent) = added_effect.parent.as_ref() {
×
2885
                let cached_parent: CachedParentRef = CachedParentRef {
2886
                    entity: parent.entity,
2887
                };
2888
                cmd.insert(cached_parent);
2889
                trace!("+ new effect declares parent entity {:?}", parent.entity);
×
2890
            } else {
2891
                cmd.remove::<CachedParentRef>();
×
2892
                trace!("+ new effect declares no parent");
×
2893
            }
2894

2895
            // Allocate storage for GPU spawn events if needed
2896
            if let Some(parent) = added_effect.parent.as_ref() {
×
2897
                let cached_events = event_cache.allocate(parent.event_count);
2898
                cmd.insert(cached_events);
2899
            } else {
2900
                cmd.remove::<CachedEffectEvents>();
×
2901
            }
2902

2903
            // Ensure the particle@1 bind group layout exists for the given configuration of
2904
            // particle layout and (optionally) parent particle layout.
2905
            {
2906
                let parent_min_binding_size = added_effect
2907
                    .parent
2908
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2909
                effect_cache.ensure_particle_bind_group_layout(
2910
                    added_effect.particle_layout.min_binding_size32(),
2911
                    parent_min_binding_size,
2912
                );
2913
            }
2914

2915
            // Ensure the metadata@3 bind group layout exists for init pass.
2916
            {
2917
                let consume_gpu_spawn_events = added_effect
2918
                    .layout_flags
2919
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2920
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2921
            }
2922

2923
            // We cannot yet determine the layout of the metadata@3 bind group for the
2924
            // update pass, because it depends on the number of children, and
2925
            // this is encoded indirectly via the number of child effects
2926
            // pointing to this parent, and only calculated later in
2927
            // resolve_parents().
2928

2929
            trace!(
2930
                "+ added effect entity {:?}: main_entity={:?} \
×
2931
                first_update_group_dispatch_buffer_index={} \
×
2932
                render_effect_dispatch_buffer_id={}",
×
2933
                added_effect.render_entity,
2934
                added_effect.entity,
2935
                update_dispatch_indirect_buffer_table_id.0,
2936
                effect_metadata_buffer_table_id.0
2937
            );
2938
        }
2939

2940
        // Once all changes are applied, immediately schedule any GPU buffer
2941
        // (re)allocation based on the new buffer size. The actual GPU buffer content
2942
        // will be written later.
2943
        if self
×
2944
            .update_dispatch_indirect_buffer
×
2945
            .allocate_gpu(render_device, render_queue)
×
2946
        {
2947
            // All those bind groups use the buffer so need to be re-created
2948
            trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
×
2949
            effect_bind_groups.particle_buffers.clear();
×
2950
        }
2951
    }
2952

2953
    pub fn allocate_spawner(
×
2954
        &mut self,
2955
        global_transform: &GlobalTransform,
2956
        spawn_count: u32,
2957
        prng_seed: u32,
2958
        effect_metadata_buffer_table_id: BufferTableId,
2959
    ) -> u32 {
2960
        let spawner_base = self.spawner_buffer.len() as u32;
×
2961
        let transform = global_transform.compute_matrix().into();
×
2962
        let inverse_transform = Mat4::from(
2963
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2964
            // efficient than inversing the Mat4.
2965
            global_transform.affine().inverse(),
×
2966
        )
2967
        .into();
2968
        let spawner_params = GpuSpawnerParams {
2969
            transform,
2970
            inverse_transform,
2971
            spawn: spawn_count as i32,
×
2972
            seed: prng_seed,
2973
            effect_metadata_index: effect_metadata_buffer_table_id.0,
×
2974
            ..default()
2975
        };
2976
        trace!("spawner params = {:?}", spawner_params);
×
2977
        self.spawner_buffer.push(spawner_params);
×
2978
        spawner_base
×
2979
    }
2980
}
2981

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

3017
impl Default for LayoutFlags {
3018
    fn default() -> Self {
1✔
3019
        Self::NONE
1✔
3020
    }
3021
}
3022

3023
/// Observer raised when the [`CachedEffect`] component is removed, which
3024
/// indicates that the effect instance was despawned.
3025
pub(crate) fn on_remove_cached_effect(
×
3026
    trigger: Trigger<OnRemove, CachedEffect>,
3027
    query: Query<(
3028
        Entity,
3029
        MainEntity,
3030
        &CachedEffect,
3031
        &DispatchBufferIndices,
3032
        Option<&CachedEffectProperties>,
3033
        Option<&CachedParentInfo>,
3034
        Option<&CachedEffectEvents>,
3035
    )>,
3036
    mut effect_cache: ResMut<EffectCache>,
3037
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3038
    mut effects_meta: ResMut<EffectsMeta>,
3039
    mut event_cache: ResMut<EventCache>,
3040
) {
3041
    #[cfg(feature = "trace")]
3042
    let _span = bevy::utils::tracing::info_span!("on_remove_cached_effect").entered();
×
3043

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

3047
    // Fecth the components of the effect being destroyed. Note that the despawn
3048
    // command above is not yet applied, so this query should always succeed.
3049
    let Ok((
3050
        render_entity,
×
3051
        main_entity,
×
3052
        cached_effect,
×
3053
        dispatch_buffer_indices,
×
3054
        _opt_props,
×
3055
        _opt_parent,
×
3056
        opt_cached_effect_events,
×
UNCOV
3057
    )) = query.get(trigger.entity())
×
3058
    else {
3059
        return;
×
3060
    };
3061

3062
    // Dealllocate the effect slice in the event buffer, if any.
3063
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
3064
        match event_cache.free(cached_effect_events) {
3065
            Err(err) => {
×
3066
                error!("Error while freeing effect event slice: {err:?}");
×
3067
            }
3068
            Ok(buffer_state) => {
×
3069
                if buffer_state != BufferState::Used {
×
3070
                    // Clear bind groups associated with the old buffer
3071
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
3072
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3073
                }
3074
            }
3075
        }
3076
    }
3077

3078
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3079
    // last slice, also deallocate the GPU buffer itself.
3080
    trace!(
3081
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
×
3082
        render_entity,
3083
        main_entity,
3084
    );
3085
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
3086
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3087
        // do.
3088
        return;
×
3089
    };
3090

3091
    // Clear bind groups associated with the removed buffer
3092
    trace!(
×
3093
        "=> GPU buffer #{} gone, destroying its bind groups...",
×
3094
        cached_effect.buffer_index
3095
    );
3096
    effect_bind_groups
3097
        .particle_buffers
3098
        .remove(&cached_effect.buffer_index);
3099
    effects_meta
3100
        .update_dispatch_indirect_buffer
3101
        .remove(dispatch_buffer_indices.update_dispatch_indirect_buffer_table_id);
3102
    effects_meta
3103
        .effect_metadata_buffer
3104
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
3105
}
3106

3107
/// Update the [`CachedEffect`] component for any newly allocated effect.
3108
///
3109
/// After this system ran, and its commands are applied, all valid extracted
3110
/// effects have a corresponding entity in the render world, with a
3111
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3112
pub(crate) fn add_effects(
×
3113
    render_device: Res<RenderDevice>,
3114
    render_queue: Res<RenderQueue>,
3115
    mesh_allocator: Res<MeshAllocator>,
3116
    render_meshes: Res<RenderAssets<RenderMesh>>,
3117
    commands: Commands,
3118
    mut effects_meta: ResMut<EffectsMeta>,
3119
    mut effect_cache: ResMut<EffectCache>,
3120
    mut property_cache: ResMut<PropertyCache>,
3121
    mut event_cache: ResMut<EventCache>,
3122
    mut extracted_effects: ResMut<ExtractedEffects>,
3123
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3124
    mut sort_bind_groups: ResMut<SortBindGroups>,
3125
) {
3126
    #[cfg(feature = "trace")]
3127
    let _span = bevy::utils::tracing::info_span!("add_effects").entered();
×
3128
    trace!("add_effects");
×
3129

3130
    // Clear last frame's buffer resizes which may have occured during last frame,
3131
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3132
    // the first point at which we can do that where we're not blocking the main
3133
    // world (so, excluding the extract system).
3134
    effects_meta
×
3135
        .update_dispatch_indirect_buffer
×
3136
        .clear_previous_frame_resizes();
3137
    effects_meta
×
3138
        .effect_metadata_buffer
×
3139
        .clear_previous_frame_resizes();
3140
    sort_bind_groups.clear_previous_frame_resizes();
×
3141

3142
    // Allocate new effects
3143
    effects_meta.add_effects(
×
3144
        commands,
×
3145
        std::mem::take(&mut extracted_effects.added_effects),
×
3146
        &render_device,
×
3147
        &render_queue,
×
3148
        &mesh_allocator,
×
3149
        &render_meshes,
×
3150
        &mut effect_bind_groups,
×
3151
        &mut effect_cache,
×
3152
        &mut property_cache,
×
3153
        &mut event_cache,
×
3154
    );
3155

3156
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3157
    // because EffectBuffer already contains a reference to the
3158
    // RenderDevice, so has done so internally. This is not ideal
3159
    // design-wise, but works.
3160
}
3161

3162
/// Check if two lists of entities are equal.
3163
fn is_child_list_changed(
×
3164
    parent_entity: Entity,
3165
    old: impl ExactSizeIterator<Item = Entity>,
3166
    new: impl ExactSizeIterator<Item = Entity>,
3167
) -> bool {
3168
    if old.len() != new.len() {
×
3169
        trace!(
×
3170
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3171
            parent_entity,
×
3172
            old.len(),
×
3173
            new.len()
×
3174
        );
3175
        return true;
×
3176
    }
3177

3178
    // TODO - this value is arbitrary
3179
    if old.len() >= 16 {
×
3180
        // For large-ish lists, use a hash set.
3181
        let old = HashSet::from_iter(old);
×
3182
        let new = HashSet::from_iter(new);
×
3183
        if old != new {
×
3184
            trace!(
×
3185
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3186
            );
3187
            true
×
3188
        } else {
3189
            false
×
3190
        }
3191
    } else {
3192
        // For small lists, just use a linear array and sort it
3193
        let mut old = old.collect::<Vec<_>>();
×
3194
        let mut new = new.collect::<Vec<_>>();
×
3195
        old.sort_unstable();
×
3196
        new.sort_unstable();
×
3197
        if old != new {
×
3198
            trace!(
×
3199
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3200
            );
3201
            true
×
3202
        } else {
3203
            false
×
3204
        }
3205
    }
3206
}
3207

3208
/// Resolve parents and children, updating their [`CachedParent`] and
3209
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3210
/// slice for all children of each parent.
3211
pub(crate) fn resolve_parents(
×
3212
    mut commands: Commands,
3213
    q_child_effects: Query<
3214
        (
3215
            Entity,
3216
            &CachedParentRef,
3217
            &CachedEffectEvents,
3218
            Option<&CachedChildInfo>,
3219
        ),
3220
        With<CachedEffect>,
3221
    >,
3222
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3223
    effect_cache: Res<EffectCache>,
3224
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3225
    mut event_cache: ResMut<EventCache>,
3226
    mut children_from_parent: Local<
3227
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3228
    >,
3229
) {
3230
    #[cfg(feature = "trace")]
3231
    let _span = bevy::utils::tracing::info_span!("resolve_parents").entered();
×
UNCOV
3232
    let num_parent_effects = q_parent_effects.iter().len();
×
3233
    trace!("resolve_parents: num_parents={num_parent_effects}");
×
3234

3235
    // Build map of render entity from main entity for all cached effects.
3236
    let render_from_main_entity = q_cached_effects
×
3237
        .iter()
3238
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
×
3239
        .collect::<HashMap<_, _>>();
3240

3241
    // Group child effects by parent, building a list of children for each parent,
3242
    // solely based on the declaration each child makes of its parent. This doesn't
3243
    // mean yet that the parent exists.
3244
    if children_from_parent.capacity() < num_parent_effects {
×
3245
        let extra = num_parent_effects - children_from_parent.capacity();
×
3246
        children_from_parent.reserve(extra);
×
3247
    }
3248
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
UNCOV
3249
        q_child_effects.iter()
×
3250
    {
3251
        // Resolve the parent reference into the render world
3252
        let parent_main_entity = cached_parent_ref.entity;
3253
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3254
            warn!(
×
3255
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3256
                parent_main_entity, child_entity
3257
            );
3258
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3259
            continue;
×
3260
        };
3261
        let parent_entity = *parent_entity;
3262

3263
        // Resolve the parent
3264
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3265
            // Since we failed to resolve, remove this component so the next systems ignore
3266
            // this effect.
3267
            warn!(
×
3268
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3269
                parent_entity, child_entity
3270
            );
3271
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3272
            continue;
×
3273
        };
3274
        let Some(parent_buffer_binding_source) = effect_cache
×
3275
            .get_buffer(parent_cached_effect.buffer_index)
3276
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3277
        else {
3278
            // Since we failed to resolve, remove this component so the next systems ignore
3279
            // this effect.
3280
            warn!(
×
3281
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3282
                parent_cached_effect.buffer_index, child_entity
3283
            );
3284
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3285
            continue;
×
3286
        };
3287

3288
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3289
        else {
3290
            // Since we failed to resolve, remove this component so the next systems ignore
3291
            // this effect.
3292
            warn!(
×
3293
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3294
                cached_effect_events.buffer_index, child_entity
3295
            );
3296
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3297
            continue;
×
3298
        };
3299
        let child_buffer_binding_source = BufferBindingSource {
3300
            buffer: child_event_buffer.clone(),
3301
            offset: cached_effect_events.range.start,
3302
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3303
        };
3304

3305
        // Push the child entity into the children list
3306
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3307
        let local_child_index = child_vec.len() as u32;
3308
        child_vec.push((child_entity, child_buffer_binding_source));
3309
        child_infos.push(GpuChildInfo {
3310
            event_count: 0,
3311
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3312
        });
3313

3314
        // Check if child info changed. Avoid overwriting if no change.
3315
        if let Some(old_cached_child_info) = cached_child_info {
×
3316
            if parent_entity == old_cached_child_info.parent
3317
                && parent_cached_effect.slice.particle_layout
×
3318
                    == old_cached_child_info.parent_particle_layout
×
3319
                && parent_buffer_binding_source
×
3320
                    == old_cached_child_info.parent_buffer_binding_source
×
3321
                // 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.
3322
                && local_child_index == old_cached_child_info.local_child_index
×
3323
                && cached_effect_events.init_indirect_dispatch_index
×
3324
                    == old_cached_child_info.init_indirect_dispatch_index
×
3325
            {
3326
                trace!(
×
3327
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3328
                    child_entity
3329
                );
3330
                continue;
×
3331
            }
3332
        }
3333

3334
        // Allocate (or overwrite, if already existing) the child info, now that the
3335
        // parent is resolved.
3336
        let cached_child_info = CachedChildInfo {
3337
            parent: parent_entity,
3338
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3339
            parent_buffer_binding_source,
3340
            local_child_index,
3341
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3342
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3343
        };
3344
        commands.entity(child_entity).insert(cached_child_info);
×
3345
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3346
    }
3347

3348
    // Once all parents are resolved, diff all children of already-cached parents,
3349
    // and re-allocate their GpuChildInfo if needed.
3350
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
×
3351
        // Fetch the newly extracted list of children
3352
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3353
        else {
3354
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3355
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3356
            continue;
×
3357
        };
3358

3359
        // Check if any child changed compared to the existing CachedChildren component
3360
        if !is_child_list_changed(
3361
            parent_entity,
3362
            cached_parent_info
3363
                .children
3364
                .iter()
3365
                .map(|(entity, _)| *entity),
×
3366
            children.iter().map(|(entity, _)| *entity),
×
3367
        ) {
3368
            continue;
×
3369
        }
3370

3371
        event_cache.reallocate_child_infos(
×
3372
            parent_entity,
×
3373
            children,
×
3374
            &child_infos[..],
×
3375
            cached_parent_info.deref_mut(),
×
3376
        );
3377
    }
3378

3379
    // Once this is done, the children hash map contains all entries which don't
3380
    // already have a CachedParentInfo component. That is, all entities which are
3381
    // new parents.
3382
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
×
3383
        let cached_parent_info =
3384
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3385
        commands.entity(parent_entity).insert(cached_parent_info);
3386
    }
3387

3388
    // // Once all changes are applied, immediately schedule any GPU buffer
3389
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3390
    // content // will be written later.
3391
    // if event_cache
3392
    //     .child_infos()
3393
    //     .allocate_gpu(render_device, render_queue)
3394
    // {
3395
    //     // All those bind groups use the buffer so need to be re-created
3396
    //     effect_bind_groups.particle_buffers.clear();
3397
    // }
3398
}
3399

3400
pub fn fixup_parents(
×
3401
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3402
    mut q_children: Query<&mut CachedChildInfo>,
3403
) {
3404
    #[cfg(feature = "trace")]
3405
    let _span = bevy::utils::tracing::info_span!("fixup_parents").entered();
×
3406
    trace!("fixup_parents");
×
3407

3408
    // Once all parents are (re-)allocated, fix up the global index of all
3409
    // children if the parent base index changed.
3410
    trace!(
×
3411
        "Updating the global index of children of parent effects whose child list just changed..."
×
3412
    );
3413
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
×
3414
        let base_index =
3415
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3416
        trace!(
3417
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3418
            cached_parent_info.children.len(),
×
3419
            parent_entity,
3420
            base_index
3421
        );
3422
        for (child_entity, _) in &cached_parent_info.children {
×
3423
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3424
                continue;
×
3425
            };
UNCOV
3426
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
UNCOV
3427
            trace!(
×
3428
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3429
                child_entity,
×
3430
                parent_entity,
×
3431
                cached_child_info.local_child_index,
×
3432
                cached_child_info.global_child_index
×
3433
            );
3434
        }
3435
    }
3436
}
3437

3438
// TEMP - Mark all cached effects as invalid for this frame until another system
3439
// explicitly marks them as valid. Otherwise we early out in some parts, and
3440
// reuse by mistake the previous frame's extraction.
3441
pub fn clear_all_effects(
×
3442
    mut commands: Commands,
3443
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3444
) {
3445
    for entity in &mut q_cached_effects {
×
3446
        if let Some(mut cmd) = commands.get_entity(entity) {
×
3447
            cmd.remove::<BatchInput>();
3448
        }
3449
    }
3450
}
3451

3452
/// Indexed mesh metadata for [`CachedMesh`].
3453
#[derive(Debug, Clone)]
3454
#[allow(dead_code)]
3455
pub(crate) struct MeshIndexSlice {
3456
    /// Index format.
3457
    pub format: IndexFormat,
3458
    /// GPU buffer containing the indices.
3459
    pub buffer: Buffer,
3460
    /// Range inside [`Self::buffer`] where the indices are.
3461
    pub range: Range<u32>,
3462
}
3463

3464
/// Render world cached mesh infos for a single effect instance.
3465
#[derive(Debug, Clone, Component)]
3466
pub(crate) struct CachedMesh {
3467
    /// Asset of the effect mesh to draw.
3468
    pub mesh: AssetId<Mesh>,
3469
    /// GPU buffer storing the [`mesh`] of the effect.
3470
    pub buffer: Buffer,
3471
    /// Range slice inside the GPU buffer for the effect mesh.
3472
    pub range: Range<u32>,
3473
    /// Indexed rendering metadata.
3474
    #[allow(unused)]
3475
    pub indexed: Option<MeshIndexSlice>,
3476
}
3477

3478
/// Render world cached properties info for a single effect instance.
3479
#[allow(unused)]
3480
#[derive(Debug, Component)]
3481
pub(crate) struct CachedProperties {
3482
    /// Layout of the effect properties.
3483
    pub layout: PropertyLayout,
3484
    /// Index of the buffer in the [`EffectCache`].
3485
    pub buffer_index: u32,
3486
    /// Offset in bytes inside the buffer.
3487
    pub offset: u32,
3488
    /// Binding size in bytes of the property struct.
3489
    pub binding_size: u32,
3490
}
3491

3492
#[derive(SystemParam)]
3493
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3494
    sim_params: Res<'w, SimParams>,
3495
    render_device: Res<'w, RenderDevice>,
3496
    render_queue: Res<'w, RenderQueue>,
3497
    #[system_param(ignore)]
3498
    marker: PhantomData<&'s usize>,
3499
}
3500

3501
#[derive(SystemParam)]
3502
pub struct PipelineSystemParams<'w, 's> {
3503
    pipeline_cache: Res<'w, PipelineCache>,
3504
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3505
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3506
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3507
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3508
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3509
    specialized_indirect_pipelines:
3510
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3511
    #[system_param(ignore)]
3512
    marker: PhantomData<&'s usize>,
3513
}
3514

3515
pub(crate) fn prepare_effects(
×
3516
    mut commands: Commands,
3517
    read_only_params: PrepareEffectsReadOnlyParams,
3518
    mut pipelines: PipelineSystemParams,
3519
    mut property_cache: ResMut<PropertyCache>,
3520
    event_cache: Res<EventCache>,
3521
    mut effect_cache: ResMut<EffectCache>,
3522
    mut effects_meta: ResMut<EffectsMeta>,
3523
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3524
    mut extracted_effects: ResMut<ExtractedEffects>,
3525
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3526
    q_cached_effects: Query<(
3527
        MainEntity,
3528
        &CachedEffect,
3529
        Ref<CachedMesh>,
3530
        &DispatchBufferIndices,
3531
        Option<&CachedEffectProperties>,
3532
        Option<&CachedParentInfo>,
3533
        Option<&CachedChildInfo>,
3534
        Option<&CachedEffectEvents>,
3535
    )>,
3536
    q_debug_all_entities: Query<MainEntity>,
3537
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperationQueue>,
3538
    mut sort_bind_groups: ResMut<SortBindGroups>,
3539
) {
3540
    #[cfg(feature = "trace")]
3541
    let _span = bevy::utils::tracing::info_span!("prepare_effects").entered();
×
3542
    trace!("prepare_effects");
×
3543

3544
    // Workaround for too many params in system (TODO: refactor to split work?)
3545
    let sim_params = read_only_params.sim_params.into_inner();
×
3546
    let render_device = read_only_params.render_device.into_inner();
×
3547
    let render_queue = read_only_params.render_queue.into_inner();
×
3548
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
×
3549
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
×
3550
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
×
3551
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
×
3552

3553
    // // sort first by z and then by handle. this ensures that, when possible,
3554
    // batches span multiple z layers // batches won't span z-layers if there is
3555
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3556
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3557
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3558
    //         other => other,
3559
    //     }
3560
    // });
3561

3562
    // Ensure the indirect pipelines are created
3563
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
×
3564
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
×
3565
            pipeline_cache,
×
3566
            &pipelines.indirect_pipeline,
×
3567
            DispatchIndirectPipelineKey { has_events: false },
×
3568
        );
3569
    }
3570
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
×
3571
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
×
3572
            pipeline_cache,
×
3573
            &pipelines.indirect_pipeline,
×
3574
            DispatchIndirectPipelineKey { has_events: true },
×
3575
        );
3576
    }
3577
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
×
3578
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3579
    } else {
3580
        // If this is the first time we insert an event buffer, we need to switch the
3581
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3582
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3583
        // more effect using GPU spawn events, we can deallocate.
3584
        let was_empty =
×
3585
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
×
3586
        let is_empty = event_cache.child_infos().is_empty();
×
3587
        if was_empty && !is_empty {
×
3588
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3589
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3590
        } else if is_empty && !was_empty {
×
3591
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3592
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3593
        }
3594
    }
3595

3596
    gpu_buffer_operation_queue.begin_frame();
×
3597

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

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

3641
        let effect_slice = EffectSlice {
3642
            slice: cached_effect.slice.range(),
3643
            buffer_index: cached_effect.buffer_index,
3644
            particle_layout: cached_effect.slice.particle_layout.clone(),
3645
        };
3646

3647
        let has_event_buffer = cached_child_info.is_some();
3648
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3649
        // should be Option<T>, not T)
3650
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3651
            None
×
3652
        } else {
3653
            Some(extracted_effect.property_layout.min_binding_size())
×
3654
        };
3655

3656
        // Schedule some GPU buffer operation to update the number of workgroups to
3657
        // dispatch during the indirect init pass of this effect based on the number of
3658
        // GPU spawn events written in its buffer.
3659
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3660
            (cached_effect_events, cached_child_info)
3661
        {
3662
            debug_assert_eq!(
3663
                GpuChildInfo::min_size().get() % 4,
3664
                0,
3665
                "Invalid GpuChildInfo alignment."
×
3666
            );
3667

3668
            // Resolve parent entry
3669
            let Ok((_, _, _, _, _, cached_parent_info, _, _)) =
×
3670
                q_cached_effects.get(cached_child_info.parent)
×
3671
            else {
3672
                continue;
×
3673
            };
3674
            let Some(cached_parent_info) = cached_parent_info else {
×
3675
                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);
×
3676
                continue;
×
3677
            };
3678

3679
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3680
            let child_info_size_u32 = GpuChildInfo::min_size().get() as u32 / 4;
3681
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3682
            let global_child_index = cached_child_info.global_child_index;
×
3683

3684
            // Schedule a fill dispatch
3685
            let event_buffer_index = cached_effect_events.buffer_index;
×
3686
            let event_slice = cached_effect_events.range.clone();
×
3687
            trace!(
×
3688
                "queue_init_fill(): event_buffer_index={} event_slice={:?} src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3689
                event_buffer_index,
3690
                event_slice,
3691
                global_child_index,
3692
                init_indirect_dispatch_index,
3693
            );
3694
            gpu_buffer_operation_queue.enqueue_init_fill(
×
3695
                event_buffer_index,
×
3696
                event_slice,
×
3697
                GpuBufferOperationArgs {
×
3698
                    src_offset: global_child_index,
×
3699
                    src_stride: child_info_size_u32,
×
3700
                    dst_offset: init_indirect_dispatch_index,
×
3701
                    dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
3702
                    count: 1, // FIXME - should be a batch here!!
×
3703
                },
3704
            );
3705
        }
3706

3707
        // Create init pipeline key flags.
3708
        let init_pipeline_key_flags = {
×
3709
            let mut flags = ParticleInitPipelineKeyFlags::empty();
×
3710
            flags.set(
×
3711
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
3712
                effect_slice.particle_layout.contains(Attribute::PREV),
×
3713
            );
3714
            flags.set(
×
3715
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
3716
                effect_slice.particle_layout.contains(Attribute::NEXT),
×
3717
            );
3718
            flags.set(
×
3719
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
×
3720
                has_event_buffer,
×
3721
            );
3722
            flags
×
3723
        };
3724

3725
        // This should always exist by the time we reach this point, because we should
3726
        // have inserted any property in the cache, which would have allocated the
3727
        // proper bind group layout (or the default no-property one).
3728
        let spawner_bind_group_layout = property_cache
×
3729
            .bind_group_layout(property_layout_min_binding_size)
×
3730
            .unwrap_or_else(|| {
×
3731
                panic!(
×
3732
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
×
3733
                    property_layout_min_binding_size,
×
3734
                )
3735
            });
3736
        trace!(
3737
            "Retrieved spawner@2 bind group layout {:?} for property binding size {:?}.",
×
3738
            spawner_bind_group_layout.id(),
×
3739
            property_layout_min_binding_size
3740
        );
3741

3742
        // Fetch the bind group layouts from the cache
3743
        trace!("cached_child_info={:?}", cached_child_info);
×
3744
        let (parent_particle_layout_min_binding_size, parent_buffer_index) =
×
3745
            if let Some(cached_child) = cached_child_info.as_ref() {
×
3746
                let Ok((_, parent_cached_effect, _, _, _, _, _, _)) =
×
3747
                    q_cached_effects.get(cached_child.parent)
3748
                else {
3749
                    // At this point we should have discarded invalid effects with a missing parent,
3750
                    // so if the parent is not found this is a bug.
3751
                    error!(
×
3752
                        "Effect main_entity {:?}: parent render entity {:?} not found.",
×
3753
                        main_entity, cached_child.parent
3754
                    );
3755
                    continue;
×
3756
                };
3757
                (
3758
                    Some(
3759
                        parent_cached_effect
3760
                            .slice
3761
                            .particle_layout
3762
                            .min_binding_size32(),
3763
                    ),
3764
                    Some(parent_cached_effect.buffer_index),
3765
                )
3766
            } else {
3767
                (None, None)
×
3768
            };
3769
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
×
3770
            effect_slice.particle_layout.min_binding_size32(),
3771
            parent_particle_layout_min_binding_size,
3772
        ) else {
3773
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", 
×
3774
            effect_slice.particle_layout.min_binding_size32(), parent_particle_layout_min_binding_size);
×
3775
            continue;
×
3776
        };
3777
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3778
        trace!(
3779
            "Retrieved particle@1 bind group layout {:?} for particle binding size {:?} and parent binding size {:?}.",
×
3780
            particle_bind_group_layout.id(),
×
3781
            effect_slice.particle_layout.min_binding_size32(),
×
3782
            parent_particle_layout_min_binding_size,
3783
        );
3784

3785
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3786
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3787

3788
        // Specialize the init pipeline based on the effect.
3789
        let init_pipeline_id = {
3790
            let consume_gpu_spawn_events = init_pipeline_key_flags
3791
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3792

3793
            // Fetch the metadata@3 bind group layout from the cache
3794
            let metadata_bind_group_layout = effect_cache
3795
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3796
                .unwrap()
3797
                .clone();
3798

3799
            // https://github.com/bevyengine/bevy/issues/17132
3800
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3801
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3802
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3803
            pipelines.init_pipeline.temp_particle_bind_group_layout =
3804
                Some(particle_bind_group_layout.clone());
3805
            pipelines.init_pipeline.temp_spawner_bind_group_layout =
3806
                Some(spawner_bind_group_layout.clone());
3807
            pipelines.init_pipeline.temp_metadata_bind_group_layout =
3808
                Some(metadata_bind_group_layout);
3809
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3810
                pipeline_cache,
3811
                &pipelines.init_pipeline,
3812
                ParticleInitPipelineKey {
3813
                    shader: extracted_effect.effect_shaders.init.clone(),
3814
                    particle_layout_min_binding_size,
3815
                    parent_particle_layout_min_binding_size,
3816
                    flags: init_pipeline_key_flags,
3817
                    particle_bind_group_layout_id,
3818
                    spawner_bind_group_layout_id,
3819
                    metadata_bind_group_layout_id,
3820
                },
3821
            );
3822
            // keep things tidy; this is just a hack, should not persist
3823
            pipelines.init_pipeline.temp_particle_bind_group_layout = None;
3824
            pipelines.init_pipeline.temp_spawner_bind_group_layout = None;
3825
            pipelines.init_pipeline.temp_metadata_bind_group_layout = None;
3826
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
×
3827

3828
            init_pipeline_id
3829
        };
3830

3831
        let update_pipeline_id = {
3832
            let num_event_buffers = cached_parent_info
3833
                .map(|p| p.children.len() as u32)
×
3834
                .unwrap_or_default();
3835

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

3843
            // Fetch the bind group layouts from the cache
3844
            let metadata_bind_group_layout = effect_cache
3845
                .metadata_update_bind_group_layout(num_event_buffers)
3846
                .unwrap()
3847
                .clone();
3848

3849
            // https://github.com/bevyengine/bevy/issues/17132
3850
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3851
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3852
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3853
            pipelines.update_pipeline.temp_particle_bind_group_layout =
3854
                Some(particle_bind_group_layout);
3855
            pipelines.update_pipeline.temp_spawner_bind_group_layout =
3856
                Some(spawner_bind_group_layout);
3857
            pipelines.update_pipeline.temp_metadata_bind_group_layout =
3858
                Some(metadata_bind_group_layout);
3859
            let update_pipeline_id = specialized_update_pipelines.specialize(
3860
                pipeline_cache,
3861
                &pipelines.update_pipeline,
3862
                ParticleUpdatePipelineKey {
3863
                    shader: extracted_effect.effect_shaders.update.clone(),
3864
                    particle_layout: effect_slice.particle_layout.clone(),
3865
                    parent_particle_layout_min_binding_size,
3866
                    num_event_buffers,
3867
                    particle_bind_group_layout_id,
3868
                    spawner_bind_group_layout_id,
3869
                    metadata_bind_group_layout_id,
3870
                },
3871
            );
3872
            // keep things tidy; this is just a hack, should not persist
3873
            pipelines.update_pipeline.temp_particle_bind_group_layout = None;
3874
            pipelines.update_pipeline.temp_spawner_bind_group_layout = None;
3875
            pipelines.update_pipeline.temp_metadata_bind_group_layout = None;
3876
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
×
3877

3878
            update_pipeline_id
3879
        };
3880

3881
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3882
            init: init_pipeline_id,
3883
            update: update_pipeline_id,
3884
        };
3885

3886
        // For ribbons, which need particle sorting, create a bind group layout for
3887
        // sorting the effect, based on its particle layout.
3888
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
3889
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
3890
                pipeline_cache,
×
3891
                &extracted_effect.particle_layout,
×
3892
            ) {
3893
                error!(
3894
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
3895
                    err
3896
                );
3897
                continue;
3898
            }
3899
        }
3900

3901
        // Output some debug info
3902
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
×
3903
        trace!(
3904
            "update_shader = {:?}",
×
3905
            extracted_effect.effect_shaders.update
3906
        );
3907
        trace!(
3908
            "render_shader = {:?}",
×
3909
            extracted_effect.effect_shaders.render
3910
        );
3911
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
3912
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
3913

3914
        let spawner_index = effects_meta.allocate_spawner(
3915
            &extracted_effect.transform,
3916
            extracted_effect.spawn_count,
3917
            extracted_effect.prng_seed,
3918
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
3919
        );
3920

3921
        trace!(
3922
            "Updating cached effect at entity {:?}...",
×
3923
            extracted_effect.render_entity.id()
×
3924
        );
3925
        let mut cmd = commands.entity(extracted_effect.render_entity.id());
3926
        cmd.insert(BatchInput {
3927
            handle: extracted_effect.handle,
3928
            entity: extracted_effect.render_entity.id(),
3929
            main_entity: extracted_effect.main_entity,
3930
            effect_slice,
3931
            init_and_update_pipeline_ids,
3932
            parent_buffer_index,
3933
            event_buffer_index: cached_effect_events.map(|cee| cee.buffer_index),
×
3934
            child_effects: cached_parent_info
3935
                .map(|cp| cp.children.clone())
×
3936
                .unwrap_or_default(),
3937
            layout_flags: extracted_effect.layout_flags,
3938
            texture_layout: extracted_effect.texture_layout.clone(),
3939
            textures: extracted_effect.textures.clone(),
3940
            alpha_mode: extracted_effect.alpha_mode,
3941
            particle_layout: extracted_effect.particle_layout.clone(),
3942
            shaders: extracted_effect.effect_shaders,
3943
            spawner_base: spawner_index,
3944
            spawn_count: extracted_effect.spawn_count,
3945
            position: extracted_effect.transform.translation(),
3946
            init_indirect_dispatch_index: cached_child_info
3947
                .map(|cc| cc.init_indirect_dispatch_index),
×
3948
        });
3949

3950
        // Update properties
3951
        if let Some(cached_effect_properties) = cached_effect_properties {
×
3952
            // Because the component is persisted, it may be there from a previous version
3953
            // of the asset. And add_remove_effects() only add new instances or remove old
3954
            // ones, but doesn't update existing ones. Check if it needs to be removed.
3955
            // FIXME - Dedupe with add_remove_effect(), we shouldn't have 2 codepaths doing
3956
            // the same thing at 2 different times.
3957
            if extracted_effect.property_layout.is_empty() {
3958
                trace!(
×
3959
                    "Render entity {:?} had CachedEffectProperties component, but newly extracted property layout is empty. Removing component...",
×
3960
                    extracted_effect.render_entity.id(),
×
3961
                );
3962
                cmd.remove::<CachedEffectProperties>();
×
3963
                // Also remove the other one. FIXME - dedupe those two...
3964
                cmd.remove::<CachedProperties>();
×
3965

3966
                if extracted_effect.property_data.is_some() {
×
3967
                    warn!(
×
3968
                        "Effect on entity {:?} doesn't declare any property in its Module, but some property values were provided. Those values will be discarded.",
×
3969
                        extracted_effect.main_entity.id(),
×
3970
                    );
3971
                }
3972
            } else {
3973
                // Insert a new component or overwrite the existing one
3974
                cmd.insert(CachedProperties {
×
3975
                    layout: extracted_effect.property_layout.clone(),
×
3976
                    buffer_index: cached_effect_properties.buffer_index,
×
3977
                    offset: cached_effect_properties.range.start,
×
3978
                    binding_size: cached_effect_properties.range.len() as u32,
×
3979
                });
3980

3981
                // Write properties for this effect if they were modified.
3982
                // FIXME - This doesn't work with batching!
3983
                if let Some(property_data) = &extracted_effect.property_data {
×
3984
                    trace!(
3985
                    "Properties changed; (re-)uploading to GPU... New data: {} bytes. Capacity: {} bytes.",
×
3986
                    property_data.len(),
×
3987
                    cached_effect_properties.range.len(),
×
3988
                );
3989
                    if property_data.len() <= cached_effect_properties.range.len() {
×
3990
                        let property_buffer = property_cache.buffers_mut()
×
3991
                            [cached_effect_properties.buffer_index as usize]
×
3992
                            .as_mut()
3993
                            .unwrap();
3994
                        property_buffer.write(cached_effect_properties.range.start, property_data);
×
3995
                    } else {
3996
                        error!(
×
3997
                            "Cannot upload properties: existing property slice in property buffer #{} is too small ({} bytes) for the new data ({} bytes).",
×
3998
                            cached_effect_properties.buffer_index,
×
3999
                            cached_effect_properties.range.len(),
×
4000
                            property_data.len()
×
4001
                        );
4002
                    }
4003
                }
4004
            }
4005
        } else {
4006
            // No property on the effect; remove the component
4007
            trace!(
×
4008
                "No CachedEffectProperties on render entity {:?}, remove any CachedProperties component too.",
×
4009
                extracted_effect.render_entity.id()
×
4010
            );
4011
            cmd.remove::<CachedProperties>();
×
4012
        }
4013

4014
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4015
        // update its GpuEffectMetadata with all those infos.
4016
        // FIXME - should do this only when the below changes (not only the mesh), via
4017
        // some invalidation mechanism and ECS change detection.
4018
        if cached_mesh.is_changed() {
4019
            let capacity = cached_effect.slice.len();
×
4020

4021
            // Global and local indices of this effect as a child of another (parent) effect
4022
            let (global_child_index, local_child_index) = cached_child_info
×
4023
                .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4024
                .unwrap_or_default();
4025

4026
            // Base index of all children of this (parent) effect
4027
            let base_child_index = cached_parent_info
×
4028
                .map(|cpi| {
×
4029
                    debug_assert_eq!(
×
4030
                        cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4031
                        0
4032
                    );
4033
                    cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4034
                })
4035
                .unwrap_or_default();
4036

4037
            let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
×
4038
            let sort_key_offset = extracted_effect
×
4039
                .particle_layout
×
4040
                .offset(Attribute::RIBBON_ID)
×
4041
                .unwrap_or(0)
×
4042
                / 4;
×
4043
            let sort_key2_offset = extracted_effect
×
4044
                .particle_layout
×
4045
                .offset(Attribute::AGE)
×
4046
                .unwrap_or(0)
×
4047
                / 4;
×
4048

4049
            let mut gpu_effect_metadata = GpuEffectMetadata {
4050
                instance_count: 0,
4051
                base_instance: 0,
4052
                alive_count: 0,
4053
                max_update: 0,
4054
                dead_count: capacity,
4055
                max_spawn: capacity,
4056
                ping: 0,
4057
                spawner_index: 0xDEADBEEF, // unused
4058
                indirect_dispatch_index: dispatch_buffer_indices
×
4059
                    .update_dispatch_indirect_buffer_table_id
4060
                    .0,
4061
                // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4062
                indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
×
4063
                init_indirect_dispatch_index: cached_effect_events
×
4064
                    .map(|cee| cee.init_indirect_dispatch_index)
4065
                    .unwrap_or_default(),
4066
                local_child_index,
4067
                global_child_index,
4068
                base_child_index,
4069
                particle_stride,
4070
                sort_key_offset,
4071
                sort_key2_offset,
4072
                ..default()
4073
            };
4074
            if let Some(indexed) = &cached_mesh.indexed {
×
4075
                gpu_effect_metadata.vertex_or_index_count = indexed.range.len() as u32;
4076
                gpu_effect_metadata.first_index_or_vertex_offset = indexed.range.start;
4077
                gpu_effect_metadata.vertex_offset_or_base_instance = cached_mesh.range.start as i32;
4078
            } else {
4079
                gpu_effect_metadata.vertex_or_index_count = cached_mesh.range.len() as u32;
×
4080
                gpu_effect_metadata.first_index_or_vertex_offset = cached_mesh.range.start;
×
4081
                gpu_effect_metadata.vertex_offset_or_base_instance = 0;
×
4082
            };
4083
            assert!(dispatch_buffer_indices
×
4084
                .effect_metadata_buffer_table_id
×
4085
                .is_valid());
×
4086
            effects_meta.effect_metadata_buffer.update(
×
4087
                dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
4088
                gpu_effect_metadata,
×
4089
            );
4090

4091
            warn!(
×
4092
                "Updated metadata entry {} for effect {:?}, this will reset it.",
×
4093
                dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4094
            );
4095
        }
4096

4097
        prepared_effect_count += 1;
×
4098
    }
4099
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
×
4100

4101
    // Once all EffectMetadata values are written, schedule a GPU upload
4102
    if effects_meta
4103
        .effect_metadata_buffer
4104
        .allocate_gpu(render_device, render_queue)
4105
    {
4106
        // All those bind groups use the buffer so need to be re-created
4107
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
×
4108
        effects_meta.indirect_metadata_bind_group = None;
×
4109
        effect_bind_groups.init_metadata_bind_groups.clear();
×
4110
        effect_bind_groups.update_metadata_bind_groups.clear();
×
4111
    }
4112

4113
    // Write the entire spawner buffer for this frame, for all effects combined
4114
    assert_eq!(
4115
        prepared_effect_count,
4116
        effects_meta.spawner_buffer.len() as u32
4117
    );
4118
    if effects_meta
×
4119
        .spawner_buffer
×
4120
        .write_buffer(render_device, render_queue)
×
4121
    {
4122
        // All property bind groups use the spawner buffer, which was reallocate
4123
        property_bind_groups.clear(true);
×
4124
        effects_meta.indirect_spawner_bind_group = None;
×
4125
    }
4126

4127
    // Update simulation parameters
4128
    effects_meta.sim_params_uniforms.set(sim_params.into());
×
4129
    {
4130
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
4131
        gpu_sim_params.num_effects = prepared_effect_count;
×
4132

4133
        trace!(
×
4134
            "Simulation parameters: time={} delta_time={} virtual_time={} \
×
4135
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
×
4136
            gpu_sim_params.time,
4137
            gpu_sim_params.delta_time,
4138
            gpu_sim_params.virtual_time,
4139
            gpu_sim_params.virtual_delta_time,
4140
            gpu_sim_params.real_time,
4141
            gpu_sim_params.real_delta_time,
4142
            gpu_sim_params.num_effects,
4143
        );
4144
    }
4145
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
×
UNCOV
4146
    effects_meta
×
UNCOV
4147
        .sim_params_uniforms
×
UNCOV
4148
        .write_buffer(render_device, render_queue);
×
4149
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
×
4150
        // Buffer changed, invalidate bind groups
4151
        effects_meta.indirect_sim_params_bind_group = None;
×
4152
    }
4153
}
4154

4155
pub(crate) fn batch_effects(
×
4156
    mut commands: Commands,
4157
    render_device: Res<RenderDevice>,
4158
    render_queue: Res<RenderQueue>,
4159
    effects_meta: Res<EffectsMeta>,
4160
    mut sort_bind_groups: ResMut<SortBindGroups>,
4161
    mut q_cached_effects: Query<(
4162
        Entity,
4163
        &CachedMesh,
4164
        Option<&CachedEffectEvents>,
4165
        Option<&CachedChildInfo>,
4166
        Option<&CachedProperties>,
4167
        &mut DispatchBufferIndices,
4168
        &mut BatchInput,
4169
    )>,
4170
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4171
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperationQueue>,
4172
) {
4173
    trace!("batch_effects");
×
4174

4175
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4176
    // inside that buffer. This is critical for batching to work, because
4177
    // batching effects is based on compatible items, which implies same GPU
4178
    // buffer and continuous slice ranges (the next slice start must be equal to
4179
    // the previous start end, without gap). EffectSlice already contains both
4180
    // information, and the proper ordering implementation.
4181
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4182

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

4187
    // Loop on all extracted effects in order, and try to batch them together to
4188
    // reduce draw calls. -- currently does nothing, batching was broken and never
4189
    // fixed.
4190
    // FIXME - This is in ECS order, if we re-add the sorting above we need a
4191
    // different order here!
4192
    trace!("Batching {} effects...", q_cached_effects.iter().len());
×
4193
    sorted_effect_batches.clear();
×
4194
    for (
4195
        entity,
×
4196
        cached_mesh,
×
4197
        cached_effect_events,
×
4198
        cached_child_info,
×
4199
        cached_properties,
×
4200
        dispatch_buffer_indices,
×
4201
        mut input,
×
4202
    ) in &mut q_cached_effects
×
4203
    {
4204
        // Detect if this cached effect was not updated this frame by a new extracted
4205
        // effect. This happens when e.g. the effect is invisible and not simulated, or
4206
        // some error prevented it from being extracted. We use the pipeline IDs vector
4207
        // as a marker, because each frame we move it out of the CachedGroup
4208
        // component during batching, so if empty this means a new one was not created
4209
        // this frame.
4210
        // if input.init_and_update_pipeline_ids.is_empty() {
4211
        //     trace!(
4212
        //         "Skipped cached effect on render entity {:?}: not extracted this
4213
        // frame.",         entity
4214
        //     );
4215
        //     continue;
4216
        // }
4217

UNCOV
4218
        let translation = input.position;
×
4219

4220
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4221
        // most of the data needed to drive rendering. However this doesn't drive
4222
        // rendering; this is just storage.
4223
        let mut effect_batch = EffectBatch::from_input(
UNCOV
4224
            cached_mesh,
×
UNCOV
4225
            cached_effect_events,
×
UNCOV
4226
            cached_child_info,
×
UNCOV
4227
            &mut input,
×
UNCOV
4228
            *dispatch_buffer_indices.as_ref(),
×
4229
            cached_properties.map(|cp| PropertyBindGroupKey {
×
4230
                buffer_index: cp.buffer_index,
×
4231
                binding_size: cp.binding_size,
×
4232
            }),
4233
            cached_properties.map(|cp| cp.offset),
×
4234
        );
4235

4236
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4237
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4238
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4239
        // ribbon).
UNCOV
4240
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
×
4241
            // This buffer is allocated in prepare_effects(), so should always be available
4242
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4243
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4244
                continue;
×
4245
            };
4246

4247
            // Allocate a GpuDispatchIndirect entry
4248
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4249
            effect_batch.sort_fill_indirect_dispatch_index =
4250
                Some(sort_fill_indirect_dispatch_index);
4251

4252
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4253
            // compute a number of workgroups to dispatch based on that particle count, and
4254
            // store the result into a GpuDispatchIndirect struct which will be used to
4255
            // dispatch the fill-sort pass.
4256
            {
4257
                let src_buffer = effect_metadata_buffer.clone();
4258
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4259
                    effect_batch
4260
                        .dispatch_buffer_indices
4261
                        .effect_metadata_buffer_table_id,
4262
                );
4263
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4264
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4265
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4266
                    continue;
×
4267
                };
4268
                let dst_buffer = dst_buffer.clone();
4269
                let dst_binding_offset = 0; // see dst_offset below
4270
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4271
                trace!(
4272
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4273
                    src_buffer.id(),
×
4274
                    src_binding_offset,
×
4275
                    src_binding_size.get(),
×
4276
                    dst_buffer.id(),
×
4277
                    dst_binding_offset,
4278
                    -1, //dst_binding_size.get(),
4279
                );
4280
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4281
                debug_assert_eq!(
4282
                    src_offset, 5,
4283
                    "GpuEffectMetadata changed, update this assert."
×
4284
                );
4285
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4286
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4287
                // os are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4288
                // binding offset of 0 and binds the entire destination buffer,
4289
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4290
                // index the proper offset in the buffer. This requires of
4291
                // course binding the entire buffer, or at least enough to index all operations
4292
                // (hence the None below). This is not really a general solution, so should be
4293
                // reviewed.
4294
                let dst_offset = sort_bind_groups
×
4295
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
×
4296
                    / 4;
×
4297
                gpu_buffer_operation_queue.enqueue(
×
4298
                    GpuBufferOperationType::FillDispatchArgs,
×
4299
                    GpuBufferOperationArgs {
×
4300
                        src_offset,
×
4301
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
×
4302
                        dst_offset,
×
4303
                        dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
4304
                        count: 1,
×
4305
                    },
4306
                    src_buffer,
×
4307
                    src_binding_offset,
×
4308
                    Some(src_binding_size),
×
4309
                    dst_buffer,
×
4310
                    dst_binding_offset,
×
4311
                    None, //Some(dst_binding_size),
×
4312
                );
4313
            }
4314
        }
4315

4316
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4317
        trace!(
×
4318
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4319
            effect_batch_index,
4320
            entity,
4321
        );
4322

4323
        // Spawn an EffectDrawBatch, to actually drive rendering.
4324
        commands
4325
            .spawn(EffectDrawBatch {
4326
                effect_batch_index,
4327
                translation,
4328
            })
4329
            .insert(TemporaryRenderEntity);
4330
    }
4331

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

4335
    sorted_effect_batches.sort();
×
4336
}
4337

4338
/// Per-buffer bind groups for a GPU effect buffer.
4339
///
4340
/// This contains all bind groups specific to a single [`EffectBuffer`].
4341
///
4342
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4343
pub(crate) struct BufferBindGroups {
4344
    /// Bind group for the render shader.
4345
    ///
4346
    /// ```wgsl
4347
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4348
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4349
    /// @binding(2) var<storage, read> spawner : Spawner;
4350
    /// ```
4351
    render: BindGroup,
4352
    // /// Bind group for filling the indirect dispatch arguments of any child init
4353
    // /// pass.
4354
    // ///
4355
    // /// This bind group is optional; it's only created if the current effect has
4356
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4357
    // /// (although normally the event buffer is not created if there's no
4358
    // /// children).
4359
    // ///
4360
    // /// The source buffer is always the current effect's event buffer. The
4361
    // /// destination buffer is the global shared buffer for indirect fill args
4362
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4363
    // /// args contains the data to index the relevant part of the global shared
4364
    // /// buffer for this effect buffer; it may contain multiple entries in case
4365
    // /// multiple effects are batched inside the current effect buffer.
4366
    // ///
4367
    // /// ```wgsl
4368
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4369
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4370
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4371
    // /// ```
4372
    // init_fill_dispatch: Option<BindGroup>,
4373
}
4374

4375
/// Combination of a texture layout and the bound textures.
4376
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4377
struct Material {
4378
    layout: TextureLayout,
4379
    textures: Vec<AssetId<Image>>,
4380
}
4381

4382
impl Material {
4383
    /// Get the bind group entries to create a bind group.
4384
    pub fn make_entries<'a>(
×
4385
        &self,
4386
        gpu_images: &'a RenderAssets<GpuImage>,
4387
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4388
        if self.textures.is_empty() {
×
4389
            return Ok(vec![]);
×
4390
        }
4391

4392
        let entries: Vec<BindGroupEntry<'a>> = self
×
4393
            .textures
×
4394
            .iter()
4395
            .enumerate()
4396
            .flat_map(|(index, id)| {
×
4397
                let base_binding = index as u32 * 2;
×
4398
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4399
                    vec![
×
4400
                        BindGroupEntry {
×
4401
                            binding: base_binding,
×
4402
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4403
                        },
4404
                        BindGroupEntry {
×
4405
                            binding: base_binding + 1,
×
4406
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4407
                        },
4408
                    ]
4409
                } else {
4410
                    vec![]
×
4411
                }
4412
            })
4413
            .collect();
4414
        if entries.len() == self.textures.len() * 2 {
×
4415
            return Ok(entries);
×
4416
        }
4417
        Err(())
×
4418
    }
4419
}
4420

4421
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4422
struct BindingKey {
4423
    pub buffer_id: BufferId,
4424
    pub offset: u32,
4425
    pub size: NonZeroU32,
4426
}
4427

4428
impl<'a> From<BufferSlice<'a>> for BindingKey {
4429
    fn from(value: BufferSlice<'a>) -> Self {
×
4430
        Self {
4431
            buffer_id: value.buffer.id(),
×
4432
            offset: value.offset,
×
4433
            size: value.size,
×
4434
        }
4435
    }
4436
}
4437

4438
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4439
    fn from(value: &BufferSlice<'a>) -> Self {
×
4440
        Self {
4441
            buffer_id: value.buffer.id(),
×
4442
            offset: value.offset,
×
4443
            size: value.size,
×
4444
        }
4445
    }
4446
}
4447

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

4458
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4459
struct ConsumeEventKey {
4460
    child_infos_buffer_id: BufferId,
4461
    events: BindingKey,
4462
}
4463

4464
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4465
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4466
        Self {
4467
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4468
            events: value.events.into(),
×
4469
        }
4470
    }
4471
}
4472

4473
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4474
struct InitMetadataBindGroupKey {
4475
    pub buffer_index: u32,
4476
    pub effect_metadata_buffer: BufferId,
4477
    pub effect_metadata_offset: u32,
4478
    pub consume_event_key: Option<ConsumeEventKey>,
4479
}
4480

4481
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4482
struct UpdateMetadataBindGroupKey {
4483
    pub buffer_index: u32,
4484
    pub effect_metadata_buffer: BufferId,
4485
    pub effect_metadata_offset: u32,
4486
    pub child_info_buffer_id: Option<BufferId>,
4487
    pub event_buffers_keys: Vec<BindingKey>,
4488
}
4489

4490
struct CachedBindGroup<K: Eq> {
4491
    /// Key the bind group was created from. Each time the key changes, the bind
4492
    /// group should be re-created.
4493
    key: K,
4494
    /// Bind group created from the key.
4495
    bind_group: BindGroup,
4496
}
4497

4498
#[derive(Debug, Clone, Copy)]
4499
struct BufferSlice<'a> {
4500
    pub buffer: &'a Buffer,
4501
    pub offset: u32,
4502
    pub size: NonZeroU32,
4503
}
4504

4505
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4506
    fn from(value: BufferSlice<'a>) -> Self {
×
4507
        Self {
4508
            buffer: value.buffer,
×
4509
            offset: value.offset.into(),
×
4510
            size: Some(value.size.into()),
×
4511
        }
4512
    }
4513
}
4514

4515
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4516
    fn from(value: &BufferSlice<'a>) -> Self {
×
4517
        Self {
4518
            buffer: value.buffer,
×
4519
            offset: value.offset.into(),
×
4520
            size: Some(value.size.into()),
×
4521
        }
4522
    }
4523
}
4524

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

4535
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4536
/// the init pass consumes GPU events as a mechanism to spawn particles.
4537
struct ConsumeEventBuffers<'a> {
4538
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4539
    /// This is dynamically indexed inside the shader.
4540
    child_infos_buffer: &'a Buffer,
4541
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4542
    events: BufferSlice<'a>,
4543
}
4544

4545
#[derive(Default, Resource)]
4546
pub struct EffectBindGroups {
4547
    /// Map from buffer index to the bind groups shared among all effects that
4548
    /// use that buffer.
4549
    particle_buffers: HashMap<u32, BufferBindGroups>,
4550
    /// Map of bind groups for image assets used as particle textures.
4551
    images: HashMap<AssetId<Image>, BindGroup>,
4552
    /// Map from buffer index to its metadata bind group (group 3) for the init
4553
    /// pass.
4554
    // FIXME - doesn't work with batching; this should be the instance ID
4555
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4556
    /// Map from buffer index to its metadata bind group (group 3) for the
4557
    /// update pass.
4558
    // FIXME - doesn't work with batching; this should be the instance ID
4559
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4560
    /// Map from an effect material to its bind group.
4561
    material_bind_groups: HashMap<Material, BindGroup>,
4562
    /// Map from an event buffer index to the bind group @0 for the init fill
4563
    /// pass in charge of filling all its init dispatches.
4564
    init_fill_dispatch: HashMap<u32, BindGroup>,
4565
}
4566

4567
impl EffectBindGroups {
4568
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4569
        self.particle_buffers
×
4570
            .get(&buffer_index)
×
4571
            .map(|bg| &bg.render)
×
4572
    }
4573

4574
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4575
    /// needed.
4576
    pub(self) fn get_or_create_init_metadata(
×
4577
        &mut self,
4578
        effect_batch: &EffectBatch,
4579
        gpu_limits: &GpuLimits,
4580
        render_device: &RenderDevice,
4581
        layout: &BindGroupLayout,
4582
        effect_metadata_buffer: &Buffer,
4583
        consume_event_buffers: Option<ConsumeEventBuffers>,
4584
    ) -> Result<&BindGroup, ()> {
4585
        let DispatchBufferIndices {
×
4586
            effect_metadata_buffer_table_id,
×
4587
            ..
×
4588
        } = &effect_batch.dispatch_buffer_indices;
×
4589

4590
        let effect_metadata_offset =
×
4591
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
×
4592
        let key = InitMetadataBindGroupKey {
4593
            buffer_index: effect_batch.buffer_index,
×
4594
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4595
            effect_metadata_offset,
4596
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
×
4597
        };
4598

4599
        let make_entry = || {
×
4600
            let mut entries = Vec::with_capacity(3);
×
4601
            entries.push(
×
4602
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4603
                BindGroupEntry {
×
4604
                    binding: 0,
×
4605
                    resource: BindingResource::Buffer(BufferBinding {
×
4606
                        buffer: effect_metadata_buffer,
×
4607
                        offset: key.effect_metadata_offset as u64,
×
4608
                        size: Some(gpu_limits.effect_metadata_size()),
×
4609
                    }),
4610
                },
4611
            );
4612
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
×
4613
                entries.push(
4614
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4615
                    // ChildInfoBuffer;
4616
                    BindGroupEntry {
4617
                        binding: 1,
4618
                        resource: BindingResource::Buffer(BufferBinding {
4619
                            buffer: consume_event_buffers.child_infos_buffer,
4620
                            offset: 0,
4621
                            size: None,
4622
                        }),
4623
                    },
4624
                );
4625
                entries.push(
4626
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4627
                    BindGroupEntry {
4628
                        binding: 2,
4629
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4630
                    },
4631
                );
4632
            }
4633

4634
            let bind_group = render_device.create_bind_group(
×
4635
                "hanabi:bind_group:init:metadata@3",
4636
                layout,
×
4637
                &entries[..],
×
4638
            );
4639

4640
            trace!(
×
4641
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
×
4642
                    effect_batch.buffer_index,
4643
                    effect_metadata_buffer_table_id.0,
4644
                );
4645

4646
            bind_group
×
4647
        };
4648

4649
        Ok(&self
×
4650
            .init_metadata_bind_groups
×
4651
            .entry(effect_batch.buffer_index)
×
4652
            .and_modify(|cbg| {
×
4653
                if cbg.key != key {
×
4654
                    trace!(
×
4655
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4656
                        cbg.key,
4657
                        key
4658
                    );
4659
                    cbg.key = key;
×
4660
                    cbg.bind_group = make_entry();
×
4661
                }
4662
            })
4663
            .or_insert_with(|| {
×
4664
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
×
4665
                CachedBindGroup {
×
4666
                    key,
×
4667
                    bind_group: make_entry(),
×
4668
                }
4669
            })
4670
            .bind_group)
×
4671
    }
4672

4673
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4674
    /// needed.
4675
    pub(self) fn get_or_create_update_metadata(
×
4676
        &mut self,
4677
        effect_batch: &EffectBatch,
4678
        gpu_limits: &GpuLimits,
4679
        render_device: &RenderDevice,
4680
        layout: &BindGroupLayout,
4681
        effect_metadata_buffer: &Buffer,
4682
        child_info_buffer: Option<&Buffer>,
4683
        event_buffers: &[(Entity, BufferBindingSource)],
4684
    ) -> Result<&BindGroup, ()> {
4685
        let DispatchBufferIndices {
×
4686
            effect_metadata_buffer_table_id,
×
4687
            ..
×
4688
        } = &effect_batch.dispatch_buffer_indices;
×
4689

4690
        // Check arguments consistency
4691
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
×
4692
        let emits_gpu_spawn_events = !event_buffers.is_empty();
×
4693
        let child_info_buffer_id = if emits_gpu_spawn_events {
×
4694
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4695
        } else {
4696
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4697
            // if relevant, that is if the effect emits GPU spawn events.
4698
            None
×
4699
        };
4700
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
×
4701

4702
        let event_buffers_keys = event_buffers
×
4703
            .iter()
4704
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4705
            .collect::<Vec<_>>();
4706

4707
        let key = UpdateMetadataBindGroupKey {
4708
            buffer_index: effect_batch.buffer_index,
×
4709
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4710
            effect_metadata_offset: gpu_limits
×
4711
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4712
                as u32,
4713
            child_info_buffer_id,
4714
            event_buffers_keys,
4715
        };
4716

4717
        let make_entry = || {
×
4718
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
×
4719
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4720
            // EffectMetadata;
4721
            entries.push(BindGroupEntry {
×
4722
                binding: 0,
×
4723
                resource: BindingResource::Buffer(BufferBinding {
×
4724
                    buffer: effect_metadata_buffer,
×
4725
                    offset: key.effect_metadata_offset as u64,
×
4726
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
×
4727
                }),
4728
            });
4729
            if emits_gpu_spawn_events {
×
4730
                let child_info_buffer = child_info_buffer.unwrap();
×
4731

4732
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4733
                // ChildInfoBuffer;
4734
                entries.push(BindGroupEntry {
×
4735
                    binding: 1,
×
4736
                    resource: BindingResource::Buffer(BufferBinding {
×
4737
                        buffer: child_info_buffer,
×
4738
                        offset: 0,
×
4739
                        size: None,
×
4740
                    }),
4741
                });
4742

4743
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4744
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4745
                    // EventBuffer;
4746
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4747
                    // then moved to counting in bytes, so now need some conversion. Need to review
4748
                    // all of this...
4749
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4750
                    buffer_binding.offset *= 4;
4751
                    buffer_binding.size = buffer_binding
4752
                        .size
4753
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4754
                    entries.push(BindGroupEntry {
4755
                        binding: 2 + index as u32,
4756
                        resource: BindingResource::Buffer(buffer_binding),
4757
                    });
4758
                }
4759
            }
4760

4761
            let bind_group = render_device.create_bind_group(
×
4762
                "hanabi:bind_group:update:metadata@3",
4763
                layout,
×
4764
                &entries[..],
×
4765
            );
4766

4767
            trace!(
×
4768
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
×
4769
                effect_batch.buffer_index,
4770
                effect_metadata_buffer_table_id.0,
4771
            );
4772

4773
            bind_group
×
4774
        };
4775

4776
        Ok(&self
×
4777
            .update_metadata_bind_groups
×
4778
            .entry(effect_batch.buffer_index)
×
4779
            .and_modify(|cbg| {
×
4780
                if cbg.key != key {
×
4781
                    trace!(
×
4782
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4783
                        cbg.key,
4784
                        key
4785
                    );
4786
                    cbg.key = key.clone();
×
4787
                    cbg.bind_group = make_entry();
×
4788
                }
4789
            })
4790
            .or_insert_with(|| {
×
4791
                trace!(
×
4792
                    "Inserting new bind group for update metadata@3 with key={:?}",
×
4793
                    key
4794
                );
4795
                CachedBindGroup {
×
4796
                    key: key.clone(),
×
4797
                    bind_group: make_entry(),
×
4798
                }
4799
            })
4800
            .bind_group)
×
4801
    }
4802

4803
    pub fn init_fill_dispatch(&self, event_buffer_index: u32) -> Option<&BindGroup> {
×
4804
        self.init_fill_dispatch.get(&event_buffer_index)
×
4805
    }
4806
}
4807

4808
#[derive(SystemParam)]
4809
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4810
    #[cfg(feature = "2d")]
4811
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4812
    #[cfg(feature = "3d")]
4813
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4814
    #[cfg(feature = "3d")]
4815
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4816
    #[cfg(feature = "3d")]
4817
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4818
    #[system_param(ignore)]
4819
    marker: PhantomData<&'s usize>,
4820
}
4821

4822
fn emit_sorted_draw<T, F>(
×
4823
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
4824
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
4825
    view_entities: &mut FixedBitSet,
4826
    sorted_effect_batches: &SortedEffectBatches,
4827
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4828
    render_pipeline: &mut ParticlesRenderPipeline,
4829
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4830
    render_meshes: &RenderAssets<RenderMesh>,
4831
    pipeline_cache: &PipelineCache,
4832
    make_phase_item: F,
4833
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4834
) where
4835
    T: SortedPhaseItem,
4836
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
4837
{
4838
    trace!("emit_sorted_draw() {} views", views.iter().len());
×
4839

4840
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
4841
        trace!(
×
4842
            "Process new sorted view with {} visible particle effect entities",
×
4843
            visible_entities.len::<WithCompiledParticleEffect>()
×
4844
        );
4845

4846
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4847
            continue;
×
4848
        };
4849

4850
        {
4851
            #[cfg(feature = "trace")]
4852
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4853

4854
            view_entities.clear();
×
4855
            view_entities.extend(
×
4856
                visible_entities
×
4857
                    .iter::<WithCompiledParticleEffect>()
×
4858
                    .map(|e| e.1.index() as usize),
×
4859
            );
4860
        }
4861

4862
        // For each view, loop over all the effect batches to determine if the effect
4863
        // needs to be rendered for that view, and enqueue a view-dependent
4864
        // batch if so.
4865
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
4866
            #[cfg(feature = "trace")]
4867
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
4868

4869
            trace!(
×
4870
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4871
                draw_entity,
×
4872
                draw_batch.effect_batch_index,
×
4873
            );
4874

4875
            // Get the EffectBatches this EffectDrawBatch is part of.
4876
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4877
            else {
×
4878
                continue;
×
4879
            };
4880

4881
            trace!(
×
4882
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4883
                effect_batch.buffer_index,
×
4884
                effect_batch.spawner_base,
×
4885
                effect_batch.layout_flags,
×
4886
            );
4887

4888
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4889
            if effect_batch
×
4890
                .layout_flags
×
4891
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4892
            {
4893
                trace!("Non-transparent batch. Skipped.");
×
4894
                continue;
×
4895
            }
4896

4897
            // Check if batch contains any entity visible in the current view. Otherwise we
4898
            // can skip the entire batch. Note: This is O(n^2) but (unlike
4899
            // the Sprite renderer this is inspired from) we don't expect more than
4900
            // a handful of particle effect instances, so would rather not pay the memory
4901
            // cost of a FixedBitSet for the sake of an arguable speed-up.
4902
            // TODO - Profile to confirm.
4903
            #[cfg(feature = "trace")]
4904
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
4905
            let has_visible_entity = effect_batch
×
4906
                .entities
×
4907
                .iter()
4908
                .any(|index| view_entities.contains(*index as usize));
×
4909
            if !has_visible_entity {
×
4910
                trace!("No visible entity for view, not emitting any draw call.");
×
4911
                continue;
×
4912
            }
4913
            #[cfg(feature = "trace")]
4914
            _span_check_vis.exit();
×
4915

4916
            // Create and cache the bind group layout for this texture layout
4917
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4918

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

4922
            let local_space_simulation = effect_batch
×
4923
                .layout_flags
×
4924
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
4925
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
4926
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
4927
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
4928
            let needs_normal = effect_batch
×
4929
                .layout_flags
×
4930
                .contains(LayoutFlags::NEEDS_NORMAL);
×
4931
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
4932
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
4933

4934
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4935
            // re-querying here...?
4936
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
×
4937
                trace!("Batch has no render mesh, skipped.");
×
4938
                continue;
×
4939
            };
4940
            let mesh_layout = render_mesh.layout.clone();
×
4941

4942
            // Specialize the render pipeline based on the effect batch
4943
            trace!(
×
4944
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
4945
                effect_batch.render_shader,
×
4946
                image_count,
×
4947
                alpha_mask,
×
4948
                flipbook,
×
4949
                view.hdr
×
4950
            );
4951

4952
            // Add a draw pass for the effect batch
4953
            trace!("Emitting individual draw for batch");
×
4954

4955
            let alpha_mode = effect_batch.alpha_mode;
×
4956

4957
            #[cfg(feature = "trace")]
4958
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
4959
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
4960
                pipeline_cache,
×
4961
                render_pipeline,
×
4962
                ParticleRenderPipelineKey {
×
4963
                    shader: effect_batch.render_shader.clone(),
×
4964
                    mesh_layout: Some(mesh_layout),
×
4965
                    particle_layout: effect_batch.particle_layout.clone(),
×
4966
                    texture_layout: effect_batch.texture_layout.clone(),
×
4967
                    local_space_simulation,
×
4968
                    alpha_mask,
×
4969
                    alpha_mode,
×
4970
                    flipbook,
×
4971
                    needs_uv,
×
4972
                    needs_normal,
×
4973
                    ribbons,
×
4974
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
4975
                    pipeline_mode,
×
4976
                    msaa_samples: msaa.samples(),
×
4977
                    hdr: view.hdr,
×
4978
                },
4979
            );
4980
            #[cfg(feature = "trace")]
4981
            _span_specialize.exit();
×
4982

4983
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
4984
            trace!(
×
4985
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
4986
                spawner_base={} handle={:?}",
×
4987
                draw_entity,
×
4988
                effect_batch.buffer_index,
×
4989
                effect_batch.spawner_base,
×
4990
                effect_batch.handle
×
4991
            );
4992
            render_phase.add(make_phase_item(
×
4993
                render_pipeline_id,
×
4994
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
4995
                draw_batch,
×
4996
                view,
×
4997
            ));
4998
        }
4999
    }
5000
}
5001

5002
#[cfg(feature = "3d")]
5003
fn emit_binned_draw<T, F>(
×
5004
    views: &Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
5005
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5006
    view_entities: &mut FixedBitSet,
5007
    sorted_effect_batches: &SortedEffectBatches,
5008
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5009
    render_pipeline: &mut ParticlesRenderPipeline,
5010
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5011
    pipeline_cache: &PipelineCache,
5012
    render_meshes: &RenderAssets<RenderMesh>,
5013
    make_bin_key: F,
5014
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5015
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5016
) where
5017
    T: BinnedPhaseItem,
5018
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BinKey,
5019
{
5020
    use bevy::render::render_phase::BinnedRenderPhaseType;
5021

5022
    trace!("emit_binned_draw() {} views", views.iter().len());
×
5023

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

5027
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
5028
            continue;
×
5029
        };
5030

5031
        {
5032
            #[cfg(feature = "trace")]
5033
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
5034

5035
            view_entities.clear();
×
5036
            view_entities.extend(
×
5037
                visible_entities
×
5038
                    .iter::<WithCompiledParticleEffect>()
×
5039
                    .map(|e| e.1.index() as usize),
×
5040
            );
5041
        }
5042

5043
        // For each view, loop over all the effect batches to determine if the effect
5044
        // needs to be rendered for that view, and enqueue a view-dependent
5045
        // batch if so.
5046
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
5047
            #[cfg(feature = "trace")]
5048
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
5049

5050
            trace!(
×
5051
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5052
                draw_entity,
×
5053
                draw_batch.effect_batch_index,
×
5054
            );
5055

5056
            // Get the EffectBatches this EffectDrawBatch is part of.
5057
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5058
            else {
×
5059
                continue;
×
5060
            };
5061

5062
            trace!(
×
5063
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5064
                effect_batch.buffer_index,
×
5065
                effect_batch.spawner_base,
×
5066
                effect_batch.layout_flags,
×
5067
            );
5068

5069
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5070
                trace!(
×
5071
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5072
                    effect_batch.layout_flags,
×
5073
                    alpha_mask
×
5074
                );
5075
                continue;
×
5076
            }
5077

5078
            // Check if batch contains any entity visible in the current view. Otherwise we
5079
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5080
            // the Sprite renderer this is inspired from) we don't expect more than
5081
            // a handful of particle effect instances, so would rather not pay the memory
5082
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5083
            // TODO - Profile to confirm.
5084
            #[cfg(feature = "trace")]
5085
            let _span_check_vis = bevy::utils::tracing::info_span!("check_visibility").entered();
×
5086
            let has_visible_entity = effect_batch
×
5087
                .entities
×
5088
                .iter()
5089
                .any(|index| view_entities.contains(*index as usize));
×
5090
            if !has_visible_entity {
×
5091
                trace!("No visible entity for view, not emitting any draw call.");
×
5092
                continue;
×
5093
            }
5094
            #[cfg(feature = "trace")]
5095
            _span_check_vis.exit();
×
5096

5097
            // Create and cache the bind group layout for this texture layout
5098
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5099

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

5103
            let local_space_simulation = effect_batch
×
5104
                .layout_flags
×
5105
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5106
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5107
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5108
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5109
            let needs_normal = effect_batch
×
5110
                .layout_flags
×
5111
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5112
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5113
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5114
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5115

5116
            // Specialize the render pipeline based on the effect batch
5117
            trace!(
×
5118
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5119
                effect_batch.render_shader,
×
5120
                image_count,
×
5121
                alpha_mask,
×
5122
                flipbook,
×
5123
                view.hdr
×
5124
            );
5125

5126
            // Add a draw pass for the effect batch
5127
            trace!("Emitting individual draw for batch");
×
5128

5129
            let alpha_mode = effect_batch.alpha_mode;
×
5130

5131
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5132
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5133
                continue;
×
5134
            };
5135

5136
            #[cfg(feature = "trace")]
5137
            let _span_specialize = bevy::utils::tracing::info_span!("specialize").entered();
×
5138
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5139
                pipeline_cache,
×
5140
                render_pipeline,
×
5141
                ParticleRenderPipelineKey {
×
5142
                    shader: effect_batch.render_shader.clone(),
×
5143
                    mesh_layout: Some(mesh_layout),
×
5144
                    particle_layout: effect_batch.particle_layout.clone(),
×
5145
                    texture_layout: effect_batch.texture_layout.clone(),
×
5146
                    local_space_simulation,
×
5147
                    alpha_mask,
×
5148
                    alpha_mode,
×
5149
                    flipbook,
×
5150
                    needs_uv,
×
5151
                    needs_normal,
×
5152
                    ribbons,
×
5153
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5154
                    pipeline_mode,
×
5155
                    msaa_samples: msaa.samples(),
×
5156
                    hdr: view.hdr,
×
5157
                },
5158
            );
5159
            #[cfg(feature = "trace")]
5160
            _span_specialize.exit();
×
5161

5162
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5163
            trace!(
×
5164
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5165
                spawner_base={} handle={:?}",
×
5166
                draw_entity,
×
5167
                effect_batch.buffer_index,
×
5168
                effect_batch.spawner_base,
×
5169
                effect_batch.handle
×
5170
            );
5171
            render_phase.add(
×
5172
                make_bin_key(render_pipeline_id, draw_batch, view),
×
5173
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5174
                BinnedRenderPhaseType::NonMesh,
×
5175
            );
5176
        }
5177
    }
5178
}
5179

5180
#[allow(clippy::too_many_arguments)]
5181
pub(crate) fn queue_effects(
×
5182
    views: Query<(Entity, &RenderVisibleEntities, &ExtractedView, &Msaa)>,
5183
    effects_meta: Res<EffectsMeta>,
5184
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5185
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5186
    pipeline_cache: Res<PipelineCache>,
5187
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5188
    sorted_effect_batches: Res<SortedEffectBatches>,
5189
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5190
    events: Res<EffectAssetEvents>,
5191
    render_meshes: Res<RenderAssets<RenderMesh>>,
5192
    read_params: QueueEffectsReadOnlyParams,
5193
    mut view_entities: Local<FixedBitSet>,
5194
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5195
        ViewSortedRenderPhases<Transparent2d>,
5196
    >,
5197
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5198
        ViewSortedRenderPhases<Transparent3d>,
5199
    >,
5200
    #[cfg(feature = "3d")] mut alpha_mask_3d_render_phases: ResMut<
5201
        ViewBinnedRenderPhases<AlphaMask3d>,
5202
    >,
5203
) {
5204
    #[cfg(feature = "trace")]
5205
    let _span = bevy::utils::tracing::info_span!("hanabi:queue_effects").entered();
×
5206

5207
    trace!("queue_effects");
×
5208

5209
    // If an image has changed, the GpuImage has (probably) changed
5210
    for event in &events.images {
×
5211
        match event {
5212
            AssetEvent::Added { .. } => None,
×
5213
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5214
            AssetEvent::Unused { .. } => None,
×
5215
            AssetEvent::Modified { id } => {
×
5216
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5217
                effect_bind_groups.images.remove(id)
×
5218
            }
5219
            AssetEvent::Removed { id } => {
×
5220
                trace!("Destroy bind group of removed image asset {:?}", id);
×
5221
                effect_bind_groups.images.remove(id)
×
5222
            }
5223
        };
5224
    }
5225

5226
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5227
        // No spawners are active
5228
        return;
×
5229
    }
5230

5231
    // Loop over all 2D cameras/views that need to render effects
5232
    #[cfg(feature = "2d")]
5233
    {
5234
        #[cfg(feature = "trace")]
5235
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
5236

5237
        let draw_effects_function_2d = read_params
5238
            .draw_functions_2d
5239
            .read()
5240
            .get_id::<DrawEffects>()
5241
            .unwrap();
5242

5243
        // Effects with full alpha blending
5244
        if !views.is_empty() {
5245
            trace!("Emit effect draw calls for alpha blended 2D views...");
×
5246
            emit_sorted_draw(
5247
                &views,
5248
                &mut transparent_2d_render_phases,
5249
                &mut view_entities,
5250
                &sorted_effect_batches,
5251
                &effect_draw_batches,
5252
                &mut render_pipeline,
5253
                specialized_render_pipelines.reborrow(),
5254
                &render_meshes,
5255
                &pipeline_cache,
5256
                |id, entity, draw_batch, _view| Transparent2d {
×
5257
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5258
                    entity,
×
5259
                    pipeline: id,
×
5260
                    draw_function: draw_effects_function_2d,
×
5261
                    batch_range: 0..1,
×
5262
                    extra_index: PhaseItemExtraIndex::NONE,
×
5263
                },
5264
                #[cfg(feature = "3d")]
5265
                PipelineMode::Camera2d,
5266
            );
5267
        }
5268
    }
5269

5270
    // Loop over all 3D cameras/views that need to render effects
5271
    #[cfg(feature = "3d")]
5272
    {
5273
        #[cfg(feature = "trace")]
5274
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
5275

5276
        // Effects with full alpha blending
5277
        if !views.is_empty() {
5278
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5279

5280
            let draw_effects_function_3d = read_params
5281
                .draw_functions_3d
5282
                .read()
5283
                .get_id::<DrawEffects>()
5284
                .unwrap();
5285

5286
            emit_sorted_draw(
5287
                &views,
5288
                &mut transparent_3d_render_phases,
5289
                &mut view_entities,
5290
                &sorted_effect_batches,
5291
                &effect_draw_batches,
5292
                &mut render_pipeline,
5293
                specialized_render_pipelines.reborrow(),
5294
                &render_meshes,
5295
                &pipeline_cache,
5296
                |id, entity, batch, view| Transparent3d {
×
5297
                    draw_function: draw_effects_function_3d,
×
5298
                    pipeline: id,
×
5299
                    entity,
×
5300
                    distance: view
×
5301
                        .rangefinder3d()
×
5302
                        .distance_translation(&batch.translation),
×
5303
                    batch_range: 0..1,
×
5304
                    extra_index: PhaseItemExtraIndex::NONE,
×
5305
                },
5306
                #[cfg(feature = "2d")]
5307
                PipelineMode::Camera3d,
5308
            );
5309
        }
5310

5311
        // Effects with alpha mask
5312
        if !views.is_empty() {
5313
            #[cfg(feature = "trace")]
5314
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
5315

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

5318
            let draw_effects_function_alpha_mask = read_params
5319
                .draw_functions_alpha_mask
5320
                .read()
5321
                .get_id::<DrawEffects>()
5322
                .unwrap();
5323

5324
            emit_binned_draw(
5325
                &views,
5326
                &mut alpha_mask_3d_render_phases,
5327
                &mut view_entities,
5328
                &sorted_effect_batches,
5329
                &effect_draw_batches,
5330
                &mut render_pipeline,
5331
                specialized_render_pipelines.reborrow(),
5332
                &pipeline_cache,
5333
                &render_meshes,
5334
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5335
                    pipeline: id,
×
5336
                    draw_function: draw_effects_function_alpha_mask,
×
5337
                    asset_id: AssetId::<Image>::default().untyped(),
×
5338
                    material_bind_group_id: None,
×
5339
                    // },
5340
                    // distance: view
5341
                    //     .rangefinder3d()
5342
                    //     .distance_translation(&batch.translation_3d),
5343
                    // batch_range: 0..1,
5344
                    // extra_index: PhaseItemExtraIndex::NONE,
5345
                },
5346
                #[cfg(feature = "2d")]
5347
                PipelineMode::Camera3d,
5348
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5349
            );
5350
        }
5351

5352
        // Opaque particles
5353
        if !views.is_empty() {
5354
            #[cfg(feature = "trace")]
5355
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
5356

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

5359
            let draw_effects_function_opaque = read_params
5360
                .draw_functions_opaque
5361
                .read()
5362
                .get_id::<DrawEffects>()
5363
                .unwrap();
5364

5365
            emit_binned_draw(
5366
                &views,
5367
                &mut alpha_mask_3d_render_phases,
5368
                &mut view_entities,
5369
                &sorted_effect_batches,
5370
                &effect_draw_batches,
5371
                &mut render_pipeline,
5372
                specialized_render_pipelines.reborrow(),
5373
                &pipeline_cache,
5374
                &render_meshes,
5375
                |id, _batch, _view| OpaqueNoLightmap3dBinKey {
×
5376
                    pipeline: id,
×
5377
                    draw_function: draw_effects_function_opaque,
×
5378
                    asset_id: AssetId::<Image>::default().untyped(),
×
5379
                    material_bind_group_id: None,
×
5380
                    // },
5381
                    // distance: view
5382
                    //     .rangefinder3d()
5383
                    //     .distance_translation(&batch.translation_3d),
5384
                    // batch_range: 0..1,
5385
                    // extra_index: PhaseItemExtraIndex::NONE,
5386
                },
5387
                #[cfg(feature = "2d")]
5388
                PipelineMode::Camera3d,
5389
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5390
            );
5391
        }
5392
    }
5393
}
5394

5395
/// Prepare GPU resources for effect rendering.
5396
///
5397
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5398
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5399
/// access to the current camera view.
5400
pub(crate) fn prepare_gpu_resources(
×
5401
    mut effects_meta: ResMut<EffectsMeta>,
5402
    //mut effect_cache: ResMut<EffectCache>,
5403
    mut event_cache: ResMut<EventCache>,
5404
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5405
    mut sort_bind_groups: ResMut<SortBindGroups>,
5406
    render_device: Res<RenderDevice>,
5407
    render_queue: Res<RenderQueue>,
5408
    view_uniforms: Res<ViewUniforms>,
5409
    render_pipeline: Res<ParticlesRenderPipeline>,
5410
) {
5411
    // Get the binding for the ViewUniform, the uniform data structure containing
5412
    // the Camera data for the current view. If not available, we cannot render
5413
    // anything.
5414
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
×
5415
        return;
×
5416
    };
5417

5418
    // Create the bind group for the camera/view parameters
5419
    // FIXME - Not here!
5420
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5421
        "hanabi:bind_group_camera_view",
5422
        &render_pipeline.view_layout,
5423
        &[
5424
            BindGroupEntry {
5425
                binding: 0,
5426
                resource: view_binding,
5427
            },
5428
            BindGroupEntry {
5429
                binding: 1,
5430
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5431
            },
5432
        ],
5433
    ));
5434

5435
    // Re-/allocate any GPU buffer if needed
5436
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5437
    // effect_bind_groups);
5438
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5439
    sort_bind_groups.prepare_buffers(&render_device);
5440
}
5441

5442
pub(crate) fn prepare_bind_groups(
×
5443
    mut effects_meta: ResMut<EffectsMeta>,
5444
    mut effect_cache: ResMut<EffectCache>,
5445
    mut event_cache: ResMut<EventCache>,
5446
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5447
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5448
    mut sort_bind_groups: ResMut<SortBindGroups>,
5449
    property_cache: Res<PropertyCache>,
5450
    sorted_effect_batched: Res<SortedEffectBatches>,
5451
    render_device: Res<RenderDevice>,
5452
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5453
    utils_pipeline: Res<UtilsPipeline>,
5454
    update_pipeline: Res<ParticlesUpdatePipeline>,
5455
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5456
    gpu_images: Res<RenderAssets<GpuImage>>,
5457
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperationQueue>,
5458
) {
5459
    // We can't simulate nor render anything without at least the spawner buffer
5460
    if effects_meta.spawner_buffer.is_empty() {
×
5461
        return;
×
5462
    }
5463
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5464
        return;
×
5465
    };
5466

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

5472
    {
5473
        #[cfg(feature = "trace")]
5474
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
5475

5476
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5477
        // loop below. Also allows earlying out before doing any work in case some
5478
        // buffer is missing.
5479
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5480
            return;
×
5481
        };
5482

5483
        // Create the sim_params@0 bind group for the global simulation parameters,
5484
        // which is shared by the init and update passes.
5485
        if effects_meta.indirect_sim_params_bind_group.is_none() {
×
5486
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
×
5487
                "hanabi:bind_group:vfx_indirect:sim_params@0",
×
5488
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
×
5489
                &[BindGroupEntry {
×
5490
                    binding: 0,
×
5491
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
5492
                }],
5493
            ));
5494
        }
5495

5496
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5497
        // effects at once
5498
        effects_meta.indirect_metadata_bind_group = match (
5499
            effects_meta.effect_metadata_buffer.buffer(),
5500
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5501
        ) {
5502
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
×
5503
                // Base bind group for indirect pass
5504
                Some(render_device.create_bind_group(
×
5505
                    "hanabi:bind_group:vfx_indirect:metadata@1",
×
5506
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
×
5507
                    &[
×
5508
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5509
                        BindGroupEntry {
×
5510
                            binding: 0,
×
5511
                            resource: BindingResource::Buffer(BufferBinding {
×
5512
                                buffer: effect_metadata_buffer,
×
5513
                                offset: 0,
×
5514
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
×
5515
                            }),
5516
                        },
5517
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5518
                        BindGroupEntry {
×
5519
                            binding: 1,
×
5520
                            resource: BindingResource::Buffer(BufferBinding {
×
5521
                                buffer: dispatch_indirect_buffer,
×
5522
                                offset: 0,
×
5523
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
×
5524
                            }),
5525
                        },
5526
                    ],
5527
                ))
5528
            }
5529

5530
            // Some buffer is not yet available, can't create the bind group
5531
            _ => None,
×
5532
        };
5533

5534
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5535
        // effects at once
5536
        if effects_meta.indirect_spawner_bind_group.is_none() {
×
5537
            let bind_group = render_device.create_bind_group(
×
5538
                "hanabi:bind_group:vfx_indirect:spawner@2",
5539
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
×
5540
                &[
×
5541
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5542
                    BindGroupEntry {
×
5543
                        binding: 0,
×
5544
                        resource: BindingResource::Buffer(BufferBinding {
×
5545
                            buffer: &spawner_buffer,
×
5546
                            offset: 0,
×
5547
                            size: None,
×
5548
                        }),
5549
                    },
5550
                ],
5551
            );
5552

5553
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5554
        }
5555
    }
5556

5557
    // Create the per-buffer bind groups
5558
    trace!("Create per-buffer bind groups...");
×
5559
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
×
5560
        #[cfg(feature = "trace")]
5561
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
5562

5563
        let Some(effect_buffer) = effect_buffer else {
×
5564
            trace!(
×
5565
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5566
                buffer_index
5567
            );
5568
            continue;
×
5569
        };
5570

5571
        // Ensure all effects in this batch have a bind group for the entire buffer of
5572
        // the group, since the update phase runs on an entire group/buffer at once,
5573
        // with all the effect instances in it batched together.
5574
        trace!("effect particle buffer_index=#{}", buffer_index);
×
5575
        effect_bind_groups
5576
            .particle_buffers
5577
            .entry(buffer_index as u32)
5578
            .or_insert_with(|| {
×
5579
                // Bind group particle@1 for render pass
5580
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
×
5581
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
×
5582
                    render_device.limits().min_storage_buffer_offset_alignment,
×
5583
                );
5584
                let entries = [
×
5585
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5586
                    BindGroupEntry {
×
5587
                        binding: 0,
×
5588
                        resource: effect_buffer.max_binding(),
×
5589
                    },
5590
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5591
                    BindGroupEntry {
×
5592
                        binding: 1,
×
5593
                        resource: effect_buffer.indirect_index_max_binding(),
×
5594
                    },
5595
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5596
                    BindGroupEntry {
×
5597
                        binding: 2,
×
5598
                        resource: BindingResource::Buffer(BufferBinding {
×
5599
                            buffer: &spawner_buffer,
×
5600
                            offset: 0,
×
5601
                            size: Some(spawner_min_binding_size),
×
5602
                        }),
5603
                    },
5604
                ];
5605
                let render = render_device.create_bind_group(
×
5606
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
×
5607
                    effect_buffer.render_particles_buffer_layout(),
×
5608
                    &entries[..],
×
5609
                );
5610

5611
                BufferBindGroups { render }
×
5612
            });
5613
    }
5614

5615
    // Create bind groups for queued GPU buffer operations
5616
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5617

5618
    // Create the per-event-buffer bind groups
5619
    for (event_buffer_index, event_buffer) in event_cache.buffers().iter().enumerate() {
×
5620
        if event_buffer.is_none() {
5621
            trace!(
×
5622
                "Event buffer index #{event_buffer_index} has no allocated EventBuffer, skipped.",
×
5623
            );
5624
            continue;
×
5625
        }
5626
        let event_buffer_index = event_buffer_index as u32;
×
5627

5628
        // Check if the entry is missing
5629
        let entry = effect_bind_groups
×
5630
            .init_fill_dispatch
×
5631
            .entry(event_buffer_index);
×
5632
        if matches!(entry, Entry::Vacant(_)) {
×
5633
            trace!(
×
5634
                "Event buffer #{} missing a bind group @0 for init fill args. Trying to create now...",
×
5635
                event_buffer_index
5636
            );
5637

5638
            // Check if the binding is available to create the bind group and fill the entry
5639
            let Some((args_binding, args_count)) =
×
5640
                gpu_buffer_operation_queue.init_args_buffer_binding(event_buffer_index)
×
5641
            else {
5642
                continue;
×
5643
            };
5644

5645
            let Some(source_binding_resource) = event_cache.child_infos().max_binding() else {
×
5646
                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.");
×
5647
                continue;
×
5648
            };
5649

5650
            let Some(target_binding_resource) =
×
5651
                event_cache.init_indirect_dispatch_binding_resource()
5652
            else {
5653
                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.");
×
5654
                continue;
×
5655
            };
5656

5657
            // Actually create the new bind group entry
5658
            entry.insert(render_device.create_bind_group(
5659
                &format!("hanabi:bind_group:init_fill_dispatch@0:event{event_buffer_index}")[..],
5660
                &utils_pipeline.bind_group_layout,
5661
                &[
5662
                    // @group(0) @binding(0) var<uniform> args : BufferOperationArgs
5663
                    BindGroupEntry {
5664
                        binding: 0,
5665
                        resource: args_binding,
5666
                    },
5667
                    // @group(0) @binding(1) var<storage, read> src_buffer : array<u32>
5668
                    BindGroupEntry {
5669
                        binding: 1,
5670
                        resource: source_binding_resource,
5671
                    },
5672
                    // @group(0) @binding(2) var<storage, read_write> dst_buffer :
5673
                    // array<u32>
5674
                    BindGroupEntry {
5675
                        binding: 2,
5676
                        resource: target_binding_resource,
5677
                    },
5678
                ],
5679
            ));
5680
            trace!(
5681
                "Created new bind group for init fill args of event buffer #{}",
×
5682
                event_buffer_index
5683
            );
5684
        }
5685
    }
5686

5687
    // Create the per-effect bind groups
5688
    let spawner_buffer_binding_size =
5689
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5690
    for effect_batch in sorted_effect_batched.iter() {
×
5691
        #[cfg(feature = "trace")]
5692
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
5693

5694
        // Create the property bind group @2 if needed
5695
        if let Some(property_key) = &effect_batch.property_key {
×
5696
            if let Err(err) = property_bind_groups.ensure_exists(
×
5697
                property_key,
5698
                &property_cache,
5699
                &spawner_buffer,
5700
                spawner_buffer_binding_size,
5701
                &render_device,
5702
            ) {
5703
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5704
                continue;
5705
            }
5706
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
×
5707
            &property_cache,
×
5708
            &spawner_buffer,
×
5709
            spawner_buffer_binding_size,
×
5710
            &render_device,
×
5711
        ) {
5712
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5713
            continue;
5714
        }
5715

5716
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5717
        // simulate particles.
5718
        if effect_cache
×
5719
            .create_particle_sim_bind_group(
5720
                effect_batch.buffer_index,
×
5721
                &render_device,
×
5722
                effect_batch.particle_layout.min_binding_size32(),
×
5723
                effect_batch.parent_min_binding_size,
×
5724
                effect_batch.parent_binding_source.as_ref(),
×
5725
            )
5726
            .is_err()
5727
        {
5728
            error!("No particle buffer allocated for effect batch.");
×
5729
            continue;
×
5730
        }
5731

5732
        // Bind group @3 of init pass
5733
        // FIXME - this is instance-dependent, not buffer-dependent
5734
        {
5735
            let consume_gpu_spawn_events = effect_batch
×
5736
                .layout_flags
×
5737
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
5738
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
×
5739
                effect_batch.spawn_info
5740
            {
5741
                assert!(consume_gpu_spawn_events);
×
5742
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
5743
                Some(ConsumeEventBuffers {
×
5744
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
5745
                    events: BufferSlice {
×
5746
                        buffer: event_cache
×
5747
                            .get_buffer(cached_effect_events.buffer_index)
×
5748
                            .unwrap(),
×
5749
                        // Note: event range is in u32 count, not bytes
5750
                        offset: cached_effect_events.range.start * 4,
×
5751
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5752
                    },
5753
                })
5754
            } else {
5755
                assert!(!consume_gpu_spawn_events);
×
5756
                None
×
5757
            };
5758
            let Some(init_metadata_layout) =
×
5759
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
5760
            else {
5761
                continue;
×
5762
            };
5763
            if effect_bind_groups
5764
                .get_or_create_init_metadata(
5765
                    effect_batch,
5766
                    &effects_meta.gpu_limits,
5767
                    &render_device,
5768
                    init_metadata_layout,
5769
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5770
                    consume_event_buffers,
5771
                )
5772
                .is_err()
5773
            {
5774
                continue;
×
5775
            }
5776
        }
5777

5778
        // Bind group @3 of update pass
5779
        // FIXME - this is instance-dependent, not buffer-dependent#
5780
        {
5781
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
5782

5783
            let Some(update_metadata_layout) =
×
5784
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5785
            else {
5786
                continue;
×
5787
            };
5788
            if effect_bind_groups
5789
                .get_or_create_update_metadata(
5790
                    effect_batch,
5791
                    &effects_meta.gpu_limits,
5792
                    &render_device,
5793
                    update_metadata_layout,
5794
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5795
                    event_cache.child_infos_buffer(),
5796
                    &effect_batch.child_event_buffers[..],
5797
                )
5798
                .is_err()
5799
            {
5800
                continue;
×
5801
            }
5802
        }
5803

5804
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5805
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5806

5807
            // Bind group @0 of sort-fill pass
5808
            let particle_buffer = effect_buffer.particle_buffer();
×
5809
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5810
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
5811
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
5812
                &effect_batch.particle_layout,
×
5813
                particle_buffer,
×
5814
                indirect_index_buffer,
×
5815
                effect_metadata_buffer,
×
5816
            ) {
5817
                error!(
5818
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
5819
                    err
5820
                );
5821
                continue;
5822
            }
5823

5824
            // Bind group @0 of sort-copy pass
5825
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5826
            if let Err(err) = sort_bind_groups
×
5827
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5828
            {
5829
                error!(
5830
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5831
                    err
5832
                );
5833
                continue;
5834
            }
5835
        }
5836

5837
        // Ensure the particle texture(s) are available as GPU resources and that a bind
5838
        // group for them exists
5839
        // FIXME fix this insert+get below
5840
        if !effect_batch.texture_layout.layout.is_empty() {
×
5841
            // This should always be available, as this is cached into the render pipeline
5842
            // just before we start specializing it.
5843
            let Some(material_bind_group_layout) =
×
5844
                render_pipeline.get_material(&effect_batch.texture_layout)
×
5845
            else {
5846
                error!(
×
5847
                    "Failed to find material bind group layout for buffer #{}",
×
5848
                    effect_batch.buffer_index
5849
                );
5850
                continue;
×
5851
            };
5852

5853
            // TODO = move
5854
            let material = Material {
5855
                layout: effect_batch.texture_layout.clone(),
5856
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5857
            };
5858
            assert_eq!(material.layout.layout.len(), material.textures.len());
5859

5860
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5861
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5862
                trace!(
×
5863
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5864
                    material
5865
                );
5866
                continue;
×
5867
            };
5868

5869
            effect_bind_groups
5870
                .material_bind_groups
5871
                .entry(material.clone())
5872
                .or_insert_with(|| {
×
5873
                    debug!("Creating material bind group for material {:?}", material);
×
5874
                    render_device.create_bind_group(
×
5875
                        &format!(
×
5876
                            "hanabi:material_bind_group_{}",
×
5877
                            material.layout.layout.len()
×
5878
                        )[..],
×
5879
                        material_bind_group_layout,
×
5880
                        &bind_group_entries[..],
×
5881
                    )
5882
                });
5883
        }
5884
    }
5885
}
5886

5887
type DrawEffectsSystemState = SystemState<(
5888
    SRes<EffectsMeta>,
5889
    SRes<EffectBindGroups>,
5890
    SRes<PipelineCache>,
5891
    SRes<RenderAssets<RenderMesh>>,
5892
    SRes<MeshAllocator>,
5893
    SQuery<Read<ViewUniformOffset>>,
5894
    SRes<SortedEffectBatches>,
5895
    SQuery<Read<EffectDrawBatch>>,
5896
)>;
5897

5898
/// Draw function for rendering all active effects for the current frame.
5899
///
5900
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5901
/// and the [`Transparent3d`] phase of the main 3D pass.
5902
pub(crate) struct DrawEffects {
5903
    params: DrawEffectsSystemState,
5904
}
5905

5906
impl DrawEffects {
5907
    pub fn new(world: &mut World) -> Self {
×
5908
        Self {
5909
            params: SystemState::new(world),
×
5910
        }
5911
    }
5912
}
5913

5914
/// Draw all particles of a single effect in view, in 2D or 3D.
5915
///
5916
/// FIXME: use pipeline ID to look up which group index it is.
5917
fn draw<'w>(
×
5918
    world: &'w World,
5919
    pass: &mut TrackedRenderPass<'w>,
5920
    view: Entity,
5921
    entity: (Entity, MainEntity),
5922
    pipeline_id: CachedRenderPipelineId,
5923
    params: &mut DrawEffectsSystemState,
5924
) {
5925
    let (
×
5926
        effects_meta,
×
5927
        effect_bind_groups,
×
5928
        pipeline_cache,
×
5929
        meshes,
×
5930
        mesh_allocator,
×
5931
        views,
×
5932
        sorted_effect_batches,
×
5933
        effect_draw_batches,
×
5934
    ) = params.get(world);
×
5935
    let view_uniform = views.get(view).unwrap();
×
5936
    let effects_meta = effects_meta.into_inner();
×
5937
    let effect_bind_groups = effect_bind_groups.into_inner();
×
5938
    let meshes = meshes.into_inner();
×
5939
    let mesh_allocator = mesh_allocator.into_inner();
×
5940
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
×
5941
    let effect_batch = sorted_effect_batches
×
5942
        .get(effect_draw_batch.effect_batch_index)
×
5943
        .unwrap();
5944

5945
    let gpu_limits = &effects_meta.gpu_limits;
×
5946

5947
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5948
        return;
×
5949
    };
5950

5951
    trace!("render pass");
×
5952

5953
    pass.set_render_pipeline(pipeline);
×
5954

5955
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5956
        return;
×
5957
    };
5958
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5959
        return;
×
5960
    };
5961

5962
    // Vertex buffer containing the particle model to draw. Generally a quad.
5963
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5964
    // "base_vertex" in the indirect struct...
5965
    assert_eq!(effect_batch.mesh_buffer_id, vertex_buffer_slice.buffer.id());
×
5966
    assert_eq!(effect_batch.mesh_slice, vertex_buffer_slice.range);
×
5967
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5968

5969
    // View properties (camera matrix, etc.)
5970
    pass.set_bind_group(
×
5971
        0,
5972
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5973
        &[view_uniform.offset],
×
5974
    );
5975

5976
    // Particles buffer
5977
    let spawner_base = effect_batch.spawner_base;
×
5978
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5979
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5980
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
5981
    pass.set_bind_group(
×
5982
        1,
5983
        effect_bind_groups
×
5984
            .particle_render(effect_batch.buffer_index)
×
5985
            .unwrap(),
×
5986
        &[spawner_offset],
×
5987
    );
5988

5989
    // Particle texture
5990
    // TODO = move
5991
    let material = Material {
5992
        layout: effect_batch.texture_layout.clone(),
×
5993
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5994
    };
5995
    if !effect_batch.texture_layout.layout.is_empty() {
×
5996
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
5997
            pass.set_bind_group(2, bind_group, &[]);
×
5998
        } else {
5999
            // Texture(s) not ready; skip this drawing for now
6000
            trace!(
×
6001
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
6002
                effect_batch.buffer_index,
×
6003
            );
6004
            return;
×
6005
        }
6006
    }
6007

6008
    let effect_metadata_index = effect_batch
×
6009
        .dispatch_buffer_indices
×
6010
        .effect_metadata_buffer_table_id
×
6011
        .0;
×
6012
    let effect_metadata_offset =
×
6013
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
6014
    trace!(
×
6015
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
6016
            (effect_metadata_index={}, offset={}B).",
×
6017
        effect_batch.slice.len(),
×
6018
        render_mesh.vertex_count,
×
6019
        effect_batch.buffer_index,
×
6020
        effect_metadata_index,
×
6021
        effect_metadata_offset,
×
6022
    );
6023

6024
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
6025
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
6026
        trace!(
×
6027
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6028
            effect_batch.buffer_index,
×
6029
        );
6030
        return;
×
6031
    };
6032

6033
    match render_mesh.buffer_info {
×
6034
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
6035
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
6036
            else {
×
6037
                return;
×
6038
            };
6039

6040
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6041
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6042
        }
6043
        RenderMeshBufferInfo::NonIndexed => {
×
6044
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6045
        }
6046
    }
6047
}
6048

6049
#[cfg(feature = "2d")]
6050
impl Draw<Transparent2d> for DrawEffects {
6051
    fn draw<'w>(
×
6052
        &mut self,
6053
        world: &'w World,
6054
        pass: &mut TrackedRenderPass<'w>,
6055
        view: Entity,
6056
        item: &Transparent2d,
6057
    ) -> Result<(), DrawError> {
6058
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6059
        draw(
6060
            world,
×
6061
            pass,
×
6062
            view,
×
6063
            item.entity,
×
6064
            item.pipeline,
×
6065
            &mut self.params,
×
6066
        );
6067
        Ok(())
×
6068
    }
6069
}
6070

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

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

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

6137
/// Render node to run the simulation sub-graph once per frame.
6138
///
6139
/// This node doesn't simulate anything by itself, but instead schedules the
6140
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6141
/// actual simulation.
6142
///
6143
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6144
/// renders all the views, such that rendered views have access to the
6145
/// just-simulated particles to render them.
6146
///
6147
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6148
pub(crate) struct VfxSimulateDriverNode;
6149

6150
impl Node for VfxSimulateDriverNode {
6151
    fn run(
×
6152
        &self,
6153
        graph: &mut RenderGraphContext,
6154
        _render_context: &mut RenderContext,
6155
        _world: &World,
6156
    ) -> Result<(), NodeRunError> {
6157
        graph.run_sub_graph(
×
6158
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
6159
            vec![],
×
6160
            None,
×
6161
        )?;
6162
        Ok(())
×
6163
    }
6164
}
6165

6166
#[derive(Debug, Clone, PartialEq, Eq)]
6167
enum HanabiPipelineId {
6168
    Invalid,
6169
    Cached(CachedComputePipelineId),
6170
}
6171

6172
pub(crate) enum ComputePipelineError {
6173
    Queued,
6174
    Creating,
6175
    Error,
6176
}
6177

6178
impl From<&CachedPipelineState> for ComputePipelineError {
6179
    fn from(value: &CachedPipelineState) -> Self {
×
6180
        match value {
×
6181
            CachedPipelineState::Queued => Self::Queued,
×
6182
            CachedPipelineState::Creating(_) => Self::Creating,
×
6183
            CachedPipelineState::Err(_) => Self::Error,
×
6184
            _ => panic!("Trying to convert Ok state to error."),
×
6185
        }
6186
    }
6187
}
6188

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

6198
impl<'a> Deref for HanabiComputePass<'a> {
6199
    type Target = ComputePass<'a>;
6200

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

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

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

6221
    pub fn set_cached_compute_pipeline(
×
6222
        &mut self,
6223
        pipeline_id: CachedComputePipelineId,
6224
    ) -> Result<(), ComputePipelineError> {
6225
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
×
6226
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
6227
            trace!("-> already set; skipped");
×
6228
            return Ok(());
×
6229
        }
6230
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
6231
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6232
            if let CachedPipelineState::Err(err) = state {
×
6233
                error!(
×
6234
                    "Failed to find compute pipeline #{}: {:?}",
×
6235
                    pipeline_id.id(),
×
6236
                    err
×
6237
                );
6238
            } else {
6239
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6240
            }
6241
            return Err(state.into());
×
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
                };
UNCOV
6417
                compute_pass.set_bind_group(
×
6418
                    2,
UNCOV
6419
                    property_bind_groups
×
UNCOV
6420
                        .get(effect_batch.property_key.as_ref())
×
UNCOV
6421
                        .unwrap(),
×
UNCOV
6422
                    &offsets[..],
×
6423
                );
UNCOV
6424
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
×
6425

6426
                // Dispatch init job
UNCOV
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
            if compute_pass
×
6527
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
×
6528
                .is_err()
6529
            {
6530
                // FIXME - Bevy doesn't allow returning custom errors here...
6531
                return Ok(());
×
6532
            }
6533

6534
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6535
            // the size exluding gaps!");
6536
            const WORKGROUP_SIZE: u32 = 64;
6537
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6538
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6539
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6540

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

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

6582
            let mut compute_pass =
6583
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6584

6585
            // Bind group simparams@0 is common to everything, only set once per update pass
6586
            compute_pass.set_bind_group(
6587
                0,
6588
                effects_meta
6589
                    .indirect_sim_params_bind_group
6590
                    .as_ref()
6591
                    .unwrap(),
6592
                &[],
6593
            );
6594

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

6608
                // Fetch bind group metadata@3
6609
                let Some(metadata_bind_group) = effect_bind_groups
×
6610
                    .update_metadata_bind_groups
6611
                    .get(&effect_batch.buffer_index)
6612
                else {
6613
                    error!(
×
6614
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6615
                        effect_batch.buffer_index
6616
                    );
6617
                    continue;
×
6618
                };
6619

6620
                // Fetch compute pipeline
6621
                if compute_pass
6622
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6623
                    .is_err()
6624
                {
6625
                    continue;
×
6626
                }
6627

6628
                // Compute dynamic offsets
6629
                let spawner_index = effect_batch.spawner_base;
×
6630
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6631
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6632
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6633
                let property_offset = effect_batch.property_offset;
×
6634

6635
                trace!(
×
6636
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6637
                    effect_batch.handle,
6638
                    spawner_index,
6639
                );
6640

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

6657
                // Dispatch update job
6658
                let dispatch_indirect_buffer_table_id = effect_batch
6659
                    .dispatch_buffer_indices
6660
                    .update_dispatch_indirect_buffer_table_id;
6661
                let dispatch_indirect_offset = dispatch_indirect_buffer_table_id.0 * 12;
6662
                trace!(
6663
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
×
6664
                    indirect_buffer,
6665
                    dispatch_indirect_offset,
6666
                );
6667
                compute_pass
6668
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6669

6670
                trace!("update compute dispatched");
×
6671
            }
6672
        }
6673

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

6684
        // Compute sort pass
6685
        {
6686
            let mut compute_pass =
×
6687
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
×
6688

6689
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6690
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
×
6691

6692
            // Loop on batches and find those which need sorting
6693
            for effect_batch in sorted_effect_batches.iter() {
×
6694
                trace!("Processing effect batch for sorting...");
×
6695
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6696
                    continue;
×
6697
                }
6698
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6699
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6700

6701
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6702
                    warn!("Missing sort-fill effect buffer.");
×
6703
                    continue;
×
6704
                };
6705

6706
                let indirect_dispatch_index = *effect_batch
6707
                    .sort_fill_indirect_dispatch_index
6708
                    .as_ref()
6709
                    .unwrap();
6710
                let indirect_offset =
6711
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6712

6713
                // Fill the sort buffer with the key-value pairs to sort
6714
                {
6715
                    compute_pass.push_debug_group("hanabi:sort_fill");
6716

6717
                    // Fetch compute pipeline
6718
                    let Some(pipeline_id) =
×
6719
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6720
                    else {
6721
                        warn!("Missing sort-fill pipeline.");
×
6722
                        continue;
×
6723
                    };
6724
                    if compute_pass
6725
                        .set_cached_compute_pipeline(pipeline_id)
6726
                        .is_err()
6727
                    {
6728
                        compute_pass.pop_debug_group();
×
6729
                        // FIXME - Bevy doesn't allow returning custom errors here...
6730
                        return Ok(());
×
6731
                    }
6732

6733
                    // Bind group sort_fill@0
6734
                    let particle_buffer = effect_buffer.particle_buffer();
×
6735
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6736
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6737
                        particle_buffer.id(),
6738
                        indirect_index_buffer.id(),
6739
                        effect_metadata_buffer.id(),
6740
                    ) else {
6741
                        warn!("Missing sort-fill bind group.");
×
6742
                        continue;
×
6743
                    };
6744
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6745
                    let indirect_index_offset =
6746
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6747
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6748
                        effect_batch
6749
                            .dispatch_buffer_indices
6750
                            .effect_metadata_buffer_table_id
6751
                            .0,
6752
                    ) as u32;
6753
                    compute_pass.set_bind_group(
6754
                        0,
6755
                        bind_group,
6756
                        &[
6757
                            particle_offset,
6758
                            indirect_index_offset,
6759
                            effect_metadata_offset,
6760
                        ],
6761
                    );
6762

6763
                    compute_pass
6764
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6765
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6766

6767
                    compute_pass.pop_debug_group();
6768
                }
6769

6770
                // Do the actual sort
6771
                {
6772
                    compute_pass.push_debug_group("hanabi:sort");
6773

6774
                    if compute_pass
6775
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
6776
                        .is_err()
6777
                    {
6778
                        compute_pass.pop_debug_group();
×
6779
                        // FIXME - Bevy doesn't allow returning custom errors here...
6780
                        return Ok(());
×
6781
                    }
6782

6783
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6784
                    compute_pass
×
6785
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
6786
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6787

6788
                    compute_pass.pop_debug_group();
6789
                }
6790

6791
                // Copy the sorted particle indices back into the indirect index buffer, where
6792
                // the render pass will read them.
6793
                {
6794
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
6795

6796
                    // Fetch compute pipeline
6797
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
6798
                    if compute_pass
6799
                        .set_cached_compute_pipeline(pipeline_id)
6800
                        .is_err()
6801
                    {
6802
                        compute_pass.pop_debug_group();
×
6803
                        // FIXME - Bevy doesn't allow returning custom errors here...
6804
                        return Ok(());
×
6805
                    }
6806

6807
                    // Bind group sort_copy@0
6808
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6809
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6810
                        indirect_index_buffer.id(),
6811
                        effect_metadata_buffer.id(),
6812
                    ) else {
6813
                        warn!("Missing sort-copy bind group.");
×
6814
                        continue;
×
6815
                    };
6816
                    let indirect_index_offset = effect_batch.slice.start;
6817
                    let effect_metadata_offset =
6818
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6819
                            effect_batch
6820
                                .dispatch_buffer_indices
6821
                                .effect_metadata_buffer_table_id,
6822
                        );
6823
                    compute_pass.set_bind_group(
6824
                        0,
6825
                        bind_group,
6826
                        &[indirect_index_offset, effect_metadata_offset],
6827
                    );
6828

6829
                    compute_pass
6830
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6831
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6832

6833
                    compute_pass.pop_debug_group();
6834
                }
6835
            }
6836
        }
6837

6838
        Ok(())
×
6839
    }
6840
}
6841

6842
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6843
    fn from(layout_flags: LayoutFlags) -> Self {
×
6844
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
6845
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6846
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
6847
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6848
        } else {
6849
            ParticleRenderAlphaMaskPipelineKey::Blend
×
6850
        }
6851
    }
6852
}
6853

6854
#[cfg(test)]
6855
mod tests {
6856
    use super::*;
6857

6858
    #[test]
6859
    fn layout_flags() {
6860
        let flags = LayoutFlags::default();
6861
        assert_eq!(flags, LayoutFlags::NONE);
6862
    }
6863

6864
    #[cfg(feature = "gpu_tests")]
6865
    #[test]
6866
    fn gpu_limits() {
6867
        use crate::test_utils::MockRenderer;
6868

6869
        let renderer = MockRenderer::new();
6870
        let device = renderer.device();
6871
        let limits = GpuLimits::from_device(&device);
6872

6873
        // assert!(limits.storage_buffer_align().get() >= 1);
6874
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6875
    }
6876

6877
    #[cfg(feature = "gpu_tests")]
6878
    #[test]
6879
    fn gpu_ops_queue() {
6880
        use crate::test_utils::MockRenderer;
6881

6882
        let renderer = MockRenderer::new();
6883
        let device = renderer.device();
6884
        let render_queue = renderer.queue();
6885

6886
        let mut world = World::new();
6887
        world.insert_resource(device.clone());
6888
        let mut queue = GpuBufferOperationQueue::from_world(&mut world);
6889

6890
        // Two consecutive ops can be merged if in order. This includes having
6891
        // contiguous slices both in source and destination.
6892
        queue.begin_frame();
6893
        queue.enqueue_init_fill(
6894
            0,
6895
            0..200,
6896
            GpuBufferOperationArgs {
6897
                src_offset: 0,
6898
                src_stride: 2,
6899
                dst_offset: 0,
6900
                dst_stride: 0,
6901
                count: 1,
6902
            },
6903
        );
6904
        queue.enqueue_init_fill(
6905
            0,
6906
            200..300,
6907
            GpuBufferOperationArgs {
6908
                src_offset: 1,
6909
                src_stride: 2,
6910
                dst_offset: 1,
6911
                dst_stride: 0,
6912
                count: 1,
6913
            },
6914
        );
6915
        queue.end_frame(&device, &render_queue);
6916
        assert_eq!(queue.init_fill_dispatch_args.len(), 1);
6917
        assert_eq!(queue.args_buffer.content().len(), 1);
6918

6919
        // However if out of order, they remain distinct. Here the source offsets are
6920
        // inverted.
6921
        queue.begin_frame();
6922
        queue.enqueue_init_fill(
6923
            0,
6924
            0..200,
6925
            GpuBufferOperationArgs {
6926
                src_offset: 1,
6927
                src_stride: 2,
6928
                dst_offset: 0,
6929
                dst_stride: 0,
6930
                count: 1,
6931
            },
6932
        );
6933
        queue.enqueue_init_fill(
6934
            0,
6935
            200..300,
6936
            GpuBufferOperationArgs {
6937
                src_offset: 0,
6938
                src_stride: 2,
6939
                dst_offset: 1,
6940
                dst_stride: 0,
6941
                count: 1,
6942
            },
6943
        );
6944
        queue.end_frame(&device, &render_queue);
6945
        assert_eq!(queue.init_fill_dispatch_args.len(), 2);
6946
        assert_eq!(queue.args_buffer.content().len(), 2);
6947
    }
6948
}
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