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

djeedai / bevy_hanabi / 13774796806

10 Mar 2025 09:07PM UTC coverage: 40.038% (+0.05%) from 39.99%
13774796806

push

github

web-flow
Remove `z_layer_2d` (#429)

Remove `ParticleEffect::z_layer_2d` in favor of just using the effect
`Transform`'s Z coordinate. This makes ordering particle effects in
layers (along Z) more consistent and expected.

Fixes #423

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

2 existing lines in 1 file now uncovered.

3199 of 7990 relevant lines covered (40.04%)

18.72 hits per line

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

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

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

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

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

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

93
use self::batch::EffectBatch;
94

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1065
        compute_pass.set_pipeline(pipeline);
×
1066

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

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

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

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

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

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

1128
        compute_pass.set_pipeline(pipeline);
×
1129

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

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

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

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

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

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

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

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

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

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

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

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

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

1314
            let shader_defs = default();
×
1315

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2190
pub struct AddedEffectParent {
2191
    pub entity: MainEntity,
2192
    pub layout: ParticleLayout,
2193
    /// GPU spawn event count to allocate for this effect.
2194
    pub event_count: u32,
2195
}
2196

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

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

2236
#[derive(Default, Resource)]
2237
pub(crate) struct EffectAssetEvents {
2238
    pub images: Vec<AssetEvent<Image>>,
2239
}
2240

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

2253
    let EffectAssetEvents { ref mut images } = *events;
×
2254
    *images = image_events.read().copied().collect();
×
2255
}
2256

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

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

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

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

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

2374
    // Manage GPU debug capture
2375
    if render_debug_settings.is_capturing {
×
2376
        render_debug_settings.captured_frames += 1;
×
2377

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

2405
    // Save simulation params into render world
2406
    sim_params.time = time.elapsed_secs_f64();
×
2407
    sim_params.delta_time = time.delta_secs();
×
2408
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
×
2409
    sim_params.virtual_delta_time = virtual_time.delta_secs();
×
2410
    sim_params.real_time = real_time.elapsed_secs_f64();
×
2411
    sim_params.real_delta_time = real_time.delta_secs();
×
2412

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

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

2444
            // 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
2445
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2446
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
×
2447
                entity: entity.into(),
×
2448
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2449
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2450
            });
2451

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

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

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

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

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

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

2534
        let texture_layout = asset.module().texture_layout();
2535
        let layout_flags = compiled_effect.layout_flags;
2536
        // let mesh = compiled_effect
2537
        //     .mesh
2538
        //     .clone()
2539
        //     .unwrap_or(default_mesh.0.clone());
2540
        let alpha_mode = compiled_effect.alpha_mode;
2541

2542
        trace!(
2543
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
×
2544
            asset.name,
×
2545
            main_entity,
×
2546
            render_entity.id(),
×
2547
            texture_layout.layout.len(),
×
2548
            compiled_effect.textures.len(),
×
2549
            layout_flags,
2550
        );
2551

2552
        extracted_effects.effects.push(ExtractedEffect {
×
2553
            render_entity: *render_entity,
×
2554
            main_entity: main_entity.into(),
×
2555
            handle: compiled_effect.asset.clone_weak(),
×
2556
            particle_layout: asset.particle_layout().clone(),
×
2557
            property_layout,
×
2558
            property_data,
×
2559
            spawn_count: effect_spawner.spawn_count,
×
2560
            prng_seed: compiled_effect.prng_seed,
×
2561
            transform: *transform,
×
2562
            layout_flags,
×
2563
            texture_layout,
×
2564
            textures: compiled_effect.textures.clone(),
×
2565
            alpha_mode,
×
2566
            effect_shaders: effect_shaders.clone(),
×
2567
        });
2568
    }
2569
}
2570

2571
/// Various GPU limits and aligned sizes computed once and cached.
2572
struct GpuLimits {
2573
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2574
    ///
2575
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2576
    storage_buffer_align: NonZeroU32,
2577

2578
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2579
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2580
    ///
2581
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2582
    effect_metadata_aligned_size: NonZeroU32,
2583
}
2584

2585
impl GpuLimits {
2586
    pub fn from_device(render_device: &RenderDevice) -> Self {
1✔
2587
        let storage_buffer_align =
1✔
2588
            render_device.limits().min_storage_buffer_offset_alignment as u64;
1✔
2589

2590
        let effect_metadata_aligned_size = NonZeroU32::new(
2591
            GpuEffectMetadata::min_size()
1✔
2592
                .get()
1✔
2593
                .next_multiple_of(storage_buffer_align) as u32,
1✔
2594
        )
2595
        .unwrap();
2596

2597
        trace!(
1✔
2598
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
×
2599
            storage_buffer_align,
×
2600
            GpuEffectMetadata::min_size().get(),
×
2601
            effect_metadata_aligned_size.get(),
×
2602
        );
2603

2604
        Self {
2605
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
1✔
2606
            effect_metadata_aligned_size,
2607
        }
2608
    }
2609

2610
    /// Byte alignment for any storage buffer binding.
2611
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
×
2612
        self.storage_buffer_align
×
2613
    }
2614

2615
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2616
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2617
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2618
    }
2619

2620
    /// Byte alignment for [`GpuEffectMetadata`].
2621
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
×
2622
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
×
2623
    }
2624
}
2625

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

