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

djeedai / bevy_hanabi / 13822045323

12 Mar 2025 09:38PM UTC coverage: 40.025% (-0.005%) from 40.03%
13822045323

push

github

web-flow
Silently skip effects if pipeline creation pending (#433)

Silently skip (with a debug log) any effect if one of the compute
pipelines is queued for creation but was not created yet. This prevents
a panic in `wgpu` and ensure we can retry later once all pipelines are
ready.

Fixes #428

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

1 existing line in 1 file now uncovered.

3200 of 7995 relevant lines covered (40.03%)

18.71 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
        }
1996

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2437
            // FIXME - fixed 256 events per child (per frame) for now... this neatly avoids any issue with alignment 32/256 byte storage buffer align for bind groups
2438
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2439
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
×
2440
                entity: entity.into(),
×
2441
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2442
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2443
            });
2444

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3314
        // Check if child info changed. Avoid overwriting if no change.
3315
        if let Some(old_cached_child_info) = cached_child_info {
×
3316
            if parent_entity == old_cached_child_info.parent
3317
                && parent_cached_effect.slice.particle_layout
×
3318
                    == old_cached_child_info.parent_particle_layout
×
3319
                && parent_buffer_binding_source
×
3320
                    == old_cached_child_info.parent_buffer_binding_source
×
3321
                // Note: if local child index didn't change, then keep global one too for now. Chances are the parent didn't change, but anyway we can't know for now without inspecting all its children.
3322
                && local_child_index == old_cached_child_info.local_child_index
×
3323
                && cached_effect_events.init_indirect_dispatch_index
×
3324
                    == old_cached_child_info.init_indirect_dispatch_index
×
3325
            {
3326
                trace!(
×
3327
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3328
                    child_entity
3329
                );
3330
                continue;
×
3331
            }
3332
        }
3333

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3596
    gpu_buffer_operation_queue.begin_frame();
×
3597

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

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

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

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

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

3668
            // Resolve parent entry
3669
            let Ok((_, _, _, _, _, cached_parent_info, _, _)) =
×
3670
                q_cached_effects.get(cached_child_info.parent)
×
3671
            else {
3672
                continue;
×
3673
            };
3674
            let Some(cached_parent_info) = cached_parent_info else {
×
3675
                error!("Effect {:?} indicates its parent is {:?}, but that parent effect is missing a CachedParentInfo component. This is a bug.", extracted_effect.render_entity.id(), cached_child_info.parent);
×
3676
                continue;
×
3677
            };
3678

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

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

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

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

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

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

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

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

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

3828
            init_pipeline_id
3829
        };
3830

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

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

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

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

3878
            update_pipeline_id
3879
        };
3880

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4218
        let translation = input.position;
4219

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

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

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

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

4305
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
×
4306
        trace!(
×
4307
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
×
4308
            effect_batch_index,
4309
            entity,
4310
        );
4311

4312
        // Spawn an EffectDrawBatch, to actually drive rendering.
4313
        commands
×
4314
            .spawn(EffectDrawBatch {
×
4315
                effect_batch_index,
×
4316
                translation,
×
4317
            })
4318
            .insert(TemporaryRenderEntity);
×
4319
    }
4320

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

4324
    sorted_effect_batches.sort();
×
4325
}
4326

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

4364
/// Combination of a texture layout and the bound textures.
4365
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4366
struct Material {
4367
    layout: TextureLayout,
4368
    textures: Vec<AssetId<Image>>,
4369
}
4370

4371
impl Material {
4372
    /// Get the bind group entries to create a bind group.
4373
    pub fn make_entries<'a>(
×
4374
        &self,
4375
        gpu_images: &'a RenderAssets<GpuImage>,
4376
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4377
        if self.textures.is_empty() {
×
4378
            return Ok(vec![]);
×
4379
        }
4380

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

4410
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4411
struct BindingKey {
4412
    pub buffer_id: BufferId,
4413
    pub offset: u32,
4414
    pub size: NonZeroU32,
4415
}
4416

4417
impl<'a> From<BufferSlice<'a>> for BindingKey {
4418
    fn from(value: BufferSlice<'a>) -> Self {
×
4419
        Self {
4420
            buffer_id: value.buffer.id(),
×
4421
            offset: value.offset,
×
4422
            size: value.size,
×
4423
        }
4424
    }
4425
}
4426

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

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

4447
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4448
struct ConsumeEventKey {
4449
    child_infos_buffer_id: BufferId,
4450
    events: BindingKey,
4451
}
4452

4453
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4454
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4455
        Self {
4456
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4457
            events: value.events.into(),
×
4458
        }
4459
    }
4460
}
4461

4462
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4463
struct InitMetadataBindGroupKey {
4464
    pub buffer_index: u32,
4465
    pub effect_metadata_buffer: BufferId,
4466
    pub effect_metadata_offset: u32,
4467
    pub consume_event_key: Option<ConsumeEventKey>,
4468
}
4469

4470
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4471
struct UpdateMetadataBindGroupKey {
4472
    pub buffer_index: u32,
4473
    pub effect_metadata_buffer: BufferId,
4474
    pub effect_metadata_offset: u32,
4475
    pub child_info_buffer_id: Option<BufferId>,
4476
    pub event_buffers_keys: Vec<BindingKey>,
4477
}
4478

4479
struct CachedBindGroup<K: Eq> {
4480
    /// Key the bind group was created from. Each time the key changes, the bind
4481
    /// group should be re-created.
4482
    key: K,
4483
    /// Bind group created from the key.
4484
    bind_group: BindGroup,
4485
}
4486

4487
#[derive(Debug, Clone, Copy)]
4488
struct BufferSlice<'a> {
4489
    pub buffer: &'a Buffer,
4490
    pub offset: u32,
4491
    pub size: NonZeroU32,
4492
}
4493