2672
impl EffectsMeta {
2673
    pub fn new(
×
2674
        device: RenderDevice,
2675
        indirect_shader_noevent: Handle<Shader>,
2676
        indirect_shader_events: Handle<Shader>,
2677
    ) -> Self {
2678
        let gpu_limits = GpuLimits::from_device(&device);
×
2679

2680
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2681
        // be addressed individually by the computer shaders.
2682
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
×
2683
        trace!(
×
2684
            "Aligning storage buffers to {} bytes as device limits requires.",
×
2685
            item_align
2686
        );
2687

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

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

2748
        trace!("Adding {} newly spawned effects", added_effects.len());
×
2749
        for added_effect in added_effects.drain(..) {
×
2750
            trace!("+ added effect: capacity={}", added_effect.capacity);
×
2751

2752
            // Allocate an indirect dispatch arguments struct for this instance
2753
            let update_dispatch_indirect_buffer_table_id = self
×
2754
                .update_dispatch_indirect_buffer
×
2755
                .insert(GpuDispatchIndirect::default());
×
2756

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

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

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

2862
            // Insert the effect into the cache. This will allocate all the necessary
2863
            // mandatory GPU resources as needed.
2864
            let cached_effect = effect_cache.insert(
2865
                added_effect.handle,
2866
                added_effect.capacity,
2867
                &added_effect.particle_layout,
2868
                added_effect.layout_flags,
2869
            );
2870
            let mut cmd = commands.entity(added_effect.render_entity.id());
2871
            cmd.insert((
2872
                added_effect.entity,
2873
                cached_effect,
2874
                dispatch_buffer_indices,
2875
                cached_mesh,
2876
            ));
2877

2878
            // Allocate storage for properties if needed
2879
            if !added_effect.property_layout.is_empty() {
×
2880
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
×
2881
                cmd.insert(cached_effect_properties);
×
2882
            } else {
2883
                cmd.remove::<CachedEffectProperties>();
×
2884
            }
2885

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

2902
            // Allocate storage for GPU spawn events if needed
2903
            if let Some(parent) = added_effect.parent.as_ref() {
×
2904
                let cached_events = event_cache.allocate(parent.event_count);
2905
                cmd.insert(cached_events);
2906
            } else {
2907
                cmd.remove::<CachedEffectEvents>();
×
2908
            }
2909

2910
            // Ensure the particle@1 bind group layout exists for the given configuration of
2911
            // particle layout and (optionally) parent particle layout.
2912
            {
2913
                let parent_min_binding_size = added_effect
2914
                    .parent
2915
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2916
                effect_cache.ensure_particle_bind_group_layout(
2917
                    added_effect.particle_layout.min_binding_size32(),
2918
                    parent_min_binding_size,
2919
                );
2920
            }
2921

2922
            // Ensure the metadata@3 bind group layout exists for init pass.
2923
            {
2924
                let consume_gpu_spawn_events = added_effect
2925
                    .layout_flags
2926
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2927
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2928
            }
2929

2930
            // We cannot yet determine the layout of the metadata@3 bind group for the
2931
            // update pass, because it depends on the number of children, and
2932
            // this is encoded indirectly via the number of child effects
2933
            // pointing to this parent, and only calculated later in
2934
            // resolve_parents().
2935

2936
            trace!(
2937
                "+ added effect entity {:?}: main_entity={:?} \
×
2938
                first_update_group_dispatch_buffer_index={} \
×
2939
                render_effect_dispatch_buffer_id={}",
×
2940
                added_effect.render_entity,
2941
                added_effect.entity,
2942
                update_dispatch_indirect_buffer_table_id.0,
2943
                effect_metadata_buffer_table_id.0
2944
            );
2945
        }
2946

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

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

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

3024
impl Default for LayoutFlags {
3025
    fn default() -> Self {
1✔
3026
        Self::NONE
1✔
3027
    }
3028
}
3029

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

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

3054
    // Fecth the components of the effect being destroyed. Note that the despawn
3055
    // command above is not yet applied, so this query should always succeed.
3056
    let Ok((
3057
        render_entity,
×
3058
        main_entity,
×
3059
        cached_effect,
×
3060
        dispatch_buffer_indices,
×
3061
        _opt_props,
×
3062
        _opt_parent,
×
3063
        opt_cached_effect_events,
×
3064
    )) = query.get(trigger.entity())
3065
    else {
3066
        return;
×
3067
    };
3068

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

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

3098
    // Clear bind groups associated with the removed buffer
3099
    trace!(
×
3100
        "=> GPU buffer #{} gone, destroying its bind groups...",
×
3101
        cached_effect.buffer_index
3102
    );
3103
    effect_bind_groups
×
3104
        .particle_buffers
×
3105
        .remove(&cached_effect.buffer_index);
×
3106
    effects_meta
×
3107
        .update_dispatch_indirect_buffer
×
3108
        .remove(dispatch_buffer_indices.update_dispatch_indirect_buffer_table_id);
×
3109
    effects_meta
×
3110
        .effect_metadata_buffer
×
3111
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
×
3112
}
3113

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

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

3149
    // Allocate new effects
3150
    effects_meta.add_effects(
×
3151
        commands,
×
3152
        std::mem::take(&mut extracted_effects.added_effects),
×
3153
        &render_device,
×
3154
        &render_queue,
×
3155
        &mesh_allocator,
×
3156
        &render_meshes,
×
3157
        &mut effect_bind_groups,
×
3158
        &mut effect_cache,
×
3159
        &mut property_cache,
×
3160
        &mut event_cache,
×
3161
    );
3162

3163
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3164
    // because EffectBuffer already contains a reference to the
3165
    // RenderDevice, so has done so internally. This is not ideal
3166
    // design-wise, but works.
3167
}
3168

3169
/// Check if two lists of entities are equal.
3170
fn is_child_list_changed(
×
3171
    parent_entity: Entity,
3172
    old: impl ExactSizeIterator<Item = Entity>,
3173
    new: impl ExactSizeIterator<Item = Entity>,
3174
) -> bool {
3175
    if old.len() != new.len() {
×
3176
        trace!(
×
3177
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3178
            parent_entity,
×
3179
            old.len(),
×
3180
            new.len()
×
3181
        );
3182
        return true;
×
3183
    }
3184

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

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

3242
    // Build map of render entity from main entity for all cached effects.
3243
    let render_from_main_entity = q_cached_effects
×
3244
        .iter()
3245
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
×
3246
        .collect::<HashMap<_, _>>();
3247

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

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

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

3312
        // Push the child entity into the children list
3313
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3314
        let local_child_index = child_vec.len() as u32;
3315
        child_vec.push((child_entity, child_buffer_binding_source));
3316
        child_infos.push(GpuChildInfo {
3317
            event_count: 0,
3318
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3319
        });
3320

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

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

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

3366
        // Check if any child changed compared to the existing CachedChildren component
3367
        if !is_child_list_changed(
3368
            parent_entity,
3369
            cached_parent_info
3370
                .children
3371
                .iter()
3372
                .map(|(entity, _)| *entity),
×
3373
            children.iter().map(|(entity, _)| *entity),
×
3374
        ) {
3375
            continue;
×
3376
        }
3377

3378
        event_cache.reallocate_child_infos(
×
3379
            parent_entity,
×
3380
            children,
×
3381
            &child_infos[..],
×
3382
            cached_parent_info.deref_mut(),
×
3383
        );
3384
    }
3385

3386
    // Once this is done, the children hash map contains all entries which don't
3387
    // already have a CachedParentInfo component. That is, all entities which are
3388
    // new parents.
3389
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
×
3390
        let cached_parent_info =
×
3391
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
×
3392
        commands.entity(parent_entity).insert(cached_parent_info);
×
3393
    }
3394

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

3407
pub fn fixup_parents(
×
3408
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3409
    mut q_children: Query<&mut CachedChildInfo>,
3410
) {
3411
    #[cfg(feature = "trace")]
3412
    let _span = bevy::utils::tracing::info_span!("fixup_parents").entered();
×
3413
    trace!("fixup_parents");
×
3414

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

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

3459
/// Indexed mesh metadata for [`CachedMesh`].
3460
#[derive(Debug, Clone)]
3461
#[allow(dead_code)]
3462
pub(crate) struct MeshIndexSlice {
3463
    /// Index format.
3464
    pub format: IndexFormat,
3465
    /// GPU buffer containing the indices.
3466
    pub buffer: Buffer,
3467
    /// Range inside [`Self::buffer`] where the indices are.
3468
    pub range: Range<u32>,
3469
}
3470

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

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

3499
#[derive(SystemParam)]
3500
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3501
    sim_params: Res<'w, SimParams>,
3502
    render_device: Res<'w, RenderDevice>,
3503
    render_queue: Res<'w, RenderQueue>,
3504
    #[system_param(ignore)]
3505
    marker: PhantomData<&'s usize>,
3506
}
3507

3508
#[derive(SystemParam)]
3509
pub struct PipelineSystemParams<'w, 's> {
3510
    pipeline_cache: Res<'w, PipelineCache>,
3511
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3512
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3513
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3514
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3515
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3516
    specialized_indirect_pipelines:
3517
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3518
    #[system_param(ignore)]
3519
    marker: PhantomData<&'s usize>,
3520
}
3521

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

3551
    // Workaround for too many params in system (TODO: refactor to split work?)
3552
    let sim_params = read_only_params.sim_params.into_inner();
×
3553
    let render_device = read_only_params.render_device.into_inner();
×
3554
    let render_queue = read_only_params.render_queue.into_inner();
×
3555
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
×
3556
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
×
3557
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
×
3558
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
×
3559

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

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

3603
    gpu_buffer_operation_queue.begin_frame();
×
3604

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

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

3648
        let effect_slice = EffectSlice {
3649
            slice: cached_effect.slice.range(),
3650
            buffer_index: cached_effect.buffer_index,
3651
            particle_layout: cached_effect.slice.particle_layout.clone(),
3652
        };
3653

3654
        let has_event_buffer = cached_child_info.is_some();
3655
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3656
        // should be Option<T>, not T)
3657
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3658
            None
×
3659
        } else {
3660
            Some(extracted_effect.property_layout.min_binding_size())
×
3661
        };
3662

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

3675
            // Resolve parent entry
3676
            let Ok((_, _, _, _, _, cached_parent_info, _, _)) =
×
3677
                q_cached_effects.get(cached_child_info.parent)
×
3678
            else {
3679
                continue;
×
3680
            };
3681
            let Some(cached_parent_info) = cached_parent_info else {
×
3682
                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);
×
3683
                continue;
×
3684
            };
3685

3686
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3687
            let child_info_size_u32 = GpuChildInfo::min_size().get() as u32 / 4;
3688
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3689
            let global_child_index = cached_child_info.global_child_index;
×
3690

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

3714
        // Create init pipeline key flags.
3715
        let init_pipeline_key_flags = {
×
3716
            let mut flags = ParticleInitPipelineKeyFlags::empty();
×
3717
            flags.set(
×
3718
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
×
3719
                effect_slice.particle_layout.contains(Attribute::PREV),
×
3720
            );
3721
            flags.set(
×
3722
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
×
3723
                effect_slice.particle_layout.contains(Attribute::NEXT),
×
3724
            );
3725
            flags.set(
×
3726
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
×
3727
                has_event_buffer,
×
3728
            );
3729
            flags
×
3730
        };
3731

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

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

3792
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
×
3793
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
×
3794

3795
        // Specialize the init pipeline based on the effect.
3796
        let init_pipeline_id = {
×
3797
            let consume_gpu_spawn_events = init_pipeline_key_flags
3798
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3799

3800
            // Fetch the metadata@3 bind group layout from the cache
3801
            let metadata_bind_group_layout = effect_cache
3802
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3803
                .unwrap()
3804
                .clone();
3805

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

3835
            init_pipeline_id
3836
        };
3837

3838
        let update_pipeline_id = {
×
3839
            let num_event_buffers = cached_parent_info
3840
                .map(|p| p.children.len() as u32)
×
3841
                .unwrap_or_default();
3842

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

3850
            // Fetch the bind group layouts from the cache
3851
            let metadata_bind_group_layout = effect_cache
3852
                .metadata_update_bind_group_layout(num_event_buffers)
3853
                .unwrap()
3854
                .clone();
3855

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

3885
            update_pipeline_id
3886
        };
3887

3888
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3889
            init: init_pipeline_id,
3890
            update: update_pipeline_id,
3891
        };
3892

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

3908
        // Output some debug info
3909
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
×
3910
        trace!(
×
3911
            "update_shader = {:?}",
×
3912
            extracted_effect.effect_shaders.update
3913
        );
3914
        trace!(
×
3915
            "render_shader = {:?}",
×
3916
            extracted_effect.effect_shaders.render
3917
        );
3918
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
3919
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
3920

UNCOV
3921
        let spawner_index = effects_meta.allocate_spawner(
×
3922
            &extracted_effect.transform,
×
3923
            extracted_effect.spawn_count,
×
3924
            extracted_effect.prng_seed,
×
3925
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
×
3926
        );
3927

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

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

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

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

4021
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4022
        // update its GpuEffectMetadata with all those infos.
4023
        // FIXME - should do this only when the below changes (not only the mesh), via
4024
        // some invalidation mechanism and ECS change detection.
4025
        if cached_mesh.is_changed() {
×
4026
            let capacity = cached_effect.slice.len();
×
4027

4028
            // Global and local indices of this effect as a child of another (parent) effect
4029
            let (global_child_index, local_child_index) = cached_child_info
×
4030
                .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4031
                .unwrap_or_default();
4032

4033
            // Base index of all children of this (parent) effect
4034
            let base_child_index = cached_parent_info
×
4035
                .map(|cpi| {
×
4036
                    debug_assert_eq!(
×
4037
                        cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4038
                        0
4039
                    );
4040
                    cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4041
                })
4042
                .unwrap_or_default();
4043

4044
            let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
×
4045
            let sort_key_offset = extracted_effect
×
4046
                .particle_layout
×
4047
                .offset(Attribute::RIBBON_ID)
×
4048
                .unwrap_or(0)
×
4049
                / 4;
×
4050
            let sort_key2_offset = extracted_effect
×
4051
                .particle_layout
×
4052
                .offset(Attribute::AGE)
×
4053
                .unwrap_or(0)
×
4054
                / 4;
×
4055

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

4098
            warn!(
×
4099
                "Updated metadata entry {} for effect {:?}, this will reset it.",
×
4100
                dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4101
            );
4102
        }
4103

4104
        prepared_effect_count += 1;
×
4105
    }
4106
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
×
4107

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

4120
    // Write the entire spawner buffer for this frame, for all effects combined
4121
    assert_eq!(
×
4122
        prepared_effect_count,
×
4123
        effects_meta.spawner_buffer.len() as u32
×
4124
    );
4125
    if effects_meta
×
4126
        .spawner_buffer
×
4127
        .write_buffer(render_device, render_queue)
×
4128
    {
4129
        // All property bind groups use the spawner buffer, which was reallocate
4130
        property_bind_groups.clear(true);
×
4131
        effects_meta.indirect_spawner_bind_group = None;
×
4132
    }
4133

4134
    // Update simulation parameters
4135
    effects_meta.sim_params_uniforms.set(sim_params.into());
×
4136
    {
4137
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
×
4138
        gpu_sim_params.num_effects = prepared_effect_count;
×
4139

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

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

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

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

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

4225
        let translation = input.position;
4226

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

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

4254
            // Allocate a GpuDispatchIndirect entry
4255
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4256
            effect_batch.sort_fill_indirect_dispatch_index =
4257
                Some(sort_fill_indirect_dispatch_index);
4258

4259
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4260
            // compute a number of workgroups to dispatch based on that particle count, and
4261
            // store the result into a GpuDispatchIndirect struct which will be used to
4262
            // dispatch the fill-sort pass.
4263
            {
4264
                let src_buffer = effect_metadata_buffer.clone();
4265
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4266
                    effect_batch
4267
                        .dispatch_buffer_indices
4268
                        .effect_metadata_buffer_table_id,
4269
                );
4270
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4271
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4272
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4273
                    continue;
×
4274
                };