4494
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4495
    fn from(value: BufferSlice<'a>) -> Self {
×
4496
        Self {
4497
            buffer: value.buffer,
×
4498
            offset: value.offset.into(),
×
4499
            size: Some(value.size.into()),
×
4500
        }
4501
    }
4502
}
4503

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

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

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

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

4556
impl EffectBindGroups {
4557
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
×
4558
        self.particle_buffers
×
4559
            .get(&buffer_index)
×
4560
            .map(|bg| &bg.render)
×
4561
    }
4562

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

4579
        let effect_metadata_offset =
×
4580
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
×
4581
        let key = InitMetadataBindGroupKey {
4582
            buffer_index: effect_batch.buffer_index,
×
4583
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4584
            effect_metadata_offset,
4585
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
×
4586
        };
4587

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

4623
            let bind_group = render_device.create_bind_group(
×
4624
                "hanabi:bind_group:init:metadata@3",
4625
                layout,
×
4626
                &entries[..],
×
4627
            );
4628

4629
            trace!(
×
4630
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
×
4631
                    effect_batch.buffer_index,
4632
                    effect_metadata_buffer_table_id.0,
4633
                );
4634

4635
            bind_group
×
4636
        };
4637

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

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

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

4691
        let event_buffers_keys = event_buffers
×
4692
            .iter()
4693
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
×
4694
            .collect::<Vec<_>>();
4695

4696
        let key = UpdateMetadataBindGroupKey {
4697
            buffer_index: effect_batch.buffer_index,
×
4698
            effect_metadata_buffer: effect_metadata_buffer.id(),
×
4699
            effect_metadata_offset: gpu_limits
×
4700
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4701
                as u32,
4702
            child_info_buffer_id,
4703
            event_buffers_keys,
4704
        };
4705

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

4721
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4722
                // ChildInfoBuffer;
4723
                entries.push(BindGroupEntry {
×
4724
                    binding: 1,
×
4725
                    resource: BindingResource::Buffer(BufferBinding {
×
4726
                        buffer: child_info_buffer,
×
4727
                        offset: 0,
×
4728
                        size: None,
×
4729
                    }),
4730
                });
4731

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

4750
            let bind_group = render_device.create_bind_group(
×
4751
                "hanabi:bind_group:update:metadata@3",
4752
                layout,
×
4753
                &entries[..],
×
4754
            );
4755

4756
            trace!(
×
4757
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
×
4758
                effect_batch.buffer_index,
4759
                effect_metadata_buffer_table_id.0,
4760
            );
4761

4762
            bind_group
×
4763
        };
4764

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

4792
    pub fn init_fill_dispatch(&self, event_buffer_index: u32) -> Option<&BindGroup> {
×
4793
        self.init_fill_dispatch.get(&event_buffer_index)
×
4794
    }
4795
}
4796

4797
#[derive(SystemParam)]
4798
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4799
    #[cfg(feature = "2d")]
4800
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4801
    #[cfg(feature = "3d")]
4802
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4803
    #[cfg(feature = "3d")]
4804
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4805
    #[cfg(feature = "3d")]
4806
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4807
    #[system_param(ignore)]
4808
    marker: PhantomData<&'s usize>,
4809
}
4810

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

4829
    for (view_entity, visible_entities, view, msaa) in views.iter() {
×
4830
        trace!(
×
4831
            "Process new sorted view with {} visible particle effect entities",
×
4832
            visible_entities.len::<WithCompiledParticleEffect>()
×
4833
        );
4834

4835
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
4836
            continue;
×
4837
        };
4838

4839
        {
4840
            #[cfg(feature = "trace")]
4841
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
4842

4843
            view_entities.clear();
×
4844
            view_entities.extend(
×
4845
                visible_entities
×
4846
                    .iter::<WithCompiledParticleEffect>()
×
4847
                    .map(|e| e.1.index() as usize),
×
4848
            );
4849
        }
4850

4851
        // For each view, loop over all the effect batches to determine if the effect
4852
        // needs to be rendered for that view, and enqueue a view-dependent
4853
        // batch if so.
4854
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
4855
            #[cfg(feature = "trace")]
4856
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
4857

4858
            trace!(
×
4859
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
4860
                draw_entity,
×
4861
                draw_batch.effect_batch_index,
×
4862
            );
4863

4864
            // Get the EffectBatches this EffectDrawBatch is part of.
4865
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
4866
            else {
×
4867
                continue;
×
4868
            };
4869

4870
            trace!(
×
4871
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
×
4872
                effect_batch.buffer_index,
×
4873
                effect_batch.spawner_base,
×
4874
                effect_batch.layout_flags,
×
4875
            );
4876

4877
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4878
            if effect_batch
×
4879
                .layout_flags
×
4880
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4881
            {
4882
                trace!("Non-transparent batch. Skipped.");
×
4883
                continue;
×
4884
            }
4885

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

4905
            // Create and cache the bind group layout for this texture layout
4906
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
4907

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

4911
            let local_space_simulation = effect_batch
×
4912
                .layout_flags
×
4913
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
4914
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
4915
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
4916
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
4917
            let needs_normal = effect_batch
×
4918
                .layout_flags
×
4919
                .contains(LayoutFlags::NEEDS_NORMAL);
×
4920
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
4921
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
4922

4923
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4924
            // re-querying here...?
4925
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
×
4926
                trace!("Batch has no render mesh, skipped.");
×
4927
                continue;
×
4928
            };
4929
            let mesh_layout = render_mesh.layout.clone();
×
4930

4931
            // Specialize the render pipeline based on the effect batch
4932
            trace!(
×
4933
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
4934
                effect_batch.render_shader,
×
4935
                image_count,
×
4936
                alpha_mask,
×
4937
                flipbook,
×
4938
                view.hdr
×
4939
            );
4940

4941
            // Add a draw pass for the effect batch
4942
            trace!("Emitting individual draw for batch");
×
4943

4944
            let alpha_mode = effect_batch.alpha_mode;
×
4945

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

4972
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
4973
            trace!(
×
4974
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
4975
                spawner_base={} handle={:?}",
×
4976
                draw_entity,
×
4977
                effect_batch.buffer_index,
×
4978
                effect_batch.spawner_base,
×
4979
                effect_batch.handle
×
4980
            );
4981
            render_phase.add(make_phase_item(
×
4982
                render_pipeline_id,
×
4983
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
4984
                draw_batch,
×
4985
                view,
×
4986
            ));
4987
        }
4988
    }
4989
}
4990

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

5011
    trace!("emit_binned_draw() {} views", views.iter().len());
×
5012

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

5016
        let Some(render_phase) = render_phases.get_mut(&view_entity) else {
×
5017
            continue;
×
5018
        };
5019

5020
        {
5021
            #[cfg(feature = "trace")]
5022
            let _span = bevy::utils::tracing::info_span!("collect_view_entities").entered();
×
5023

5024
            view_entities.clear();
×
5025
            view_entities.extend(
×
5026
                visible_entities
×
5027
                    .iter::<WithCompiledParticleEffect>()
×
5028
                    .map(|e| e.1.index() as usize),
×
5029
            );
5030
        }
5031

5032
        // For each view, loop over all the effect batches to determine if the effect
5033
        // needs to be rendered for that view, and enqueue a view-dependent
5034
        // batch if so.
5035
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
×
5036
            #[cfg(feature = "trace")]
5037
            let _span_draw = bevy::utils::tracing::info_span!("draw_batch").entered();
×
5038

5039
            trace!(
×
5040
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5041
                draw_entity,
×
5042
                draw_batch.effect_batch_index,
×
5043
            );
5044

5045
            // Get the EffectBatches this EffectDrawBatch is part of.
5046
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
×
5047
            else {
×
5048
                continue;
×
5049
            };
5050

5051
            trace!(
×
5052
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
×
5053
                effect_batch.buffer_index,
×
5054
                effect_batch.spawner_base,
×
5055
                effect_batch.layout_flags,
×
5056
            );
5057

5058
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5059
                trace!(
×
5060
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5061
                    effect_batch.layout_flags,
×
5062
                    alpha_mask
×
5063
                );
5064
                continue;
×
5065
            }
5066

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

5086
            // Create and cache the bind group layout for this texture layout
5087
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5088

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

5092
            let local_space_simulation = effect_batch
×
5093
                .layout_flags
×
5094
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5095
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5096
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5097
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5098
            let needs_normal = effect_batch
×
5099
                .layout_flags
×
5100
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5101
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5102
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5103
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5104

5105
            // Specialize the render pipeline based on the effect batch
5106
            trace!(
×
5107
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5108
                effect_batch.render_shader,
×
5109
                image_count,
×
5110
                alpha_mask,
×
5111
                flipbook,
×
5112
                view.hdr
×
5113
            );
5114

5115
            // Add a draw pass for the effect batch
5116
            trace!("Emitting individual draw for batch");
×
5117

5118
            let alpha_mode = effect_batch.alpha_mode;
×
5119

5120
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5121
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5122
                continue;
×
5123
            };
5124

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

5151
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5152
            trace!(
×
5153
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5154
                spawner_base={} handle={:?}",
×
5155
                draw_entity,
×
5156
                effect_batch.buffer_index,
×
5157
                effect_batch.spawner_base,
×
5158
                effect_batch.handle
×
5159
            );
5160
            render_phase.add(
×
5161
                make_bin_key(render_pipeline_id, draw_batch, view),
×
5162
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5163
                BinnedRenderPhaseType::NonMesh,
×
5164
            );
5165
        }
5166
    }
5167
}
5168

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

5196
    trace!("queue_effects");
×
5197

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

5215
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
×
5216
        // No spawners are active
5217
        return;
×
5218
    }
5219

5220
    // Loop over all 2D cameras/views that need to render effects
5221
    #[cfg(feature = "2d")]
5222
    {
5223
        #[cfg(feature = "trace")]
5224
        let _span_draw = bevy::utils::tracing::info_span!("draw_2d").entered();
×
5225

5226
        let draw_effects_function_2d = read_params
5227
            .draw_functions_2d
5228
            .read()
5229
            .get_id::<DrawEffects>()
5230
            .unwrap();
5231

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

5259
    // Loop over all 3D cameras/views that need to render effects
5260
    #[cfg(feature = "3d")]
5261
    {
5262
        #[cfg(feature = "trace")]
5263
        let _span_draw = bevy::utils::tracing::info_span!("draw_3d").entered();
×
5264

5265
        // Effects with full alpha blending
5266
        if !views.is_empty() {
5267
            trace!("Emit effect draw calls for alpha blended 3D views...");
×
5268

5269
            let draw_effects_function_3d = read_params
×
5270
                .draw_functions_3d
×
5271
                .read()
5272
                .get_id::<DrawEffects>()
5273
                .unwrap();
5274

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

5300
        // Effects with alpha mask
5301
        if !views.is_empty() {
×
5302
            #[cfg(feature = "trace")]
5303
            let _span_draw = bevy::utils::tracing::info_span!("draw_alphamask").entered();
×
5304

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

5307
            let draw_effects_function_alpha_mask = read_params
×
5308
                .draw_functions_alpha_mask
×
5309
                .read()
5310
                .get_id::<DrawEffects>()
5311
                .unwrap();
5312

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

5341
        // Opaque particles
5342
        if !views.is_empty() {
×
5343
            #[cfg(feature = "trace")]
5344
            let _span_draw = bevy::utils::tracing::info_span!("draw_opaque").entered();
×
5345

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

5348
            let draw_effects_function_opaque = read_params
×
5349
                .draw_functions_opaque
×
5350
                .read()
5351
                .get_id::<DrawEffects>()
5352
                .unwrap();
5353

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

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

5407
    // Create the bind group for the camera/view parameters
5408
    // FIXME - Not here!
5409
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5410
        "hanabi:bind_group_camera_view",
5411
        &render_pipeline.view_layout,
5412
        &[
5413
            BindGroupEntry {
5414
                binding: 0,
5415
                resource: view_binding,
5416
            },
5417
            BindGroupEntry {
5418
                binding: 1,
5419
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5420
            },
5421
        ],
5422
    ));
5423

5424
    // Re-/allocate any GPU buffer if needed
5425
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5426
    // effect_bind_groups);
5427
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5428
    sort_bind_groups.prepare_buffers(&render_device);
5429
}
5430

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

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

5461
    {
5462
        #[cfg(feature = "trace")]
5463
        let _span = bevy::utils::tracing::info_span!("shared_bind_groups").entered();
×
5464

5465
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5466
        // loop below. Also allows earlying out before doing any work in case some
5467
        // buffer is missing.
5468
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
×
5469
            return;
×
5470
        };
5471

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

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

5519
            // Some buffer is not yet available, can't create the bind group
5520
            _ => None,
×
5521
        };
5522

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

5542
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
×
5543
        }