4275
                let dst_buffer = dst_buffer.clone();
4276
                let dst_binding_offset = sort_bind_groups
4277
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index);
4278
                let dst_binding_size = NonZeroU32::new(12).unwrap();
4279
                trace!(
4280
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4281
                    src_buffer.id(),
×
4282
                    src_binding_offset,
×
4283
                    src_binding_size.get(),
×
4284
                    dst_buffer.id(),
×
4285
                    dst_binding_offset,
×
4286
                    dst_binding_size.get(),
×
4287
                );
4288
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
×
4289
                debug_assert_eq!(
×
4290
                    src_offset, 5,
4291
                    "GpuEffectMetadata changed, update this assert."
×
4292
                );
4293
                gpu_buffer_operation_queue.enqueue(
×
4294
                    GpuBufferOperationType::FillDispatchArgs,
×
4295
                    GpuBufferOperationArgs {
×
4296
                        src_offset,
×
4297
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
×
4298
                        dst_offset: 0,
×
4299
                        dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
4300
                        count: 1,
×
4301
                    },
4302
                    src_buffer,
×
4303
                    src_binding_offset,
×
4304
                    Some(src_binding_size),
×
4305
                    dst_buffer,
×
4306
                    dst_binding_offset,
×
4307
                    Some(dst_binding_size),
×
4308
                );
4309
            }
4310
        }
4311

4312
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4313
        trace!(
×
4314
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4315
            effect_batch_index,
4316
            entity,
4317
        );
4318

4319
        // Spawn an EffectDrawBatch, to actually drive rendering.
4320
        commands
×
4321
            .spawn(EffectDrawBatch {
×
4322
                effect_batch_index,
×
NEW
4323
                translation,
×
4324
            })
4325
            .insert(TemporaryRenderEntity);
×
4326
    }
4327

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

4331
    sorted_effect_batches.sort();
×
4332
}
4333

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

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

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

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

4417
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4418
struct BindingKey {
4419
    pub buffer_id: BufferId,
4420
    pub offset: u32,
4421
    pub size: NonZeroU32,
4422
}
4423

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

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

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

4454
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4455
struct ConsumeEventKey {
4456
    child_infos_buffer_id: BufferId,
4457
    events: BindingKey,
4458
}
4459

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

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

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

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

4494
#[derive(Debug, Clone, Copy)]
4495
struct BufferSlice<'a> {
4496
    pub buffer: &'a Buffer,
4497
    pub offset: u32,
4498
    pub size: NonZeroU32,
4499
}
4500

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

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

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

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

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

4563
impl EffectBindGroups {
4564
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4565
        self.particle_buffers
×
4566
            .get(&buffer_index)
×
4567
            .map(|bg| &bg.render)
×
4568
    }
4569

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

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

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

4630
            let bind_group = render_device.create_bind_group(
×
4631
                "hanabi:bind_group:init:metadata@3",
4632
                layout,
×
4633
                &entries[..],
×
4634
            );
4635

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

4642
            bind_group
×
4643
        };
4644

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

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

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

4698
        let event_buffers_keys = event_buffers
×
4699
            .iter()
4700
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4701
            .collect::<Vec<_>>();
4702

4703
        let key = UpdateMetadataBindGroupKey {
4704
            buffer_index: effect_batch.buffer_index,
×
4705
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4706
            effect_metadata_offset: gpu_limits
×
4707
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4708
                as u32,
4709
            child_info_buffer_id,
4710
            event_buffers_keys,
4711
        };
4712

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

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

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

4757
            let bind_group = render_device.create_bind_group(
×
4758
                "hanabi:bind_group:update:metadata@3",
4759
                layout,
×
4760
                &entries[..],
×
4761
            );
4762

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

4769
            bind_group
×
4770
        };
4771

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

4799
    pub fn init_fill_dispatch(&self, event_buffer_index: u32) -> Option<&BindGroup> {
×
4800
        self.init_fill_dispatch.get(&event_buffer_index)
×
4801
    }
4802
}
4803

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

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

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

4842
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4843
            continue;
×
4844
        };
4845

4846
        {
4847
            #[cfg(feature = "trace")]
4848
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4849

4850
            view_entities.clear();
×
4851
            view_entities.extend(
×
4852
                visible_entities
×
4853
                    .iter::<WithCompiledParticleEffect>()
×
4854
                    .map(|e| e.1.index() as usize),
×
4855
            );
4856
        }
4857

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

4865
            trace!(
×
4866
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4867
                draw_entity,
×
4868
                draw_batch.effect_batch_index,
×
4869
            );
4870

4871
            // Get the EffectBatches this EffectDrawBatch is part of.
4872
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4873
            else {
×
4874
                continue;
×
4875
            };
4876

4877
            trace!(
×
4878
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4879
                effect_batch.buffer_index,
×
4880
                effect_batch.spawner_base,
×
4881
                effect_batch.layout_flags,
×
4882
            );
4883

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

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

4912
            // Create and cache the bind group layout for this texture layout
4913
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4914

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

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

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

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

4948
            // Add a draw pass for the effect batch
4949
            trace!("Emitting individual draw for batch");
×
4950

4951
            let alpha_mode = effect_batch.alpha_mode;
×
4952

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

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

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

5018
    trace!("emit_binned_draw() {} views", views.iter().len());
×
5019

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

5023
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
5024
            continue;
×
5025
        };
5026

5027
        {
5028
            #[cfg(feature = "trace")]
5029
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
5030

5031
            view_entities.clear();
×
5032
            view_entities.extend(
×
5033
                visible_entities
×
5034
                    .iter::<WithCompiledParticleEffect>()
×
5035
                    .map(|e| e.1.index() as usize),
×
5036
            );
5037
        }
5038

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

5046
            trace!(
×
5047
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5048
                draw_entity,
×
5049
                draw_batch.effect_batch_index,
×
5050
            );
5051

5052
            // Get the EffectBatches this EffectDrawBatch is part of.
5053
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5054
            else {
×
5055
                continue;
×
5056
            };
5057

5058
            trace!(
×
5059
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5060
                effect_batch.buffer_index,
×
5061
                effect_batch.spawner_base,
×
5062
                effect_batch.layout_flags,
×
5063
            );
5064

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

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

5093
            // Create and cache the bind group layout for this texture layout
5094
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5095

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

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

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

5122
            // Add a draw pass for the effect batch
5123
            trace!("Emitting individual draw for batch");
×
5124

5125
            let alpha_mode = effect_batch.alpha_mode;
×
5126

5127
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5128
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5129
                continue;
×
5130
            };
5131

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

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

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

5203
    trace!("queue_effects");
×
5204

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

5222
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5223
        // No spawners are active
5224
        return;
×
5225
    }
5226

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

5233
        let draw_effects_function_2d = read_params