5544
    }
5545

5546
    // Create the per-buffer bind groups
5547
    trace!("Create per-buffer bind groups...");
×
5548
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
×
5549
        #[cfg(feature = "trace")]
5550
        let _span_buffer = bevy::utils::tracing::info_span!("create_buffer_bind_groups").entered();
×
5551

5552
        let Some(effect_buffer) = effect_buffer else {
×
5553
            trace!(
×
5554
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5555
                buffer_index
5556
            );
5557
            continue;
×
5558
        };
5559

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

5600
                BufferBindGroups { render }
×
5601
            });
5602
    }
5603

5604
    // Create bind groups for queued GPU buffer operations
5605
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
×
5606

5607
    // Create the per-event-buffer bind groups
5608
    for (event_buffer_index, event_buffer) in event_cache.buffers().iter().enumerate() {
×
5609
        if event_buffer.is_none() {
×
5610
            trace!(
×
5611
                "Event buffer index #{event_buffer_index} has no allocated EventBuffer, skipped.",
×
5612
            );
5613
            continue;
×
5614
        }
5615
        let event_buffer_index = event_buffer_index as u32;
×
5616

5617
        // Check if the entry is missing
5618
        let entry = effect_bind_groups
×
5619
            .init_fill_dispatch
×
5620
            .entry(event_buffer_index);
×
5621
        if matches!(entry, Entry::Vacant(_)) {
×
5622
            trace!(
×
5623
                "Event buffer #{} missing a bind group @0 for init fill args. Trying to create now...",
×
5624
                event_buffer_index
5625
            );
5626

5627
            // Check if the binding is available to create the bind group and fill the entry
5628
            let Some((args_binding, args_count)) =
×
5629
                gpu_buffer_operation_queue.init_args_buffer_binding(event_buffer_index)
×
5630
            else {
5631
                continue;
×
5632
            };
5633

5634
            let Some(source_binding_resource) = event_cache.child_infos().max_binding() else {
×
5635
                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.");
×
5636
                continue;
×
5637
            };
5638

5639
            let Some(target_binding_resource) =
×
5640
                event_cache.init_indirect_dispatch_binding_resource()
5641
            else {
5642
                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.");
×
5643
                continue;
×
5644
            };
5645

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

5676
    // Create the per-effect bind groups
5677
    let spawner_buffer_binding_size =
×
5678
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
×
5679
    for effect_batch in sorted_effect_batched.iter() {
×
5680
        #[cfg(feature = "trace")]
5681
        let _span_buffer = bevy::utils::tracing::info_span!("create_batch_bind_groups").entered();
×
5682

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

5705
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5706
        // simulate particles.
5707
        if effect_cache
×
5708
            .create_particle_sim_bind_group(
5709
                effect_batch.buffer_index,
×
5710
                &render_device,
×
5711
                effect_batch.particle_layout.min_binding_size32(),
×
5712
                effect_batch.parent_min_binding_size,
×
5713
                effect_batch.parent_binding_source.as_ref(),
×
5714
            )
5715
            .is_err()
5716
        {
5717
            error!("No particle buffer allocated for effect batch.");
×
5718
            continue;
×
5719
        }
5720

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

5767
        // Bind group @3 of update pass
5768
        // FIXME - this is instance-dependent, not buffer-dependent#
5769
        {
5770
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
5771

5772
            let Some(update_metadata_layout) =
×
5773
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5774
            else {
5775
                continue;
×
5776
            };
5777
            if effect_bind_groups
5778
                .get_or_create_update_metadata(
5779
                    effect_batch,
5780
                    &effects_meta.gpu_limits,
5781
                    &render_device,
5782
                    update_metadata_layout,
5783
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5784
                    event_cache.child_infos_buffer(),
5785
                    &effect_batch.child_event_buffers[..],
5786
                )
5787
                .is_err()
5788
            {
5789
                continue;
×
5790
            }
5791
        }
5792

5793
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
5794
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5795

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

5813
            // Bind group @0 of sort-copy pass
5814
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5815
            if let Err(err) = sort_bind_groups
×
5816
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5817
            {
5818
                error!(
5819
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5820
                    err
5821
                );
5822
                continue;
×
5823
            }
5824
        }
5825

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

5842
            // TODO = move
5843
            let material = Material {
5844
                layout: effect_batch.texture_layout.clone(),
5845
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5846
            };
5847
            assert_eq!(material.layout.layout.len(), material.textures.len());
5848

5849
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5850
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5851
                trace!(
×
5852
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5853
                    material
5854
                );
5855
                continue;
×
5856
            };
5857

5858
            effect_bind_groups
5859
                .material_bind_groups
5860
                .entry(material.clone())
5861
                .or_insert_with(|| {
×
5862
                    debug!("Creating material bind group for material {:?}", material);
×
5863
                    render_device.create_bind_group(
×
5864
                        &format!(
×
5865
                            "hanabi:material_bind_group_{}",
×
5866
                            material.layout.layout.len()
×
5867
                        )[..],
×
5868
                        material_bind_group_layout,
×
5869
                        &bind_group_entries[..],
×
5870
                    )
5871
                });
5872
        }