5234
            .draw_functions_2d
5235
            .read()
5236
            .get_id::<DrawEffects>()
5237
            .unwrap();
5238

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

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

5272
        // Effects with full alpha blending
5273
        if !views.is_empty() {
5274
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5275

5276
            let draw_effects_function_3d = read_params
×
5277
                .draw_functions_3d
×
5278
                .read()
5279
                .get_id::<DrawEffects>()
5280
                .unwrap();
5281

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

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

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

5314
            let draw_effects_function_alpha_mask = read_params
×
5315
                .draw_functions_alpha_mask
×
5316
                .read()
5317
                .get_id::<DrawEffects>()
5318
                .unwrap();
5319

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

5348
        // Opaque particles
5349
        if !views.is_empty() {
×
5350
            #[cfg(feature = "trace")]
5351
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
5352

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

5355
            let draw_effects_function_opaque = read_params
×
5356
                .draw_functions_opaque
×
5357
                .read()
5358
                .get_id::<DrawEffects>()
5359
                .unwrap();
5360

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

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

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

5431
    // Re-/allocate any GPU buffer if needed
5432
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5433
    // effect_bind_groups);
5434
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5435
    sort_bind_groups.prepare_buffers(&render_device);
5436
}
5437

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

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

5468
    {
5469
        #[cfg(feature = "trace")]
5470
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
×
5471

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

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

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

5526
            // Some buffer is not yet available, can't create the bind group
5527
            _ => None,
×
5528
        };
5529

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

5549
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5550
        }
5551
    }
5552

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

5559
        let Some(effect_buffer) = effect_buffer else {
×
5560
            trace!(
×
5561
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5562
                buffer_index
5563
            );
5564
            continue;
×
5565
        };
5566

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

5607
                BufferBindGroups { render }
×
5608
            });
5609
    }
5610

5611
    // Create bind groups for queued GPU buffer operations
5612
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
×
5613

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

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

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

5641
            let Some(source_binding_resource) = event_cache.child_infos().max_binding() else {
×
5642
                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.");
×
5643
                continue;
×
5644
            };
5645

5646
            let Some(target_binding_resource) =
×
5647
                event_cache.init_indirect_dispatch_binding_resource()
5648
            else {
5649
                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.");
×
5650
                continue;
×
5651
            };
5652

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

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

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

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

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

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

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

5800
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5801
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5802

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

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

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

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

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

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

5883
type DrawEffectsSystemState = SystemState<(
5884
    SRes<EffectsMeta>,
5885
    SRes<EffectBindGroups>,
5886
    SRes<PipelineCache>,
5887
    SRes<RenderAssets<RenderMesh>>,
5888
    SRes<MeshAllocator>,
5889
    SQuery<Read<ViewUniformOffset>>,
5890
    SRes<SortedEffectBatches>,
5891
    SQuery<Read<EffectDrawBatch>>,
5892
)>;
5893

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

5902
impl DrawEffects {
5903
    pub fn new(world: &mut World) -> Self {
×
5904
        Self {
5905
            params: SystemState::new(world),
×
5906
        }
5907
    }
5908
}
5909

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

5941
    let gpu_limits = &effects_meta.gpu_limits;
×
5942

5943
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5944
        return;
×
5945
    };
5946

5947
    trace!("render pass");
×
5948

5949
    pass.set_render_pipeline(pipeline);
×
5950

5951
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5952
        return;
×
5953
    };
5954
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5955
        return;
×
5956
    };
5957

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

5965
    // View properties (camera matrix, etc.)
5966
    pass.set_bind_group(
×
5967
        0,
5968
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5969
        &[view_uniform.offset],
×
5970
    );
5971

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

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

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

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

6029
    match render_mesh.buffer_info {
×
6030
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
6031
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
6032
            else {
×
6033
                return;
×
6034
            };
6035

6036
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6037
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6038
        }
6039
        RenderMeshBufferInfo::NonIndexed => {
×
6040
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6041
        }
6042
    }
6043
}
6044

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

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

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

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

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

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

6162
#[derive(Debug, Clone, PartialEq, Eq)]
6163
enum HanabiPipelineId {
6164
    Invalid,
6165
    Cached(CachedComputePipelineId),
6166
}
6167

6168
pub(crate) struct HanabiComputePass<'a> {
6169
    /// Pipeline cache to fetch cached compute pipelines by ID.
6170
    pipeline_cache: &'a PipelineCache,
6171
    /// WGPU compute pass.
6172
    compute_pass: ComputePass<'a>,
6173
    /// Current pipeline (cached).
6174
    pipeline_id: HanabiPipelineId,
6175
}
6176

6177
impl<'a> Deref for HanabiComputePass<'a> {
6178
    type Target = ComputePass<'a>;
6179

6180
    fn deref(&self) -> &Self::Target {
×
6181
        &self.compute_pass
×
6182
    }
6183
}
6184

6185
impl DerefMut for HanabiComputePass<'_> {
6186
    fn deref_mut(&mut self) -> &mut Self::Target {
×
6187
        &mut self.compute_pass
×
6188
    }
6189
}
6190

6191
impl<'a> HanabiComputePass<'a> {
6192
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
×
6193
        Self {
6194
            pipeline_cache,
6195
            compute_pass,
6196
            pipeline_id: HanabiPipelineId::Invalid,
6197
        }
6198
    }
6199

6200
    pub fn set_cached_compute_pipeline(
×
6201
        &mut self,
6202
        pipeline_id: CachedComputePipelineId,
6203
    ) -> Result<(), NodeRunError> {
6204
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
6205
            return Ok(());
×
6206
        }
6207
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
6208
            if let CachedPipelineState::Err(err) =
×
6209
                self.pipeline_cache.get_compute_pipeline_state(pipeline_id)
×
6210
            {
6211
                error!(
×
6212
                    "Failed to find compute pipeline #{}: {:?}",
×
6213
                    pipeline_id.id(),
×
6214
                    err
×
6215
                );
6216
            }
6217
            // FIXME - Bevy doesn't allow returning custom errors here...
6218
            return Ok(());
×
6219
        };
6220
        self.compute_pass.set_pipeline(pipeline);
×
6221
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6222
        Ok(())
×
6223
    }
6224
}
6225

6226
/// Render node to run the simulation of all effects once per frame.
6227
///
6228
/// Runs inside the simulation sub-graph, looping over all extracted effect
6229
/// batches to simulate them.
6230
pub(crate) struct VfxSimulateNode {}
6231

6232
impl VfxSimulateNode {
6233
    /// Create a new node for simulating the effects of the given world.
6234
    pub fn new(_world: &mut World) -> Self {
×
6235
        Self {}
6236
    }
6237

6238
    /// Begin a new compute pass and return a wrapper with extra
6239
    /// functionalities.
6240
    pub fn begin_compute_pass<'encoder>(
×
6241
        &self,
6242
        label: &str,
6243
        pipeline_cache: &'encoder PipelineCache,
6244
        render_context: &'encoder mut RenderContext,
6245
    ) -> HanabiComputePass<'encoder> {
6246
        let compute_pass =
×
6247
            render_context
×
6248
                .command_encoder()