5873
    }
5874
}
5875

5876
type DrawEffectsSystemState = SystemState<(
5877
    SRes<EffectsMeta>,
5878
    SRes<EffectBindGroups>,
5879
    SRes<PipelineCache>,
5880
    SRes<RenderAssets<RenderMesh>>,
5881
    SRes<MeshAllocator>,
5882
    SQuery<Read<ViewUniformOffset>>,
5883
    SRes<SortedEffectBatches>,
5884
    SQuery<Read<EffectDrawBatch>>,
5885
)>;
5886

5887
/// Draw function for rendering all active effects for the current frame.
5888
///
5889
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5890
/// and the [`Transparent3d`] phase of the main 3D pass.
5891
pub(crate) struct DrawEffects {
5892
    params: DrawEffectsSystemState,
5893
}
5894

5895
impl DrawEffects {
5896
    pub fn new(world: &mut World) -> Self {
×
5897
        Self {
5898
            params: SystemState::new(world),
×
5899
        }
5900
    }
5901
}
5902

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

5934
    let gpu_limits = &effects_meta.gpu_limits;
×
5935

5936
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
×
5937
        return;
×
5938
    };
5939

5940
    trace!("render pass");
×
5941

5942
    pass.set_render_pipeline(pipeline);
×
5943

5944
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
×
5945
        return;
×
5946
    };
5947
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
×
5948
        return;
×
5949
    };
5950

5951
    // Vertex buffer containing the particle model to draw. Generally a quad.
5952
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5953
    // "base_vertex" in the indirect struct...
5954
    assert_eq!(effect_batch.mesh_buffer_id, vertex_buffer_slice.buffer.id());
×
5955
    assert_eq!(effect_batch.mesh_slice, vertex_buffer_slice.range);
×
5956
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5957

5958
    // View properties (camera matrix, etc.)
5959
    pass.set_bind_group(
×
5960
        0,
5961
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5962
        &[view_uniform.offset],
×
5963
    );
5964

5965
    // Particles buffer
5966
    let spawner_base = effect_batch.spawner_base;
×
5967
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5968
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5969
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
×
5970
    pass.set_bind_group(
×
5971
        1,
5972
        effect_bind_groups
×
5973
            .particle_render(effect_batch.buffer_index)
×
5974
            .unwrap(),
×
5975
        &[spawner_offset],
×
5976
    );
5977

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

5997
    let effect_metadata_index = effect_batch
×
5998
        .dispatch_buffer_indices
×
5999
        .effect_metadata_buffer_table_id
×
6000
        .0;
×
6001
    let effect_metadata_offset =
×
6002
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
6003
    trace!(
×
6004
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
×
6005
            (effect_metadata_index={}, offset={}B).",
×
6006
        effect_batch.slice.len(),
×
6007
        render_mesh.vertex_count,
×
6008
        effect_batch.buffer_index,
×
6009
        effect_metadata_index,
×
6010
        effect_metadata_offset,
×
6011
    );
6012

6013
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
6014
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
6015
        trace!(
×
6016
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6017
            effect_batch.buffer_index,
×
6018
        );
6019
        return;
×
6020
    };
6021

6022
    match render_mesh.buffer_info {
×
6023
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
×
6024
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
×
6025
            else {
×
6026
                return;
×
6027
            };
6028

6029
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6030
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6031
        }
6032
        RenderMeshBufferInfo::NonIndexed => {
×
6033
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6034
        }
6035
    }
6036
}
6037

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

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

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

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

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

6139
impl Node for VfxSimulateDriverNode {
6140
    fn run(
×
6141
        &self,
6142
        graph: &mut RenderGraphContext,
6143
        _render_context: &mut RenderContext,
6144
        _world: &World,
6145
    ) -> Result<(), NodeRunError> {
6146
        graph.run_sub_graph(
×
6147
            crate::plugin::simulate_graph::HanabiSimulateGraph,
×
6148
            vec![],
×
6149
            None,
×
6150
        )?;
6151
        Ok(())
×
6152
    }
6153
}
6154

6155
#[derive(Debug, Clone, PartialEq, Eq)]
6156
enum HanabiPipelineId {
6157
    Invalid,
6158
    Cached(CachedComputePipelineId),
6159
}
6160

6161
pub(crate) enum ComputePipelineError {
6162
    Queued,
6163
    Creating,
6164
    Error,
6165
}
6166

6167
impl From<&CachedPipelineState> for ComputePipelineError {
NEW
6168
    fn from(value: &CachedPipelineState) -> Self {
×
NEW
6169
        match value {
×
NEW
6170
            CachedPipelineState::Queued => Self::Queued,
×
NEW
6171
            CachedPipelineState::Creating(_) => Self::Creating,
×
NEW
6172
            CachedPipelineState::Err(_) => Self::Error,
×
NEW
6173
            _ => panic!("Trying to convert Ok state to error."),
×
6174
        }
6175
    }
6176
}
6177

6178
pub(crate) struct HanabiComputePass<'a> {
6179
    /// Pipeline cache to fetch cached compute pipelines by ID.
6180
    pipeline_cache: &'a PipelineCache,
6181
    /// WGPU compute pass.
6182
    compute_pass: ComputePass<'a>,
6183
    /// Current pipeline (cached).
6184
    pipeline_id: HanabiPipelineId,
6185
}
6186

6187
impl<'a> Deref for HanabiComputePass<'a> {
6188
    type Target = ComputePass<'a>;
6189

6190
    fn deref(&self) -> &Self::Target {
×
6191
        &self.compute_pass
×
6192
    }
6193
}
6194

6195
impl DerefMut for HanabiComputePass<'_> {
6196
    fn deref_mut(&mut self) -> &mut Self::Target {
×
6197
        &mut self.compute_pass
×
6198
    }
6199
}
6200

6201
impl<'a> HanabiComputePass<'a> {
6202
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
×
6203
        Self {
6204
            pipeline_cache,
6205
            compute_pass,
6206
            pipeline_id: HanabiPipelineId::Invalid,
6207
        }
6208
    }
6209

6210
    pub fn set_cached_compute_pipeline(
×
6211
        &mut self,
6212
        pipeline_id: CachedComputePipelineId,
6213
    ) -> Result<(), ComputePipelineError> {
NEW
6214
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
×
6215
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
×
NEW
6216
            trace!("-> already set; skipped");
×
UNCOV
6217
            return Ok(());
×
6218
        }
6219
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
×
NEW
6220
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
NEW
6221
            if let CachedPipelineState::Err(err) = state {
×
6222
                error!(
×
6223
                    "Failed to find compute pipeline #{}: {:?}",
×
6224
                    pipeline_id.id(),
×
6225
                    err
×
6226
                );
6227
            } else {
NEW
6228
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6229
            }
NEW
6230
            return Err(state.into());
×
6231
        };
6232
        self.compute_pass.set_pipeline(pipeline);
×
6233
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6234
        Ok(())
×
6235
    }
6236
}
6237

6238
/// Render node to run the simulation of all effects once per frame.
6239
///
6240
/// Runs inside the simulation sub-graph, looping over all extracted effect
6241
/// batches to simulate them.
6242
pub(crate) struct VfxSimulateNode {}
6243

6244
impl VfxSimulateNode {
6245
    /// Create a new node for simulating the effects of the given world.
6246
    pub fn new(_world: &mut World) -> Self {
×
6247
        Self {}
6248
    }
6249

6250
    /// Begin a new compute pass and return a wrapper with extra
6251
    /// functionalities.
6252
    pub fn begin_compute_pass<'encoder>(
×
6253
        &self,
6254
        label: &str,
6255
        pipeline_cache: &'encoder PipelineCache,
6256
        render_context: &'encoder mut RenderContext,
6257
    ) -> HanabiComputePass<'encoder> {
6258
        let compute_pass =
×
6259
            render_context
×
6260
                .command_encoder()
6261
                .begin_compute_pass(&ComputePassDescriptor {
×
6262
                    label: Some(label),
×
6263
                    timestamp_writes: None,
×
6264
                });
6265
        HanabiComputePass::new(pipeline_cache, compute_pass)
×
6266
    }
6267
}
6268