6249
                .begin_compute_pass(&ComputePassDescriptor {
×
6250
                    label: Some(label),
×
6251
                    timestamp_writes: None,
×
6252
                });
6253
        HanabiComputePass::new(pipeline_cache, compute_pass)
×
6254
    }
6255
}
6256

6257
impl Node for VfxSimulateNode {
6258
    fn input(&self) -> Vec<SlotInfo> {
×
6259
        vec![]
×
6260
    }
6261

6262
    fn update(&mut self, _world: &mut World) {}
×
6263

6264
    fn run(
×
6265
        &self,
6266
        _graph: &mut RenderGraphContext,
6267
        render_context: &mut RenderContext,
6268
        world: &World,
6269
    ) -> Result<(), NodeRunError> {
6270
        trace!("VfxSimulateNode::run()");
×
6271

6272
        let pipeline_cache = world.resource::<PipelineCache>();
×
6273
        let effects_meta = world.resource::<EffectsMeta>();
×
6274
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
6275
        let property_bind_groups = world.resource::<PropertyBindGroups>();
×
6276
        let sort_bind_groups = world.resource::<SortBindGroups>();
×
6277
        let utils_pipeline = world.resource::<UtilsPipeline>();
×
6278
        let effect_cache = world.resource::<EffectCache>();
×
6279
        let event_cache = world.resource::<EventCache>();
×
6280
        let gpu_buffer_operation_queue = world.resource::<GpuBufferOperationQueue>();
×
6281
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
×
6282

6283
        // Make sure to schedule any buffer copy before accessing their content later in
6284
        // the GPU commands below.
6285
        {
6286
            let command_encoder = render_context.command_encoder();
×
6287
            effects_meta
×
6288
                .update_dispatch_indirect_buffer
×
6289
                .write_buffer(command_encoder);
×
6290
            effects_meta
×
6291
                .effect_metadata_buffer
×
6292
                .write_buffer(command_encoder);
×
6293
            sort_bind_groups.write_buffers(command_encoder);
×
6294
        }
6295

6296
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6297
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6298
        // the update pass of their parent effect during the previous frame.
6299
        gpu_buffer_operation_queue.dispatch_init_fill(
×
6300
            render_context,
×
6301
            utils_pipeline.get_pipeline(GpuBufferOperationType::InitFillDispatchArgs),
×
6302
            effect_bind_groups,
×
6303
        );
6304

6305
        // If there's no batch, there's nothing more to do. Avoid continuing because
6306
        // some GPU resources are missing, which is expected when there's no effect but
6307
        // is an error (and will log warnings/errors) otherwise.
6308
        if sorted_effect_batches.is_empty() {
×
6309
            return Ok(());
×
6310
        }
6311

6312
        // Compute init pass
6313
        {
6314
            trace!("init: loop over effect batches...");
×
6315

6316
            let mut compute_pass =
×
6317
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
×
6318

6319
            // Bind group simparams@0 is common to everything, only set once per init pass
6320
            compute_pass.set_bind_group(
×
6321
                0,
6322
                effects_meta
×
6323
                    .indirect_sim_params_bind_group
×
6324
                    .as_ref()
×
6325
                    .unwrap(),
×
6326
                &[],
×
6327
            );
6328

6329
            // Dispatch init compute jobs for all batches
6330
            for effect_batch in sorted_effect_batches.iter() {
×
6331
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6332
                // batch. Note that this hopefully should have been skipped earlier.
6333
                {
6334
                    let use_indirect_dispatch = effect_batch
×
6335
                        .layout_flags
×
6336
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6337
                    match effect_batch.spawn_info {
×
6338
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
×
6339
                            assert!(!use_indirect_dispatch);
×
6340
                            if total_spawn_count == 0 {
×
6341
                                continue;
×
6342
                            }
6343
                        }
6344
                        BatchSpawnInfo::GpuSpawner { .. } => {
6345
                            assert!(use_indirect_dispatch);
×
6346
                        }
6347
                    }
6348
                }
6349

6350
                // Fetch bind group particle@1
6351
                let Some(particle_bind_group) =
×
6352
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6353
                else {
6354
                    error!(
×
6355
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6356
                        effect_batch.buffer_index
6357
                    );
6358
                    continue;
×
6359
                };
6360

6361
                // Fetch bind group metadata@3
6362
                let Some(metadata_bind_group) = effect_bind_groups
×
6363
                    .init_metadata_bind_groups
6364
                    .get(&effect_batch.buffer_index)
6365
                else {
6366
                    error!(
×
6367
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6368
                        effect_batch.buffer_index
6369
                    );
6370
                    continue;
×
6371
                };
6372

6373
                if compute_pass
6374
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6375
                    .is_err()
6376
                {
6377
                    continue;
×
6378
                }
6379

6380
                // Compute dynamic offsets
6381
                let spawner_index = effect_batch.spawner_base;
×
6382
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6383
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6384
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6385
                let property_offset = effect_batch.property_offset;
×
6386

6387
                // Setup init pass
6388
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6389
                let offsets = if let Some(property_offset) = property_offset {
×
6390
                    vec![spawner_offset, property_offset]
6391
                } else {
6392
                    vec![spawner_offset]
×
6393
                };
6394
                compute_pass.set_bind_group(
6395
                    2,
6396
                    property_bind_groups
6397
                        .get(effect_batch.property_key.as_ref())
6398
                        .unwrap(),
6399
                    &offsets[..],
6400
                );
6401
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6402

6403
                // Dispatch init job
6404
                match effect_batch.spawn_info {
6405
                    // Indirect dispatch via GPU spawn events
6406
                    BatchSpawnInfo::GpuSpawner {
6407
                        init_indirect_dispatch_index,
×
6408
                        ..
×
6409
                    } => {
×
6410
                        assert!(effect_batch
×
6411
                            .layout_flags
×
6412
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6413

6414
                        // Note: the indirect offset of a dispatch workgroup only needs
6415
                        // 4-byte alignment
6416
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6417
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6418

6419
                        trace!(
×
6420
                            "record commands for indirect init pipeline of effect {:?} \
×
6421
                                init_indirect_dispatch_index={} \
×
6422
                                indirect_offset={} \
×
6423
                                spawner_base={} \
×
6424
                                spawner_offset={} \
×
6425
                                property_key={:?}...",
×
6426
                            effect_batch.handle,
6427
                            init_indirect_dispatch_index,
6428
                            indirect_offset,
6429
                            spawner_index,
6430
                            spawner_offset,
6431
                            effect_batch.property_key,
6432
                        );
6433

6434
                        compute_pass.dispatch_workgroups_indirect(
×
6435
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6436
                            indirect_offset,
×
6437
                        );
6438
                    }
6439

6440
                    // Direct dispatch via CPU spawn count
6441
                    BatchSpawnInfo::CpuSpawner {
6442
                        total_spawn_count: spawn_count,
×
6443
                    } => {
×
6444
                        assert!(!effect_batch
×
6445
                            .layout_flags
×
6446
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6447

6448
                        const WORKGROUP_SIZE: u32 = 64;
6449
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
×
6450

6451
                        trace!(
×
6452
                            "record commands for init pipeline of effect {:?} \
×
6453
                                (spawn {} particles => {} workgroups) spawner_base={} \
×
6454
                                spawner_offset={} \
×
6455
                                property_key={:?}...",
×
6456
                            effect_batch.handle,
6457
                            spawn_count,
6458
                            workgroup_count,
6459
                            spawner_index,
6460
                            spawner_offset,
6461
                            effect_batch.property_key,
6462
                        );
6463

6464
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
6465
                    }
6466
                }
6467

6468
                trace!("init compute dispatched");
×
6469
            }
6470
        }
6471

6472
        // Compute indirect dispatch pass
6473
        if effects_meta.spawner_buffer.buffer().is_some()
×
6474
            && !effects_meta.spawner_buffer.is_empty()
×
6475
            && effects_meta.indirect_metadata_bind_group.is_some()
×
6476
            && effects_meta.indirect_sim_params_bind_group.is_some()
×
6477
        {
6478
            // Only start a compute pass if there's an effect; makes things clearer in
6479
            // debugger.
6480
            let mut compute_pass =
×
6481
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
×
6482

6483
            // Dispatch indirect dispatch compute job
6484
            trace!("record commands for indirect dispatch pipeline...");
×
6485

6486
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
×
6487
            if has_gpu_spawn_events {
×
6488
                if let Some(indirect_child_info_buffer_bind_group) =
×
6489
                    event_cache.indirect_child_info_buffer_bind_group()
×
6490
                {
6491
                    assert!(has_gpu_spawn_events);
6492
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6493
                } else {
6494
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6495
                    render_context
×
6496
                        .command_encoder()
6497
                        .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6498
                    // FIXME - Bevy doesn't allow returning custom errors here...
6499
                    return Ok(());
×
6500
                }
6501
            }
6502

6503
            compute_pass.set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)?;
×
6504

6505
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6506
            // the size exluding gaps!");
6507
            const WORKGROUP_SIZE: u32 = 64;
6508
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6509
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
×
6510
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
×
6511

6512
            // Setup vfx_indirect pass
6513
            compute_pass.set_bind_group(
×
6514
                0,
6515
                effects_meta
×
6516
                    .indirect_sim_params_bind_group
×
6517
                    .as_ref()
×
6518
                    .unwrap(),
×
6519
                &[],
×
6520
            );
6521
            compute_pass.set_bind_group(
×
6522
                1,
6523
                // FIXME - got some unwrap() panic here, investigate... possibly race
6524
                // condition!
6525
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
×
6526
                &[],
×
6527
            );
6528
            compute_pass.set_bind_group(
×
6529
                2,
6530
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
×
6531
                &[],
×
6532
            );
6533
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
6534
            trace!(
×
6535
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
×
6536
                total_effect_count,
6537
                workgroup_count
6538
            );
6539
        }
6540

6541
        // Compute update pass
6542
        {
6543
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
×
6544
            else {
6545
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6546
                render_context
×
6547
                    .command_encoder()
6548
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6549
                // FIXME - Bevy doesn't allow returning custom errors here...
6550
                return Ok(());
×
6551
            };
6552

6553
            let mut compute_pass =
6554
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6555

6556
            // Bind group simparams@0 is common to everything, only set once per update pass
6557
            compute_pass.set_bind_group(
6558
                0,
6559
                effects_meta
6560
                    .indirect_sim_params_bind_group
6561
                    .as_ref()
6562
                    .unwrap(),
6563
                &[],
6564
            );
6565

6566
            // Dispatch update compute jobs
6567
            for effect_batch in sorted_effect_batches.iter() {
×
6568
                // Fetch bind group particle@1
6569
                let Some(particle_bind_group) =
×
6570
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6571
                else {
6572
                    error!(
×
6573
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6574
                        effect_batch.buffer_index
6575
                    );
6576
                    continue;
×
6577
                };
6578

6579
                // Fetch bind group metadata@3
6580
                let Some(metadata_bind_group) = effect_bind_groups
×
6581
                    .update_metadata_bind_groups
6582
                    .get(&effect_batch.buffer_index)
6583
                else {
6584
                    error!(
×
6585
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6586
                        effect_batch.buffer_index
6587
                    );
6588
                    continue;
×
6589
                };
6590

6591
                // Fetch compute pipeline
6592
                if compute_pass
6593
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6594
                    .is_err()
6595
                {
6596
                    continue;
×
6597
                }
6598

6599
                // Compute dynamic offsets
6600
                let spawner_index = effect_batch.spawner_base;
×
6601
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6602
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6603
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6604
                let property_offset = effect_batch.property_offset;
×
6605

6606
                trace!(
×
6607
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6608
                    effect_batch.handle,
6609
                    spawner_index,
6610
                );
6611

6612
                // Setup update pass
6613
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6614
                let offsets = if let Some(property_offset) = property_offset {
×
6615
                    vec![spawner_offset, property_offset]
6616
                } else {
6617
                    vec![spawner_offset]
×
6618
                };
6619
                compute_pass.set_bind_group(
6620
                    2,
6621
                    property_bind_groups
6622
                        .get(effect_batch.property_key.as_ref())
6623
                        .unwrap(),
6624
                    &offsets[..],
6625
                );
6626
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6627

6628
                // Dispatch update job
6629
                let dispatch_indirect_buffer_table_id = effect_batch
6630
                    .dispatch_buffer_indices
6631
                    .update_dispatch_indirect_buffer_table_id;
6632
                let dispatch_indirect_offset = dispatch_indirect_buffer_table_id.0 * 12;
6633
                trace!(
6634
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
×
6635
                    indirect_buffer,
6636
                    dispatch_indirect_offset,
6637
                );
6638
                compute_pass
×
6639
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
×
6640

6641
                trace!("update compute dispatched");
×
6642
            }
6643
        }
6644

6645
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6646
        // batch of particles which needs sorting, based on the actual number of alive
6647
        // particles in the batch after their update in the compute update pass. Since
6648
        // particles may die during update, this may be different from the number of
6649
        // particles updated.
6650
        gpu_buffer_operation_queue.dispatch_fill(
×
6651
            render_context,
×
6652
            utils_pipeline.get_pipeline(GpuBufferOperationType::FillDispatchArgs),
×
6653
        );
6654

6655
        // Compute sort pass
6656
        {
6657
            let mut compute_pass =
×
6658
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
×
6659

6660
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6661
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
×
6662

6663
            // Loop on batches and find those which need sorting
6664
            for effect_batch in sorted_effect_batches.iter() {
×
6665
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6666
                    continue;
×
6667
                }
6668
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6669
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6670

6671
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6672
                    warn!("Missing sort-fill effect buffer.");
×
6673
                    continue;
×
6674
                };
6675

6676
                // Fill the sort buffer with the key-value pairs to sort
6677
                {
6678
                    compute_pass.push_debug_group("hanabi:sort_fill");
6679

6680
                    // Fetch compute pipeline
6681
                    let Some(pipeline_id) =
×
6682
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6683
                    else {
6684
                        warn!("Missing sort-fill pipeline.");
×
6685
                        continue;
×
6686
                    };
6687
                    compute_pass.set_cached_compute_pipeline(pipeline_id)?;
×
6688

6689
                    // Bind group sort_fill@0
6690
                    let particle_buffer = effect_buffer.particle_buffer();
×
6691
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6692
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6693
                        particle_buffer.id(),
6694
                        indirect_index_buffer.id(),
6695
                        effect_metadata_buffer.id(),
6696
                    ) else {
6697
                        warn!("Missing sort-fill bind group.");
×
6698
                        continue;
×
6699
                    };
6700
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6701
                    let indirect_index_offset =
6702
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6703
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6704
                        effect_batch
6705
                            .dispatch_buffer_indices
6706
                            .effect_metadata_buffer_table_id
6707
                            .0,
6708
                    ) as u32;
6709
                    compute_pass.set_bind_group(
6710
                        0,
6711
                        bind_group,
6712
                        &[
6713
                            particle_offset,
6714
                            indirect_index_offset,
6715
                            effect_metadata_offset,
6716
                        ],
6717
                    );
6718

6719
                    let indirect_dispatch_index = *effect_batch
6720
                        .sort_fill_indirect_dispatch_index
6721
                        .as_ref()
6722
                        .unwrap();
6723
                    let indirect_offset =
6724
                        sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6725
                    compute_pass
6726
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6727
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6728

6729
                    compute_pass.pop_debug_group();
×
6730
                }