6269
impl Node for VfxSimulateNode {
6270
    fn input(&self) -> Vec<SlotInfo> {
×
6271
        vec![]
×
6272
    }
6273

6274
    fn update(&mut self, _world: &mut World) {}
×
6275

6276
    fn run(
×
6277
        &self,
6278
        _graph: &mut RenderGraphContext,
6279
        render_context: &mut RenderContext,
6280
        world: &World,
6281
    ) -> Result<(), NodeRunError> {
6282
        trace!("VfxSimulateNode::run()");
×
6283

6284
        let pipeline_cache = world.resource::<PipelineCache>();
×
6285
        let effects_meta = world.resource::<EffectsMeta>();
×
6286
        let effect_bind_groups = world.resource::<EffectBindGroups>();
×
6287
        let property_bind_groups = world.resource::<PropertyBindGroups>();
×
6288
        let sort_bind_groups = world.resource::<SortBindGroups>();
×
6289
        let utils_pipeline = world.resource::<UtilsPipeline>();
×
6290
        let effect_cache = world.resource::<EffectCache>();
×
6291
        let event_cache = world.resource::<EventCache>();
×
6292
        let gpu_buffer_operation_queue = world.resource::<GpuBufferOperationQueue>();
×
6293
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
×
6294

6295
        // Make sure to schedule any buffer copy before accessing their content later in
6296
        // the GPU commands below.
6297
        {
6298
            let command_encoder = render_context.command_encoder();
×
6299
            effects_meta
×
6300
                .update_dispatch_indirect_buffer
×
6301
                .write_buffer(command_encoder);
×
6302
            effects_meta
×
6303
                .effect_metadata_buffer
×
6304
                .write_buffer(command_encoder);
×
6305
            sort_bind_groups.write_buffers(command_encoder);
×
6306
        }
6307

6308
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6309
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6310
        // the update pass of their parent effect during the previous frame.
6311
        gpu_buffer_operation_queue.dispatch_init_fill(
×
6312
            render_context,
×
6313
            utils_pipeline.get_pipeline(GpuBufferOperationType::InitFillDispatchArgs),
×
6314
            effect_bind_groups,
×
6315
        );
6316

6317
        // If there's no batch, there's nothing more to do. Avoid continuing because
6318
        // some GPU resources are missing, which is expected when there's no effect but
6319
        // is an error (and will log warnings/errors) otherwise.
6320
        if sorted_effect_batches.is_empty() {
×
6321
            return Ok(());
×
6322
        }
6323

6324
        // Compute init pass
6325
        {
6326
            trace!("init: loop over effect batches...");
×
6327

6328
            let mut compute_pass =
×
6329
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
×
6330

6331
            // Bind group simparams@0 is common to everything, only set once per init pass
6332
            compute_pass.set_bind_group(
×
6333
                0,
6334
                effects_meta
×
6335
                    .indirect_sim_params_bind_group
×
6336
                    .as_ref()
×
6337
                    .unwrap(),
×
6338
                &[],
×
6339
            );
6340

6341
            // Dispatch init compute jobs for all batches
6342
            for effect_batch in sorted_effect_batches.iter() {
×
6343
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6344
                // batch. Note that this hopefully should have been skipped earlier.
6345
                {
6346
                    let use_indirect_dispatch = effect_batch
×
6347
                        .layout_flags
×
6348
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6349
                    match effect_batch.spawn_info {
×
6350
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
×
6351
                            assert!(!use_indirect_dispatch);
×
6352
                            if total_spawn_count == 0 {
×
6353
                                continue;
×
6354
                            }
6355
                        }
6356
                        BatchSpawnInfo::GpuSpawner { .. } => {
6357
                            assert!(use_indirect_dispatch);
×
6358
                        }
6359
                    }
6360
                }
6361

6362
                // Fetch bind group particle@1
6363
                let Some(particle_bind_group) =
×
6364
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
×
6365
                else {
6366
                    error!(
×
6367
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6368
                        effect_batch.buffer_index
6369
                    );
6370
                    continue;
×
6371
                };
6372

6373
                // Fetch bind group metadata@3
6374
                let Some(metadata_bind_group) = effect_bind_groups
×
6375
                    .init_metadata_bind_groups
6376
                    .get(&effect_batch.buffer_index)
6377
                else {
6378
                    error!(
×
6379
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6380
                        effect_batch.buffer_index
6381
                    );
6382
                    continue;
×
6383
                };
6384

6385
                if compute_pass
6386
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6387
                    .is_err()
6388
                {
6389
                    continue;
×
6390
                }
6391

6392
                // Compute dynamic offsets
6393
                let spawner_index = effect_batch.spawner_base;
×
6394
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6395
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6396
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6397
                let property_offset = effect_batch.property_offset;
×
6398

6399
                // Setup init pass
6400
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
6401
                let offsets = if let Some(property_offset) = property_offset {
×
6402
                    vec![spawner_offset, property_offset]
6403
                } else {
6404
                    vec![spawner_offset]
×
6405
                };
6406
                compute_pass.set_bind_group(
6407
                    2,
6408
                    property_bind_groups
6409
                        .get(effect_batch.property_key.as_ref())
6410
                        .unwrap(),
6411
                    &offsets[..],
6412
                );
6413
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6414

6415
                // Dispatch init job
6416
                match effect_batch.spawn_info {
6417
                    // Indirect dispatch via GPU spawn events
6418
                    BatchSpawnInfo::GpuSpawner {
6419
                        init_indirect_dispatch_index,
×
6420
                        ..
×
6421
                    } => {
×
6422
                        assert!(effect_batch
×
6423
                            .layout_flags
×
6424
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6425

6426
                        // Note: the indirect offset of a dispatch workgroup only needs
6427
                        // 4-byte alignment
6428
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6429
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6430

6431
                        trace!(
×
6432
                            "record commands for indirect init pipeline of effect {:?} \
×
6433
                                init_indirect_dispatch_index={} \
×
6434
                                indirect_offset={} \
×
6435
                                spawner_base={} \
×
6436
                                spawner_offset={} \
×
6437
                                property_key={:?}...",
×
6438
                            effect_batch.handle,
6439
                            init_indirect_dispatch_index,
6440
                            indirect_offset,
6441
                            spawner_index,
6442
                            spawner_offset,
6443
                            effect_batch.property_key,
6444
                        );
6445

6446
                        compute_pass.dispatch_workgroups_indirect(
×
6447
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6448
                            indirect_offset,
×
6449
                        );
6450
                    }
6451

6452
                    // Direct dispatch via CPU spawn count
6453
                    BatchSpawnInfo::CpuSpawner {
6454
                        total_spawn_count: spawn_count,
×
6455
                    } => {
×
6456
                        assert!(!effect_batch
×
6457
                            .layout_flags
×
6458
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6459

6460
                        const WORKGROUP_SIZE: u32 = 64;
6461
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
×
6462

6463
                        trace!(
×
6464
                            "record commands for init pipeline of effect {:?} \
×
6465
                                (spawn {} particles => {} workgroups) spawner_base={} \
×
6466
                                spawner_offset={} \
×
6467
                                property_key={:?}...",
×
6468
                            effect_batch.handle,
6469
                            spawn_count,
6470
                            workgroup_count,
6471
                            spawner_index,
6472
                            spawner_offset,
6473
                            effect_batch.property_key,
6474
                        );
6475

6476
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
6477
                    }
6478
                }
6479

6480
                trace!("init compute dispatched");
×
6481
            }
6482
        }
6483

6484
        // Compute indirect dispatch pass
6485
        if effects_meta.spawner_buffer.buffer().is_some()
×
6486
            && !effects_meta.spawner_buffer.is_empty()
×
6487
            && effects_meta.indirect_metadata_bind_group.is_some()
×
6488
            && effects_meta.indirect_sim_params_bind_group.is_some()
×
6489
        {
6490
            // Only start a compute pass if there's an effect; makes things clearer in
6491
            // debugger.
6492
            let mut compute_pass =
×
6493
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
×
6494

6495
            // Dispatch indirect dispatch compute job
6496
            trace!("record commands for indirect dispatch pipeline...");
×
6497

6498
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
×
6499
            if has_gpu_spawn_events {
×
6500
                if let Some(indirect_child_info_buffer_bind_group) =
×
6501
                    event_cache.indirect_child_info_buffer_bind_group()
×
6502
                {
6503
                    assert!(has_gpu_spawn_events);
6504
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6505
                } else {
6506
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6507
                    render_context
×
6508
                        .command_encoder()
6509
                        .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6510
                    // FIXME - Bevy doesn't allow returning custom errors here...
6511
                    return Ok(());
×
6512
                }
6513
            }
6514

NEW
6515
            if compute_pass
×
NEW
6516
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
×
6517
                .is_err()
6518
            {
6519
                // FIXME - Bevy doesn't allow returning custom errors here...
NEW
6520
                return Ok(());
×
6521
            }
6522

6523
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6524
            // the size exluding gaps!");
6525
            const WORKGROUP_SIZE: u32 = 64;
6526
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6527
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6528
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6529

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

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

6571
            let mut compute_pass =
6572
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6573

6574
            // Bind group simparams@0 is common to everything, only set once per update pass
6575
            compute_pass.set_bind_group(
6576
                0,
6577
                effects_meta
6578
                    .indirect_sim_params_bind_group
6579
                    .as_ref()
6580
                    .unwrap(),
6581
                &[],
6582
            );
6583

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

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

6609
                // Fetch compute pipeline
6610
                if compute_pass
6611
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6612
                    .is_err()
6613
                {
6614
                    continue;
×
6615
                }
6616

6617
                // Compute dynamic offsets
6618
                let spawner_index = effect_batch.spawner_base;
×
6619
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
6620
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6621
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
×
6622
                let property_offset = effect_batch.property_offset;
×
6623

6624
                trace!(
×
6625
                    "record commands for update pipeline of effect {:?} spawner_base={}",
×
6626
                    effect_batch.handle,
6627
                    spawner_index,
6628
                );
6629

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

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

6659
                trace!("update compute dispatched");
×
6660
            }
6661
        }
6662

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

6673
        // Compute sort pass
6674
        {
6675
            let mut compute_pass =
×
6676
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
×
6677

6678
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6679
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
×
6680

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

6689
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6690
                    warn!("Missing sort-fill effect buffer.");
×
6691
                    continue;
×
6692
                };
6693

6694
                // Fill the sort buffer with the key-value pairs to sort
6695
                {
6696
                    compute_pass.push_debug_group("hanabi:sort_fill");
6697

6698
                    // Fetch compute pipeline
6699
                    let Some(pipeline_id) =
×
6700
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6701
                    else {
6702
                        warn!("Missing sort-fill pipeline.");
×
6703
                        continue;
×
6704
                    };
6705
                    if compute_pass
6706
                        .set_cached_compute_pipeline(pipeline_id)
6707
                        .is_err()
6708
                    {
NEW
6709
                        compute_pass.pop_debug_group();
×
6710
                        // FIXME - Bevy doesn't allow returning custom errors here...
NEW
6711
                        return Ok(());
×
6712
                    }
6713

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

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

6754
                    compute_pass.pop_debug_group();
×
6755
                }
6756

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

NEW
6761
                    if compute_pass
×
NEW
6762
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
×
6763
                        .is_err()
6764
                    {
NEW
6765
                        compute_pass.pop_debug_group();
×
6766
                        // FIXME - Bevy doesn't allow returning custom errors here...
NEW
6767
                        return Ok(());
×
6768
                    }
6769

6770
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
6771
                    let indirect_offset =
×
6772
                        sort_bind_groups.get_sort_indirect_dispatch_byte_offset() as u64;
×
6773
                    compute_pass.dispatch_workgroups_indirect(indirect_buffer, indirect_offset);
×
6774
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6775

6776
                    compute_pass.pop_debug_group();
×
6777
                }
6778

6779
                // Copy the sorted particle indices back into the indirect index buffer, where
6780
                // the render pass will read them.
6781
                {
6782
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
×
6783

6784
                    // Fetch compute pipeline
6785
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
×
NEW
6786
                    if compute_pass
×
NEW
6787
                        .set_cached_compute_pipeline(pipeline_id)
×
6788
                        .is_err()
6789
                    {
NEW
6790
                        compute_pass.pop_debug_group();
×
6791
                        // FIXME - Bevy doesn't allow returning custom errors here...
NEW
6792
                        return Ok(());
×
6793
                    }
6794

6795
                    // Bind group sort_copy@0
6796
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6797
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6798
                        indirect_index_buffer.id(),
6799
                        effect_metadata_buffer.id(),
6800
                    ) else {
6801
                        warn!("Missing sort-copy bind group.");
×
6802
                        continue;
×
6803
                    };
6804
                    let indirect_index_offset = effect_batch.slice.start;
6805
                    let effect_metadata_offset =
6806
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6807
                            effect_batch
6808
                                .dispatch_buffer_indices
6809
                                .effect_metadata_buffer_table_id,
6810
                        );