6731

6732
                // Do the actual sort
6733
                {
6734
                    compute_pass.push_debug_group("hanabi:sort");
×
6735

6736
                    compute_pass
×
6737
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())?;
×
6738
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6739
                    let indirect_offset =
×
6740
                        sort_bind_groups.get_sort_indirect_dispatch_byte_offset() as u64;
×
6741
                    compute_pass.dispatch_workgroups_indirect(indirect_buffer, indirect_offset);
×
6742
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6743

6744
                    compute_pass.pop_debug_group();
×
6745
                }
6746

6747
                // Copy the sorted particle indices back into the indirect index buffer, where
6748
                // the render pass will read them.
6749
                {
6750
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
×
6751

6752
                    // Fetch compute pipeline
6753
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
×
6754
                    compute_pass.set_cached_compute_pipeline(pipeline_id)?;
×
6755

6756
                    // Bind group sort_copy@0
6757
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6758
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6759
                        indirect_index_buffer.id(),
6760
                        effect_metadata_buffer.id(),
6761
                    ) else {
6762
                        warn!("Missing sort-copy bind group.");
×
6763
                        continue;
×
6764
                    };
6765
                    let indirect_index_offset = effect_batch.slice.start;
6766
                    let effect_metadata_offset =
6767
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6768
                            effect_batch