6811
                    compute_pass.set_bind_group(
6812
                        0,
6813
                        bind_group,
6814
                        &[indirect_index_offset, effect_metadata_offset],
6815
                    );
6816

6817
                    // Note: we can reuse the same indirect buffer as for copying key-value pairs,
6818
                    // since we're copying the same number of particles indices than we sorted.
6819
                    let indirect_dispatch_index = *effect_batch
6820
                        .sort_fill_indirect_dispatch_index
6821
                        .as_ref()
6822
                        .unwrap();
6823
                    let indirect_offset =
6824
                        sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6825
                    compute_pass
6826
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6827
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6828

6829
                    compute_pass.pop_debug_group();
×
6830
                }
6831
            }
6832
        }
6833

6834
        Ok(())
×
6835
    }
6836
}
6837

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

6850
#[cfg(test)]
6851
mod tests {
6852
    use super::*;
6853

6854
    #[test]
6855
    fn layout_flags() {
6856
        let flags = LayoutFlags::default();
6857
        assert_eq!(flags, LayoutFlags::NONE);
6858
    }
6859

6860
    #[cfg(feature = "gpu_tests")]
6861
    #[test]
6862
    fn gpu_limits() {
6863
        use crate::test_utils::MockRenderer;
6864

6865
        let renderer = MockRenderer::new();
6866
        let device = renderer.device();
6867
        let limits = GpuLimits::from_device(&device);
6868

6869
        // assert!(limits.storage_buffer_align().get() >= 1);
6870
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6871
    }
6872

6873
    #[cfg(feature = "gpu_tests")]
6874
    #[test]
6875
    fn gpu_ops_queue() {
6876
        use crate::test_utils::MockRenderer;
6877

6878
        let renderer = MockRenderer::new();
6879
        let device = renderer.device();
6880
        let render_queue = renderer.queue();
6881

6882
        let mut world = World::new();
6883
        world.insert_resource(device.clone());
6884
        let mut queue = GpuBufferOperationQueue::from_world(&mut world);
6885

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

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