6769
                                .dispatch_buffer_indices
6770
                                .effect_metadata_buffer_table_id,
6771
                        );
6772
                    compute_pass.set_bind_group(
6773
                        0,
6774
                        bind_group,
6775
                        &[indirect_index_offset, effect_metadata_offset],
6776
                    );
6777

6778
                    // Note: we can reuse the same indirect buffer as for copying key-value pairs,
6779
                    // since we're copying the same number of particles indices than we sorted.
6780
                    let indirect_dispatch_index = *effect_batch
6781
                        .sort_fill_indirect_dispatch_index
6782
                        .as_ref()
6783
                        .unwrap();
6784
                    let indirect_offset =
6785
                        sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6786
                    compute_pass
6787
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6788
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6789

6790
                    compute_pass.pop_debug_group();
×
6791
                }
6792
            }
6793
        }
6794

6795
        Ok(())
×
6796
    }
6797
}
6798

6799
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6800
    fn from(layout_flags: LayoutFlags) -> Self {
×
6801
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
×
6802
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6803
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
×
6804
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6805
        } else {
6806
            ParticleRenderAlphaMaskPipelineKey::Blend
×
6807
        }
6808
    }
6809
}
6810

6811
#[cfg(test)]
6812
mod tests {
6813
    use super::*;
6814

6815
    #[test]
6816
    fn layout_flags() {
6817
        let flags = LayoutFlags::default();
6818
        assert_eq!(flags, LayoutFlags::NONE);
6819
    }
6820

6821
    #[cfg(feature = "gpu_tests")]
6822
    #[test]
6823
    fn gpu_limits() {
6824
        use crate::test_utils::MockRenderer;
6825

6826
        let renderer = MockRenderer::new();
6827
        let device = renderer.device();
6828
        let limits = GpuLimits::from_device(&device);
6829

6830
        // assert!(limits.storage_buffer_align().get() >= 1);
6831
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6832
    }
6833

6834
    #[cfg(feature = "gpu_tests")]
6835
    #[test]
6836
    fn gpu_ops_queue() {
6837
        use crate::test_utils::MockRenderer;
6838

6839
        let renderer = MockRenderer::new();
6840
        let device = renderer.device();
6841
        let render_queue = renderer.queue();
6842

6843
        let mut world = World::new();
6844
        world.insert_resource(device.clone());
6845
        let mut queue = GpuBufferOperationQueue::from_world(&mut world);
6846

6847
        // Two consecutive ops can be merged if in order. This includes having
6848
        // contiguous slices both in source and destination.
6849
        queue.begin_frame();
6850
        queue.enqueue_init_fill(
6851
            0,
6852
            0..200,
6853
            GpuBufferOperationArgs {
6854
                src_offset: 0,
6855
                src_stride: 2,
6856
                dst_offset: 0,
6857
                dst_stride: 0,
6858
                count: 1,
6859
            },
6860
        );
6861
        queue.enqueue_init_fill(
6862
            0,
6863
            200..300,
6864
            GpuBufferOperationArgs {
6865
                src_offset: 1,
6866
                src_stride: 2,
6867
                dst_offset: 1,
6868
                dst_stride: 0,
6869
                count: 1,
6870
            },
6871
        );
6872
        queue.end_frame(&device, &render_queue);
6873
        assert_eq!(queue.init_fill_dispatch_args.len(), 1);
6874
        assert_eq!(queue.args_buffer.content().len(), 1);
6875

6876
        // However if out of order, they remain distinct. Here the source offsets are
6877
        // inverted.
6878
        queue.begin_frame();
6879
        queue.enqueue_init_fill(
6880
            0,
6881
            0..200,
6882
            GpuBufferOperationArgs {
6883
                src_offset: 1,
6884
                src_stride: 2,
6885
                dst_offset: 0,
6886
                dst_stride: 0,
6887
                count: 1,
6888
            },
6889
        );
6890
        queue.enqueue_init_fill(
6891
            0,
6892
            200..300,
6893
            GpuBufferOperationArgs {
6894
                src_offset: 0,
6895
                src_stride: 2,
6896
                dst_offset: 1,
6897
                dst_stride: 0,
6898
                count: 1,
6899
            },
6900
        );
6901
        queue.end_frame(&device, &render_queue);
6902
        assert_eq!(queue.init_fill_dispatch_args.len(), 2);
6903
        assert_eq!(queue.args_buffer.content().len(), 2);
6904
    }
6905
}
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