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

djeedai / bevy_hanabi / 18605893103

17 Oct 2025 10:04PM UTC coverage: 66.442% (-0.1%) from 66.545%
18605893103

push

github

web-flow
Pack multiple effects per particle slab (#508)

Sub-allocate each particle slab with the content of multiple effects.
- Introduce a `base_particle` value per effect instance, which is the
equivalent of the `base_vertex` for rendering, and corresponds to the
index of the first particle in the sub-allocated slice for that effect,
inside the overall slab buffer.
- Store that `base_particle` in the `Spawner` and give access to all
shaders which need it (most of them).
- Restore the default 64k particle count per slab, which allows packing
multiple effects per buffer/slab.

When `debug_assertions` is active (in Debug build), fill the first value
of each particle to a `NaN` (0xFFFFFFFF) to make it easier to see in
RenderDoc or any other GPU debugger that the particle is unused.

25 of 54 new or added lines in 4 files covered. (46.3%)

7 existing lines in 1 file now uncovered.

5128 of 7718 relevant lines covered (66.44%)

214.75 hits per line

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

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

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

63
use crate::{
64
    asset::{DefaultMesh, EffectAsset},
65
    calc_func_id,
66
    render::{
67
        batch::{BatchInput, EffectDrawBatch, EffectSorter, InitAndUpdatePipelineIds},
68
        effect_cache::{
69
            AnyDrawIndirectArgs, CachedDrawIndirectArgs, DispatchBufferIndices, SlabId,
70
        },
71
    },
72
    AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShader, EffectSimulation,
73
    EffectSpawner, EffectVisibilityClass, ParticleLayout, PropertyLayout, SimulationCondition,
74
    TextureLayout,
75
};
76

77
mod aligned_buffer_vec;
78
mod batch;
79
mod buffer_table;
80
mod effect_cache;
81
mod event;
82
mod gpu_buffer;
83
mod property;
84
mod shader_cache;
85
mod sort;
86

87
use aligned_buffer_vec::AlignedBufferVec;
88
use batch::BatchSpawnInfo;
89
pub(crate) use batch::SortedEffectBatches;
90
use buffer_table::{BufferTable, BufferTableId};
91
pub(crate) use effect_cache::EffectCache;
92
pub(crate) use event::{allocate_events, on_remove_cached_effect_events, EventCache};
93
pub(crate) use property::{
94
    allocate_properties, on_remove_cached_properties, prepare_property_buffers, PropertyBindGroups,
95
    PropertyCache,
96
};
97
use property::{CachedEffectProperties, PropertyBindGroupKey};
98
pub use shader_cache::ShaderCache;
99
pub(crate) use sort::SortBindGroups;
100

101
use self::batch::EffectBatch;
102

103
// Size of an indirect index (including both parts of the ping-pong buffer) in
104
// bytes.
105
const INDIRECT_INDEX_SIZE: u32 = 12;
106

107
/// Helper to calculate a hash of a given hashable value.
108
fn calc_hash<H: Hash>(value: &H) -> u64 {
10✔
109
    let mut hasher = DefaultHasher::default();
20✔
110
    value.hash(&mut hasher);
30✔
111
    hasher.finish()
20✔
112
}
113

114
/// Source data (buffer and range inside the buffer) to create a buffer binding.
115
#[derive(Debug, Clone)]
116
pub(crate) struct BufferBindingSource {
117
    buffer: Buffer,
118
    offset: u32,
119
    size: NonZeroU32,
120
}
121

122
impl BufferBindingSource {
123
    /// Get a binding over the source data.
124
    pub fn as_binding(&self) -> BindingResource<'_> {
×
125
        BindingResource::Buffer(BufferBinding {
×
126
            buffer: &self.buffer,
×
127
            offset: self.offset as u64 * 4,
×
128
            size: Some(self.size.into()),
×
129
        })
130
    }
131
}
132

133
impl PartialEq for BufferBindingSource {
134
    fn eq(&self, other: &Self) -> bool {
×
135
        self.buffer.id() == other.buffer.id()
×
136
            && self.offset == other.offset
×
137
            && self.size == other.size
×
138
    }
139
}
140

141
impl<'a> From<&'a BufferBindingSource> for BufferBinding<'a> {
142
    fn from(value: &'a BufferBindingSource) -> Self {
×
143
        BufferBinding {
144
            buffer: &value.buffer,
×
145
            offset: value.offset as u64,
×
146
            size: Some(value.size.into()),
×
147
        }
148
    }
149
}
150

151
/// Simulation parameters, available to all shaders of all effects.
152
#[derive(Debug, Default, Clone, Copy, Resource)]
153
pub(crate) struct SimParams {
154
    /// Current effect system simulation time since startup, in seconds.
155
    /// This is based on the [`Time<EffectSimulation>`](EffectSimulation) clock.
156
    time: f64,
157
    /// Delta time, in seconds, since last effect system update.
158
    delta_time: f32,
159

160
    /// Current virtual time since startup, in seconds.
161
    /// This is based on the [`Time<Virtual>`](Virtual) clock.
162
    virtual_time: f64,
163
    /// Virtual delta time, in seconds, since last effect system update.
164
    virtual_delta_time: f32,
165

166
    /// Current real time since startup, in seconds.
167
    /// This is based on the [`Time<Real>`](Real) clock.
168
    real_time: f64,
169
    /// Real delta time, in seconds, since last effect system update.
170
    real_delta_time: f32,
171
}
172

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

204
impl Default for GpuSimParams {
205
    fn default() -> Self {
333✔
206
        Self {
207
            delta_time: 0.04,
208
            time: 0.0,
209
            virtual_delta_time: 0.04,
210
            virtual_time: 0.0,
211
            real_delta_time: 0.04,
212
            real_time: 0.0,
213
            num_effects: 0,
214
        }
215
    }
216
}
217

218
impl From<SimParams> for GpuSimParams {
219
    #[inline]
220
    fn from(src: SimParams) -> Self {
×
221
        Self::from(&src)
×
222
    }
223
}
224

225
impl From<&SimParams> for GpuSimParams {
226
    fn from(src: &SimParams) -> Self {
330✔
227
        Self {
228
            delta_time: src.delta_time,
660✔
229
            time: src.time as f32,
660✔
230
            virtual_delta_time: src.virtual_delta_time,
660✔
231
            virtual_time: src.virtual_time as f32,
660✔
232
            real_delta_time: src.real_delta_time,
660✔
233
            real_time: src.real_time as f32,
330✔
234
            ..default()
235
        }
236
    }
237
}
238

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

255
impl From<Mat4> for GpuCompressedTransform {
256
    fn from(value: Mat4) -> Self {
624✔
257
        let tr = value.transpose();
1,872✔
258
        #[cfg(test)]
259
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
260
        Self {
261
            x_row: tr.x_axis.to_array(),
1,872✔
262
            y_row: tr.y_axis.to_array(),
1,872✔
263
            z_row: tr.z_axis.to_array(),
624✔
264
        }
265
    }
266
}
267

268
impl From<&Mat4> for GpuCompressedTransform {
269
    fn from(value: &Mat4) -> Self {
×
270
        let tr = value.transpose();
×
271
        #[cfg(test)]
272
        crate::test_utils::assert_approx_eq!(tr.w_axis, Vec4::W);
273
        Self {
274
            x_row: tr.x_axis.to_array(),
×
275
            y_row: tr.y_axis.to_array(),
×
276
            z_row: tr.z_axis.to_array(),
×
277
        }
278
    }
279
}
280

281
impl GpuCompressedTransform {
282
    /// Returns the translation as represented by this transform.
283
    #[allow(dead_code)]
284
    pub fn translation(&self) -> Vec3 {
×
285
        Vec3 {
286
            x: self.x_row[3],
×
287
            y: self.y_row[3],
×
288
            z: self.z_row[3],
×
289
        }
290
    }
291
}
292

293
/// Extension trait for shader types stored in a WGSL storage buffer.
294
pub(crate) trait StorageType {
295
    /// Get the aligned size, in bytes, of this type such that it aligns to the
296
    /// given alignment, in bytes.
297
    ///
298
    /// This is mainly used to align GPU types to device requirements.
299
    fn aligned_size(alignment: u32) -> NonZeroU64;
300

301
    /// Get the WGSL padding code to append to the GPU struct to align it.
302
    ///
303
    /// This is useful if the struct needs to be bound directly with a dynamic
304
    /// bind group offset, which requires the offset to be a multiple of a GPU
305
    /// device specific alignment value.
306
    fn padding_code(alignment: u32) -> String;
307
}
308

309
impl<T: ShaderType> StorageType for T {
310
    fn aligned_size(alignment: u32) -> NonZeroU64 {
56✔
311
        NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
336✔
312
    }
313

314
    fn padding_code(alignment: u32) -> String {
12✔
315
        let aligned_size = T::aligned_size(alignment);
36✔
316
        trace!(
12✔
317
            "Aligning {} to {} bytes as device limits requires. Orignal size: {} bytes. Aligned size: {} bytes.",
4✔
318
            std::any::type_name::<T>(),
4✔
319
            alignment,
×
320
            T::min_size().get(),
8✔
321
            aligned_size
×
322
        );
323

324
        // We need to pad the Spawner WGSL struct based on the device padding so that we
325
        // can use it as an array element but also has a direct struct binding.
326
        if T::min_size() != aligned_size {
12✔
327
            let padding_size = aligned_size.get() - T::min_size().get();
48✔
328
            assert!(padding_size % 4 == 0);
24✔
329
            format!("padding: array<u32, {}>", padding_size / 4)
36✔
330
        } else {
331
            "".to_string()
×
332
        }
333
    }
334
}
335

336
/// GPU representation of spawner parameters.
337
#[repr(C)]
338
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
339
pub(crate) struct GpuSpawnerParams {
340
    /// Transform of the effect (origin of the emitter). This is either added to
341
    /// emitted particles at spawn time, if the effect simulated in world
342
    /// space, or to all simulated particles during rendering if the effect is
343
    /// simulated in local space.
344
    transform: GpuCompressedTransform,
345
    /// Inverse of [`transform`], stored with the same convention.
346
    ///
347
    /// [`transform`]: Self::transform
348
    inverse_transform: GpuCompressedTransform,
349
    /// Number of particles to spawn this frame.
350
    spawn: i32,
351
    /// Spawn seed, for randomized modifiers.
352
    seed: u32,
353
    /// Index of the pong (read) buffer for indirect indices, used by the render
354
    /// shader to fetch particles and render them. Only temporarily stored
355
    /// between indirect and render passes, and overwritten each frame by CPU
356
    /// upload. This is mostly a hack to transfer a value between those 2
357
    /// compute passes.
358
    render_pong: u32,
359
    /// Index of the [`GpuEffectMetadata`] for this effect.
360
    effect_metadata_index: u32,
361
    /// Index of the [`GpuDrawIndirect`] or [`GpuDrawIndexedIndirect`] for this
362
    /// effect.
363
    draw_indirect_index: u32,
364
    /// Start offset of the particles and indirect indices into the effect's
365
    /// slab, in number of particles (row index).
366
    slab_offset: u32,
367
    /// Start offset of the particles and indirect indices into the parent effect's
368
    /// slab (if the effect has a parent effect), in number of particles (row index).
369
    /// This is ignored if the effect has no parent.
370
    parent_slab_offset: u32,
371
}
372

373
/// GPU representation of an indirect compute dispatch input.
374
///
375
/// Note that unlike most other data structure, this doesn't need to be aligned
376
/// (except for the default 4-byte align for most GPU types) to any uniform or
377
/// storage buffer offset alignment, because the buffer storing this is only
378
/// ever used as input to indirect dispatch commands, and never bound as a
379
/// shader resource.
380
///
381
/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DispatchIndirectArgs.html.
382
#[repr(C)]
383
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
384
pub struct GpuDispatchIndirectArgs {
385
    pub x: u32,
386
    pub y: u32,
387
    pub z: u32,
388
}
389

390
impl Default for GpuDispatchIndirectArgs {
391
    fn default() -> Self {
×
392
        Self { x: 0, y: 1, z: 1 }
393
    }
394
}
395

396
/// GPU representation of an indirect (non-indexed) render input.
397
///
398
/// Note that unlike most other data structure, this doesn't need to be aligned
399
/// (except for the default 4-byte align for most GPU types) to any uniform or
400
/// storage buffer offset alignment, because the buffer storing this is only
401
/// ever used as input to indirect render commands, and never bound as a shader
402
/// resource.
403
///
404
/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DrawIndirectArgs.html.
405
#[repr(C)]
406
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
407
pub struct GpuDrawIndirectArgs {
408
    pub vertex_count: u32,
409
    pub instance_count: u32,
410
    pub first_vertex: u32,
411
    pub first_instance: u32,
412
}
413

414
impl Default for GpuDrawIndirectArgs {
415
    fn default() -> Self {
×
416
        Self {
417
            vertex_count: 0,
418
            instance_count: 1,
419
            first_vertex: 0,
420
            first_instance: 0,
421
        }
422
    }
423
}
424

425
/// GPU representation of an indirect indexed render input.
426
///
427
/// Note that unlike most other data structure, this doesn't need to be aligned
428
/// (except for the default 4-byte align for most GPU types) to any uniform or
429
/// storage buffer offset alignment, because the buffer storing this is only
430
/// ever used as input to indirect render commands, and never bound as a shader
431
/// resource.
432
///
433
/// See https://docs.rs/wgpu/latest/wgpu/util/struct.DrawIndexedIndirectArgs.html.
434
#[repr(C)]
435
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
436
pub struct GpuDrawIndexedIndirectArgs {
437
    pub index_count: u32,
438
    pub instance_count: u32,
439
    pub first_index: u32,
440
    pub base_vertex: i32,
441
    pub first_instance: u32,
442
}
443

444
impl Default for GpuDrawIndexedIndirectArgs {
445
    fn default() -> Self {
×
446
        Self {
447
            index_count: 0,
448
            instance_count: 1,
449
            first_index: 0,
450
            base_vertex: 0,
451
            first_instance: 0,
452
        }
453
    }
454
}
455

456
/// Stores metadata about each particle effect.
457
///
458
/// This is written by the CPU and read by the GPU.
459
#[repr(C)]
460
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Pod, Zeroable, ShaderType)]
461
pub struct GpuEffectMetadata {
462
    //
463
    // Some runtime variables modified on GPU only (capacity is constant)
464
    /// Effect capacity, in number of particles.
465
    pub capacity: u32,
466
    // Additional data not part of the required draw indirect args
467
    /// Number of alive particles.
468
    pub alive_count: u32,
469
    /// Cached value of `alive_count` to cap threads in update pass.
470
    pub max_update: u32,
471
    /// Cached value of `dead_count` to cap threads in init pass.
472
    pub max_spawn: u32,
473
    /// Index of the ping buffer for particle indices. Init and update compute
474
    /// passes always write into the ping buffer and read from the pong buffer.
475
    /// The buffers are swapped (ping = 1 - ping) during the indirect dispatch.
476
    pub indirect_write_index: u32,
477

478
    //
479
    // Some real metadata values depending on where the effect instance is allocated.
480
    /// Index of the [`GpuDispatchIndirect`] struct inside the global
481
    /// [`EffectsMeta::dispatch_indirect_buffer`].
482
    pub indirect_dispatch_index: u32,
483
    /// Index of the [`GpuDrawIndirect`] or [`GpuDrawIndexedIndirect`] struct
484
    /// inside the global [`EffectsMeta::draw_indirect_buffer`] or
485
    /// [`EffectsMeta::draw_indexed_indirect_buffer`]. The actual buffer depends
486
    /// on whether the mesh is indexed or not, which is stored in
487
    /// [`CachedMeshLocation`].
488
    pub indirect_draw_index: u32,
489
    /// Offset (in u32 count) of the init indirect dispatch struct inside its
490
    /// buffer. This avoids having to align those 16-byte structs to the GPU
491
    /// alignment (at least 32 bytes, even 256 bytes on some).
492
    pub init_indirect_dispatch_index: u32,
493
    /// Index of this effect into its parent's ChildInfo array
494
    /// ([`EffectChildren::effect_cache_ids`] and its associated GPU
495
    /// array). This starts at zero for the first child of each effect, and is
496
    /// only unique per parent, not globally. Only available if this effect is a
497
    /// child of another effect (i.e. if it has a parent).
498
    pub local_child_index: u32,
499
    /// For children, global index of the ChildInfo into the shared array.
500
    pub global_child_index: u32,
501
    /// For parents, base index of the their first ChildInfo into the shared
502
    /// array.
503
    pub base_child_index: u32,
504

505
    /// Particle stride, in number of u32.
506
    pub particle_stride: u32,
507
    /// Offset from the particle start to the first sort key, in number of u32.
508
    pub sort_key_offset: u32,
509
    /// Offset from the particle start to the second sort key, in number of u32.
510
    pub sort_key2_offset: u32,
511

512
    //
513
    // Again some runtime-only GPU-mutated data
514
    /// Atomic counter incremented each time a particle spawns. Useful for
515
    /// things like RIBBON_ID or any other use where a unique value is needed.
516
    /// The value loops back after some time, but unless some particle lives
517
    /// forever there's little chance of repetition.
518
    pub particle_counter: u32,
519
}
520

521
/// Single init fill dispatch item in an [`InitFillDispatchQueue`].
522
#[derive(Debug)]
523
pub(super) struct InitFillDispatchItem {
524
    /// Index of the source [`GpuChildInfo`] entry to read the event count from.
525
    pub global_child_index: u32,
526
    /// Index of the [`GpuDispatchIndirect`] entry to write the workgroup count
527
    /// to.
528
    pub dispatch_indirect_index: u32,
529
}
530

531
/// Queue of fill dispatch operations for the init indirect pass.
532
///
533
/// The queue stores the init fill dispatch operations for the current frame,
534
/// without the reference to the source and destination buffers, which may be
535
/// reallocated later in the frame. This allows enqueuing operations during the
536
/// prepare rendering phase, while deferring GPU buffer (re-)allocation to a
537
/// later stage.
538
#[derive(Debug, Default, Resource)]
539
pub(super) struct InitFillDispatchQueue {
540
    queue: Vec<InitFillDispatchItem>,
541
    submitted_queue_index: Option<u32>,
542
}
543

544
impl InitFillDispatchQueue {
545
    /// Clear the queue.
546
    #[inline]
547
    pub fn clear(&mut self) {
330✔
548
        self.queue.clear();
660✔
549
        self.submitted_queue_index = None;
330✔
550
    }
551

552
    /// Check if the queue is empty.
553
    #[inline]
554
    pub fn is_empty(&self) -> bool {
330✔
555
        self.queue.is_empty()
660✔
556
    }
557

558
    /// Enqueue a new operation.
559
    #[inline]
560
    pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
6✔
561
        assert!(global_child_index != u32::MAX);
12✔
562
        self.queue.push(InitFillDispatchItem {
18✔
563
            global_child_index,
6✔
564
            dispatch_indirect_index,
6✔
565
        });
566
    }
567

568
    /// Submit pending operations for this frame.
569
    pub fn submit(
3✔
570
        &mut self,
571
        src_buffer: &Buffer,
572
        dst_buffer: &Buffer,
573
        gpu_buffer_operations: &mut GpuBufferOperations,
574
    ) {
575
        if self.queue.is_empty() {
6✔
576
            return;
×
577
        }
578

579
        // Sort by source. We can only batch if the destination is also contiguous, so
580
        // we can check with a linear walk if the source is already sorted.
581
        self.queue
582
            .sort_unstable_by_key(|item| item.global_child_index);
583

584
        let mut fill_queue = GpuBufferOperationQueue::new();
585

586
        // Batch and schedule all init indirect dispatch operations
587
        assert!(
588
            self.queue[0].global_child_index != u32::MAX,
589
            "Global child index not initialized"
×
590
        );
591
        let mut src_start = self.queue[0].global_child_index;
6✔
592
        let mut dst_start = self.queue[0].dispatch_indirect_index;
6✔
593
        let mut src_end = src_start + 1;
6✔
594
        let mut dst_end = dst_start + 1;
6✔
595
        let src_stride = GpuChildInfo::min_size().get() as u32 / 4;
6✔
596
        let dst_stride = GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4;
6✔
597
        for i in 1..self.queue.len() {
9✔
598
            let InitFillDispatchItem {
599
                global_child_index: src,
3✔
600
                dispatch_indirect_index: dst,
3✔
601
            } = self.queue[i];
3✔
602
            if src != src_end || dst != dst_end {
6✔
603
                let count = src_end - src_start;
1✔
604
                debug_assert_eq!(count, dst_end - dst_start);
605
                let args = GpuBufferOperationArgs {
606
                    src_offset: src_start * src_stride + 1,
607
                    src_stride,
608
                    dst_offset: dst_start * dst_stride,
609
                    dst_stride,
610
                    count,
611
                };
612
                trace!(
613
                "enqueue_init_fill(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
×
614
                src_start,
615
                dst_start,
616
                args,
617
                src_buffer.id(),
×
618
                dst_buffer.id(),
×
619
            );
620
                fill_queue.enqueue(
621
                    GpuBufferOperationType::FillDispatchArgs,
622
                    args,
623
                    src_buffer.clone(),
624
                    0,
625
                    None,
626
                    dst_buffer.clone(),
627
                    0,
628
                    None,
629
                );
630
                src_start = src;
631
                dst_start = dst;
632
            }
633
            src_end = src + 1;
3✔
634
            dst_end = dst + 1;
635
        }
636
        if src_start != src_end || dst_start != dst_end {
3✔
637
            let count = src_end - src_start;
3✔
638
            debug_assert_eq!(count, dst_end - dst_start);
639
            let args = GpuBufferOperationArgs {
640
                src_offset: src_start * src_stride + 1,
6✔
641
                src_stride,
642
                dst_offset: dst_start * dst_stride,
6✔
643
                dst_stride,
644
                count,
645
            };
646
            trace!(
3✔
647
            "IFDA::submit(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
×
648
            src_start,
649
            dst_start,
650
            args,
651
            src_buffer.id(),
×
652
            dst_buffer.id(),
×
653
        );
654
            fill_queue.enqueue(
6✔
655
                GpuBufferOperationType::FillDispatchArgs,
3✔
656
                args,
3✔
657
                src_buffer.clone(),
6✔
658
                0,
659
                None,
3✔
660
                dst_buffer.clone(),
6✔
661
                0,
662
                None,
3✔
663
            );
664
        }
665

666
        debug_assert!(self.submitted_queue_index.is_none());
3✔
667
        if !fill_queue.operation_queue.is_empty() {
6✔
668
            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
3✔
669
        }
670
    }
671
}
672

673
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
674
/// shader.
675
#[derive(Resource)]
676
pub(crate) struct DispatchIndirectPipeline {
677
    /// Layout of bind group sim_params@0.
678
    sim_params_bind_group_layout: BindGroupLayout,
679
    /// Layout of bind group effect_metadata@1.
680
    effect_metadata_bind_group_layout: BindGroupLayout,
681
    /// Layout of bind group spawner@2.
682
    spawner_bind_group_layout: BindGroupLayout,
683
    /// Layout of bind group child_infos@3.
684
    child_infos_bind_group_layout: BindGroupLayout,
685
    /// Shader when no GPU events are used (no bind group @3).
686
    indirect_shader_noevent: Handle<Shader>,
687
    /// Shader when GPU events are used (bind group @3 present).
688
    indirect_shader_events: Handle<Shader>,
689
}
690

691
impl FromWorld for DispatchIndirectPipeline {
692
    fn from_world(world: &mut World) -> Self {
3✔
693
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
694

695
        // Copy the indirect pipeline shaders to self, because we can't access anything
696
        // else during pipeline specialization.
697
        let (indirect_shader_noevent, indirect_shader_events) = {
9✔
698
            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
15✔
699
            (
700
                effects_meta.indirect_shader_noevent.clone(),
9✔
701
                effects_meta.indirect_shader_events.clone(),
3✔
702
            )
703
        };
704

705
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
6✔
706
        let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
9✔
707
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
9✔
708

709
        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
710
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
7✔
711
        let sim_params_bind_group_layout = render_device.create_bind_group_layout(
9✔
712
            "hanabi:bind_group_layout:dispatch_indirect:sim_params",
713
            &[BindGroupLayoutEntry {
3✔
714
                binding: 0,
3✔
715
                visibility: ShaderStages::COMPUTE,
3✔
716
                ty: BindingType::Buffer {
3✔
717
                    ty: BufferBindingType::Uniform,
3✔
718
                    has_dynamic_offset: false,
3✔
719
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
720
                },
721
                count: None,
3✔
722
            }],
723
        );
724

725
        trace!(
3✔
726
            "GpuEffectMetadata: min_size={} padded_size={}",
2✔
727
            GpuEffectMetadata::min_size(),
2✔
728
            effect_metadata_size,
729
        );
730
        let effect_metadata_bind_group_layout = render_device.create_bind_group_layout(
9✔
731
            "hanabi:bind_group_layout:dispatch_indirect:effect_metadata@1",
732
            &[
3✔
733
                // @group(0) @binding(0) var<storage, read_write> effect_metadata_buffer :
734
                // array<u32>;
735
                BindGroupLayoutEntry {
6✔
736
                    binding: 0,
6✔
737
                    visibility: ShaderStages::COMPUTE,
6✔
738
                    ty: BindingType::Buffer {
6✔
739
                        ty: BufferBindingType::Storage { read_only: false },
6✔
740
                        has_dynamic_offset: false,
6✔
741
                        min_binding_size: Some(effect_metadata_size),
6✔
742
                    },
743
                    count: None,
6✔
744
                },
745
                // @group(0) @binding(1) var<storage, read_write> dispatch_indirect_buffer :
746
                // array<u32>;
747
                BindGroupLayoutEntry {
6✔
748
                    binding: 1,
6✔
749
                    visibility: ShaderStages::COMPUTE,
6✔
750
                    ty: BindingType::Buffer {
6✔
751
                        ty: BufferBindingType::Storage { read_only: false },
9✔
752
                        has_dynamic_offset: false,
6✔
753
                        min_binding_size: Some(
6✔
754
                            NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap(),
9✔
755
                        ),
756
                    },
757
                    count: None,
6✔
758
                },
759
                // @group(0) @binding(2) var<storage, read_write> draw_indirect_buffer :
760
                // array<u32>;
761
                BindGroupLayoutEntry {
3✔
762
                    binding: 2,
3✔
763
                    visibility: ShaderStages::COMPUTE,
3✔
764
                    ty: BindingType::Buffer {
3✔
765
                        ty: BufferBindingType::Storage { read_only: false },
3✔
766
                        has_dynamic_offset: false,
3✔
767
                        min_binding_size: Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
3✔
768
                    },
769
                    count: None,
3✔
770
                },
771
            ],
772
        );
773

774
        // @group(2) @binding(0) var<storage, read_write> spawner_buffer :
775
        // array<Spawner>;
776
        let spawner_bind_group_layout = render_device.create_bind_group_layout(
9✔
777
            "hanabi:bind_group_layout:dispatch_indirect:spawner@2",
778
            &[BindGroupLayoutEntry {
3✔
779
                binding: 0,
3✔
780
                visibility: ShaderStages::COMPUTE,
3✔
781
                ty: BindingType::Buffer {
3✔
782
                    ty: BufferBindingType::Storage { read_only: false },
3✔
783
                    has_dynamic_offset: false,
3✔
784
                    min_binding_size: Some(spawner_min_binding_size),
3✔
785
                },
786
                count: None,
3✔
787
            }],
788
        );
789

790
        // @group(3) @binding(0) var<storage, read_write> child_info_buffer :
791
        // ChildInfoBuffer;
792
        let child_infos_bind_group_layout = render_device.create_bind_group_layout(
9✔
793
            "hanabi:bind_group_layout:dispatch_indirect:child_infos",
794
            &[BindGroupLayoutEntry {
3✔
795
                binding: 0,
3✔
796
                visibility: ShaderStages::COMPUTE,
3✔
797
                ty: BindingType::Buffer {
3✔
798
                    ty: BufferBindingType::Storage { read_only: false },
3✔
799
                    has_dynamic_offset: false,
3✔
800
                    min_binding_size: Some(GpuChildInfo::min_size()),
3✔
801
                },
802
                count: None,
3✔
803
            }],
804
        );
805

806
        Self {
807
            sim_params_bind_group_layout,
808
            effect_metadata_bind_group_layout,
809
            spawner_bind_group_layout,
810
            child_infos_bind_group_layout,
811
            indirect_shader_noevent,
812
            indirect_shader_events,
813
        }
814
    }
815
}
816

817
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
818
pub(crate) struct DispatchIndirectPipelineKey {
819
    /// True if any allocated effect uses GPU spawn events. In that case, the
820
    /// pipeline is specialized to clear all GPU events each frame after the
821
    /// indirect init pass consumed them to spawn particles, and before the
822
    /// update pass optionally produce more events.
823
    /// Key: HAS_GPU_SPAWN_EVENTS
824
    has_events: bool,
825
}
826

827
impl SpecializedComputePipeline for DispatchIndirectPipeline {
828
    type Key = DispatchIndirectPipelineKey;
829

830
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
6✔
831
        trace!(
6✔
832
            "Specializing indirect pipeline (has_events={})",
4✔
833
            key.has_events
834
        );
835

836
        let mut shader_defs = Vec::with_capacity(2);
12✔
837
        // Spawner struct needs to be defined with padding, because it's bound as an
838
        // array
839
        shader_defs.push("SPAWNER_PADDING".into());
24✔
840
        if key.has_events {
9✔
841
            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
9✔
842
        }
843

844
        let mut layout = Vec::with_capacity(4);
12✔
845
        layout.push(self.sim_params_bind_group_layout.clone());
24✔
846
        layout.push(self.effect_metadata_bind_group_layout.clone());
24✔
847
        layout.push(self.spawner_bind_group_layout.clone());
24✔
848
        if key.has_events {
9✔
849
            layout.push(self.child_infos_bind_group_layout.clone());
9✔
850
        }
851

852
        let label = format!(
12✔
853
            "hanabi:compute_pipeline:dispatch_indirect{}",
854
            if key.has_events {
6✔
855
                "_events"
3✔
856
            } else {
857
                "_noevent"
3✔
858
            }
859
        );
860

861
        ComputePipelineDescriptor {
862
            label: Some(label.into()),
6✔
863
            layout,
864
            shader: if key.has_events {
6✔
865
                self.indirect_shader_events.clone()
866
            } else {
867
                self.indirect_shader_noevent.clone()
868
            },
869
            shader_defs,
870
            entry_point: Some("main".into()),
6✔
871
            push_constant_ranges: vec![],
6✔
872
            zero_initialize_workgroup_memory: false,
873
        }
874
    }
875
}
876

877
/// Type of GPU buffer operation.
878
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
879
pub(super) enum GpuBufferOperationType {
880
    /// Clear the destination buffer to zero.
881
    ///
882
    /// The source parameters [`src_offset`] and [`src_stride`] are ignored.
883
    ///
884
    /// [`src_offset`]: crate::GpuBufferOperationArgs::src_offset
885
    /// [`src_stride`]: crate::GpuBufferOperationArgs::src_stride
886
    #[allow(dead_code)]
887
    Zero,
888
    /// Copy a source buffer into a destination buffer.
889
    ///
890
    /// The source can have a stride between each `u32` copied. The destination
891
    /// is always a contiguous buffer.
892
    #[allow(dead_code)]
893
    Copy,
894
    /// Fill the arguments for a later indirect dispatch call.
895
    ///
896
    /// This is similar to a copy, but will round up the source value to the
897
    /// number of threads per workgroup (64) before writing it into the
898
    /// destination.
899
    FillDispatchArgs,
900
    /// Fill the arguments for a later indirect dispatch call.
901
    ///
902
    /// This is the same as [`FillDispatchArgs`], but the source element count
903
    /// is read from the fourth entry in the destination buffer directly,
904
    /// and the source buffer and source arguments are unused.
905
    #[allow(dead_code)]
906
    FillDispatchArgsSelf,
907
}
908

909
/// GPU representation of the arguments of a block operation on a buffer.
910
#[repr(C)]
911
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, Zeroable, ShaderType)]
912
pub(super) struct GpuBufferOperationArgs {
913
    /// Offset, as u32 count, where the operation starts in the source buffer.
914
    src_offset: u32,
915
    /// Stride, as u32 count, between elements in the source buffer.
916
    src_stride: u32,
917
    /// Offset, as u32 count, where the operation starts in the destination
918
    /// buffer.
919
    dst_offset: u32,
920
    /// Stride, as u32 count, between elements in the destination buffer.
921
    dst_stride: u32,
922
    /// Number of u32 elements to process for this operation.
923
    count: u32,
924
}
925

926
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
927
struct QueuedOperationBindGroupKey {
928
    src_buffer: BufferId,
929
    src_binding_size: Option<NonZeroU32>,
930
    dst_buffer: BufferId,
931
    dst_binding_size: Option<NonZeroU32>,
932
}
933

934
#[derive(Debug, Clone)]
935
struct QueuedOperation {
936
    op: GpuBufferOperationType,
937
    args_index: u32,
938
    src_buffer: Buffer,
939
    src_binding_offset: u32,
940
    src_binding_size: Option<NonZeroU32>,
941
    dst_buffer: Buffer,
942
    dst_binding_offset: u32,
943
    dst_binding_size: Option<NonZeroU32>,
944
}
945

946
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
947
    fn from(value: &QueuedOperation) -> Self {
×
948
        Self {
949
            src_buffer: value.src_buffer.id(),
×
950
            src_binding_size: value.src_binding_size,
×
951
            dst_buffer: value.dst_buffer.id(),
×
952
            dst_binding_size: value.dst_binding_size,
×
953
        }
954
    }
955
}
956

957
/// Queue of GPU buffer operations.
958
///
959
/// The queue records a series of ordered operations on GPU buffers. It can be
960
/// submitted for this frame via [`GpuBufferOperations::submit()`], and
961
/// subsequently dispatched as a compute pass via
962
/// [`GpuBufferOperations::dispatch()`].
963
pub struct GpuBufferOperationQueue {
964
    /// Operation arguments.
965
    args: Vec<GpuBufferOperationArgs>,
966
    /// Queued operations.
967
    operation_queue: Vec<QueuedOperation>,
968
}
969

970
impl GpuBufferOperationQueue {
971
    /// Create a new empty queue.
972
    pub fn new() -> Self {
333✔
973
        Self {
974
            args: vec![],
333✔
975
            operation_queue: vec![],
333✔
976
        }
977
    }
978

979
    /// Enqueue a generic operation.
980
    pub fn enqueue(
4✔
981
        &mut self,
982
        op: GpuBufferOperationType,
983
        args: GpuBufferOperationArgs,
984
        src_buffer: Buffer,
985
        src_binding_offset: u32,
986
        src_binding_size: Option<NonZeroU32>,
987
        dst_buffer: Buffer,
988
        dst_binding_offset: u32,
989
        dst_binding_size: Option<NonZeroU32>,
990
    ) -> u32 {
991
        trace!(
4✔
992
            "Queue {:?} op: args={:?} src_buffer={:?} src_binding_offset={} src_binding_size={:?} dst_buffer={:?} dst_binding_offset={} dst_binding_size={:?}",
×
993
            op,
994
            args,
995
            src_buffer,
996
            src_binding_offset,
997
            src_binding_size,
998
            dst_buffer,
999
            dst_binding_offset,
1000
            dst_binding_size,
1001
        );
1002
        let args_index = self.args.len() as u32;
8✔
1003
        self.args.push(args);
12✔
1004
        self.operation_queue.push(QueuedOperation {
12✔
1005
            op,
8✔
1006
            args_index,
8✔
1007
            src_buffer,
8✔
1008
            src_binding_offset,
8✔
1009
            src_binding_size,
8✔
1010
            dst_buffer,
8✔
1011
            dst_binding_offset,
4✔
1012
            dst_binding_size,
4✔
1013
        });
1014
        args_index
4✔
1015
    }
1016
}
1017

1018
/// GPU buffer operations for this frame.
1019
///
1020
/// This resource contains a list of submitted [`GpuBufferOperationQueue`] for
1021
/// the current frame, and ensures the bind groups for those operations are up
1022
/// to date.
1023
#[derive(Resource)]
1024
pub(super) struct GpuBufferOperations {
1025
    /// Arguments for the buffer operations submitted this frame.
1026
    args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
1027

1028
    /// Bind groups for the submitted operations.
1029
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
1030

1031
    /// Submitted queues for this frame.
1032
    queues: Vec<Vec<QueuedOperation>>,
1033
}
1034

1035
impl FromWorld for GpuBufferOperations {
1036
    fn from_world(world: &mut World) -> Self {
4✔
1037
        let render_device = world.get_resource::<RenderDevice>().unwrap();
16✔
1038
        let align = render_device.limits().min_uniform_buffer_offset_alignment;
8✔
1039
        Self::new(align)
8✔
1040
    }
1041
}
1042

1043
impl GpuBufferOperations {
1044
    pub fn new(align: u32) -> Self {
4✔
1045
        let args_buffer = AlignedBufferVec::new(
1046
            BufferUsages::UNIFORM,
1047
            Some(NonZeroU64::new(align as u64).unwrap()),
8✔
1048
            Some("hanabi:buffer:gpu_operation_args".to_string()),
4✔
1049
        );
1050
        Self {
1051
            args_buffer,
1052
            bind_groups: default(),
4✔
1053
            queues: vec![],
4✔
1054
        }
1055
    }
1056

1057
    /// Clear the queue and begin recording operations for a new frame.
1058
    pub fn begin_frame(&mut self) {
333✔
1059
        self.args_buffer.clear();
666✔
1060
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
666✔
1061
        self.queues.clear();
666✔
1062
    }
1063

1064
    /// Submit a recorded queue.
1065
    ///
1066
    /// # Panics
1067
    ///
1068
    /// Panics if the queue submitted is empty.
1069
    pub fn submit(&mut self, mut queue: GpuBufferOperationQueue) -> u32 {
3✔
1070
        assert!(!queue.operation_queue.is_empty());
6✔
1071
        let queue_index = self.queues.len() as u32;
6✔
1072
        for qop in &mut queue.operation_queue {
11✔
1073
            qop.args_index = self.args_buffer.push(queue.args[qop.args_index as usize]) as u32;
1074
        }
1075
        self.queues.push(queue.operation_queue);
9✔
1076
        queue_index
3✔
1077
    }
1078

1079
    /// Finish recording operations for this frame, and schedule buffer writes
1080
    /// to GPU.
1081
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
333✔
1082
        assert_eq!(
333✔
1083
            self.args_buffer.len(),
666✔
1084
            self.queues.iter().fold(0, |len, q| len + q.len())
675✔
1085
        );
1086

1087
        // Upload to GPU buffer
1088
        self.args_buffer.write_buffer(device, render_queue);
1,332✔
1089
    }
1090

1091
    /// Create all necessary bind groups for all queued operations.
1092
    pub fn create_bind_groups(
312✔
1093
        &mut self,
1094
        render_device: &RenderDevice,
1095
        utils_pipeline: &UtilsPipeline,
1096
    ) {
1097
        trace!(
312✔
1098
            "Creating bind groups for {} operation queues...",
312✔
1099
            self.queues.len()
624✔
1100
        );
1101
        for queue in &self.queues {
312✔
1102
            for qop in queue {
×
1103
                let key: QueuedOperationBindGroupKey = qop.into();
1104
                self.bind_groups.entry(key).or_insert_with(|| {
×
1105
                    let src_id: NonZeroU32 = qop.src_buffer.id().into();
×
1106
                    let dst_id: NonZeroU32 = qop.dst_buffer.id().into();
×
1107
                    let label = format!("hanabi:bind_group:util_{}_{}", src_id.get(), dst_id.get());
×
1108
                    let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
×
1109
                    let bind_group_layout =
×
1110
                        utils_pipeline.bind_group_layout(qop.op, use_dynamic_offset);
×
1111
                    let (src_offset, dst_offset) = if use_dynamic_offset {
×
1112
                        (0, 0)
×
1113
                    } else {
1114
                        (qop.src_binding_offset as u64, qop.dst_binding_offset as u64)
×
1115
                    };
1116
                    trace!(
×
1117
                        "-> Creating new bind group '{}': src#{} (@+{}B:{:?}B) dst#{} (@+{}B:{:?}B)",
×
1118
                        label,
1119
                        src_id,
1120
                        src_offset,
1121
                        qop.src_binding_size,
1122
                        dst_id,
1123
                        dst_offset,
1124
                        qop.dst_binding_size,
1125
                    );
1126
                    render_device.create_bind_group(
×
1127
                        Some(&label[..]),
×
1128
                        bind_group_layout,
×
1129
                        &[
×
1130
                            BindGroupEntry {
×
1131
                                binding: 0,
×
1132
                                resource: BindingResource::Buffer(BufferBinding {
×
1133
                                    buffer: self.args_buffer.buffer().unwrap(),
×
1134
                                    offset: 0,
×
1135
                                    // We always bind exactly 1 row of arguments
1136
                                    size: Some(
×
1137
                                        NonZeroU64::new(self.args_buffer.aligned_size() as u64)
×
1138
                                            .unwrap(),
×
1139
                                    ),
1140
                                }),
1141
                            },
1142
                            BindGroupEntry {
×
1143
                                binding: 1,
×
1144
                                resource: BindingResource::Buffer(BufferBinding {
×
1145
                                    buffer: &qop.src_buffer,
×
1146
                                    offset: src_offset,
×
1147
                                    size: qop.src_binding_size.map(Into::into),
×
1148
                                }),
1149
                            },
1150
                            BindGroupEntry {
×
1151
                                binding: 2,
×
1152
                                resource: BindingResource::Buffer(BufferBinding {
×
1153
                                    buffer: &qop.dst_buffer,
×
1154
                                    offset: dst_offset,
×
1155
                                    size: qop.dst_binding_size.map(Into::into),
×
1156
                                }),
1157
                            },
1158
                        ],
1159
                    )
1160
                });
1161
            }
1162
        }
1163
    }
1164

1165
    /// Dispatch a submitted queue by index.
1166
    ///
1167
    /// This creates a new, optionally labelled, compute pass, and records to
1168
    /// the render context a series of compute workgroup dispatch, one for each
1169
    /// enqueued operation.
1170
    ///
1171
    /// The compute pipeline(s) used for each operation are fetched from the
1172
    /// [`UtilsPipeline`], and the associated bind groups are used from a
1173
    /// previous call to [`Self::create_bind_groups()`].
1174
    pub fn dispatch(
×
1175
        &self,
1176
        index: u32,
1177
        render_context: &mut RenderContext,
1178
        utils_pipeline: &UtilsPipeline,
1179
        compute_pass_label: Option<&str>,
1180
    ) {
1181
        let queue = &self.queues[index as usize];
×
1182
        trace!(
×
1183
            "Recording GPU commands for queue #{} ({} ops)...",
×
1184
            index,
1185
            queue.len(),
×
1186
        );
1187

1188
        if queue.is_empty() {
×
1189
            return;
×
1190
        }
1191

1192
        let mut compute_pass =
1193
            render_context
1194
                .command_encoder()
1195
                .begin_compute_pass(&ComputePassDescriptor {
1196
                    label: compute_pass_label,
1197
                    timestamp_writes: None,
1198
                });
1199

1200
        let mut prev_op = None;
1201
        for qop in queue {
×
1202
            trace!("qop={:?}", qop);
×
1203

1204
            if Some(qop.op) != prev_op {
×
1205
                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
×
1206
                prev_op = Some(qop.op);
×
1207
            }
1208

1209
            let key: QueuedOperationBindGroupKey = qop.into();
1210
            if let Some(bind_group) = self.bind_groups.get(&key) {
×
1211
                let args_offset = self.args_buffer.dynamic_offset(qop.args_index as usize);
1212
                let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
×
1213
                let (src_offset, dst_offset) = if use_dynamic_offset {
1214
                    (qop.src_binding_offset, qop.dst_binding_offset)
×
1215
                } else {
1216
                    (0, 0)
×
1217
                };
1218
                compute_pass.set_bind_group(0, bind_group, &[args_offset, src_offset, dst_offset]);
1219
                trace!(
1220
                    "set bind group with args_offset=+{}B src_offset=+{}B dst_offset=+{}B",
×
1221
                    args_offset,
1222
                    src_offset,
1223
                    dst_offset
1224
                );
1225
            } else {
1226
                error!("GPU fill dispatch buffer operation bind group not found for buffers src#{:?} dst#{:?}", qop.src_buffer.id(), qop.dst_buffer.id());
×
1227
                continue;
×
1228
            }
1229

1230
            // Dispatch the operations for this buffer
1231
            const WORKGROUP_SIZE: u32 = 64;
1232
            let num_ops = 1u32; // TODO - batching!
1233
            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
1234
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
1235
            trace!(
1236
                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
×
1237
                num_ops,
1238
                workgroup_count
1239
            );
1240
        }
1241
    }
1242
}
1243

1244
/// Compute pipeline to run the `vfx_utils` shader.
1245
#[derive(Resource)]
1246
pub(crate) struct UtilsPipeline {
1247
    #[allow(dead_code)]
1248
    bind_group_layout: BindGroupLayout,
1249
    bind_group_layout_dyn: BindGroupLayout,
1250
    bind_group_layout_no_src: BindGroupLayout,
1251
    pipelines: [ComputePipeline; 4],
1252
}
1253

1254
impl FromWorld for UtilsPipeline {
1255
    fn from_world(world: &mut World) -> Self {
3✔
1256
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1257

1258
        let bind_group_layout = render_device.create_bind_group_layout(
9✔
1259
            "hanabi:bind_group_layout:utils",
1260
            &[
3✔
1261
                BindGroupLayoutEntry {
6✔
1262
                    binding: 0,
6✔
1263
                    visibility: ShaderStages::COMPUTE,
6✔
1264
                    ty: BindingType::Buffer {
6✔
1265
                        ty: BufferBindingType::Uniform,
6✔
1266
                        has_dynamic_offset: false,
6✔
1267
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
6✔
1268
                    },
1269
                    count: None,
6✔
1270
                },
1271
                BindGroupLayoutEntry {
6✔
1272
                    binding: 1,
6✔
1273
                    visibility: ShaderStages::COMPUTE,
6✔
1274
                    ty: BindingType::Buffer {
6✔
1275
                        ty: BufferBindingType::Storage { read_only: true },
6✔
1276
                        has_dynamic_offset: false,
6✔
1277
                        min_binding_size: NonZeroU64::new(4),
6✔
1278
                    },
1279
                    count: None,
6✔
1280
                },
1281
                BindGroupLayoutEntry {
3✔
1282
                    binding: 2,
3✔
1283
                    visibility: ShaderStages::COMPUTE,
3✔
1284
                    ty: BindingType::Buffer {
3✔
1285
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1286
                        has_dynamic_offset: false,
3✔
1287
                        min_binding_size: NonZeroU64::new(4),
3✔
1288
                    },
1289
                    count: None,
3✔
1290
                },
1291
            ],
1292
        );
1293

1294
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1295
            label: Some("hanabi:pipeline_layout:utils"),
6✔
1296
            bind_group_layouts: &[&bind_group_layout],
3✔
1297
            push_constant_ranges: &[],
3✔
1298
        });
1299

1300
        let bind_group_layout_dyn = render_device.create_bind_group_layout(
9✔
1301
            "hanabi:bind_group_layout:utils_dyn",
1302
            &[
3✔
1303
                BindGroupLayoutEntry {
6✔
1304
                    binding: 0,
6✔
1305
                    visibility: ShaderStages::COMPUTE,
6✔
1306
                    ty: BindingType::Buffer {
6✔
1307
                        ty: BufferBindingType::Uniform,
6✔
1308
                        has_dynamic_offset: true,
6✔
1309
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
6✔
1310
                    },
1311
                    count: None,
6✔
1312
                },
1313
                BindGroupLayoutEntry {
6✔
1314
                    binding: 1,
6✔
1315
                    visibility: ShaderStages::COMPUTE,
6✔
1316
                    ty: BindingType::Buffer {
6✔
1317
                        ty: BufferBindingType::Storage { read_only: true },
6✔
1318
                        has_dynamic_offset: true,
6✔
1319
                        min_binding_size: NonZeroU64::new(4),
6✔
1320
                    },
1321
                    count: None,
6✔
1322
                },
1323
                BindGroupLayoutEntry {
3✔
1324
                    binding: 2,
3✔
1325
                    visibility: ShaderStages::COMPUTE,
3✔
1326
                    ty: BindingType::Buffer {
3✔
1327
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1328
                        has_dynamic_offset: true,
3✔
1329
                        min_binding_size: NonZeroU64::new(4),
3✔
1330
                    },
1331
                    count: None,
3✔
1332
                },
1333
            ],
1334
        );
1335

1336
        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1337
            label: Some("hanabi:pipeline_layout:utils_dyn"),
6✔
1338
            bind_group_layouts: &[&bind_group_layout_dyn],
3✔
1339
            push_constant_ranges: &[],
3✔
1340
        });
1341

1342
        let bind_group_layout_no_src = render_device.create_bind_group_layout(
9✔
1343
            "hanabi:bind_group_layout:utils_no_src",
1344
            &[
3✔
1345
                BindGroupLayoutEntry {
6✔
1346
                    binding: 0,
6✔
1347
                    visibility: ShaderStages::COMPUTE,
6✔
1348
                    ty: BindingType::Buffer {
6✔
1349
                        ty: BufferBindingType::Uniform,
6✔
1350
                        has_dynamic_offset: false,
6✔
1351
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
6✔
1352
                    },
1353
                    count: None,
6✔
1354
                },
1355
                BindGroupLayoutEntry {
3✔
1356
                    binding: 2,
3✔
1357
                    visibility: ShaderStages::COMPUTE,
3✔
1358
                    ty: BindingType::Buffer {
3✔
1359
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1360
                        has_dynamic_offset: false,
3✔
1361
                        min_binding_size: NonZeroU64::new(4),
3✔
1362
                    },
1363
                    count: None,
3✔
1364
                },
1365
            ],
1366
        );
1367

1368
        let pipeline_layout_no_src =
3✔
1369
            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
9✔
1370
                label: Some("hanabi:pipeline_layout:utils_no_src"),
6✔
1371
                bind_group_layouts: &[&bind_group_layout_no_src],
3✔
1372
                push_constant_ranges: &[],
3✔
1373
            });
1374

1375
        let shader_code = include_str!("vfx_utils.wgsl");
6✔
1376

1377
        // Resolve imports. Because we don't insert this shader into Bevy' pipeline
1378
        // cache, we don't get that part "for free", so we have to do it manually here.
1379
        let shader_source = {
3✔
1380
            let mut composer = Composer::default();
6✔
1381

1382
            let shader_defs = default();
6✔
1383

1384
            match composer.make_naga_module(NagaModuleDescriptor {
9✔
1385
                source: shader_code,
6✔
1386
                file_path: "vfx_utils.wgsl",
6✔
1387
                shader_defs,
3✔
1388
                ..Default::default()
3✔
1389
            }) {
1390
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
6✔
1391
                Err(compose_error) => panic!(
×
1392
                    "Failed to compose vfx_utils.wgsl, naga_oil returned: {}",
1393
                    compose_error.emit_to_string(&composer)
1394
                ),
1395
            }
1396
        };
1397

1398
        debug!("Create utils shader module:\n{}", shader_code);
6✔
1399
        #[allow(unsafe_code)]
1400
        let shader_module = unsafe {
1401
            render_device.create_shader_module(ShaderModuleDescriptor {
9✔
1402
                label: Some("hanabi:shader:utils"),
3✔
1403
                source: shader_source,
3✔
1404
            })
1405
        };
1406

1407
        trace!("Create vfx_utils pipelines...");
5✔
1408
        let zero_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
12✔
1409
            label: Some("hanabi:compute_pipeline:zero_buffer"),
6✔
1410
            layout: Some(&pipeline_layout),
6✔
1411
            module: &shader_module,
6✔
1412
            entry_point: Some("zero_buffer"),
6✔
1413
            compilation_options: PipelineCompilationOptions {
3✔
1414
                constants: &[],
3✔
1415
                zero_initialize_workgroup_memory: false,
3✔
1416
            },
1417
            cache: None,
3✔
1418
        });
1419
        let copy_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
12✔
1420
            label: Some("hanabi:compute_pipeline:copy_buffer"),
6✔
1421
            layout: Some(&pipeline_layout_dyn),
6✔
1422
            module: &shader_module,
6✔
1423
            entry_point: Some("copy_buffer"),
6✔
1424
            compilation_options: PipelineCompilationOptions {
3✔
1425
                constants: &[],
3✔
1426
                zero_initialize_workgroup_memory: false,
3✔
1427
            },
1428
            cache: None,
3✔
1429
        });
1430
        let fill_dispatch_args_pipeline =
3✔
1431
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
9✔
1432
                label: Some("hanabi:compute_pipeline:fill_dispatch_args"),
6✔
1433
                layout: Some(&pipeline_layout_dyn),
6✔
1434
                module: &shader_module,
6✔
1435
                entry_point: Some("fill_dispatch_args"),
6✔
1436
                compilation_options: PipelineCompilationOptions {
3✔
1437
                    constants: &[],
3✔
1438
                    zero_initialize_workgroup_memory: false,
3✔
1439
                },
1440
                cache: None,
3✔
1441
            });
1442
        let fill_dispatch_args_self_pipeline =
3✔
1443
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
9✔
1444
                label: Some("hanabi:compute_pipeline:fill_dispatch_args_self"),
6✔
1445
                layout: Some(&pipeline_layout_no_src),
6✔
1446
                module: &shader_module,
6✔
1447
                entry_point: Some("fill_dispatch_args_self"),
6✔
1448
                compilation_options: PipelineCompilationOptions {
3✔
1449
                    constants: &[],
3✔
1450
                    zero_initialize_workgroup_memory: false,
3✔
1451
                },
1452
                cache: None,
3✔
1453
            });
1454

1455
        Self {
1456
            bind_group_layout,
1457
            bind_group_layout_dyn,
1458
            bind_group_layout_no_src,
1459
            pipelines: [
3✔
1460
                zero_pipeline,
1461
                copy_pipeline,
1462
                fill_dispatch_args_pipeline,
1463
                fill_dispatch_args_self_pipeline,
1464
            ],
1465
        }
1466
    }
1467
}
1468

1469
impl UtilsPipeline {
1470
    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
×
1471
        match op {
×
1472
            GpuBufferOperationType::Zero => &self.pipelines[0],
×
1473
            GpuBufferOperationType::Copy => &self.pipelines[1],
×
1474
            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
×
1475
            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
×
1476
        }
1477
    }
1478

1479
    fn bind_group_layout(
×
1480
        &self,
1481
        op: GpuBufferOperationType,
1482
        with_dynamic_offsets: bool,
1483
    ) -> &BindGroupLayout {
1484
        if op == GpuBufferOperationType::FillDispatchArgsSelf {
×
1485
            assert!(
×
1486
                !with_dynamic_offsets,
×
1487
                "FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
×
1488
            );
1489
            &self.bind_group_layout_no_src
×
1490
        } else if with_dynamic_offsets {
×
1491
            &self.bind_group_layout_dyn
×
1492
        } else {
1493
            &self.bind_group_layout
×
1494
        }
1495
    }
1496
}
1497

1498
#[derive(Resource)]
1499
pub(crate) struct ParticlesInitPipeline {
1500
    sim_params_layout: BindGroupLayout,
1501

1502
    // Temporary values passed to specialize()
1503
    // https://github.com/bevyengine/bevy/issues/17132
1504
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1505
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1506
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1507
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1508
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1509
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1510
}
1511

1512
impl FromWorld for ParticlesInitPipeline {
1513
    fn from_world(world: &mut World) -> Self {
3✔
1514
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1515

1516
        let sim_params_layout = render_device.create_bind_group_layout(
9✔
1517
            "hanabi:bind_group_layout:vfx_init:sim_params@0",
1518
            // @group(0) @binding(0) var<uniform> sim_params: SimParams;
1519
            &[BindGroupLayoutEntry {
3✔
1520
                binding: 0,
3✔
1521
                visibility: ShaderStages::COMPUTE,
3✔
1522
                ty: BindingType::Buffer {
3✔
1523
                    ty: BufferBindingType::Uniform,
3✔
1524
                    has_dynamic_offset: false,
3✔
1525
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
1526
                },
1527
                count: None,
3✔
1528
            }],
1529
        );
1530

1531
        Self {
1532
            sim_params_layout,
1533
            temp_particle_bind_group_layout: None,
1534
            temp_spawner_bind_group_layout: None,
1535
            temp_metadata_bind_group_layout: None,
1536
        }
1537
    }
1538
}
1539

1540
bitflags! {
1541
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1542
    pub struct ParticleInitPipelineKeyFlags: u8 {
1543
        //const CLONE = (1u8 << 0); // DEPRECATED
1544
        const ATTRIBUTE_PREV = (1u8 << 1);
1545
        const ATTRIBUTE_NEXT = (1u8 << 2);
1546
        const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
1547
    }
1548
}
1549

1550
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1551
pub(crate) struct ParticleInitPipelineKey {
1552
    /// Compute shader, with snippets applied, but not preprocessed yet.
1553
    shader: Handle<Shader>,
1554
    /// Minimum binding size in bytes for the particle layout buffer.
1555
    particle_layout_min_binding_size: NonZeroU32,
1556
    /// Minimum binding size in bytes for the particle layout buffer of the
1557
    /// parent effect, if any.
1558
    /// Key: READ_PARENT_PARTICLE
1559
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1560
    /// Pipeline flags.
1561
    flags: ParticleInitPipelineKeyFlags,
1562
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1563
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1564
    particle_bind_group_layout_id: BindGroupLayoutId,
1565
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1566
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1567
    spawner_bind_group_layout_id: BindGroupLayoutId,
1568
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1569
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1570
    metadata_bind_group_layout_id: BindGroupLayoutId,
1571
}
1572

1573
impl SpecializedComputePipeline for ParticlesInitPipeline {
1574
    type Key = ParticleInitPipelineKey;
1575

1576
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
2✔
1577
        // We use the hash to correlate the key content with the GPU resource name
1578
        let hash = calc_hash(&key);
6✔
1579
        trace!("Specializing init pipeline {hash:016X} with key {key:?}");
4✔
1580

1581
        let mut shader_defs = Vec::with_capacity(4);
4✔
1582
        if key
2✔
1583
            .flags
2✔
1584
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
2✔
1585
        {
1586
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1587
        }
1588
        if key
2✔
1589
            .flags
2✔
1590
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
2✔
1591
        {
1592
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1593
        }
1594
        let consume_gpu_spawn_events = key
4✔
1595
            .flags
2✔
1596
            .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
2✔
1597
        if consume_gpu_spawn_events {
2✔
1598
            shader_defs.push("CONSUME_GPU_SPAWN_EVENTS".into());
×
1599
        }
1600
        // FIXME - for now this needs to keep in sync with consume_gpu_spawn_events
1601
        if key.parent_particle_layout_min_binding_size.is_some() {
4✔
1602
            assert!(consume_gpu_spawn_events);
×
1603
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1604
        } else {
1605
            assert!(!consume_gpu_spawn_events);
2✔
1606
        }
1607

1608
        // This should always be valid when specialize() is called, by design. This is
1609
        // how we pass the value to specialize() to work around the lack of access to
1610
        // external data.
1611
        // https://github.com/bevyengine/bevy/issues/17132
1612
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
2✔
1613
        assert_eq!(
1614
            particle_bind_group_layout.id(),
1615
            key.particle_bind_group_layout_id
1616
        );
1617
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
8✔
1618
        assert_eq!(
2✔
1619
            spawner_bind_group_layout.id(),
4✔
1620
            key.spawner_bind_group_layout_id
1621
        );
1622
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
8✔
1623
        assert_eq!(
2✔
1624
            metadata_bind_group_layout.id(),
4✔
1625
            key.metadata_bind_group_layout_id
1626
        );
1627

1628
        let label = format!("hanabi:pipeline:init_{hash:016X}");
6✔
1629
        trace!(
2✔
1630
            "-> creating pipeline '{}' with shader defs:{}",
2✔
1631
            label,
1632
            shader_defs
2✔
1633
                .iter()
2✔
1634
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
1635
        );
1636

1637
        ComputePipelineDescriptor {
1638
            label: Some(label.into()),
4✔
1639
            layout: vec![
4✔
1640
                self.sim_params_layout.clone(),
1641
                particle_bind_group_layout.clone(),
1642
                spawner_bind_group_layout.clone(),
1643
                metadata_bind_group_layout.clone(),
1644
            ],
1645
            shader: key.shader,
4✔
1646
            shader_defs,
1647
            entry_point: Some("main".into()),
2✔
1648
            push_constant_ranges: vec![],
2✔
1649
            zero_initialize_workgroup_memory: false,
1650
        }
1651
    }
1652
}
1653

1654
#[derive(Resource)]
1655
pub(crate) struct ParticlesUpdatePipeline {
1656
    sim_params_layout: BindGroupLayout,
1657

1658
    // Temporary values passed to specialize()
1659
    // https://github.com/bevyengine/bevy/issues/17132
1660
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1661
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1662
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1663
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1664
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1665
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1666
}
1667

1668
impl FromWorld for ParticlesUpdatePipeline {
1669
    fn from_world(world: &mut World) -> Self {
3✔
1670
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1671

1672
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
7✔
1673
        let sim_params_layout = render_device.create_bind_group_layout(
9✔
1674
            "hanabi:bind_group_layout:vfx_update:sim_params@0",
1675
            &[
3✔
1676
                // @group(0) @binding(0) var<uniform> sim_params : SimParams;
1677
                BindGroupLayoutEntry {
6✔
1678
                    binding: 0,
6✔
1679
                    visibility: ShaderStages::COMPUTE,
6✔
1680
                    ty: BindingType::Buffer {
6✔
1681
                        ty: BufferBindingType::Uniform,
6✔
1682
                        has_dynamic_offset: false,
6✔
1683
                        min_binding_size: Some(GpuSimParams::min_size()),
6✔
1684
                    },
1685
                    count: None,
6✔
1686
                },
1687
                // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
1688
                // array<DrawIndexedIndirectArgs>;
1689
                BindGroupLayoutEntry {
3✔
1690
                    binding: 1,
3✔
1691
                    visibility: ShaderStages::COMPUTE,
3✔
1692
                    ty: BindingType::Buffer {
3✔
1693
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1694
                        has_dynamic_offset: false,
3✔
1695
                        min_binding_size: Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
3✔
1696
                    },
1697
                    count: None,
3✔
1698
                },
1699
            ],
1700
        );
1701

1702
        Self {
1703
            sim_params_layout,
1704
            temp_particle_bind_group_layout: None,
1705
            temp_spawner_bind_group_layout: None,
1706
            temp_metadata_bind_group_layout: None,
1707
        }
1708
    }
1709
}
1710

1711
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1712
pub(crate) struct ParticleUpdatePipelineKey {
1713
    /// Compute shader, with snippets applied, but not preprocessed yet.
1714
    shader: Handle<Shader>,
1715
    /// Particle layout.
1716
    particle_layout: ParticleLayout,
1717
    /// Minimum binding size in bytes for the particle layout buffer of the
1718
    /// parent effect, if any.
1719
    /// Key: READ_PARENT_PARTICLE
1720
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1721
    /// Key: EMITS_GPU_SPAWN_EVENTS
1722
    num_event_buffers: u32,
1723
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1724
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1725
    particle_bind_group_layout_id: BindGroupLayoutId,
1726
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1727
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1728
    spawner_bind_group_layout_id: BindGroupLayoutId,
1729
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1730
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1731
    metadata_bind_group_layout_id: BindGroupLayoutId,
1732
}
1733

1734
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1735
    type Key = ParticleUpdatePipelineKey;
1736

1737
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
2✔
1738
        // We use the hash to correlate the key content with the GPU resource name
1739
        let hash = calc_hash(&key);
6✔
1740
        trace!("Specializing update pipeline {hash:016X} with key {key:?}");
4✔
1741

1742
        let mut shader_defs = Vec::with_capacity(6);
4✔
1743
        shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
8✔
1744
        // ChildInfo needs atomic event_count because all threads append to the event
1745
        // buffer(s) in parallel.
1746
        shader_defs.push("CHILD_INFO_EVENT_COUNT_IS_ATOMIC".into());
8✔
1747
        if key.particle_layout.contains(Attribute::PREV) {
4✔
1748
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1749
        }
1750
        if key.particle_layout.contains(Attribute::NEXT) {
4✔
1751
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1752
        }
1753
        if key.parent_particle_layout_min_binding_size.is_some() {
4✔
1754
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1755
        }
1756
        if key.num_event_buffers > 0 {
2✔
1757
            shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
×
1758
        }
1759

1760
        // This should always be valid when specialize() is called, by design. This is
1761
        // how we pass the value to specialize() to work around the lack of access to
1762
        // external data.
1763
        // https://github.com/bevyengine/bevy/issues/17132
1764
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
8✔
1765
        assert_eq!(
2✔
1766
            particle_bind_group_layout.id(),
4✔
1767
            key.particle_bind_group_layout_id
1768
        );
1769
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
8✔
1770
        assert_eq!(
2✔
1771
            spawner_bind_group_layout.id(),
4✔
1772
            key.spawner_bind_group_layout_id
1773
        );
1774
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
8✔
1775
        assert_eq!(
2✔
1776
            metadata_bind_group_layout.id(),
4✔
1777
            key.metadata_bind_group_layout_id
1778
        );
1779

1780
        let hash = calc_func_id(&key);
6✔
1781
        let label = format!("hanabi:pipeline:update_{hash:016X}");
6✔
1782
        trace!(
2✔
1783
            "-> creating pipeline '{}' with shader defs:{}",
2✔
1784
            label,
1785
            shader_defs
2✔
1786
                .iter()
2✔
1787
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
12✔
1788
        );
1789

1790
        ComputePipelineDescriptor {
1791
            label: Some(label.into()),
4✔
1792
            layout: vec![
4✔
1793
                self.sim_params_layout.clone(),
1794
                particle_bind_group_layout.clone(),
1795
                spawner_bind_group_layout.clone(),
1796
                metadata_bind_group_layout.clone(),
1797
            ],
1798
            shader: key.shader,
4✔
1799
            shader_defs,
1800
            entry_point: Some("main".into()),
2✔
1801
            push_constant_ranges: Vec::new(),
2✔
1802
            zero_initialize_workgroup_memory: false,
1803
        }
1804
    }
1805
}
1806

1807
#[derive(Resource)]
1808
pub(crate) struct ParticlesRenderPipeline {
1809
    render_device: RenderDevice,
1810
    view_layout: BindGroupLayout,
1811
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
1812
}
1813

1814
impl ParticlesRenderPipeline {
1815
    /// Cache a material, creating its bind group layout based on the texture
1816
    /// layout.
1817
    pub fn cache_material(&mut self, layout: &TextureLayout) {
312✔
1818
        if layout.layout.is_empty() {
624✔
1819
            return;
312✔
1820
        }
1821

1822
        // FIXME - no current stable API to insert an entry into a HashMap only if it
1823
        // doesn't exist, and without having to build a key (as opposed to a reference).
1824
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
1825
        // cached (which should be the common case).
1826
        if self.material_layouts.contains_key(layout) {
1827
            return;
×
1828
        }
1829

1830
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
1831
        let mut index = 0;
1832
        for _slot in &layout.layout {
×
1833
            entries.push(BindGroupLayoutEntry {
1834
                binding: index,
1835
                visibility: ShaderStages::FRAGMENT,
1836
                ty: BindingType::Texture {
1837
                    multisampled: false,
1838
                    sample_type: TextureSampleType::Float { filterable: true },
1839
                    view_dimension: TextureViewDimension::D2,
1840
                },
1841
                count: None,
1842
            });
1843
            entries.push(BindGroupLayoutEntry {
1844
                binding: index + 1,
1845
                visibility: ShaderStages::FRAGMENT,
1846
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
1847
                count: None,
1848
            });
1849
            index += 2;
1850
        }
1851
        debug!(
1852
            "Creating material bind group with {} entries [{:?}] for layout {:?}",
×
1853
            entries.len(),
×
1854
            entries,
1855
            layout
1856
        );
1857
        let material_bind_group_layout = self
1858
            .render_device
1859
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
1860

1861
        self.material_layouts
1862
            .insert(layout.clone(), material_bind_group_layout);
1863
    }
1864

1865
    /// Retrieve a bind group layout for a cached material.
1866
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
2✔
1867
        // Prevent a hash and lookup for the trivial case of an empty layout
1868
        if layout.layout.is_empty() {
4✔
1869
            return None;
2✔
1870
        }
1871

1872
        self.material_layouts.get(layout)
1873
    }
1874
}
1875

1876
impl FromWorld for ParticlesRenderPipeline {
1877
    fn from_world(world: &mut World) -> Self {
3✔
1878
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1879

1880
        let view_layout = render_device.create_bind_group_layout(
9✔
1881
            "hanabi:bind_group_layout:render:view@0",
1882
            &[
3✔
1883
                // @group(0) @binding(0) var<uniform> view: View;
1884
                BindGroupLayoutEntry {
6✔
1885
                    binding: 0,
6✔
1886
                    visibility: ShaderStages::VERTEX_FRAGMENT,
6✔
1887
                    ty: BindingType::Buffer {
6✔
1888
                        ty: BufferBindingType::Uniform,
6✔
1889
                        has_dynamic_offset: true,
6✔
1890
                        min_binding_size: Some(ViewUniform::min_size()),
6✔
1891
                    },
1892
                    count: None,
6✔
1893
                },
1894
                // @group(0) @binding(1) var<uniform> sim_params : SimParams;
1895
                BindGroupLayoutEntry {
3✔
1896
                    binding: 1,
3✔
1897
                    visibility: ShaderStages::VERTEX_FRAGMENT,
3✔
1898
                    ty: BindingType::Buffer {
3✔
1899
                        ty: BufferBindingType::Uniform,
3✔
1900
                        has_dynamic_offset: false,
3✔
1901
                        min_binding_size: Some(GpuSimParams::min_size()),
3✔
1902
                    },
1903
                    count: None,
3✔
1904
                },
1905
            ],
1906
        );
1907

1908
        Self {
1909
            render_device: render_device.clone(),
9✔
1910
            view_layout,
1911
            material_layouts: default(),
3✔
1912
        }
1913
    }
1914
}
1915

1916
#[cfg(all(feature = "2d", feature = "3d"))]
1917
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1918
enum PipelineMode {
1919
    Camera2d,
1920
    Camera3d,
1921
}
1922

1923
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1924
pub(crate) struct ParticleRenderPipelineKey {
1925
    /// Render shader, with snippets applied, but not preprocessed yet.
1926
    shader: Handle<Shader>,
1927
    /// Particle layout.
1928
    particle_layout: ParticleLayout,
1929
    mesh_layout: Option<MeshVertexBufferLayoutRef>,
1930
    /// Texture layout.
1931
    texture_layout: TextureLayout,
1932
    /// Key: LOCAL_SPACE_SIMULATION
1933
    /// The effect is simulated in local space, and during rendering all
1934
    /// particles are transformed by the effect's [`GlobalTransform`].
1935
    local_space_simulation: bool,
1936
    /// Key: USE_ALPHA_MASK, OPAQUE
1937
    /// The particle's alpha masking behavior.
1938
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
1939
    /// The effect needs Alpha blend.
1940
    alpha_mode: AlphaMode,
1941
    /// Key: FLIPBOOK
1942
    /// The effect is rendered with flipbook texture animation based on the
1943
    /// sprite index of each particle.
1944
    flipbook: bool,
1945
    /// Key: NEEDS_UV
1946
    /// The effect needs UVs.
1947
    needs_uv: bool,
1948
    /// Key: NEEDS_NORMAL
1949
    /// The effect needs normals.
1950
    needs_normal: bool,
1951
    /// Key: NEEDS_PARTICLE_IN_FRAGMENT
1952
    /// The effect needs access to the particle index and buffer in the fragment
1953
    /// shader.
1954
    needs_particle_fragment: bool,
1955
    /// Key: RIBBONS
1956
    /// The effect has ribbons.
1957
    ribbons: bool,
1958
    /// For dual-mode configurations only, the actual mode of the current render
1959
    /// pipeline. Otherwise the mode is implicitly determined by the active
1960
    /// feature.
1961
    #[cfg(all(feature = "2d", feature = "3d"))]
1962
    pipeline_mode: PipelineMode,
1963
    /// MSAA sample count.
1964
    msaa_samples: u32,
1965
    /// Is the camera using an HDR render target?
1966
    hdr: bool,
1967
}
1968

1969
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1970
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1971
    #[default]
1972
    Blend,
1973
    /// Key: USE_ALPHA_MASK
1974
    /// The effect is rendered with alpha masking.
1975
    AlphaMask,
1976
    /// Key: OPAQUE
1977
    /// The effect is rendered fully-opaquely.
1978
    Opaque,
1979
}
1980

1981
impl Default for ParticleRenderPipelineKey {
1982
    fn default() -> Self {
×
1983
        Self {
1984
            shader: Handle::default(),
×
1985
            particle_layout: ParticleLayout::empty(),
×
1986
            mesh_layout: None,
1987
            texture_layout: default(),
×
1988
            local_space_simulation: false,
1989
            alpha_mask: default(),
×
1990
            alpha_mode: AlphaMode::Blend,
1991
            flipbook: false,
1992
            needs_uv: false,
1993
            needs_normal: false,
1994
            needs_particle_fragment: false,
1995
            ribbons: false,
1996
            #[cfg(all(feature = "2d", feature = "3d"))]
1997
            pipeline_mode: PipelineMode::Camera3d,
1998
            msaa_samples: Msaa::default().samples(),
×
1999
            hdr: false,
2000
        }
2001
    }
2002
}
2003

2004
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
2005
    type Key = ParticleRenderPipelineKey;
2006

2007
    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
2✔
2008
        trace!("Specializing render pipeline for key: {key:?}");
4✔
2009

2010
        trace!("Creating layout for bind group particle@1 of render pass");
4✔
2011
        let alignment = self
4✔
2012
            .render_device
2✔
2013
            .limits()
2✔
2014
            .min_storage_buffer_offset_alignment;
2✔
2015
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(alignment);
6✔
2016
        let entries = [
4✔
2017
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
2018
            BindGroupLayoutEntry {
4✔
2019
                binding: 0,
4✔
2020
                visibility: ShaderStages::VERTEX_FRAGMENT,
4✔
2021
                ty: BindingType::Buffer {
4✔
2022
                    ty: BufferBindingType::Storage { read_only: true },
6✔
2023
                    has_dynamic_offset: false,
4✔
2024
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
4✔
2025
                },
2026
                count: None,
4✔
2027
            },
2028
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
2029
            BindGroupLayoutEntry {
4✔
2030
                binding: 1,
4✔
2031
                visibility: ShaderStages::VERTEX,
4✔
2032
                ty: BindingType::Buffer {
4✔
2033
                    ty: BufferBindingType::Storage { read_only: true },
6✔
2034
                    has_dynamic_offset: false,
4✔
2035
                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
6✔
2036
                },
2037
                count: None,
4✔
2038
            },
2039
            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
2040
            BindGroupLayoutEntry {
2✔
2041
                binding: 2,
2✔
2042
                visibility: ShaderStages::VERTEX,
2✔
2043
                ty: BindingType::Buffer {
2✔
2044
                    ty: BufferBindingType::Storage { read_only: true },
2✔
2045
                    has_dynamic_offset: true,
2✔
2046
                    min_binding_size: Some(spawner_min_binding_size),
2✔
2047
                },
2048
                count: None,
2✔
2049
            },
2050
        ];
2051
        let particle_bind_group_layout = self
4✔
2052
            .render_device
2✔
2053
            .create_bind_group_layout("hanabi:bind_group_layout:render:particle@1", &entries[..]);
4✔
2054

2055
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
10✔
2056
        let mut shader_defs = vec![];
4✔
2057

2058
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
10✔
2059
            mesh_layout
4✔
2060
                .0
4✔
2061
                .get_layout(&[
4✔
2062
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
6✔
2063
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
6✔
2064
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
2✔
2065
                ])
2066
                .ok()
2✔
2067
        });
2068

2069
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
4✔
2070
            layout.push(material_bind_group_layout.clone());
2071
        }
2072

2073
        // Key: LOCAL_SPACE_SIMULATION
2074
        if key.local_space_simulation {
2✔
2075
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2076
        }
2077

2078
        match key.alpha_mask {
2✔
2079
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
2✔
2080
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2081
                // Key: USE_ALPHA_MASK
2082
                shader_defs.push("USE_ALPHA_MASK".into())
×
2083
            }
2084
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2085
                // Key: OPAQUE
2086
                shader_defs.push("OPAQUE".into())
×
2087
            }
2088
        }
2089

2090
        // Key: FLIPBOOK
2091
        if key.flipbook {
2✔
2092
            shader_defs.push("FLIPBOOK".into());
×
2093
        }
2094

2095
        // Key: NEEDS_UV
2096
        if key.needs_uv {
2✔
2097
            shader_defs.push("NEEDS_UV".into());
×
2098
        }
2099

2100
        // Key: NEEDS_NORMAL
2101
        if key.needs_normal {
2✔
2102
            shader_defs.push("NEEDS_NORMAL".into());
×
2103
        }
2104

2105
        if key.needs_particle_fragment {
2✔
2106
            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
×
2107
        }
2108

2109
        // Key: RIBBONS
2110
        if key.ribbons {
2✔
2111
            shader_defs.push("RIBBONS".into());
×
2112
        }
2113

2114
        #[cfg(feature = "2d")]
2115
        let depth_stencil_2d = DepthStencilState {
2116
            format: CORE_2D_DEPTH_FORMAT,
2117
            // Use depth buffer with alpha-masked particles, not with transparent ones
2118
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2119
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2120
            depth_compare: CompareFunction::GreaterEqual,
2121
            stencil: StencilState::default(),
2✔
2122
            bias: DepthBiasState::default(),
2✔
2123
        };
2124

2125
        #[cfg(feature = "3d")]
2126
        let depth_stencil_3d = DepthStencilState {
2127
            format: CORE_3D_DEPTH_FORMAT,
2128
            // Use depth buffer with alpha-masked or opaque particles, not
2129
            // with transparent ones
2130
            depth_write_enabled: matches!(
2✔
2131
                key.alpha_mask,
2132
                ParticleRenderAlphaMaskPipelineKey::AlphaMask
2133
                    | ParticleRenderAlphaMaskPipelineKey::Opaque
2134
            ),
2135
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2136
            depth_compare: CompareFunction::GreaterEqual,
2137
            stencil: StencilState::default(),
2✔
2138
            bias: DepthBiasState::default(),
2✔
2139
        };
2140

2141
        #[cfg(all(feature = "2d", feature = "3d"))]
2142
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2✔
2143
        #[cfg(all(feature = "2d", feature = "3d"))]
2144
        let depth_stencil = match key.pipeline_mode {
4✔
2145
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2146
            PipelineMode::Camera3d => Some(depth_stencil_3d),
2✔
2147
        };
2148

2149
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2150
        let depth_stencil = Some(depth_stencil_2d);
2151

2152
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2153
        let depth_stencil = Some(depth_stencil_3d);
2154

2155
        let format = if key.hdr {
4✔
2156
            ViewTarget::TEXTURE_FORMAT_HDR
×
2157
        } else {
2158
            TextureFormat::bevy_default()
2✔
2159
        };
2160

2161
        let hash = calc_func_id(&key);
6✔
2162
        let label = format!("hanabi:pipeline:render_{hash:016X}");
6✔
2163
        trace!(
2✔
2164
            "-> creating pipeline '{}' with shader defs:{}",
2✔
2165
            label,
2166
            shader_defs
2✔
2167
                .iter()
2✔
2168
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
2169
        );
2170

2171
        RenderPipelineDescriptor {
2172
            label: Some(label.into()),
4✔
2173
            vertex: VertexState {
4✔
2174
                shader: key.shader.clone(),
2175
                entry_point: Some("vertex".into()),
2176
                shader_defs: shader_defs.clone(),
2177
                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
2178
            },
2179
            fragment: Some(FragmentState {
4✔
2180
                shader: key.shader,
2181
                shader_defs,
2182
                entry_point: Some("fragment".into()),
2183
                targets: vec![Some(ColorTargetState {
2184
                    format,
2185
                    blend: Some(key.alpha_mode.into()),
2186
                    write_mask: ColorWrites::ALL,
2187
                })],
2188
            }),
2189
            layout,
2190
            primitive: PrimitiveState {
4✔
2191
                front_face: FrontFace::Ccw,
2192
                cull_mode: None,
2193
                unclipped_depth: false,
2194
                polygon_mode: PolygonMode::Fill,
2195
                conservative: false,
2196
                topology: PrimitiveTopology::TriangleList,
2197
                strip_index_format: None,
2198
            },
2199
            depth_stencil,
2200
            multisample: MultisampleState {
2✔
2201
                count: key.msaa_samples,
2202
                mask: !0,
2203
                alpha_to_coverage_enabled: false,
2204
            },
2205
            push_constant_ranges: Vec::new(),
2✔
2206
            zero_initialize_workgroup_memory: false,
2207
        }
2208
    }
2209
}
2210

2211
/// A single effect instance extracted from a [`ParticleEffect`] as a
2212
/// render world item.
2213
///
2214
/// [`ParticleEffect`]: crate::ParticleEffect
2215
#[derive(Debug, Clone, PartialEq, Component)]
2216
#[require(CachedPipelines, CachedReadyState, CachedEffectMetadata)]
2217
pub(crate) struct ExtractedEffect {
2218
    /// Handle to the effect asset this instance is based on.
2219
    /// The handle is weak to prevent refcount cycles and gracefully handle
2220
    /// assets unloaded or destroyed after a draw call has been submitted.
2221
    pub handle: Handle<EffectAsset>,
2222
    /// Particle layout for the effect.
2223
    pub particle_layout: ParticleLayout,
2224
    /// Effect capacity, in number of particles.
2225
    pub capacity: u32,
2226
    /// Layout flags.
2227
    pub layout_flags: LayoutFlags,
2228
    /// Texture layout.
2229
    pub texture_layout: TextureLayout,
2230
    /// Textures.
2231
    pub textures: Vec<Handle<Image>>,
2232
    /// Alpha mode.
2233
    pub alpha_mode: AlphaMode,
2234
    /// Effect shaders.
2235
    pub effect_shaders: EffectShader,
2236
    /// Condition under which the effect is simulated.
2237
    pub simulation_condition: SimulationCondition,
2238
}
2239

2240
/// Extracted data for the [`GpuSpawnerParams`].
2241
///
2242
/// This contains all data which may change each frame during the regular usage
2243
/// of the effect, but doesn't require any particular GPU resource update
2244
/// (except re-uploading that new data to GPU, of course).
2245
#[derive(Debug, Clone, PartialEq, Component)]
2246
pub(crate) struct ExtractedSpawner {
2247
    /// Number of particles to spawn this frame.
2248
    ///
2249
    /// This is ignored if the effect is a child effect consuming GPU spawn
2250
    /// events.
2251
    pub spawn_count: u32,
2252
    /// PRNG seed.
2253
    pub prng_seed: u32,
2254
    /// Global transform of the effect origin.
2255
    pub transform: GlobalTransform,
2256
    /// Is the effect visible this frame?
2257
    pub is_visible: bool,
2258
}
2259

2260
/// Cache info for the metadata of the effect.
2261
///
2262
/// This manages the GPU allocation of the [`GpuEffectMetadata`] for this
2263
/// effect.
2264
#[derive(Debug, Default, Component)]
2265
pub(crate) struct CachedEffectMetadata {
2266
    /// Allocation ID.
2267
    pub table_id: BufferTableId,
2268
    /// Current metadata values, cached on CPU for change detection.
2269
    pub metadata: GpuEffectMetadata,
2270
}
2271

2272
/// Extracted parent information for a child effect.
2273
///
2274
/// This component is present on the [`RenderEntity`] of an extracted effect if
2275
/// the effect has a parent effect. Otherwise, it's removed.
2276
///
2277
/// This components forms an ECS relationship with [`ChildrenEffects`].
2278
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
2279
#[relationship(relationship_target = ChildrenEffects)]
2280
pub(crate) struct ChildEffectOf {
2281
    /// Render entity of the parent.
2282
    pub parent: Entity,
2283
}
2284

2285
/// Extracted children information for a parent effect.
2286
///
2287
/// This component is present on the [`RenderEntity`] of an extracted effect if
2288
/// the effect is a parent effect for one or more child effects. Otherwise, it's
2289
/// removed.
2290
///
2291
/// This components forms an ECS relationship with [`ChildEffectOf`]. Note that
2292
/// we don't use `linked_spawn` because:
2293
/// 1. This would fight with the `SyncToRenderWorld` as the main world
2294
///    parent-child hierarchy is by design not an ECS relationship (it's a lose
2295
///    declarative coupling).
2296
/// 2. The components on the render entity often store GPU resources or other
2297
///    data we need to clean-up manually, and not all of them currently use
2298
///    lifecycle hooks, so we want to manage despawning manually to prevent
2299
///    leaks.
2300
#[derive(Debug, Clone, PartialEq, Eq, Component)]
2301
#[relationship_target(relationship = ChildEffectOf)]
2302
pub(crate) struct ChildrenEffects(Vec<Entity>);
2303

2304
impl<'a> IntoIterator for &'a ChildrenEffects {
2305
    type Item = <Self::IntoIter as Iterator>::Item;
2306

2307
    type IntoIter = std::slice::Iter<'a, Entity>;
2308

2309
    #[inline(always)]
2310
    fn into_iter(self) -> Self::IntoIter {
×
2311
        self.0.iter()
×
2312
    }
2313
}
2314

2315
impl Deref for ChildrenEffects {
2316
    type Target = [Entity];
2317

2318
    fn deref(&self) -> &Self::Target {
×
2319
        &self.0
×
2320
    }
2321
}
2322

2323
/// Extracted data for an effect's properties, if any.
2324
///
2325
/// This component is present on the [`RenderEntity`] of an extracted effect if
2326
/// that effect has properties. It optionally contains new CPU data to
2327
/// (re-)upload this frame. If the effect has no property, this component is
2328
/// removed.
2329
#[derive(Debug, Component)]
2330
pub(crate) struct ExtractedProperties {
2331
    /// Property layout for the effect.
2332
    pub property_layout: PropertyLayout,
2333
    /// Values of properties written in a binary blob according to
2334
    /// [`property_layout`].
2335
    ///
2336
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2337
    /// `None` if nothing needs to be done for this frame.
2338
    ///
2339
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2340
    pub property_data: Option<Vec<u8>>,
2341
}
2342

2343
#[derive(Default, Resource)]
2344
pub(crate) struct EffectAssetEvents {
2345
    pub images: Vec<AssetEvent<Image>>,
2346
}
2347

2348
/// System extracting all the asset events for the [`Image`] assets to enable
2349
/// dynamic update of images bound to any effect.
2350
///
2351
/// This system runs in parallel of [`extract_effects`].
2352
pub(crate) fn extract_effect_events(
330✔
2353
    mut events: ResMut<EffectAssetEvents>,
2354
    mut image_events: Extract<MessageReader<AssetEvent<Image>>>,
2355
) {
2356
    #[cfg(feature = "trace")]
2357
    let _span = bevy::log::info_span!("extract_effect_events").entered();
990✔
2358
    trace!("extract_effect_events()");
650✔
2359

2360
    let EffectAssetEvents { ref mut images } = *events;
660✔
2361
    *images = image_events.read().copied().collect();
1,320✔
2362
}
2363

2364
/// Debugging settings.
2365
///
2366
/// Settings used to debug Hanabi. These have no effect on the actual behavior
2367
/// of Hanabi, but may affect its performance.
2368
///
2369
/// # Example
2370
///
2371
/// ```
2372
/// # use bevy::prelude::*;
2373
/// # use bevy_hanabi::*;
2374
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2375
///     // Each time a new effect is spawned, capture 2 frames
2376
///     debug_settings.start_capture_on_new_effect = true;
2377
///     debug_settings.capture_frame_count = 2;
2378
/// }
2379
/// ```
2380
#[derive(Debug, Default, Clone, Copy, Resource)]
2381
pub struct DebugSettings {
2382
    /// Enable automatically starting a GPU debugger capture as soon as this
2383
    /// frame starts rendering (extract phase).
2384
    ///
2385
    /// Enable this feature to automatically capture one or more GPU frames when
2386
    /// the `extract_effects()` system runs next. This instructs any attached
2387
    /// GPU debugger to start a capture; this has no effect if no debugger
2388
    /// is attached.
2389
    ///
2390
    /// If a capture is already on-going this has no effect; the on-going
2391
    /// capture needs to be terminated first. Note however that a capture can
2392
    /// stop and another start in the same frame.
2393
    ///
2394
    /// This value is not reset automatically. If you set this to `true`, you
2395
    /// should set it back to `false` on next frame to avoid capturing forever.
2396
    pub start_capture_this_frame: bool,
2397

2398
    /// Enable automatically starting a GPU debugger capture when one or more
2399
    /// effects are spawned.
2400
    ///
2401
    /// Enable this feature to automatically capture one or more GPU frames when
2402
    /// a new effect is spawned (as detected by ECS change detection). This
2403
    /// instructs any attached GPU debugger to start a capture; this has no
2404
    /// effect if no debugger is attached.
2405
    pub start_capture_on_new_effect: bool,
2406

2407
    /// Number of frames to capture with a GPU debugger.
2408
    ///
2409
    /// By default this value is zero, and a GPU debugger capture runs for a
2410
    /// single frame. If a non-zero frame count is specified here, the capture
2411
    /// will instead stop once the specified number of frames has been recorded.
2412
    ///
2413
    /// You should avoid setting this to a value too large, to prevent the
2414
    /// capture size from getting out of control. A typical value is 1 to 3
2415
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2416
    /// debuggers or graphics APIs might further limit this value on their own,
2417
    /// so there's no guarantee the graphics API will honor this value.
2418
    pub capture_frame_count: u32,
2419
}
2420

2421
#[derive(Debug, Default, Clone, Copy, Resource)]
2422
pub(crate) struct RenderDebugSettings {
2423
    /// Is a GPU debugger capture on-going?
2424
    is_capturing: bool,
2425
    /// Start time of any on-going GPU debugger capture.
2426
    capture_start: Duration,
2427
    /// Number of frames captured so far for on-going GPU debugger capture.
2428
    captured_frames: u32,
2429
}
2430

2431
/// Manage GPU debug capture start/stop.
2432
///
2433
/// If any GPU debug capture is configured to start or stop in
2434
/// [`DebugSettings`], they do so during this system's run. This ensures
2435
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2436
/// from Bevy itself, if another Bevy system runs before this one).
2437
///
2438
/// We do this during extract to try and capture as close as possible to an
2439
/// entire GPU frame.
2440
pub(crate) fn start_stop_gpu_debug_capture(
330✔
2441
    real_time: Extract<Res<Time<Real>>>,
2442
    render_device: Res<RenderDevice>,
2443
    debug_settings: Extract<Res<DebugSettings>>,
2444
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2445
    q_added_effects: Extract<Query<(), Added<CompiledParticleEffect>>>,
2446
) {
2447
    #[cfg(feature = "trace")]
2448
    let _span = bevy::log::info_span!("start_stop_debug_capture").entered();
990✔
2449
    trace!("start_stop_debug_capture()");
650✔
2450

2451
    // Stop any pending capture if needed
2452
    if render_debug_settings.is_capturing {
330✔
2453
        render_debug_settings.captured_frames += 1;
×
2454

2455
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2456
            #[expect(unsafe_code, reason = "Debugging only")]
2457
            unsafe {
2458
                render_device.wgpu_device().stop_graphics_debugger_capture();
×
2459
            }
2460
            render_debug_settings.is_capturing = false;
×
2461
            warn!(
×
2462
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2463
                render_debug_settings.captured_frames,
×
2464
                real_time.elapsed().as_secs_f64()
×
2465
            );
2466
        }
2467
    }
2468

2469
    // If no pending capture, consider starting a new one
2470
    if !render_debug_settings.is_capturing
330✔
2471
        && (debug_settings.start_capture_this_frame
330✔
2472
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty()))
330✔
2473
    {
2474
        #[expect(unsafe_code, reason = "Debugging only")]
2475
        unsafe {
2476
            render_device
×
2477
                .wgpu_device()
2478
                .start_graphics_debugger_capture();
2479
        }
2480
        render_debug_settings.is_capturing = true;
2481
        render_debug_settings.capture_start = real_time.elapsed();
2482
        render_debug_settings.captured_frames = 0;
2483
        warn!(
2484
            "Started GPU debug capture of {} frames at t={}s.",
×
2485
            debug_settings.capture_frame_count,
×
2486
            render_debug_settings.capture_start.as_secs_f64()
×
2487
        );
2488
    }
2489
}
2490

2491
/// Write the ready state of all render world effects back into their source
2492
/// effect in the main world.
2493
pub(crate) fn report_ready_state(
330✔
2494
    mut main_world: ResMut<MainWorld>,
2495
    q_ready_state: Query<&CachedReadyState>,
2496
) {
2497
    let mut q_effects = main_world.query::<(RenderEntity, &mut CompiledParticleEffect)>();
660✔
2498
    for (render_entity, mut compiled_particle_effect) in q_effects.iter_mut(&mut main_world) {
1,314✔
2499
        if let Ok(cached_ready_state) = q_ready_state.get(render_entity) {
312✔
2500
            compiled_particle_effect.is_ready = cached_ready_state.is_ready();
2501
        }
2502
    }
2503
}
2504

2505
/// System extracting data for rendering of all active [`ParticleEffect`]
2506
/// components.
2507
///
2508
/// [`ParticleEffect`]: crate::ParticleEffect
2509
pub(crate) fn extract_effects(
330✔
2510
    mut commands: Commands,
2511
    effects: Extract<Res<Assets<EffectAsset>>>,
2512
    default_mesh: Extract<Res<DefaultMesh>>,
2513
    // Main world effects to extract
2514
    q_effects: Extract<
2515
        Query<(
2516
            Entity,
2517
            RenderEntity,
2518
            Option<&InheritedVisibility>,
2519
            Option<&ViewVisibility>,
2520
            &EffectSpawner,
2521
            &CompiledParticleEffect,
2522
            Option<Ref<EffectProperties>>,
2523
            &GlobalTransform,
2524
        )>,
2525
    >,
2526
    // Render world effects extracted from a previous frame, if any
2527
    mut q_extracted_effects: Query<(
2528
        &mut ExtractedEffect,
2529
        Option<&mut ExtractedSpawner>,
2530
        Option<&ChildEffectOf>, // immutable, because of relationship
2531
        Option<&mut ExtractedEffectMesh>,
2532
        Option<&mut ExtractedProperties>,
2533
    )>,
2534
) {
2535
    #[cfg(feature = "trace")]
2536
    let _span = bevy::log::info_span!("extract_effects").entered();
990✔
2537
    trace!("extract_effects()");
650✔
2538

2539
    // Loop over all existing effects to extract them
2540
    trace!("Extracting {} effects...", q_effects.iter().len());
1,290✔
2541
    for (
2542
        main_entity,
314✔
2543
        render_entity,
2544
        maybe_inherited_visibility,
2545
        maybe_view_visibility,
2546
        effect_spawner,
2547
        compiled_effect,
2548
        maybe_properties,
2549
        transform,
2550
    ) in q_effects.iter()
660✔
2551
    {
2552
        // Check if shaders are configured
2553
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
314✔
2554
            trace!("Effect {:?}: no configured shader, skipped.", main_entity);
×
2555
            continue;
×
2556
        };
2557

2558
        // Check if asset is available, otherwise silently ignore
2559
        let Some(asset) = effects.get(&compiled_effect.asset) else {
314✔
2560
            trace!(
×
2561
                "Effect {:?}: EffectAsset not ready, skipped. asset:{:?}",
×
2562
                main_entity,
2563
                compiled_effect.asset
2564
            );
2565
            continue;
×
2566
        };
2567

2568
        let is_visible = maybe_inherited_visibility
2569
            .map(|cv| cv.get())
628✔
2570
            .unwrap_or(true)
2571
            && maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true);
1,570✔
2572

2573
        let mut cmd = commands.entity(render_entity);
2574

2575
        // Fetch the existing extraction compoennts, if any, which we need to update.
2576
        // Because we use SyncToRenderWorld, there's always a render entity, but it may
2577
        // miss all components. And because we can't query only optional components
2578
        // (that would match all entities in the entire world), we force querying
2579
        // ExtractedEffect, which means we get a miss if it's the first extraction and
2580
        // it's not spawned yet. That's OK, we'll spawn it below.
2581
        let (
2582
            maybe_extracted_effect,
2583
            maybe_extracted_spawner,
2584
            maybe_child_of,
2585
            maybe_extracted_mesh,
2586
            maybe_extracted_properties,
2587
        ) = q_extracted_effects
2588
            .get_mut(render_entity)
2589
            .map(|(extracted_effect, b, c, d, e)| (Some(extracted_effect), b, c, d, e))
1,560✔
2590
            .unwrap_or((None, None, None, None, None));
2591

2592
        // Extract general effect data
2593
        let texture_layout = asset.module().texture_layout();
2594
        let layout_flags = compiled_effect.layout_flags;
2595
        let alpha_mode = compiled_effect.alpha_mode;
2596
        trace!(
2597
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
314✔
2598
            asset.name,
2599
            main_entity,
2600
            render_entity,
2601
            texture_layout.layout.len(),
628✔
2602
            compiled_effect.textures.len(),
628✔
2603
            layout_flags,
2604
        );
2605
        let new_extracted_effect = ExtractedEffect {
2606
            handle: compiled_effect.asset.clone(),
2607
            particle_layout: asset.particle_layout().clone(),
2608
            capacity: asset.capacity(),
2609
            layout_flags,
2610
            texture_layout,
2611
            textures: compiled_effect.textures.clone(),
2612
            alpha_mode,
2613
            effect_shaders: effect_shaders.clone(),
2614
            simulation_condition: asset.simulation_condition,
2615
        };
2616
        if let Some(mut extracted_effect) = maybe_extracted_effect {
312✔
2617
            extracted_effect.set_if_neq(new_extracted_effect);
2618
        } else {
2619
            trace!(
2✔
2620
                "Inserting new ExtractedEffect component on {:?}",
2✔
2621
                render_entity
2622
            );
2623
            cmd.insert(new_extracted_effect);
6✔
2624
        }
2625

2626
        // Extract the spawner data
2627
        let new_spawner = ExtractedSpawner {
2628
            spawn_count: effect_spawner.spawn_count,
2629
            prng_seed: compiled_effect.prng_seed,
2630
            transform: *transform,
2631
            is_visible,
2632
        };
2633
        trace!(
2634
            "[Effect {}] spawn_count={} prng_seed={}",
314✔
2635
            render_entity,
2636
            new_spawner.spawn_count,
2637
            new_spawner.prng_seed
2638
        );
2639
        if let Some(mut extracted_spawner) = maybe_extracted_spawner {
312✔
2640
            extracted_spawner.set_if_neq(new_spawner);
2641
        } else {
2642
            trace!(
2✔
2643
                "Inserting new ExtractedSpawner component on {}",
2✔
2644
                render_entity
2645
            );
2646
            cmd.insert(new_spawner);
6✔
2647
        }
2648

2649
        // Extract the effect mesh
2650
        let mesh = compiled_effect
2651
            .mesh
2652
            .clone()
2653
            .unwrap_or(default_mesh.0.clone());
2654
        let new_mesh = ExtractedEffectMesh { mesh: mesh.id() };
2655
        if let Some(mut extracted_mesh) = maybe_extracted_mesh {
312✔
2656
            extracted_mesh.set_if_neq(new_mesh);
2657
        } else {
2658
            trace!(
2✔
2659
                "Inserting new ExtractedEffectMesh component on {:?}",
2✔
2660
                render_entity
2661
            );
2662
            cmd.insert(new_mesh);
6✔
2663
        }
2664

2665
        // Extract the parent, if any, and resolve its render entity
2666
        let parent_render_entity = if let Some(main_entity) = compiled_effect.parent {
314✔
2667
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2668
                error!(
×
2669
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2670
                    main_entity
2671
                );
2672
                cmd.remove::<ChildEffectOf>();
×
2673
                // TODO - prevent extraction altogether here, instead of just de-parenting?
2674
                continue;
×
2675
            };
2676
            Some(render_entity)
2677
        } else {
2678
            None
314✔
2679
        };
2680
        if let Some(render_entity) = parent_render_entity {
×
2681
            let new_child_of = ChildEffectOf {
2682
                parent: render_entity,
2683
            };
2684
            // If there's already an ExtractedParent component, ensure we overwrite only if
2685
            // different, to not trigger ECS change detection that we rely on.
2686
            if let Some(child_effect_of) = maybe_child_of {
×
2687
                // The relationship makes ChildEffectOf immutable, so re-insert to mutate
2688
                if *child_effect_of != new_child_of {
×
2689
                    cmd.insert(new_child_of);
×
2690
                }
2691
            } else {
2692
                trace!(
×
2693
                    "Inserting new ChildEffectOf component on {:?}",
×
2694
                    render_entity
2695
                );
2696
                cmd.insert(new_child_of);
×
2697
            }
2698
        } else {
2699
            cmd.remove::<ChildEffectOf>();
314✔
2700
        }
2701

2702
        // Extract property data
2703
        let property_layout = asset.property_layout();
2704
        if property_layout.is_empty() {
305✔
2705
            cmd.remove::<ExtractedProperties>();
305✔
2706
        } else {
2707
            // Re-extract CPU property data if any. Note that this data is not a "new value"
2708
            // but instead a "value that must be uploaded this frame", and therefore is
2709
            // empty when there's no change (as opposed to, having a constant value
2710
            // frame-to-frame).
2711
            let property_data = if let Some(properties) = maybe_properties {
9✔
2712
                if properties.is_changed() {
2713
                    trace!("Detected property change, re-serializing...");
×
2714
                    Some(properties.serialize(&property_layout))
×
2715
                } else {
2716
                    None
×
2717
                }
2718
            } else {
2719
                None
9✔
2720
            };
2721

2722
            let new_properties = ExtractedProperties {
2723
                property_layout,
2724
                property_data,
2725
            };
2726
            trace!("new_properties = {new_properties:?}");
9✔
2727

2728
            if let Some(mut extracted_properties) = maybe_extracted_properties {
8✔
2729
                // Always mutate if there's new CPU data to re-upload. Otherwise check for any
2730
                // other change.
2731
                if new_properties.property_data.is_some()
2732
                    || (extracted_properties.property_layout != new_properties.property_layout)
8✔
2733
                {
2734
                    trace!(
×
2735
                        "Updating existing ExtractedProperties (was: {:?})",
×
2736
                        extracted_properties.as_ref()
×
2737
                    );
2738
                    *extracted_properties = new_properties;
2739
                }
2740
            } else {
2741
                trace!(
1✔
2742
                    "Inserting new ExtractedProperties component on {:?}",
1✔
2743
                    render_entity
2744
                );
2745
                cmd.insert(new_properties);
3✔
2746
            }
2747
        }
2748
    }
2749
}
2750

2751
pub(crate) fn extract_sim_params(
330✔
2752
    real_time: Extract<Res<Time<Real>>>,
2753
    virtual_time: Extract<Res<Time<Virtual>>>,
2754
    time: Extract<Res<Time<EffectSimulation>>>,
2755
    mut sim_params: ResMut<SimParams>,
2756
) {
2757
    #[cfg(feature = "trace")]
2758
    let _span = bevy::log::info_span!("extract_sim_params").entered();
990✔
2759
    trace!("extract_sim_params()");
650✔
2760

2761
    // Save simulation params into render world
2762
    sim_params.time = time.elapsed_secs_f64();
660✔
2763
    sim_params.delta_time = time.delta_secs();
660✔
2764
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
660✔
2765
    sim_params.virtual_delta_time = virtual_time.delta_secs();
660✔
2766
    sim_params.real_time = real_time.elapsed_secs_f64();
660✔
2767
    sim_params.real_delta_time = real_time.delta_secs();
660✔
2768
    trace!(
330✔
2769
        "SimParams: time={} delta_time={} vtime={} delta_vtime={} rtime={} delta_rtime={}",
320✔
2770
        sim_params.time,
320✔
2771
        sim_params.delta_time,
320✔
2772
        sim_params.virtual_time,
320✔
2773
        sim_params.virtual_delta_time,
320✔
2774
        sim_params.real_time,
320✔
2775
        sim_params.real_delta_time,
320✔
2776
    );
2777
}
2778

2779
/// Various GPU limits and aligned sizes computed once and cached.
2780
struct GpuLimits {
2781
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2782
    ///
2783
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2784
    storage_buffer_align: NonZeroU32,
2785

2786
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2787
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2788
    ///
2789
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2790
    effect_metadata_aligned_size: NonZeroU32,
2791
}
2792

2793
impl GpuLimits {
2794
    pub fn from_device(render_device: &RenderDevice) -> Self {
4✔
2795
        let storage_buffer_align =
4✔
2796
            render_device.limits().min_storage_buffer_offset_alignment as u64;
4✔
2797

2798
        let effect_metadata_aligned_size = NonZeroU32::new(
2799
            GpuEffectMetadata::min_size()
8✔
2800
                .get()
8✔
2801
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2802
        )
2803
        .unwrap();
2804

2805
        trace!(
4✔
2806
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2✔
2807
            storage_buffer_align,
2808
            GpuEffectMetadata::min_size().get(),
4✔
2809
            effect_metadata_aligned_size.get(),
4✔
2810
        );
2811

2812
        Self {
2813
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
12✔
2814
            effect_metadata_aligned_size,
2815
        }
2816
    }
2817

2818
    /// Byte alignment for any storage buffer binding.
2819
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2820
        self.storage_buffer_align
3✔
2821
    }
2822

2823
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2824
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2825
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2826
    }
2827
}
2828

2829
/// Global render world resource containing the GPU data to draw all the
2830
/// particle effects in all views.
2831
///
2832
/// The resource is populated by [`prepare_effects()`] with all the effects to
2833
/// render for the current frame, for all views in the frame, and consumed by
2834
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2835
/// effects.
2836
#[derive(Resource)]
2837
pub struct EffectsMeta {
2838
    /// Bind group for the camera view, containing the camera projection and
2839
    /// other uniform values related to the camera.
2840
    view_bind_group: Option<BindGroup>,
2841
    /// Bind group #0 of the vfx_update shader, for the simulation parameters
2842
    /// like the current time and frame delta time.
2843
    update_sim_params_bind_group: Option<BindGroup>,
2844
    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2845
    /// like the current time and frame delta time. This is shared with the
2846
    /// vfx_init pass too.
2847
    indirect_sim_params_bind_group: Option<BindGroup>,
2848
    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2849
    /// compute dispatch and render buffers.
2850
    indirect_metadata_bind_group: Option<BindGroup>,
2851
    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2852
    indirect_spawner_bind_group: Option<BindGroup>,
2853
    /// Global shared GPU uniform buffer storing the simulation parameters,
2854
    /// uploaded each frame from CPU to GPU.
2855
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2856
    /// Global shared GPU buffer storing the various spawner parameter structs
2857
    /// for the active effect instances.
2858
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2859
    /// Global shared GPU buffer storing the various indirect dispatch structs
2860
    /// for the indirect dispatch of the Update pass.
2861
    dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
2862
    /// Global shared GPU buffer storing the various indirect draw structs
2863
    /// for the indirect Render pass. Note that we use
2864
    /// GpuDrawIndexedIndirectArgs as the largest of the two variants (the
2865
    /// other being GpuDrawIndirectArgs). For non-indexed entries, we ignore
2866
    /// the last `u32` value.
2867
    draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
2868
    /// Global shared GPU buffer storing the various `EffectMetadata`
2869
    /// structs for the active effect instances.
2870
    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2871
    /// Various GPU limits and aligned sizes lazily allocated and cached for
2872
    /// convenience.
2873
    gpu_limits: GpuLimits,
2874
    indirect_shader_noevent: Handle<Shader>,
2875
    indirect_shader_events: Handle<Shader>,
2876
    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2877
    /// -noevent and -events variants).
2878
    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2879
    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2880
    /// is either the -noevent or -events variant depending on whether there's
2881
    /// any child effect with GPU events currently active.
2882
    active_indirect_pipeline_id: CachedComputePipelineId,
2883
}
2884

2885
impl EffectsMeta {
2886
    pub fn new(
3✔
2887
        device: RenderDevice,
2888
        indirect_shader_noevent: Handle<Shader>,
2889
        indirect_shader_events: Handle<Shader>,
2890
    ) -> Self {
2891
        let gpu_limits = GpuLimits::from_device(&device);
9✔
2892

2893
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2894
        // be addressed individually by the computer shaders.
2895
        let item_align = gpu_limits.storage_buffer_align();
9✔
2896
        trace!(
3✔
2897
            "Aligning storage buffers to {} bytes as device limits requires.",
2✔
2898
            item_align.get()
4✔
2899
        );
2900

2901
        Self {
2902
            view_bind_group: None,
2903
            update_sim_params_bind_group: None,
2904
            indirect_sim_params_bind_group: None,
2905
            indirect_metadata_bind_group: None,
2906
            indirect_spawner_bind_group: None,
2907
            sim_params_uniforms: UniformBuffer::default(),
6✔
2908
            spawner_buffer: AlignedBufferVec::new(
6✔
2909
                BufferUsages::STORAGE,
2910
                Some(item_align.into()),
2911
                Some("hanabi:buffer:spawner".to_string()),
2912
            ),
2913
            dispatch_indirect_buffer: GpuBuffer::new(
6✔
2914
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2915
                Some("hanabi:buffer:dispatch_indirect".to_string()),
2916
            ),
2917
            draw_indirect_buffer: BufferTable::new(
6✔
2918
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2919
                Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
2920
                Some("hanabi:buffer:draw_indirect".to_string()),
2921
            ),
2922
            effect_metadata_buffer: BufferTable::new(
6✔
2923
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2924
                Some(item_align.into()),
2925
                Some("hanabi:buffer:effect_metadata".to_string()),
2926
            ),
2927
            gpu_limits,
2928
            indirect_shader_noevent,
2929
            indirect_shader_events,
2930
            indirect_pipeline_ids: [
3✔
2931
                CachedComputePipelineId::INVALID,
2932
                CachedComputePipelineId::INVALID,
2933
            ],
2934
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2935
        }
2936
    }
2937

2938
    pub fn allocate_spawner(
312✔
2939
        &mut self,
2940
        global_transform: &GlobalTransform,
2941
        spawn_count: u32,
2942
        prng_seed: u32,
2943
        slab_offset: u32,
2944
        parent_slab_offset: Option<u32>,
2945
        effect_metadata_buffer_table_id: BufferTableId,
2946
        maybe_cached_draw_indirect_args: Option<&CachedDrawIndirectArgs>,
2947
    ) -> u32 {
2948
        let spawner_base = self.spawner_buffer.len() as u32;
624✔
2949
        let transform = global_transform.to_matrix().into();
1,248✔
2950
        let inverse_transform = Mat4::from(
2951
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2952
            // efficient than inversing the Mat4.
2953
            global_transform.affine().inverse(),
624✔
2954
        )
2955
        .into();
2956
        let spawner_params = GpuSpawnerParams {
2957
            transform,
2958
            inverse_transform,
2959
            spawn: spawn_count as i32,
312✔
2960
            seed: prng_seed,
2961
            effect_metadata_index: effect_metadata_buffer_table_id.0,
312✔
2962
            draw_indirect_index: maybe_cached_draw_indirect_args
312✔
2963
                .map(|cdia| cdia.get_row().0)
2964
                .unwrap_or_default(),
2965
            slab_offset,
2966
            parent_slab_offset: parent_slab_offset.unwrap_or(u32::MAX),
624✔
2967
            ..default()
2968
        };
2969
        trace!("spawner params = {:?}", spawner_params);
624✔
2970
        self.spawner_buffer.push(spawner_params);
936✔
2971
        spawner_base
312✔
2972
    }
2973

2974
    pub fn allocate_draw_indirect(
2✔
2975
        &mut self,
2976
        draw_args: &AnyDrawIndirectArgs,
2977
    ) -> CachedDrawIndirectArgs {
2978
        let row = self
4✔
2979
            .draw_indirect_buffer
2✔
2980
            .insert(draw_args.bitcast_to_row_entry());
6✔
2981
        CachedDrawIndirectArgs {
2982
            row,
2983
            args: *draw_args,
2✔
2984
        }
2985
    }
2986

2987
    pub fn update_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
×
2988
        self.draw_indirect_buffer
×
2989
            .update(row_index.get_row(), row_index.args.bitcast_to_row_entry());
×
2990
    }
2991

2992
    pub fn free_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
1✔
2993
        self.draw_indirect_buffer.remove(row_index.get_row());
4✔
2994
    }
2995
}
2996

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

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

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

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

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

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

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

3108
    // Clear bind groups associated with the removed buffer
3109
    trace!(
1✔
3110
        "=> GPU particle slab #{} gone, destroying its bind groups...",
1✔
3111
        cached_effect.slab_id.index()
2✔
3112
    );
3113
    effect_bind_groups
3114
        .particle_slabs
3115
        .remove(&cached_effect.slab_id);
3116
    effects_meta
3117
        .dispatch_indirect_buffer
3118
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
3119
}
3120

3121
/// Observer raised when the [`CachedEffectMetadata`] component is removed, to
3122
/// deallocate the GPU resources associated with the indirect draw args.
3123
pub(crate) fn on_remove_cached_metadata(
1✔
3124
    trigger: On<Remove, CachedEffectMetadata>,
3125
    query: Query<&CachedEffectMetadata>,
3126
    mut effects_meta: ResMut<EffectsMeta>,
3127
) {
3128
    #[cfg(feature = "trace")]
3129
    let _span = bevy::log::info_span!("on_remove_cached_metadata").entered();
3✔
3130

3131
    if let Ok(cached_metadata) = query.get(trigger.event().entity) {
4✔
3132
        if cached_metadata.table_id.is_valid() {
1✔
3133
            effects_meta
2✔
3134
                .effect_metadata_buffer
2✔
3135
                .remove(cached_metadata.table_id);
1✔
3136
        }
3137
    };
3138
}
3139

3140
/// Observer raised when the [`CachedDrawIndirectArgs`] component is removed, to
3141
/// deallocate the GPU resources associated with the indirect draw args.
3142
pub(crate) fn on_remove_cached_draw_indirect_args(
1✔
3143
    trigger: On<Remove, CachedDrawIndirectArgs>,
3144
    query: Query<&CachedDrawIndirectArgs>,
3145
    mut effects_meta: ResMut<EffectsMeta>,
3146
) {
3147
    #[cfg(feature = "trace")]
3148
    let _span = bevy::log::info_span!("on_remove_cached_draw_indirect_args").entered();
3✔
3149

3150
    if let Ok(cached_draw_args) = query.get(trigger.event().entity) {
4✔
3151
        effects_meta.free_draw_indirect(cached_draw_args);
3152
    };
3153
}
3154

3155
/// Clear pending GPU resources left from previous frame.
3156
///
3157
/// Those generally are source buffers for buffer-to-buffer copies on capacity
3158
/// growth, which need the source buffer to be alive until the copy is done,
3159
/// then can be discarded here.
3160
pub(crate) fn clear_previous_frame_resizes(
330✔
3161
    mut effects_meta: ResMut<EffectsMeta>,
3162
    mut sort_bind_groups: ResMut<SortBindGroups>,
3163
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3164
) {
3165
    #[cfg(feature = "trace")]
3166
    let _span = bevy::log::info_span!("clear_previous_frame_resizes").entered();
990✔
3167
    trace!("clear_previous_frame_resizes");
650✔
3168

3169
    init_fill_dispatch_queue.clear();
330✔
3170

3171
    // Clear last frame's buffer resizes which may have occured during last frame,
3172
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3173
    // the first point at which we can do that where we're not blocking the main
3174
    // world (so, excluding the extract system).
3175
    effects_meta
330✔
3176
        .dispatch_indirect_buffer
330✔
3177
        .clear_previous_frame_resizes();
3178
    effects_meta
330✔
3179
        .draw_indirect_buffer
330✔
3180
        .clear_previous_frame_resizes();
3181
    effects_meta
330✔
3182
        .effect_metadata_buffer
330✔
3183
        .clear_previous_frame_resizes();
3184
    sort_bind_groups.clear_previous_frame_resizes();
330✔
3185
}
3186

3187
// Fixup the [`CachedChildInfo::global_child_index`] once all child infos have
3188
// been allocated.
3189
pub fn fixup_parents(
330✔
3190
    q_changed_parents: Query<(Entity, Ref<CachedParentInfo>)>,
3191
    mut q_children: Query<&mut CachedChildInfo>,
3192
) {
3193
    #[cfg(feature = "trace")]
3194
    let _span = bevy::log::info_span!("fixup_parents").entered();
990✔
3195
    trace!("fixup_parents");
650✔
3196

3197
    // Once all parents are (re-)allocated, fix up the global index of all
3198
    // children if the parent base index changed.
3199
    trace!(
330✔
3200
        "Updating the global index of children of parent effects whose child list just changed..."
320✔
3201
    );
3202
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
660✔
3203
        let base_index =
3204
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3205
        let parent_changed = cached_parent_info.is_changed();
3206
        trace!(
3207
            "Updating {} children of parent effect {:?} with base child index {} (parent_changed:{})...",
×
3208
            cached_parent_info.children.len(),
×
3209
            parent_entity,
3210
            base_index,
3211
            parent_changed
3212
        );
3213
        for (child_entity, _) in &cached_parent_info.children {
×
3214
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3215
                error!(
×
3216
                    "Cannot find child {:?} declared by parent {:?}",
×
3217
                    *child_entity, parent_entity
3218
                );
3219
                continue;
×
3220
            };
3221
            if !cached_child_info.is_changed() && !parent_changed {
×
3222
                continue;
×
3223
            }
3224
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3225
            trace!(
×
3226
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3227
                child_entity,
3228
                parent_entity,
3229
                cached_child_info.local_child_index,
×
3230
                cached_child_info.global_child_index
×
3231
            );
3232
        }
3233
    }
3234
}
3235

3236
/// Allocate the GPU resources for all extracted effects.
3237
///
3238
/// This adds the [`CachedEffect`] component as needed, and update it with the
3239
/// allocation in the [`EffectCache`].
3240
pub fn allocate_effects(
330✔
3241
    mut commands: Commands,
3242
    mut q_extracted_effects: Query<
3243
        (
3244
            Entity,
3245
            &ExtractedEffect,
3246
            Has<ChildEffectOf>,
3247
            Option<&mut CachedEffect>,
3248
            Has<DispatchBufferIndices>,
3249
        ),
3250
        Changed<ExtractedEffect>,
3251
    >,
3252
    mut effect_cache: ResMut<EffectCache>,
3253
    mut effects_meta: ResMut<EffectsMeta>,
3254
) {
3255
    #[cfg(feature = "trace")]
3256
    let _span = bevy::log::info_span!("allocate_effects").entered();
990✔
3257
    trace!("allocate_effects");
650✔
3258

3259
    for (entity, extracted_effect, has_parent, maybe_cached_effect, has_dispatch_buffer_indices) in
3✔
3260
        &mut q_extracted_effects
333✔
3261
    {
3262
        // Insert or update the effect into the EffectCache
3263
        if let Some(mut cached_effect) = maybe_cached_effect {
1✔
3264
            trace!("Updating EffectCache entry for entity {entity:?}...");
1✔
3265
            let _ = effect_cache.remove(cached_effect.as_ref());
3266
            *cached_effect = effect_cache.insert(
3267
                extracted_effect.handle.clone(),
3268
                extracted_effect.capacity,
3269
                &extracted_effect.particle_layout,
3270
            );
3271
        } else {
3272
            trace!("Allocating new entry in EffectCache for entity {entity:?}...");
4✔
3273
            let cached_effect = effect_cache.insert(
8✔
3274
                extracted_effect.handle.clone(),
6✔
3275
                extracted_effect.capacity,
2✔
3276
                &extracted_effect.particle_layout,
2✔
3277
            );
3278
            commands.entity(entity).insert(cached_effect);
8✔
3279
        }
3280

3281
        // Ensure the particle@1 bind group layout exists for the given configuration of
3282
        // particle layout. We do this here only for effects without a parent; for those
3283
        // with a parent, we'll do it after we resolved that parent.
3284
        if !has_parent {
3✔
3285
            let parent_min_binding_size = None;
3✔
3286
            effect_cache.ensure_particle_bind_group_layout(
3✔
3287
                extracted_effect.particle_layout.min_binding_size32(),
3✔
3288
                parent_min_binding_size,
3✔
3289
            );
3290
        }
3291

3292
        // Ensure the metadata@3 bind group layout exists for the init pass.
3293
        {
3294
            let consume_gpu_spawn_events = extracted_effect
3295
                .layout_flags
3296
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
3297
            effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
3298
        }
3299

3300
        // Allocate DispatchBufferIndices if not present yet
3301
        if !has_dispatch_buffer_indices {
2✔
3302
            let update_dispatch_indirect_buffer_row_index =
2✔
3303
                effects_meta.dispatch_indirect_buffer.allocate();
2✔
3304
            commands.entity(entity).insert(DispatchBufferIndices {
2✔
3305
                update_dispatch_indirect_buffer_row_index,
2✔
3306
            });
3307
        }
3308
    }
3309
}
3310

3311
/// Update any cached mesh info based on any relocation done by Bevy itself.
3312
///
3313
/// Bevy will merge small meshes into larger GPU buffers automatically. When
3314
/// this happens, the mesh location changes, and we need to update our
3315
/// references to it in order to know how to issue the draw commands.
3316
///
3317
/// This system updates both the [`CachedMeshLocation`] and the
3318
/// [`CachedIndirectDrawArgs`] components.
3319
pub fn update_mesh_locations(
330✔
3320
    mut commands: Commands,
3321
    mut effects_meta: ResMut<EffectsMeta>,
3322
    mesh_allocator: Res<MeshAllocator>,
3323
    render_meshes: Res<RenderAssets<RenderMesh>>,
3324
    mut q_cached_effects: Query<(
3325
        Entity,
3326
        &ExtractedEffectMesh,
3327
        Option<&mut CachedMeshLocation>,
3328
        Option<&mut CachedDrawIndirectArgs>,
3329
    )>,
3330
) {
3331
    #[cfg(feature = "trace")]
3332
    let _span = bevy::log::info_span!("update_mesh_locations").entered();
990✔
3333
    trace!("update_mesh_locations");
650✔
3334

3335
    for (entity, extracted_mesh, maybe_cached_mesh_location, maybe_cached_draw_indirect_args) in
1,256✔
3336
        &mut q_cached_effects
644✔
3337
    {
3338
        let mut cmds = commands.entity(entity);
1,256✔
3339

3340
        // Resolve the render mesh
3341
        let Some(render_mesh) = render_meshes.get(extracted_mesh.mesh) else {
942✔
3342
            warn!(
×
3343
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3344
                entity, extracted_mesh.mesh
3345
            );
3346
            cmds.remove::<CachedMeshLocation>();
×
3347
            continue;
×
3348
        };
3349

3350
        // Find the location where the render mesh was allocated. This is handled by
3351
        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3352
        // re-batch the vertex and optional index data of meshes together at any point,
3353
        // so we need to confirm that the location data we may have cached is still
3354
        // valid.
3355
        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&extracted_mesh.mesh)
314✔
3356
        else {
3357
            trace!(
×
3358
                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
3359
                entity,
3360
                extracted_mesh.mesh
3361
            );
3362
            cmds.remove::<CachedMeshLocation>();
×
3363
            continue;
×
3364
        };
3365
        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&extracted_mesh.mesh);
3366
        let indexed =
314✔
3367
            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
314✔
3368
                if let Some(ref slice) = mesh_index_buffer_slice {
314✔
3369
                    Some(MeshIndexSlice {
3370
                        format: index_format,
3371
                        buffer: slice.buffer.clone(),
3372
                        range: slice.range.clone(),
3373
                    })
3374
                } else {
3375
                    trace!(
×
3376
                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
3377
                        entity,
3378
                        extracted_mesh.mesh
3379
                    );
3380
                    cmds.remove::<CachedMeshLocation>();
×
3381
                    continue;
×
3382
                }
3383
            } else {
3384
                None
×
3385
            };
3386

3387
        // Calculate the new draw args and mesh location based on Bevy's info
3388
        let new_draw_args = AnyDrawIndirectArgs::from_slices(
3389
            &mesh_vertex_buffer_slice,
3390
            mesh_index_buffer_slice.as_ref(),
3391
        );
3392
        let new_mesh_location = match &mesh_index_buffer_slice {
3393
            // Indexed mesh rendering
3394
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3395
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
942✔
3396
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
628✔
3397
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
628✔
3398
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
314✔
3399
                indexed,
3400
            },
3401
            // Non-indexed mesh rendering
3402
            None => CachedMeshLocation {
3403
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3404
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
3405
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3406
                vertex_offset_or_base_instance: 0,
3407
                indexed: None,
3408
            },
3409
        };
3410

3411
        // We don't allocate the draw indirect args ahead of time because we need to
3412
        // select the indexed vs. non-indexed buffer. Now that we know whether the mesh
3413
        // is indexed, we can allocate it (or reallocate it if indexing mode changed).
3414
        if let Some(mut cached_draw_indirect) = maybe_cached_draw_indirect_args {
312✔
3415
            assert!(cached_draw_indirect.row.is_valid());
3416

3417
            // If the GPU draw args changed, re-upload to GPU.
3418
            if new_draw_args != cached_draw_indirect.args {
312✔
3419
                debug!(
×
3420
                    "Indirect draw args changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3421
                    entity, cached_draw_indirect.args, new_draw_args
×
3422
                );
3423
                cached_draw_indirect.args = new_draw_args;
×
3424
                effects_meta.update_draw_indirect(cached_draw_indirect.as_ref());
×
3425
            }
3426
        } else {
3427
            cmds.insert(effects_meta.allocate_draw_indirect(&new_draw_args));
8✔
3428
        }
3429

3430
        // Compare to any cached data and update if necessary, or insert if missing.
3431
        // This will trigger change detection in the ECS, which will in turn trigger
3432
        // GpuEffectMetadata re-upload.
3433
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
626✔
3434
            if *old_mesh_location != new_mesh_location {
3435
                debug!(
×
3436
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3437
                    entity, old_mesh_location, new_mesh_location
3438
                );
3439
                *old_mesh_location = new_mesh_location;
×
3440
            }
3441
        } else {
3442
            cmds.insert(new_mesh_location);
4✔
3443
        }
3444
    }
3445
}
3446

3447
/// Allocate an entry in the GPU table for any [`CachedEffectMetadata`] missing
3448
/// one.
3449
///
3450
/// This system does NOT take care of (re-)uploading recent CPU data to GPU.
3451
/// This is done much later in the frame, after batching and once all data for
3452
/// it is ready. But it's necessary to ensure the allocation is determined
3453
/// already ahead of time, in order to do batching of contiguous metadata
3454
/// blocks (TODO; not currently used, also may end up using binary search in
3455
/// shader, in which case we won't need continguous-ness and can maybe remove
3456
/// this system).
3457
// TODO - consider using observer OnAdd instead?
3458
pub fn allocate_metadata(
330✔
3459
    mut effects_meta: ResMut<EffectsMeta>,
3460
    mut q_metadata: Query<&mut CachedEffectMetadata>,
3461
) {
3462
    for mut metadata in &mut q_metadata {
958✔
3463
        if !metadata.table_id.is_valid() {
2✔
3464
            metadata.table_id = effects_meta
2✔
3465
                .effect_metadata_buffer
2✔
3466
                .insert(metadata.metadata);
2✔
3467
        } else {
3468
            // Unless this is the first time we allocate the GPU entry (above),
3469
            // we should never reach the beginning of this frame
3470
            // with a changed metadata which has not
3471
            // been re-uploaded last frame.
3472
            // NO! We can only detect the change *since last run of THIS system*
3473
            // so wont' see that a latter system the data.
3474
            // assert!(!metadata.is_changed());
3475
        }
3476
    }
3477
}
3478

3479
/// Update the [`CachedParentInfo`] of parent effects and the
3480
/// [`CachedChildInfo`] of child effects.
3481
pub fn allocate_parent_child_infos(
330✔
3482
    mut commands: Commands,
3483
    mut effect_cache: ResMut<EffectCache>,
3484
    mut event_cache: ResMut<EventCache>,
3485
    // All extracted child effects. May or may not already have a CachedChildInfo. If not, this
3486
    // will be spawned below.
3487
    mut q_child_effects: Query<(
3488
        Entity,
3489
        &ExtractedEffect,
3490
        &ChildEffectOf,
3491
        &CachedEffectEvents,
3492
        Option<&mut CachedChildInfo>,
3493
    )>,
3494
    // All parent effects from a previous frame (already have CachedParentInfo), which can be
3495
    // updated in-place without spawning a new CachedParentInfo.
3496
    mut q_parent_effects: Query<(
3497
        Entity,
3498
        &ExtractedEffect,
3499
        &CachedEffect,
3500
        &ChildrenEffects,
3501
        Option<&mut CachedParentInfo>,
3502
    )>,
3503
) {
3504
    #[cfg(feature = "trace")]
3505
    let _span = bevy::log::info_span!("allocate_child_infos").entered();
990✔
3506
    trace!("allocate_child_infos");
650✔
3507

3508
    // Loop on all child effects and ensure their CachedChildInfo is up-to-date.
3509
    for (child_entity, _, child_effect_of, cached_effect_events, maybe_cached_child_info) in
×
3510
        &mut q_child_effects
330✔
3511
    {
3512
        // Fetch the parent effect
3513
        let parent_entity = child_effect_of.parent;
3514
        let Ok((_, _, parent_cached_effect, children_effects, _)) =
×
3515
            q_parent_effects.get(parent_entity)
3516
        else {
3517
            warn!("Unknown parent #{parent_entity:?} on child entity {child_entity:?}, removing CachedChildInfo.");
×
3518
            if maybe_cached_child_info.is_some() {
×
3519
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3520
            }
3521
            continue;
×
3522
        };
3523

3524
        // Find the index of this child entity in its parent's storage
3525
        let Some(local_child_index) = children_effects.0.iter().position(|e| *e == child_entity)
×
3526
        else {
3527
            warn!("Cannot find child entity {child_entity:?} in the children collection of parent entity {parent_entity:?}. Relationship desync?");
×
3528
            if maybe_cached_child_info.is_some() {
×
3529
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3530
            }
3531
            continue;
×
3532
        };
3533
        let local_child_index = local_child_index as u32;
3534

3535
        // Fetch the effect buffer of the parent effect
3536
        let Some(parent_buffer_binding_source) = effect_cache
×
3537
            .get_slab(&parent_cached_effect.slab_id)
3538
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3539
        else {
3540
            warn!(
×
3541
                "Unknown parent slab #{} on parent entity {:?}, removing CachedChildInfo.",
×
3542
                parent_cached_effect.slab_id.index(),
×
3543
                parent_entity
3544
            );
3545
            if maybe_cached_child_info.is_some() {
×
3546
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3547
            }
3548
            continue;
×
3549
        };
3550

3551
        let new_cached_child_info = CachedChildInfo {
3552
            parent_slab_id: parent_cached_effect.slab_id,
3553
            parent_slab_offset: parent_cached_effect.slice.range().start,
3554
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
3555
            parent_buffer_binding_source,
3556
            local_child_index,
3557
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3558
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3559
        };
3560
        if let Some(mut cached_child_info) = maybe_cached_child_info {
×
3561
            if !cached_child_info.is_locally_equal(&new_cached_child_info) {
×
3562
                *cached_child_info = new_cached_child_info;
×
3563
            }
3564
        } else {
3565
            commands.entity(child_entity).insert(new_cached_child_info);
×
3566
        }
3567
    }
3568

3569
    // Loop on all parent effects and ensure their CachedParentInfo is up-to-date.
3570
    for (parent_entity, parent_extracted_effect, _, children_effects, maybe_cached_parent_info) in
×
3571
        &mut q_parent_effects
330✔
3572
    {
3573
        let parent_min_binding_size = parent_extracted_effect.particle_layout.min_binding_size32();
×
3574

3575
        // Loop over children and gather GpuChildInfo
3576
        let mut new_children = Vec::with_capacity(children_effects.0.len());
×
3577
        let mut new_child_infos = Vec::with_capacity(children_effects.0.len());
×
3578
        for child_entity in children_effects.0.iter() {
×
3579
            // Fetch the child's event buffer allocation info
3580
            let Ok((_, child_extracted_effect, _, cached_effect_events, _)) =
×
3581
                q_child_effects.get(*child_entity)
×
3582
            else {
3583
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} didnt't resolve to a child instance. The parent effect cannot be processed.");
×
3584
                if maybe_cached_parent_info.is_some() {
×
3585
                    commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3586
                }
3587
                break;
×
3588
            };
3589

3590
            // Fetch the GPU event buffer of the child
3591
            let Some(event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3592
            else {
3593
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} doesn't have an allocated GPU event buffer. The parent effect cannot be processed.");
×
3594
                break;
3595
            };
3596

3597
            let buffer_binding_source = BufferBindingSource {
3598
                buffer: event_buffer.clone(),
3599
                offset: cached_effect_events.range.start,
3600
                size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3601
            };
3602
            new_children.push((*child_entity, buffer_binding_source));
3603

3604
            new_child_infos.push(GpuChildInfo {
3605
                event_count: 0,
3606
                init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3607
            });
3608

3609
            // Ensure the particle@1 bind group layout exists for the given configuration of
3610
            // particle layout. We do this here only for effects with a parent; for those
3611
            // without a parent, we already did this in allocate_effects().
3612
            effect_cache.ensure_particle_bind_group_layout(
3613
                child_extracted_effect.particle_layout.min_binding_size32(),
3614
                Some(parent_min_binding_size),
3615
            );
3616
        }
3617

3618
        // If we don't have all children, just abort this effect. We don't try to have
3619
        // partial relationships, this is too complex for shader bindings.
3620
        debug_assert_eq!(new_children.len(), new_child_infos.len());
×
3621
        if (new_children.len() < children_effects.len()) && maybe_cached_parent_info.is_some() {
×
3622
            warn!("One or more child effect(s) on parent effect {parent_entity:?} failed to configure. The parent effect cannot be processed.");
×
3623
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3624
            continue;
×
3625
        }
3626

3627
        // Insert or update the CachedParentInfo component of the parent effect
3628
        if let Some(mut cached_parent_info) = maybe_cached_parent_info {
×
3629
            if cached_parent_info.children != new_children {
×
3630
                // FIXME - missing way to just update in-place without changing the allocation
3631
                // size!
3632
                // if cached_parent_info.children.len() == new_children.len() {
3633
                //} else {
3634
                event_cache.reallocate_child_infos(
×
3635
                    parent_entity,
×
3636
                    new_children,
×
3637
                    &new_child_infos[..],
×
3638
                    cached_parent_info.as_mut(),
×
3639
                );
3640
                //}
3641
            }
3642
        } else {
3643
            let cached_parent_info =
×
3644
                event_cache.allocate_child_infos(parent_entity, new_children, &new_child_infos[..]);
×
3645
            commands.entity(parent_entity).insert(cached_parent_info);
×
3646
        }
3647
    }
3648
}
3649

3650
/// Prepare the init and update compute pipelines for an effect.
3651
///
3652
/// This caches the pipeline IDs once resolved, and their compiling state when
3653
/// it changes, to determine when an effect is ready to be used.
3654
///
3655
/// Note that we do that proactively even if the effect will be skipped this
3656
/// frame (for example because it's not visible). This ensures we queue pipeline
3657
/// compilations ASAP, as they can take a long time (10+ frames). We also use
3658
/// the pipeline compiling state, which we query here, to inform whether the
3659
/// effect is ready for this frame. So in general if this is a new pipeline, it
3660
/// won't be ready this frame.
3661
pub fn prepare_init_update_pipelines(
330✔
3662
    mut q_effects: Query<(
3663
        Entity,
3664
        &ExtractedEffect,
3665
        &CachedEffect,
3666
        Option<&CachedChildInfo>,
3667
        Option<&CachedParentInfo>,
3668
        Option<&CachedEffectProperties>,
3669
        &mut CachedPipelines,
3670
    )>,
3671
    // FIXME - need mut for bind group layout creation; shouldn't be create there though
3672
    mut effect_cache: ResMut<EffectCache>,
3673
    pipeline_cache: Res<PipelineCache>,
3674
    property_cache: ResMut<PropertyCache>,
3675
    mut init_pipeline: ResMut<ParticlesInitPipeline>,
3676
    mut update_pipeline: ResMut<ParticlesUpdatePipeline>,
3677
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
3678
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3679
) {
3680
    #[cfg(feature = "trace")]
3681
    let _span = bevy::log::info_span!("prepare_init_update_pipelines").entered();
990✔
3682
    trace!("prepare_init_update_pipelines");
650✔
3683

3684
    // Note: As of Bevy 0.16 we can't evict old pipelines from the cache. They're
3685
    // inserted forever. https://github.com/bevyengine/bevy/issues/19925
3686

3687
    for (
3688
        entity,
314✔
3689
        extracted_effect,
3690
        cached_effect,
3691
        maybe_cached_child_info,
3692
        maybe_cached_parent_info,
3693
        maybe_cached_properties,
3694
        mut cached_pipelines,
3695
    ) in &mut q_effects
644✔
3696
    {
3697
        trace!(
3698
            "Preparing pipelines for effect {:?}... (flags: {:?})",
314✔
3699
            entity,
3700
            cached_pipelines.flags
314✔
3701
        );
3702

3703
        let particle_layout = &cached_effect.slice.particle_layout;
3704
        let particle_layout_min_binding_size = particle_layout.min_binding_size32();
3705
        let has_event_buffer = maybe_cached_child_info.is_some();
3706
        let parent_particle_layout_min_binding_size = maybe_cached_child_info
3707
            .as_ref()
3708
            .map(|cci| cci.parent_particle_layout.min_binding_size32());
×
3709

3710
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
314✔
3711
            particle_layout_min_binding_size,
3712
            parent_particle_layout_min_binding_size,
3713
        ) else {
3714
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}",
×
3715
                particle_layout_min_binding_size, parent_particle_layout_min_binding_size);
3716
            continue;
×
3717
        };
3718
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3719

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

3739
        // Resolve the init pipeline
3740
        let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
312✔
3741
            *init_pipeline_id
3742
        } else {
3743
            // Clear flag just in case, to ensure consistency.
3744
            cached_pipelines
2✔
3745
                .flags
2✔
3746
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3747

3748
            // Fetch the metadata@3 bind group layout from the cache
3749
            let metadata_bind_group_layout = effect_cache
6✔
3750
                .metadata_init_bind_group_layout(has_event_buffer)
2✔
3751
                .unwrap()
3752
                .clone();
3753

3754
            let init_pipeline_key_flags = {
2✔
3755
                let mut flags = ParticleInitPipelineKeyFlags::empty();
4✔
3756
                flags.set(
4✔
3757
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3758
                    particle_layout.contains(Attribute::PREV),
4✔
3759
                );
3760
                flags.set(
4✔
3761
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3762
                    particle_layout.contains(Attribute::NEXT),
4✔
3763
                );
3764
                flags.set(
4✔
3765
                    ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3766
                    has_event_buffer,
2✔
3767
                );
3768
                flags
2✔
3769
            };
3770

3771
            // https://github.com/bevyengine/bevy/issues/17132
3772
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
6✔
3773
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
6✔
3774
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
6✔
3775
            init_pipeline.temp_particle_bind_group_layout =
2✔
3776
                Some(particle_bind_group_layout.clone());
2✔
3777
            init_pipeline.temp_spawner_bind_group_layout = Some(spawner_bind_group_layout.clone());
4✔
3778
            init_pipeline.temp_metadata_bind_group_layout = Some(metadata_bind_group_layout);
4✔
3779
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
10✔
3780
                pipeline_cache.as_ref(),
4✔
3781
                &init_pipeline,
4✔
3782
                ParticleInitPipelineKey {
2✔
3783
                    shader: extracted_effect.effect_shaders.init.clone(),
6✔
3784
                    particle_layout_min_binding_size,
4✔
3785
                    parent_particle_layout_min_binding_size,
4✔
3786
                    flags: init_pipeline_key_flags,
4✔
3787
                    particle_bind_group_layout_id,
4✔
3788
                    spawner_bind_group_layout_id,
2✔
3789
                    metadata_bind_group_layout_id,
2✔
3790
                },
3791
            );
3792
            // keep things tidy; this is just a hack, should not persist
3793
            init_pipeline.temp_particle_bind_group_layout = None;
4✔
3794
            init_pipeline.temp_spawner_bind_group_layout = None;
4✔
3795
            init_pipeline.temp_metadata_bind_group_layout = None;
4✔
3796
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
4✔
3797

3798
            cached_pipelines.init = Some(init_pipeline_id);
2✔
3799
            init_pipeline_id
2✔
3800
        };
3801

3802
        // Resolve the update pipeline
3803
        let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
312✔
3804
        {
3805
            *update_pipeline_id
3806
        } else {
3807
            // Clear flag just in case, to ensure consistency.
3808
            cached_pipelines
2✔
3809
                .flags
2✔
3810
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
2✔
3811

3812
            let num_event_buffers = maybe_cached_parent_info
4✔
3813
                .as_ref()
3814
                .map(|p| p.children.len() as u32)
2✔
3815
                .unwrap_or_default();
3816

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

3824
            // Fetch the bind group layouts from the cache
3825
            let metadata_bind_group_layout = effect_cache
6✔
3826
                .metadata_update_bind_group_layout(num_event_buffers)
2✔
3827
                .unwrap()
3828
                .clone();
3829

3830
            // https://github.com/bevyengine/bevy/issues/17132
3831
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
6✔
3832
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
6✔
3833
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
6✔
3834
            update_pipeline.temp_particle_bind_group_layout = Some(particle_bind_group_layout);
4✔
3835
            update_pipeline.temp_spawner_bind_group_layout =
2✔
3836
                Some(spawner_bind_group_layout.clone());
2✔
3837
            update_pipeline.temp_metadata_bind_group_layout = Some(metadata_bind_group_layout);
4✔
3838
            let update_pipeline_id = specialized_update_pipelines.specialize(
8✔
3839
                pipeline_cache.as_ref(),
4✔
3840
                &update_pipeline,
4✔
3841
                ParticleUpdatePipelineKey {
2✔
3842
                    shader: extracted_effect.effect_shaders.update.clone(),
6✔
3843
                    particle_layout: particle_layout.clone(),
6✔
3844
                    parent_particle_layout_min_binding_size,
4✔
3845
                    num_event_buffers,
4✔
3846
                    particle_bind_group_layout_id,
4✔
3847
                    spawner_bind_group_layout_id,
2✔
3848
                    metadata_bind_group_layout_id,
2✔
3849
                },
3850
            );
3851
            // keep things tidy; this is just a hack, should not persist
3852
            update_pipeline.temp_particle_bind_group_layout = None;
4✔
3853
            update_pipeline.temp_spawner_bind_group_layout = None;
4✔
3854
            update_pipeline.temp_metadata_bind_group_layout = None;
4✔
3855
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
4✔
3856

3857
            cached_pipelines.update = Some(update_pipeline_id);
2✔
3858
            update_pipeline_id
2✔
3859
        };
3860

3861
        // Never batch an effect with a pipeline not available; this will prevent its
3862
        // init/update pass from running, but the vfx_indirect pass will run
3863
        // nonetheless, which causes desyncs and leads to bugs.
3864
        if pipeline_cache
3865
            .get_compute_pipeline(init_pipeline_id)
3866
            .is_none()
3867
        {
3868
            trace!(
2✔
3869
                "Skipping effect from render entity {:?} due to missing or not ready init pipeline (status: {:?})",
2✔
3870
                entity,
3871
                pipeline_cache.get_compute_pipeline_state(init_pipeline_id)
4✔
3872
            );
3873
            cached_pipelines
2✔
3874
                .flags
2✔
3875
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3876
            continue;
2✔
3877
        }
3878

3879
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3880
        // ready
3881
        cached_pipelines
3882
            .flags
3883
            .insert(CachedPipelineFlags::INIT_PIPELINE_READY);
3884
        trace!("[Effect {:?}] Init pipeline ready.", entity);
312✔
3885

3886
        // Never batch an effect with a pipeline not available; this will prevent its
3887
        // init/update pass from running, but the vfx_indirect pass will run
3888
        // nonetheless, which causes desyncs and leads to bugs.
3889
        if pipeline_cache
3890
            .get_compute_pipeline(update_pipeline_id)
3891
            .is_none()
3892
        {
3893
            trace!(
×
3894
                "Skipping effect from render entity {:?} due to missing or not ready update pipeline (status: {:?})",
×
3895
                entity,
3896
                pipeline_cache.get_compute_pipeline_state(update_pipeline_id)
×
3897
            );
3898
            cached_pipelines
×
3899
                .flags
×
3900
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
×
3901
            continue;
×
3902
        }
3903

3904
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3905
        // ready
3906
        cached_pipelines
3907
            .flags
3908
            .insert(CachedPipelineFlags::UPDATE_PIPELINE_READY);
3909
        trace!("[Effect {:?}] Update pipeline ready.", entity);
312✔
3910
    }
3911
}
3912

3913
pub fn prepare_indirect_pipeline(
330✔
3914
    event_cache: Res<EventCache>,
3915
    mut effects_meta: ResMut<EffectsMeta>,
3916
    pipeline_cache: Res<PipelineCache>,
3917
    indirect_pipeline: Res<DispatchIndirectPipeline>,
3918
    mut specialized_indirect_pipelines: ResMut<
3919
        SpecializedComputePipelines<DispatchIndirectPipeline>,
3920
    >,
3921
) {
3922
    // Ensure the 2 variants of the indirect pipelines are created.
3923
    // TODO - move that elsewhere in some one-time setup?
3924
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
333✔
3925
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
12✔
3926
            pipeline_cache.as_ref(),
6✔
3927
            &indirect_pipeline,
3✔
3928
            DispatchIndirectPipelineKey { has_events: false },
3✔
3929
        );
3930
    }
3931
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
333✔
3932
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
12✔
3933
            pipeline_cache.as_ref(),
6✔
3934
            &indirect_pipeline,
3✔
3935
            DispatchIndirectPipelineKey { has_events: true },
3✔
3936
        );
3937
    }
3938

3939
    // Select the active one depending on whether there's any child info to consume
3940
    let is_empty = event_cache.child_infos().is_empty();
990✔
3941
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
330✔
3942
        if is_empty {
6✔
3943
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
6✔
3944
        } else {
3945
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3946
        }
3947
    } else {
3948
        // If this is the first time we insert an event buffer, we need to switch the
3949
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3950
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3951
        // more effect using GPU spawn events, we can deallocate.
3952
        let was_empty =
327✔
3953
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
3954
        if was_empty && !is_empty {
327✔
3955
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3956
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3957
        } else if is_empty && !was_empty {
654✔
3958
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3959
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3960
        }
3961
    }
3962
}
3963

3964
// TEMP - Mark all cached effects as invalid for this frame until another system
3965
// explicitly marks them as valid. Otherwise we early out in some parts, and
3966
// reuse by mistake the previous frame's extraction.
3967
pub fn clear_transient_batch_inputs(
330✔
3968
    mut commands: Commands,
3969
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3970
) {
3971
    for entity in &mut q_cached_effects {
950✔
3972
        if let Ok(mut cmd) = commands.get_entity(entity) {
310✔
3973
            cmd.remove::<BatchInput>();
3974
        }
3975
    }
3976
}
3977

3978
/// Effect mesh extracted from the main world.
3979
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
3980
pub(crate) struct ExtractedEffectMesh {
3981
    /// Asset of the effect mesh to draw.
3982
    pub mesh: AssetId<Mesh>,
3983
}
3984

3985
/// Indexed mesh metadata for [`CachedMesh`].
3986
#[derive(Debug, Clone)]
3987
#[allow(dead_code)]
3988
pub(crate) struct MeshIndexSlice {
3989
    /// Index format.
3990
    pub format: IndexFormat,
3991
    /// GPU buffer containing the indices.
3992
    pub buffer: Buffer,
3993
    /// Range inside [`Self::buffer`] where the indices are.
3994
    pub range: Range<u32>,
3995
}
3996

3997
impl PartialEq for MeshIndexSlice {
3998
    fn eq(&self, other: &Self) -> bool {
312✔
3999
        self.format == other.format
312✔
4000
            && self.buffer.id() == other.buffer.id()
624✔
4001
            && self.range == other.range
312✔
4002
    }
4003
}
4004

4005
impl Eq for MeshIndexSlice {}
4006

4007
/// Cached info about a mesh location in a Bevy buffer. This information is
4008
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
4009
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
4010
/// so we can invalidate that GPU data.
4011
#[derive(Debug, Clone, PartialEq, Eq, Component)]
4012
pub(crate) struct CachedMeshLocation {
4013
    /// Vertex buffer.
4014
    pub vertex_buffer: BufferId,
4015
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
4016
    pub vertex_or_index_count: u32,
4017
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
4018
    pub first_index_or_vertex_offset: u32,
4019
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
4020
    pub vertex_offset_or_base_instance: i32,
4021
    /// Indexed rendering metadata.
4022
    pub indexed: Option<MeshIndexSlice>,
4023
}
4024

4025
bitflags! {
4026
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4027
    pub struct CachedPipelineFlags: u8 {
4028
        const NONE = 0;
4029
        /// The init pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
4030
        const INIT_PIPELINE_READY = (1u8 << 0);
4031
        /// The update pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
4032
        const UPDATE_PIPELINE_READY = (1u8 << 1);
4033
    }
4034
}
4035

4036
impl Default for CachedPipelineFlags {
4037
    fn default() -> Self {
2✔
4038
        Self::NONE
2✔
4039
    }
4040
}
4041

4042
/// Render world cached shader pipelines for a [`CachedEffect`].
4043
///
4044
/// This is updated with the IDs of the pipelines when they are queued for
4045
/// compiling, and with the state of those pipelines to detect when the effect
4046
/// is ready to be used.
4047
///
4048
/// This component is always auto-inserted alongside [`ExtractedEffect`] as soon
4049
/// as a new effect instance is spawned, because it contains the readiness state
4050
/// of those pipelines, which we want to query each frame. The pipelines are
4051
/// also mandatory, so this component is always needed.
4052
#[derive(Debug, Default, Component)]
4053
pub(crate) struct CachedPipelines {
4054
    /// Caching flags indicating the pipelines readiness.
4055
    pub flags: CachedPipelineFlags,
4056
    /// ID of the cached init pipeline. This is valid once the pipeline is
4057
    /// queued for compilation, but this doesn't mean the pipeline is ready for
4058
    /// use. Readiness is encoded in [`Self::flags`].
4059
    pub init: Option<CachedComputePipelineId>,
4060
    /// ID of the cached update pipeline. This is valid once the pipeline is
4061
    /// queued for compilation, but this doesn't mean the pipeline is ready for
4062
    /// use. Readiness is encoded in [`Self::flags`].
4063
    pub update: Option<CachedComputePipelineId>,
4064
}
4065

4066
impl CachedPipelines {
4067
    /// Check if all pipelines for this effect are ready.
4068
    #[inline]
4069
    pub fn is_ready(&self) -> bool {
628✔
4070
        self.flags.contains(
1,256✔
4071
            CachedPipelineFlags::INIT_PIPELINE_READY | CachedPipelineFlags::UPDATE_PIPELINE_READY,
628✔
4072
        )
4073
    }
4074
}
4075

4076
/// Ready state for this effect.
4077
///
4078
/// An effect is ready if:
4079
/// - Its init and update pipelines are ready, as reported by
4080
///   [`CachedPipelines::is_ready()`].
4081
///
4082
/// This components holds the calculated ready state propagated from all
4083
/// ancestor effects, if any. That propagation is done by the
4084
/// [`propagate_ready_state()`] system.
4085
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
4086
pub(crate) struct CachedReadyState {
4087
    is_ready: bool,
4088
}
4089

4090
impl CachedReadyState {
4091
    #[inline(always)]
4092
    pub fn new(is_ready: bool) -> Self {
314✔
4093
        Self { is_ready }
4094
    }
4095

4096
    #[inline(always)]
4097
    pub fn and(mut self, ancestors_ready: bool) -> Self {
×
4098
        self.and_with(ancestors_ready);
×
4099
        self
×
4100
    }
4101

4102
    #[inline(always)]
4103
    pub fn and_with(&mut self, ancestors_ready: bool) {
×
4104
        self.is_ready = self.is_ready && ancestors_ready;
×
4105
    }
4106

4107
    #[inline(always)]
4108
    pub fn is_ready(&self) -> bool {
628✔
4109
        self.is_ready
628✔
4110
    }
4111
}
4112

4113
#[derive(SystemParam)]
4114
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
4115
    sim_params: Res<'w, SimParams>,
4116
    render_device: Res<'w, RenderDevice>,
4117
    render_queue: Res<'w, RenderQueue>,
4118
    marker: PhantomData<&'s usize>,
4119
}
4120

4121
/// Update the ready state of all effects, and propagate recursively to
4122
/// children.
4123
pub(crate) fn propagate_ready_state(
330✔
4124
    mut q_root_effects: Query<
4125
        (
4126
            Entity,
4127
            Option<&ChildrenEffects>,
4128
            Ref<CachedPipelines>,
4129
            &mut CachedReadyState,
4130
        ),
4131
        Without<ChildEffectOf>,
4132
    >,
4133
    mut orphaned: RemovedComponents<ChildEffectOf>,
4134
    q_ready_state: Query<
4135
        (
4136
            Ref<CachedPipelines>,
4137
            &mut CachedReadyState,
4138
            Option<&ChildrenEffects>,
4139
        ),
4140
        With<ChildEffectOf>,
4141
    >,
4142
    q_child_effects: Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4143
    mut orphaned_entities: Local<Vec<Entity>>,
4144
) {
4145
    #[cfg(feature = "trace")]
4146
    let _span = bevy::log::info_span!("propagate_ready_state").entered();
990✔
4147
    trace!("propagate_ready_state");
650✔
4148

4149
    // Update orphaned list for this frame, and sort it so we can efficiently binary
4150
    // search it
4151
    orphaned_entities.clear();
330✔
4152
    orphaned_entities.extend(orphaned.read());
990✔
4153
    orphaned_entities.sort_unstable();
330✔
4154

4155
    // Iterate in parallel over all root effects (those without any parent). This is
4156
    // the most common case, so should take care of the heavy lifting of propagating
4157
    // to most effects. For child effects, we then descend recursively.
4158
    q_root_effects.par_iter_mut().for_each(
990✔
4159
        |(entity, maybe_children, cached_pipelines, mut cached_ready_state)| {
314✔
4160
            // Update the ready state of this root effect
4161
            let changed = cached_pipelines.is_changed() || cached_ready_state.is_added() || orphaned_entities.binary_search(&entity).is_ok();
942✔
4162
            trace!("[Entity {}] changed={} cached_pipelines={} ready_state={}", entity, changed, cached_pipelines.is_ready(), cached_ready_state.is_ready);
1,256✔
4163
            if changed {
314✔
4164
                // Root effects by default are ready since they have no ancestors to check. After that we check the ready conditions for this effect alone.
4165
                let new_ready_state = CachedReadyState::new(cached_pipelines.is_ready());
942✔
4166
                if *cached_ready_state != new_ready_state {
314✔
4167
                    debug!(
2✔
4168
                        "[Entity {}] Changed ready to: {}",
2✔
4169
                        entity,
4170
                        new_ready_state.is_ready()
4✔
4171
                    );
4172
                    *cached_ready_state = new_ready_state;
2✔
4173
                }
4174
            }
4175

4176
            // Recursively update the ready state of its descendants
4177
            if let Some(children) = maybe_children {
314✔
4178
                for (child, child_of) in q_child_effects.iter_many(children) {
×
4179
                    assert_eq!(
×
4180
                        child_of.parent, entity,
×
4181
                        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
×
4182
                    );
4183
                    // SAFETY:
4184
                    // - `child` must have consistent parentage, or the above assertion would panic.
4185
                    //   Since `child` is parented to a root entity, the entire hierarchy leading to it
4186
                    //   is consistent.
4187
                    // - We may operate as if all descendants are consistent, since
4188
                    //   `propagate_ready_state_recursive` will panic before continuing to propagate if it
4189
                    //   encounters an entity with inconsistent parentage.
4190
                    // - Since each root entity is unique and the hierarchy is consistent and
4191
                    //   forest-like, other root entities' `propagate_ready_state_recursive` calls will not conflict
4192
                    //   with this one.
4193
                    // - Since this is the only place where `transform_query` gets used, there will be
4194
                    //   no conflicting fetches elsewhere.
4195
                    #[expect(unsafe_code, reason = "`propagate_ready_state_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
4196
                    unsafe {
4197
                        propagate_ready_state_recursive(
×
4198
                            &cached_ready_state,
4199
                            &q_ready_state,
4200
                            &q_child_effects,
4201
                            child,
4202
                            changed || child_of.is_changed(),
×
4203
                        );
4204
                    }
4205
                }
4206
            }
4207
        },
4208
    );
4209
}
4210

4211
#[expect(
4212
    unsafe_code,
4213
    reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
4214
)]
4215
unsafe fn propagate_ready_state_recursive(
×
4216
    parent_state: &CachedReadyState,
4217
    q_ready_state: &Query<
4218
        (
4219
            Ref<CachedPipelines>,
4220
            &mut CachedReadyState,
4221
            Option<&ChildrenEffects>,
4222
        ),
4223
        With<ChildEffectOf>,
4224
    >,
4225
    q_child_of: &Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4226
    entity: Entity,
4227
    mut changed: bool,
4228
) {
4229
    // Update this effect in-place by checking its own state and the state of its
4230
    // parent (which has already been propagated from all the parent's ancestors, so
4231
    // is correct for this frame).
4232
    let (cached_ready_state, maybe_children) = {
×
4233
        let Ok((cached_pipelines, mut cached_ready_state, maybe_children)) =
4234
        // SAFETY: Copied from Bevy's transform propagation, same reasoning
4235
        (unsafe { q_ready_state.get_unchecked(entity) }) else {
×
4236
            return;
×
4237
        };
4238

4239
        changed |= cached_pipelines.is_changed() || cached_ready_state.is_added();
×
4240
        if changed {
4241
            let new_ready_state =
×
4242
                CachedReadyState::new(parent_state.is_ready()).and(cached_pipelines.is_ready());
×
4243
            // Ensure we don't trigger ECS change detection here if state didn't change, so
4244
            // we can avoid this effect branch on next iteration.
4245
            if *cached_ready_state != new_ready_state {
×
4246
                debug!(
×
4247
                    "[Entity {}] Changed ready to: {}",
×
4248
                    entity,
4249
                    new_ready_state.is_ready()
×
4250
                );
4251
                *cached_ready_state = new_ready_state;
×
4252
            }
4253
        }
4254
        (cached_ready_state, maybe_children)
4255
    };
4256

4257
    // Recurse into descendants
4258
    let Some(children) = maybe_children else {
×
4259
        return;
×
4260
    };
4261
    for (child, child_of) in q_child_of.iter_many(children) {
×
4262
        assert_eq!(
×
4263
        child_of.parent, entity,
×
4264
        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
×
4265
    );
4266
        // SAFETY: The caller guarantees that `transform_query` will not be fetched for
4267
        // any descendants of `entity`, so it is safe to call
4268
        // `propagate_recursive` for each child.
4269
        //
4270
        // The above assertion ensures that each child has one and only one unique
4271
        // parent throughout the entire hierarchy.
4272
        unsafe {
4273
            propagate_ready_state_recursive(
4274
                cached_ready_state.as_ref(),
4275
                q_ready_state,
4276
                q_child_of,
4277
                child,
4278
                changed || child_of.is_changed(),
×
4279
            );
4280
        }
4281
    }
4282
}
4283

4284
/// Once all effects are extracted and all cached components are updated, it's
4285
/// time to prepare for sorting and batching. Collect all relevant data and
4286
/// insert/update the [`BatchInput`] for each effect.
4287
pub(crate) fn prepare_batch_inputs(
330✔
4288
    mut commands: Commands,
4289
    read_only_params: PrepareEffectsReadOnlyParams,
4290
    pipeline_cache: Res<PipelineCache>,
4291
    mut effects_meta: ResMut<EffectsMeta>,
4292
    mut effect_bind_groups: ResMut<EffectBindGroups>,
4293
    mut property_bind_groups: ResMut<PropertyBindGroups>,
4294
    q_cached_effects: Query<(
4295
        MainEntity,
4296
        Entity,
4297
        &ExtractedEffect,
4298
        &ExtractedSpawner,
4299
        &CachedEffect,
4300
        &CachedEffectMetadata,
4301
        &CachedReadyState,
4302
        &CachedPipelines,
4303
        Option<&CachedDrawIndirectArgs>,
4304
        Option<&CachedParentInfo>,
4305
        Option<&ChildEffectOf>,
4306
        Option<&CachedChildInfo>,
4307
        Option<&CachedEffectEvents>,
4308
    )>,
4309
    mut sort_bind_groups: ResMut<SortBindGroups>,
4310
) {
4311
    #[cfg(feature = "trace")]
4312
    let _span = bevy::log::info_span!("prepare_batch_inputs").entered();
990✔
4313
    trace!("prepare_batch_inputs");
650✔
4314

4315
    // Workaround for too many params in system (TODO: refactor to split work?)
4316
    let sim_params = read_only_params.sim_params.into_inner();
990✔
4317
    let render_device = read_only_params.render_device.into_inner();
990✔
4318
    let render_queue = read_only_params.render_queue.into_inner();
990✔
4319

4320
    // Clear per-instance buffers, which are filled below and re-uploaded each frame
4321
    effects_meta.spawner_buffer.clear();
660✔
4322

4323
    // Build batcher inputs from extracted effects, updating all cached components
4324
    // for each effect on the fly.
4325
    let mut extracted_effect_count = 0;
660✔
4326
    let mut prepared_effect_count = 0;
660✔
4327
    for (
4328
        main_entity,
314✔
4329
        render_entity,
314✔
4330
        extracted_effect,
314✔
4331
        extracted_spawner,
314✔
4332
        cached_effect,
314✔
4333
        cached_effect_metadata,
314✔
4334
        cached_ready_state,
314✔
4335
        cached_pipelines,
314✔
4336
        maybe_cached_draw_indirect_args,
314✔
4337
        maybe_cached_parent_info,
314✔
4338
        maybe_child_effect_of,
314✔
4339
        maybe_cached_child_info,
314✔
4340
        maybe_cached_effect_events,
314✔
4341
    ) in &q_cached_effects
644✔
4342
    {
4343
        extracted_effect_count += 1;
314✔
4344

4345
        // Skip this effect if not ready
4346
        if !cached_ready_state.is_ready() {
314✔
4347
            trace!("Pipelines not ready for effect {}, skipped.", render_entity);
4✔
4348
            continue;
4349
        }
4350

4351
        // Skip this effect if not visible and not simulating when hidden
4352
        if !extracted_spawner.is_visible
312✔
4353
            && (extracted_effect.simulation_condition == SimulationCondition::WhenVisible)
×
4354
        {
4355
            trace!(
×
4356
                "Effect {} not visible, and simulation condition is WhenVisible, so skipped.",
×
4357
                render_entity
4358
            );
4359
            continue;
×
4360
        }
4361

4362
        // Fetch the init and update pipelines.
4363
        // SAFETY: If is_ready() returns true, this means the pipelines are cached and
4364
        // ready, so the IDs must be valid.
4365
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
4366
            init: cached_pipelines.init.unwrap(),
4367
            update: cached_pipelines.update.unwrap(),
4368
        };
4369

4370
        let effect_slice = EffectSlice {
4371
            slice: cached_effect.slice.range(),
4372
            slab_id: cached_effect.slab_id,
4373
            particle_layout: cached_effect.slice.particle_layout.clone(),
4374
        };
4375

4376
        // Fetch the bind group layouts from the cache
4377
        trace!("child_effect_of={:?}", maybe_child_effect_of);
312✔
4378
        let parent_slab_id = if let Some(child_effect_of) = maybe_child_effect_of {
312✔
4379
            let Ok((_, _, _, _, parent_cached_effect, _, _, _, _, _, _, _, _)) =
×
4380
                q_cached_effects.get(child_effect_of.parent)
4381
            else {
4382
                // At this point we should have discarded invalid effects with a missing parent,
4383
                // so if the parent is not found this is a bug.
4384
                error!(
×
4385
                    "Effect main_entity {:?}: parent render entity {:?} not found.",
×
4386
                    main_entity, child_effect_of.parent
4387
                );
4388
                continue;
×
4389
            };
4390
            Some(parent_cached_effect.slab_id)
4391
        } else {
4392
            None
312✔
4393
        };
4394

4395
        // For ribbons, we need the sorting pipeline to be ready to sort the ribbon's
4396
        // particles by age in order to build a contiguous mesh.
4397
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4398
            // Ensure the bind group layout for sort-fill is ready. This will also ensure
4399
            // the pipeline is created and queued if needed.
4400
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
4401
                &pipeline_cache,
×
4402
                &extracted_effect.particle_layout,
×
4403
            ) {
4404
                error!(
4405
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
4406
                    err
4407
                );
4408
                continue;
4409
            }
4410

4411
            // Check sort pipelines are ready, otherwise we might desync some buffers if
4412
            // running only some of them but not all.
4413
            if !sort_bind_groups
×
4414
                .is_pipeline_ready(&extracted_effect.particle_layout, &pipeline_cache)
×
4415
            {
4416
                trace!(
×
4417
                    "Sort pipeline not ready for effect on main entity {:?}; skipped.",
×
4418
                    main_entity
4419
                );
4420
                continue;
4421
            }
4422
        }
4423

4424
        // Output some debug info
4425
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
624✔
4426
        trace!(
4427
            "update_shader = {:?}",
312✔
4428
            extracted_effect.effect_shaders.update
4429
        );
4430
        trace!(
4431
            "render_shader = {:?}",
312✔
4432
            extracted_effect.effect_shaders.render
4433
        );
4434
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
312✔
4435
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
312✔
4436

4437
        let parent_slab_offset = maybe_cached_child_info.map(|cci| cci.parent_slab_offset);
4438

4439
        assert!(cached_effect_metadata.table_id.is_valid());
4440
        let spawner_index = effects_meta.allocate_spawner(
312✔
4441
            &extracted_spawner.transform,
4442
            extracted_spawner.spawn_count,
4443
            extracted_spawner.prng_seed,
4444
            cached_effect.slice.range().start,
4445
            parent_slab_offset,
4446
            cached_effect_metadata.table_id,
4447
            maybe_cached_draw_indirect_args,
4448
        );
4449

4450
        trace!("Updating cached effect at entity {render_entity:?}...");
312✔
4451
        let mut cmd = commands.entity(render_entity);
4452
        // Inserting the BatchInput component marks the effect as ready for this frame
4453
        cmd.insert(BatchInput {
4454
            effect_slice,
4455
            init_and_update_pipeline_ids,
4456
            parent_slab_id,
4457
            event_buffer_index: maybe_cached_effect_events.map(|cee| cee.buffer_index),
4458
            child_effects: maybe_cached_parent_info
4459
                .as_ref()
4460
                .map(|cp| cp.children.clone())
×
4461
                .unwrap_or_default(),
4462
            spawner_index,
4463
            init_indirect_dispatch_index: maybe_cached_child_info
4464
                .as_ref()
4465
                .map(|cc| cc.init_indirect_dispatch_index),
4466
        });
4467

4468
        prepared_effect_count += 1;
4469
    }
4470
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
650✔
4471

4472
    // Update simulation parameters, including the total effect count for this frame
4473
    {
4474
        let mut gpu_sim_params: GpuSimParams = sim_params.into();
4475
        gpu_sim_params.num_effects = prepared_effect_count;
4476
        trace!(
4477
            "Simulation parameters: time={} delta_time={} virtual_time={} \
320✔
4478
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
320✔
4479
            gpu_sim_params.time,
4480
            gpu_sim_params.delta_time,
4481
            gpu_sim_params.virtual_time,
4482
            gpu_sim_params.virtual_delta_time,
4483
            gpu_sim_params.real_time,
4484
            gpu_sim_params.real_delta_time,
4485
            gpu_sim_params.num_effects,
4486
        );
4487
        effects_meta.sim_params_uniforms.set(gpu_sim_params);
4488
    }
4489

4490
    // Write the entire spawner buffer for this frame, for all effects combined
4491
    assert_eq!(
4492
        prepared_effect_count,
4493
        effects_meta.spawner_buffer.len() as u32
4494
    );
4495
    if effects_meta
330✔
4496
        .spawner_buffer
330✔
4497
        .write_buffer(render_device, render_queue)
990✔
4498
    {
4499
        // All property bind groups use the spawner buffer, which was reallocate
4500
        effect_bind_groups.particle_slabs.clear();
6✔
4501
        property_bind_groups.clear(true);
4✔
4502
        effects_meta.indirect_spawner_bind_group = None;
2✔
4503
    }
4504
}
4505

4506
/// Batch compatible effects together into a single pass.
4507
///
4508
/// For all effects marked as ready for this frame (have a BatchInput
4509
/// component), sort the effects by grouping compatible effects together, then
4510
/// batch those groups together. Each batch can be updated and rendered with a
4511
/// single compute dispatch or draw call.
4512
pub(crate) fn batch_effects(
330✔
4513
    mut commands: Commands,
4514
    effects_meta: Res<EffectsMeta>,
4515
    mut sort_bind_groups: ResMut<SortBindGroups>,
4516
    mut q_cached_effects: Query<(
4517
        Entity,
4518
        &MainEntity,
4519
        &ExtractedEffect,
4520
        &ExtractedSpawner,
4521
        &ExtractedEffectMesh,
4522
        &CachedDrawIndirectArgs,
4523
        &CachedEffectMetadata,
4524
        Option<&CachedEffectEvents>,
4525
        Option<&ChildEffectOf>,
4526
        Option<&CachedChildInfo>,
4527
        Option<&CachedEffectProperties>,
4528
        &mut DispatchBufferIndices,
4529
        // The presence of BatchInput ensure the effect is ready
4530
        &mut BatchInput,
4531
    )>,
4532
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4533
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4534
) {
4535
    #[cfg(feature = "trace")]
4536
    let _span = bevy::log::info_span!("batch_effects").entered();
990✔
4537
    trace!("batch_effects");
650✔
4538

4539
    // Sort effects in batching order, so that we can batch by simply doing a linear
4540
    // scan of the effects in this order. Currently compatible effects mean:
4541
    // - same effect slab (so we can bind the buffers once for all batched effects)
4542
    // - in order of increasing sub-allocation inside those buffers (to make the
4543
    //   sort stable)
4544
    // - with parents before their children, to ensure ???? FIXME don't we need to
4545
    //   opposite?!!!
4546
    let mut effect_sorter = EffectSorter::new();
660✔
4547
    for (entity, _, _, _, _, _, _, _, child_of, _, _, _, input) in &q_cached_effects {
954✔
4548
        effect_sorter.insert(
4549
            entity,
4550
            input.effect_slice.slab_id,
4551
            input.effect_slice.slice.start,
4552
            child_of.map(|co| co.parent),
4553
        );
4554
    }
4555
    effect_sorter.sort();
660✔
4556

4557
    // For now we re-create that buffer each frame. Since there's no CPU -> GPU
4558
    // transfer, this is pretty cheap in practice.
4559
    sort_bind_groups.clear_indirect_dispatch_buffer();
330✔
4560

4561
    let mut sort_queue = GpuBufferOperationQueue::new();
660✔
4562

4563
    // Loop on all extracted effects in sorted order, and try to batch them together
4564
    // to reduce draw calls. -- currently does nothing, batching was broken and
4565
    // never fixed, but at least we minimize the GPU state changes with the sorting!
4566
    trace!("Batching {} effects...", q_cached_effects.iter().len());
1,290✔
4567
    sorted_effect_batches.clear();
330✔
4568
    for entity in effect_sorter.effects.iter().map(|e| e.entity) {
972✔
4569
        let Ok((
4570
            entity,
312✔
4571
            main_entity,
4572
            extracted_effect,
4573
            extracted_spawner,
4574
            extracted_effect_mesh,
4575
            cached_draw_indirect_args,
4576
            cached_effect_metadata,
4577
            cached_effect_events,
4578
            _,
4579
            cached_child_info,
4580
            cached_properties,
4581
            dispatch_buffer_indices,
4582
            mut input,
4583
        )) = q_cached_effects.get_mut(entity)
624✔
4584
        else {
4585
            continue;
×
4586
        };
4587

4588
        let translation = extracted_spawner.transform.translation();
4589

4590
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4591
        // most of the data needed to drive rendering. However this doesn't drive
4592
        // rendering; this is just storage.
4593
        let mut effect_batch = EffectBatch::from_input(
4594
            main_entity.id(),
4595
            extracted_effect,
4596
            extracted_spawner,
4597
            extracted_effect_mesh,
4598
            cached_effect_events,
4599
            cached_child_info,
4600
            &mut input,
4601
            *dispatch_buffer_indices,
4602
            cached_draw_indirect_args.row,
4603
            cached_effect_metadata.table_id,
4604
            cached_properties.map(|cp| PropertyBindGroupKey {
4605
                buffer_index: cp.buffer_index,
13✔
4606
                binding_size: cp.property_layout.min_binding_size().get() as u32,
26✔
4607
            }),
4608
            cached_properties.map(|cp| cp.range.start),
4609
        );
4610

4611
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4612
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4613
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4614
        // ribbon).
4615
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4616
            // This buffer is allocated in prepare_effects(), so should always be available
4617
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4618
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4619
                continue;
×
4620
            };
4621

4622
            // Allocate a GpuDispatchIndirect entry
4623
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4624
            effect_batch.sort_fill_indirect_dispatch_index =
4625
                Some(sort_fill_indirect_dispatch_index);
4626

4627
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4628
            // compute a number of workgroups to dispatch based on that particle count, and
4629
            // store the result into a GpuDispatchIndirect struct which will be used to
4630
            // dispatch the fill-sort pass.
4631
            {
4632
                let src_buffer = effect_metadata_buffer.clone();
4633
                let src_binding_offset = effects_meta
4634
                    .effect_metadata_buffer
4635
                    .dynamic_offset(effect_batch.metadata_table_id);
4636
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4637
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4638
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4639
                    continue;
×
4640
                };
4641
                let dst_buffer = dst_buffer.clone();
4642
                let dst_binding_offset = 0; // see dst_offset below
4643
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4644
                trace!(
4645
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4646
                    src_buffer.id(),
×
4647
                    src_binding_offset,
4648
                    src_binding_size.get(),
×
4649
                    dst_buffer.id(),
×
4650
                    dst_binding_offset,
4651
                    -1, //dst_binding_size.get(),
4652
                );
4653
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4654
                debug_assert_eq!(
4655
                    src_offset, 1,
4656
                    "GpuEffectMetadata changed, update this assert."
×
4657
                );
4658
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4659
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4660
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4661
                // binding offset of 0 and binds the entire destination buffer,
4662
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4663
                // index the proper offset in the buffer. This requires of
4664
                // course binding the entire buffer, or at least enough to index all operations
4665
                // (hence the None below). This is not really a general solution, so should be
4666
                // reviewed.
4667
                let dst_offset = sort_bind_groups
×
4668
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
4669
                    / 4;
4670
                sort_queue.enqueue(
4671
                    GpuBufferOperationType::FillDispatchArgs,
4672
                    GpuBufferOperationArgs {
4673
                        src_offset,
4674
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
4675
                        dst_offset,
4676
                        dst_stride: GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4,
4677
                        count: 1,
4678
                    },
4679
                    src_buffer,
4680
                    src_binding_offset,
4681
                    Some(src_binding_size),
4682
                    dst_buffer,
4683
                    dst_binding_offset,
4684
                    None, //Some(dst_binding_size),
4685
                );
4686
            }
4687
        }
4688

4689
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
312✔
4690
        trace!(
4691
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
312✔
4692
            effect_batch_index,
4693
            entity,
4694
        );
4695

4696
        // Spawn an EffectDrawBatch, to actually drive rendering.
4697
        commands
4698
            .spawn(EffectDrawBatch {
4699
                effect_batch_index,
4700
                translation,
4701
                main_entity: *main_entity,
4702
            })
4703
            .insert(TemporaryRenderEntity);
4704
    }
4705

4706
    gpu_buffer_operations.begin_frame();
330✔
4707
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
4708
    if !sort_queue.operation_queue.is_empty() {
330✔
4709
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4710
    }
4711
}
4712

4713
/// Per-buffer bind groups for a GPU effect buffer.
4714
///
4715
/// This contains all bind groups specific to a single [`EffectBuffer`].
4716
///
4717
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4718
pub(crate) struct BufferBindGroups {
4719
    /// Bind group for the render shader.
4720
    ///
4721
    /// ```wgsl
4722
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4723
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4724
    /// @binding(2) var<storage, read> spawner : Spawner;
4725
    /// ```
4726
    render: BindGroup,
4727
    // /// Bind group for filling the indirect dispatch arguments of any child init
4728
    // /// pass.
4729
    // ///
4730
    // /// This bind group is optional; it's only created if the current effect has
4731
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4732
    // /// (although normally the event buffer is not created if there's no
4733
    // /// children).
4734
    // ///
4735
    // /// The source buffer is always the current effect's event buffer. The
4736
    // /// destination buffer is the global shared buffer for indirect fill args
4737
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4738
    // /// args contains the data to index the relevant part of the global shared
4739
    // /// buffer for this effect buffer; it may contain multiple entries in case
4740
    // /// multiple effects are batched inside the current effect buffer.
4741
    // ///
4742
    // /// ```wgsl
4743
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4744
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4745
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4746
    // /// ```
4747
    // init_fill_dispatch: Option<BindGroup>,
4748
}
4749

4750
/// Combination of a texture layout and the bound textures.
4751
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4752
struct Material {
4753
    layout: TextureLayout,
4754
    textures: Vec<AssetId<Image>>,
4755
}
4756

4757
impl Material {
4758
    /// Get the bind group entries to create a bind group.
4759
    pub fn make_entries<'a>(
×
4760
        &self,
4761
        gpu_images: &'a RenderAssets<GpuImage>,
4762
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4763
        if self.textures.is_empty() {
×
4764
            return Ok(vec![]);
×
4765
        }
4766

4767
        let entries: Vec<BindGroupEntry<'a>> = self
×
4768
            .textures
×
4769
            .iter()
4770
            .enumerate()
4771
            .flat_map(|(index, id)| {
×
4772
                let base_binding = index as u32 * 2;
×
4773
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4774
                    vec![
×
4775
                        BindGroupEntry {
×
4776
                            binding: base_binding,
×
4777
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4778
                        },
4779
                        BindGroupEntry {
×
4780
                            binding: base_binding + 1,
×
4781
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4782
                        },
4783
                    ]
4784
                } else {
4785
                    vec![]
×
4786
                }
4787
            })
4788
            .collect();
4789
        if entries.len() == self.textures.len() * 2 {
×
4790
            return Ok(entries);
×
4791
        }
4792
        Err(())
×
4793
    }
4794
}
4795

4796
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4797
struct BindingKey {
4798
    pub buffer_id: BufferId,
4799
    pub offset: u32,
4800
    pub size: NonZeroU32,
4801
}
4802

4803
impl<'a> From<BufferSlice<'a>> for BindingKey {
4804
    fn from(value: BufferSlice<'a>) -> Self {
×
4805
        Self {
4806
            buffer_id: value.buffer.id(),
×
4807
            offset: value.offset,
×
4808
            size: value.size,
×
4809
        }
4810
    }
4811
}
4812

4813
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4814
    fn from(value: &BufferSlice<'a>) -> Self {
×
4815
        Self {
4816
            buffer_id: value.buffer.id(),
×
4817
            offset: value.offset,
×
4818
            size: value.size,
×
4819
        }
4820
    }
4821
}
4822

4823
impl From<&BufferBindingSource> for BindingKey {
4824
    fn from(value: &BufferBindingSource) -> Self {
×
4825
        Self {
4826
            buffer_id: value.buffer.id(),
×
4827
            offset: value.offset,
×
4828
            size: value.size,
×
4829
        }
4830
    }
4831
}
4832

4833
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4834
struct ConsumeEventKey {
4835
    child_infos_buffer_id: BufferId,
4836
    events: BindingKey,
4837
}
4838

4839
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4840
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4841
        Self {
4842
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4843
            events: value.events.into(),
×
4844
        }
4845
    }
4846
}
4847

4848
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4849
struct InitMetadataBindGroupKey {
4850
    pub slab_id: SlabId,
4851
    pub effect_metadata_buffer: BufferId,
4852
    pub consume_event_key: Option<ConsumeEventKey>,
4853
}
4854

4855
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4856
struct UpdateMetadataBindGroupKey {
4857
    pub slab_id: SlabId,
4858
    pub effect_metadata_buffer: BufferId,
4859
    pub child_info_buffer_id: Option<BufferId>,
4860
    pub event_buffers_keys: Vec<BindingKey>,
4861
}
4862

4863
/// Bind group cached with an associated key.
4864
///
4865
/// The cached bind group is associated with the given key representing the
4866
/// inputs that the bind group depends on. When those inputs change, the key
4867
/// should change, indicating the bind group needs to be recreated.
4868
///
4869
/// This object manages a single bind group and its key.
4870
struct CachedBindGroup<K: Eq> {
4871
    /// Key the bind group was created from. Each time the key changes, the bind
4872
    /// group should be re-created.
4873
    key: K,
4874
    /// Bind group created from the key.
4875
    bind_group: BindGroup,
4876
}
4877

4878
#[derive(Debug, Clone, Copy)]
4879
struct BufferSlice<'a> {
4880
    pub buffer: &'a Buffer,
4881
    pub offset: u32,
4882
    pub size: NonZeroU32,
4883
}
4884

4885
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4886
    fn from(value: BufferSlice<'a>) -> Self {
×
4887
        Self {
4888
            buffer: value.buffer,
×
4889
            offset: value.offset.into(),
×
4890
            size: Some(value.size.into()),
×
4891
        }
4892
    }
4893
}
4894

4895
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4896
    fn from(value: &BufferSlice<'a>) -> Self {
×
4897
        Self {
4898
            buffer: value.buffer,
×
4899
            offset: value.offset.into(),
×
4900
            size: Some(value.size.into()),
×
4901
        }
4902
    }
4903
}
4904

4905
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4906
    fn from(value: &'a BufferBindingSource) -> Self {
×
4907
        Self {
4908
            buffer: &value.buffer,
×
4909
            offset: value.offset,
×
4910
            size: value.size,
×
4911
        }
4912
    }
4913
}
4914

4915
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4916
/// the init pass consumes GPU events as a mechanism to spawn particles.
4917
struct ConsumeEventBuffers<'a> {
4918
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4919
    /// This is dynamically indexed inside the shader.
4920
    child_infos_buffer: &'a Buffer,
4921
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4922
    events: BufferSlice<'a>,
4923
}
4924

4925
#[derive(Default, Resource)]
4926
pub struct EffectBindGroups {
4927
    /// Map from a slab ID to the bind groups shared among all effects that
4928
    /// use that particle slab.
4929
    particle_slabs: HashMap<SlabId, BufferBindGroups>,
4930
    /// Map of bind groups for image assets used as particle textures.
4931
    images: HashMap<AssetId<Image>, BindGroup>,
4932
    /// Map from buffer index to its metadata bind group (group 3) for the init
4933
    /// pass.
4934
    // FIXME - doesn't work with batching; this should be the instance ID
4935
    init_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<InitMetadataBindGroupKey>>,
4936
    /// Map from buffer index to its metadata bind group (group 3) for the
4937
    /// update pass.
4938
    // FIXME - doesn't work with batching; this should be the instance ID
4939
    update_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4940
    /// Map from an effect material to its bind group.
4941
    material_bind_groups: HashMap<Material, BindGroup>,
4942
}
4943

4944
impl EffectBindGroups {
4945
    pub fn particle_render(&self, slab_id: &SlabId) -> Option<&BindGroup> {
311✔
4946
        self.particle_slabs.get(slab_id).map(|bg| &bg.render)
1,244✔
4947
    }
4948

4949
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4950
    /// needed.
4951
    pub(self) fn get_or_create_init_metadata(
312✔
4952
        &mut self,
4953
        effect_batch: &EffectBatch,
4954
        render_device: &RenderDevice,
4955
        layout: &BindGroupLayout,
4956
        effect_metadata_buffer: &Buffer,
4957
        consume_event_buffers: Option<ConsumeEventBuffers>,
4958
    ) -> Result<&BindGroup, ()> {
4959
        assert!(effect_batch.metadata_table_id.is_valid());
936✔
4960

4961
        let key = InitMetadataBindGroupKey {
4962
            slab_id: effect_batch.slab_id,
624✔
4963
            effect_metadata_buffer: effect_metadata_buffer.id(),
936✔
4964
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
624✔
4965
        };
4966

4967
        let make_entry = || {
314✔
4968
            let mut entries = Vec::with_capacity(3);
4✔
4969
            entries.push(
4✔
4970
                // @group(3) @binding(0) var<storage, read_write> effect_metadatas :
4971
                // array<EffectMetadata>;
4972
                BindGroupEntry {
2✔
4973
                    binding: 0,
2✔
4974
                    resource: effect_metadata_buffer.as_entire_binding(),
2✔
4975
                },
4976
            );
4977
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
4978
                entries.push(
4979
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4980
                    // ChildInfoBuffer;
4981
                    BindGroupEntry {
4982
                        binding: 1,
4983
                        resource: BindingResource::Buffer(BufferBinding {
4984
                            buffer: consume_event_buffers.child_infos_buffer,
4985
                            offset: 0,
4986
                            size: None,
4987
                        }),
4988
                    },
4989
                );
4990
                entries.push(
4991
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4992
                    BindGroupEntry {
4993
                        binding: 2,
4994
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4995
                    },
4996
                );
4997
            }
4998

4999
            let bind_group = render_device.create_bind_group(
6✔
5000
                "hanabi:bind_group:init:metadata@3",
5001
                layout,
2✔
5002
                &entries[..],
2✔
5003
            );
5004

5005
            trace!(
2✔
5006
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
5007
                    effect_batch.slab_id.index(),
4✔
5008
                    effect_batch.metadata_table_id.0,
5009
                );
5010

5011
            bind_group
2✔
5012
        };
5013

5014
        Ok(&self
312✔
5015
            .init_metadata_bind_groups
312✔
5016
            .entry(effect_batch.slab_id)
624✔
5017
            .and_modify(|cbg| {
622✔
5018
                if cbg.key != key {
310✔
5019
                    trace!(
×
5020
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
5021
                        cbg.key,
5022
                        key
5023
                    );
5024
                    cbg.key = key;
×
5025
                    cbg.bind_group = make_entry();
×
5026
                }
5027
            })
5028
            .or_insert_with(|| {
314✔
5029
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
5030
                CachedBindGroup {
2✔
5031
                    key,
2✔
5032
                    bind_group: make_entry(),
2✔
5033
                }
5034
            })
5035
            .bind_group)
5036
    }
5037

5038
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
5039
    /// needed.
5040
    pub(self) fn get_or_create_update_metadata(
312✔
5041
        &mut self,
5042
        effect_batch: &EffectBatch,
5043
        render_device: &RenderDevice,
5044
        layout: &BindGroupLayout,
5045
        effect_metadata_buffer: &Buffer,
5046
        child_info_buffer: Option<&Buffer>,
5047
        event_buffers: &[(Entity, BufferBindingSource)],
5048
    ) -> Result<&BindGroup, ()> {
5049
        assert!(effect_batch.metadata_table_id.is_valid());
936✔
5050

5051
        // Check arguments consistency
5052
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
1,560✔
5053
        let emits_gpu_spawn_events = !event_buffers.is_empty();
624✔
5054
        let child_info_buffer_id = if emits_gpu_spawn_events {
624✔
5055
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
5056
        } else {
5057
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
5058
            // if relevant, that is if the effect emits GPU spawn events.
5059
            None
312✔
5060
        };
5061
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
936✔
5062

5063
        let event_buffers_keys = event_buffers
624✔
5064
            .iter()
5065
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
312✔
5066
            .collect::<Vec<_>>();
5067

5068
        let key = UpdateMetadataBindGroupKey {
5069
            slab_id: effect_batch.slab_id,
624✔
5070
            effect_metadata_buffer: effect_metadata_buffer.id(),
936✔
5071
            child_info_buffer_id,
5072
            event_buffers_keys,
5073
        };
5074

5075
        let make_entry = || {
314✔
5076
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
6✔
5077
            // @group(3) @binding(0) var<storage, read_write> effect_metadatas :
5078
            // array<EffectMetadata>;
5079
            entries.push(BindGroupEntry {
6✔
5080
                binding: 0,
2✔
5081
                resource: effect_metadata_buffer.as_entire_binding(),
2✔
5082
            });
5083
            if emits_gpu_spawn_events {
2✔
5084
                let child_info_buffer = child_info_buffer.unwrap();
×
5085

5086
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
5087
                // ChildInfoBuffer;
5088
                entries.push(BindGroupEntry {
×
5089
                    binding: 1,
×
5090
                    resource: BindingResource::Buffer(BufferBinding {
×
5091
                        buffer: child_info_buffer,
×
5092
                        offset: 0,
×
5093
                        size: None,
×
5094
                    }),
5095
                });
5096

5097
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
5098
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
5099
                    // EventBuffer;
5100
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
5101
                    // then moved to counting in bytes, so now need some conversion. Need to review
5102
                    // all of this...
5103
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
5104
                    buffer_binding.offset *= 4;
5105
                    buffer_binding.size = buffer_binding
5106
                        .size
5107
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
5108
                    entries.push(BindGroupEntry {
5109
                        binding: 2 + index as u32,
5110
                        resource: BindingResource::Buffer(buffer_binding),
5111
                    });
5112
                }
5113
            }
5114

5115
            let bind_group = render_device.create_bind_group(
6✔
5116
                "hanabi:bind_group:update:metadata@3",
5117
                layout,
2✔
5118
                &entries[..],
2✔
5119
            );
5120

5121
            trace!(
2✔
5122
                "Created new metadata@3 bind group for update pass and slab ID {}: effect_metadata={}",
2✔
5123
                effect_batch.slab_id.index(),
4✔
5124
                effect_batch.metadata_table_id.0,
5125
            );
5126

5127
            bind_group
2✔
5128
        };
5129

5130
        Ok(&self
312✔
5131
            .update_metadata_bind_groups
312✔
5132
            .entry(effect_batch.slab_id)
624✔
5133
            .and_modify(|cbg| {
622✔
5134
                if cbg.key != key {
310✔
5135
                    trace!(
×
5136
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
5137
                        cbg.key,
5138
                        key
5139
                    );
5140
                    cbg.key = key.clone();
×
5141
                    cbg.bind_group = make_entry();
×
5142
                }
5143
            })
5144
            .or_insert_with(|| {
314✔
5145
                trace!(
2✔
5146
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
5147
                    key
5148
                );
5149
                CachedBindGroup {
2✔
5150
                    key: key.clone(),
4✔
5151
                    bind_group: make_entry(),
2✔
5152
                }
5153
            })
5154
            .bind_group)
5155
    }
5156
}
5157

5158
#[derive(SystemParam)]
5159
pub struct QueueEffectsReadOnlyParams<'w, 's> {
5160
    #[cfg(feature = "2d")]
5161
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5162
    #[cfg(feature = "3d")]
5163
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5164
    #[cfg(feature = "3d")]
5165
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5166
    #[cfg(feature = "3d")]
5167
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5168
    marker: PhantomData<&'s usize>,
5169
}
5170

5171
fn emit_sorted_draw<T, F>(
624✔
5172
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5173
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5174
    view_entities: &mut FixedBitSet,
5175
    sorted_effect_batches: &SortedEffectBatches,
5176
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5177
    render_pipeline: &mut ParticlesRenderPipeline,
5178
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5179
    render_meshes: &RenderAssets<RenderMesh>,
5180
    pipeline_cache: &PipelineCache,
5181
    make_phase_item: F,
5182
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5183
) where
5184
    T: SortedPhaseItem,
5185
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5186
{
5187
    trace!("emit_sorted_draw() {} views", views.iter().len());
2,496✔
5188

5189
    for (visible_entities, view, msaa) in views.iter() {
1,872✔
5190
        trace!(
×
5191
            "Process new sorted view with {} visible particle effect entities",
624✔
5192
            visible_entities.len::<CompiledParticleEffect>()
1,248✔
5193
        );
5194

5195
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
312✔
5196
            continue;
312✔
5197
        };
5198

5199
        {
5200
            #[cfg(feature = "trace")]
5201
            let _span = bevy::log::info_span!("collect_view_entities").entered();
936✔
5202

5203
            view_entities.clear();
624✔
5204
            view_entities.extend(
624✔
5205
                visible_entities
312✔
5206
                    .iter::<EffectVisibilityClass>()
312✔
5207
                    .map(|e| e.1.index() as usize),
624✔
5208
            );
5209
        }
5210

5211
        // For each view, loop over all the effect batches to determine if the effect
5212
        // needs to be rendered for that view, and enqueue a view-dependent
5213
        // batch if so.
5214
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
936✔
5215
            #[cfg(feature = "trace")]
5216
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5217

5218
            trace!(
×
5219
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
312✔
5220
                draw_entity,
×
5221
                draw_batch.effect_batch_index,
×
5222
            );
5223

5224
            // Get the EffectBatches this EffectDrawBatch is part of.
5225
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
312✔
5226
            else {
×
5227
                continue;
×
5228
            };
5229

5230
            trace!(
×
5231
                "-> EffectBach: slab_id={} spawner_base={} layout_flags={:?}",
312✔
5232
                effect_batch.slab_id.index(),
624✔
5233
                effect_batch.spawner_base,
×
5234
                effect_batch.layout_flags,
×
5235
            );
5236

5237
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5238
            if effect_batch
×
5239
                .layout_flags
×
5240
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5241
            {
5242
                trace!("Non-transparent batch. Skipped.");
×
5243
                continue;
×
5244
            }
5245

5246
            // Check if batch contains any entity visible in the current view. Otherwise we
5247
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5248
            // the Sprite renderer this is inspired from) we don't expect more than
5249
            // a handful of particle effect instances, so would rather not pay the memory
5250
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5251
            // TODO - Profile to confirm.
5252
            #[cfg(feature = "trace")]
5253
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5254
            let has_visible_entity = effect_batch
×
5255
                .entities
×
5256
                .iter()
5257
                .any(|index| view_entities.contains(*index as usize));
936✔
5258
            if !has_visible_entity {
×
5259
                trace!("No visible entity for view, not emitting any draw call.");
×
5260
                continue;
×
5261
            }
5262
            #[cfg(feature = "trace")]
5263
            _span_check_vis.exit();
624✔
5264

5265
            // Create and cache the bind group layout for this texture layout
5266
            render_pipeline.cache_material(&effect_batch.texture_layout);
936✔
5267

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

5271
            let local_space_simulation = effect_batch
624✔
5272
                .layout_flags
312✔
5273
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
312✔
5274
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
936✔
5275
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
936✔
5276
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
936✔
5277
            let needs_normal = effect_batch
624✔
5278
                .layout_flags
312✔
5279
                .contains(LayoutFlags::NEEDS_NORMAL);
312✔
5280
            let needs_particle_fragment = effect_batch
624✔
5281
                .layout_flags
312✔
5282
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
312✔
5283
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
936✔
5284
            let image_count = effect_batch.texture_layout.layout.len() as u8;
624✔
5285

5286
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5287
            // re-querying here...?
5288
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
936✔
5289
                trace!("Batch has no render mesh, skipped.");
×
5290
                continue;
×
5291
            };
5292
            let mesh_layout = render_mesh.layout.clone();
×
5293

5294
            // Specialize the render pipeline based on the effect batch
5295
            trace!(
×
5296
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
312✔
5297
                effect_batch.render_shader,
×
5298
                image_count,
×
5299
                alpha_mask,
×
5300
                flipbook,
×
5301
                view.hdr
×
5302
            );
5303

5304
            // Add a draw pass for the effect batch
5305
            trace!("Emitting individual draw for batch");
312✔
5306

5307
            let alpha_mode = effect_batch.alpha_mode;
×
5308

5309
            #[cfg(feature = "trace")]
5310
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5311
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5312
                pipeline_cache,
×
5313
                render_pipeline,
×
5314
                ParticleRenderPipelineKey {
×
5315
                    shader: effect_batch.render_shader.clone(),
×
5316
                    mesh_layout: Some(mesh_layout),
×
5317
                    particle_layout: effect_batch.particle_layout.clone(),
×
5318
                    texture_layout: effect_batch.texture_layout.clone(),
×
5319
                    local_space_simulation,
×
5320
                    alpha_mask,
×
5321
                    alpha_mode,
×
5322
                    flipbook,
×
5323
                    needs_uv,
×
5324
                    needs_normal,
×
5325
                    needs_particle_fragment,
×
5326
                    ribbons,
×
5327
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5328
                    pipeline_mode,
×
5329
                    msaa_samples: msaa.samples(),
×
5330
                    hdr: view.hdr,
×
5331
                },
5332
            );
5333
            #[cfg(feature = "trace")]
5334
            _span_specialize.exit();
×
5335

5336
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
312✔
5337
            trace!(
×
5338
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
312✔
5339
                spawner_base={} handle={:?}",
312✔
5340
                draw_entity,
×
5341
                effect_batch.slab_id.index(),
624✔
5342
                effect_batch.spawner_base,
×
5343
                effect_batch.handle
×
5344
            );
5345
            render_phase.add(make_phase_item(
×
5346
                render_pipeline_id,
×
5347
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5348
                draw_batch,
×
5349
                view,
×
5350
            ));
5351
        }
5352
    }
5353
}
5354

5355
#[cfg(feature = "3d")]
5356
fn emit_binned_draw<T, F, G>(
624✔
5357
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5358
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5359
    view_entities: &mut FixedBitSet,
5360
    sorted_effect_batches: &SortedEffectBatches,
5361
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5362
    render_pipeline: &mut ParticlesRenderPipeline,
5363
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5364
    pipeline_cache: &PipelineCache,
5365
    render_meshes: &RenderAssets<RenderMesh>,
5366
    make_batch_set_key: F,
5367
    make_bin_key: G,
5368
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5369
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5370
    change_tick: &mut Tick,
5371
) where
5372
    T: BinnedPhaseItem,
5373
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5374
    G: Fn() -> T::BinKey,
5375
{
5376
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5377

5378
    trace!("emit_binned_draw() {} views", views.iter().len());
2,496✔
5379

5380
    for (visible_entities, view, msaa) in views.iter() {
1,872✔
5381
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
624✔
5382

5383
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
624✔
5384
            continue;
×
5385
        };
5386

5387
        {
5388
            #[cfg(feature = "trace")]
5389
            let _span = bevy::log::info_span!("collect_view_entities").entered();
1,872✔
5390

5391
            view_entities.clear();
1,248✔
5392
            view_entities.extend(
1,248✔
5393
                visible_entities
624✔
5394
                    .iter::<EffectVisibilityClass>()
624✔
5395
                    .map(|e| e.1.index() as usize),
1,248✔
5396
            );
5397
        }
5398

5399
        // For each view, loop over all the effect batches to determine if the effect
5400
        // needs to be rendered for that view, and enqueue a view-dependent
5401
        // batch if so.
5402
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
1,872✔
5403
            #[cfg(feature = "trace")]
5404
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5405

5406
            trace!(
×
5407
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
624✔
5408
                draw_entity,
×
5409
                draw_batch.effect_batch_index,
×
5410
            );
5411

5412
            // Get the EffectBatches this EffectDrawBatch is part of.
5413
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
624✔
5414
            else {
×
5415
                continue;
×
5416
            };
5417

5418
            trace!(
×
5419
                "-> EffectBaches: slab_id={} spawner_base={} layout_flags={:?}",
624✔
5420
                effect_batch.slab_id.index(),
1,248✔
5421
                effect_batch.spawner_base,
×
5422
                effect_batch.layout_flags,
×
5423
            );
5424

5425
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5426
                trace!(
624✔
5427
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
624✔
5428
                    effect_batch.layout_flags,
×
5429
                    alpha_mask
×
5430
                );
5431
                continue;
624✔
5432
            }
5433

5434
            // Check if batch contains any entity visible in the current view. Otherwise we
5435
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5436
            // the Sprite renderer this is inspired from) we don't expect more than
5437
            // a handful of particle effect instances, so would rather not pay the memory
5438
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5439
            // TODO - Profile to confirm.
5440
            #[cfg(feature = "trace")]
5441
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5442
            let has_visible_entity = effect_batch
×
5443
                .entities
×
5444
                .iter()
5445
                .any(|index| view_entities.contains(*index as usize));
×
5446
            if !has_visible_entity {
×
5447
                trace!("No visible entity for view, not emitting any draw call.");
×
5448
                continue;
×
5449
            }
5450
            #[cfg(feature = "trace")]
5451
            _span_check_vis.exit();
×
5452

5453
            // Create and cache the bind group layout for this texture layout
5454
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5455

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

5459
            let local_space_simulation = effect_batch
×
5460
                .layout_flags
×
5461
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5462
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5463
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5464
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5465
            let needs_normal = effect_batch
×
5466
                .layout_flags
×
5467
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5468
            let needs_particle_fragment = effect_batch
×
5469
                .layout_flags
×
5470
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5471
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5472
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5473
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5474

5475
            // Specialize the render pipeline based on the effect batch
5476
            trace!(
×
5477
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5478
                effect_batch.render_shader,
×
5479
                image_count,
×
5480
                alpha_mask,
×
5481
                flipbook,
×
5482
                view.hdr
×
5483
            );
5484

5485
            // Add a draw pass for the effect batch
5486
            trace!("Emitting individual draw for batch");
×
5487

5488
            let alpha_mode = effect_batch.alpha_mode;
×
5489

5490
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5491
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5492
                continue;
×
5493
            };
5494

5495
            #[cfg(feature = "trace")]
5496
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5497
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5498
                pipeline_cache,
×
5499
                render_pipeline,
×
5500
                ParticleRenderPipelineKey {
×
5501
                    shader: effect_batch.render_shader.clone(),
×
5502
                    mesh_layout: Some(mesh_layout),
×
5503
                    particle_layout: effect_batch.particle_layout.clone(),
×
5504
                    texture_layout: effect_batch.texture_layout.clone(),
×
5505
                    local_space_simulation,
×
5506
                    alpha_mask,
×
5507
                    alpha_mode,
×
5508
                    flipbook,
×
5509
                    needs_uv,
×
5510
                    needs_normal,
×
5511
                    needs_particle_fragment,
×
5512
                    ribbons,
×
5513
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5514
                    pipeline_mode,
×
5515
                    msaa_samples: msaa.samples(),
×
5516
                    hdr: view.hdr,
×
5517
                },
5518
            );
5519
            #[cfg(feature = "trace")]
5520
            _span_specialize.exit();
×
5521

5522
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5523
            trace!(
×
5524
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
×
5525
                spawner_base={} handle={:?}",
×
5526
                draw_entity,
×
5527
                effect_batch.slab_id.index(),
×
5528
                effect_batch.spawner_base,
×
5529
                effect_batch.handle
×
5530
            );
5531
            render_phase.add(
×
5532
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
5533
                make_bin_key(),
×
5534
                (draw_entity, draw_batch.main_entity),
×
5535
                InputUniformIndex::default(),
×
5536
                BinnedRenderPhaseType::NonMesh,
×
5537
                *change_tick,
×
5538
            );
5539
        }
5540
    }
5541
}
5542

5543
#[allow(clippy::too_many_arguments)]
5544
pub(crate) fn queue_effects(
330✔
5545
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5546
    effects_meta: Res<EffectsMeta>,
5547
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5548
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5549
    pipeline_cache: Res<PipelineCache>,
5550
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5551
    sorted_effect_batches: Res<SortedEffectBatches>,
5552
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5553
    events: Res<EffectAssetEvents>,
5554
    render_meshes: Res<RenderAssets<RenderMesh>>,
5555
    read_params: QueueEffectsReadOnlyParams,
5556
    mut view_entities: Local<FixedBitSet>,
5557
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5558
        ViewSortedRenderPhases<Transparent2d>,
5559
    >,
5560
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5561
        ViewSortedRenderPhases<Transparent3d>,
5562
    >,
5563
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5564
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5565
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5566
    ),
5567
    mut change_tick: Local<Tick>,
5568
) {
5569
    #[cfg(feature = "trace")]
5570
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
990✔
5571

5572
    trace!("queue_effects");
650✔
5573

5574
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5575
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5576
    // reuse stale data.
5577
    let next_change_tick = change_tick.get() + 1;
660✔
5578
    change_tick.set(next_change_tick);
660✔
5579

5580
    // If an image has changed, the GpuImage has (probably) changed
5581
    for event in &events.images {
369✔
5582
        match event {
5583
            AssetEvent::Added { .. } => (),
27✔
5584
            AssetEvent::LoadedWithDependencies { .. } => (),
6✔
5585
            AssetEvent::Unused { .. } => (),
×
5586
            AssetEvent::Modified { id } => {
×
5587
                if effect_bind_groups.images.remove(id).is_some() {
×
5588
                    trace!("Destroyed bind group of modified image asset {:?}", id);
×
5589
                }
5590
            }
5591
            AssetEvent::Removed { id } => {
6✔
5592
                if effect_bind_groups.images.remove(id).is_some() {
18✔
5593
                    trace!("Destroyes bind group of removed image asset {:?}", id);
×
5594
                }
5595
            }
5596
        };
5597
    }
5598

5599
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
978✔
5600
        // No spawners are active
5601
        return;
18✔
5602
    }
5603

5604
    // Loop over all 2D cameras/views that need to render effects
5605
    #[cfg(feature = "2d")]
5606
    {
5607
        #[cfg(feature = "trace")]
5608
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5609

5610
        let draw_effects_function_2d = read_params
5611
            .draw_functions_2d
5612
            .read()
5613
            .get_id::<DrawEffects>()
5614
            .unwrap();
5615

5616
        // Effects with full alpha blending
5617
        if !views.is_empty() {
5618
            trace!("Emit effect draw calls for alpha blended 2D views...");
624✔
5619
            emit_sorted_draw(
5620
                &views,
5621
                &mut transparent_2d_render_phases,
5622
                &mut view_entities,
5623
                &sorted_effect_batches,
5624
                &effect_draw_batches,
5625
                &mut render_pipeline,
5626
                specialized_render_pipelines.reborrow(),
5627
                &render_meshes,
5628
                &pipeline_cache,
5629
                |id, entity, draw_batch, _view| Transparent2d {
5630
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5631
                    entity,
×
5632
                    pipeline: id,
×
5633
                    draw_function: draw_effects_function_2d,
×
5634
                    batch_range: 0..1,
×
5635
                    extracted_index: 0, // ???
5636
                    extra_index: PhaseItemExtraIndex::None,
×
5637
                    indexed: true, // ???
5638
                },
5639
                #[cfg(feature = "3d")]
5640
                PipelineMode::Camera2d,
5641
            );
5642
        }
5643
    }
5644

5645
    // Loop over all 3D cameras/views that need to render effects
5646
    #[cfg(feature = "3d")]
5647
    {
5648
        #[cfg(feature = "trace")]
5649
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5650

5651
        // Effects with full alpha blending
5652
        if !views.is_empty() {
5653
            trace!("Emit effect draw calls for alpha blended 3D views...");
624✔
5654

5655
            let draw_effects_function_3d = read_params
5656
                .draw_functions_3d
5657
                .read()
5658
                .get_id::<DrawEffects>()
5659
                .unwrap();
5660

5661
            emit_sorted_draw(
5662
                &views,
5663
                &mut transparent_3d_render_phases,
5664
                &mut view_entities,
5665
                &sorted_effect_batches,
5666
                &effect_draw_batches,
5667
                &mut render_pipeline,
5668
                specialized_render_pipelines.reborrow(),
5669
                &render_meshes,
5670
                &pipeline_cache,
5671
                |id, entity, batch, view| Transparent3d {
5672
                    distance: view
312✔
5673
                        .rangefinder3d()
312✔
5674
                        .distance_translation(&batch.translation),
624✔
5675
                    pipeline: id,
312✔
5676
                    entity,
312✔
5677
                    draw_function: draw_effects_function_3d,
312✔
5678
                    batch_range: 0..1,
312✔
5679
                    extra_index: PhaseItemExtraIndex::None,
312✔
5680
                    indexed: true, // ???
5681
                },
5682
                #[cfg(feature = "2d")]
5683
                PipelineMode::Camera3d,
5684
            );
5685
        }
5686

5687
        // Effects with alpha mask
5688
        if !views.is_empty() {
5689
            #[cfg(feature = "trace")]
5690
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
312✔
5691

5692
            trace!("Emit effect draw calls for alpha masked 3D views...");
312✔
5693

5694
            let draw_effects_function_alpha_mask = read_params
5695
                .draw_functions_alpha_mask
5696
                .read()
5697
                .get_id::<DrawEffects>()
5698
                .unwrap();
5699

5700
            emit_binned_draw(
5701
                &views,
5702
                &mut alpha_mask_3d_render_phases,
5703
                &mut view_entities,
5704
                &sorted_effect_batches,
5705
                &effect_draw_batches,
5706
                &mut render_pipeline,
5707
                specialized_render_pipelines.reborrow(),
5708
                &pipeline_cache,
5709
                &render_meshes,
5710
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
5711
                    pipeline: id,
×
5712
                    draw_function: draw_effects_function_alpha_mask,
×
5713
                    material_bind_group_index: None,
×
5714
                    vertex_slab: default(),
×
5715
                    index_slab: None,
×
5716
                },
5717
                // Unused for now
5718
                || OpaqueNoLightmap3dBinKey {
5719
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5720
                },
5721
                #[cfg(feature = "2d")]
5722
                PipelineMode::Camera3d,
5723
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5724
                &mut change_tick,
5725
            );
5726
        }
5727

5728
        // Opaque particles
5729
        if !views.is_empty() {
5730
            #[cfg(feature = "trace")]
5731
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
312✔
5732

5733
            trace!("Emit effect draw calls for opaque 3D views...");
312✔
5734

5735
            let draw_effects_function_opaque = read_params
5736
                .draw_functions_opaque
5737
                .read()
5738
                .get_id::<DrawEffects>()
5739
                .unwrap();
5740

5741
            emit_binned_draw(
5742
                &views,
5743
                &mut opaque_3d_render_phases,
5744
                &mut view_entities,
5745
                &sorted_effect_batches,
5746
                &effect_draw_batches,
5747
                &mut render_pipeline,
5748
                specialized_render_pipelines.reborrow(),
5749
                &pipeline_cache,
5750
                &render_meshes,
5751
                |id, _batch, _view| Opaque3dBatchSetKey {
5752
                    pipeline: id,
×
5753
                    draw_function: draw_effects_function_opaque,
×
5754
                    material_bind_group_index: None,
×
5755
                    vertex_slab: default(),
×
5756
                    index_slab: None,
×
5757
                    lightmap_slab: None,
×
5758
                },
5759
                // Unused for now
5760
                || Opaque3dBinKey {
5761
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5762
                },
5763
                #[cfg(feature = "2d")]
5764
                PipelineMode::Camera3d,
5765
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5766
                &mut change_tick,
5767
            );
5768
        }
5769
    }
5770
}
5771

5772
/// Once a child effect is batched, and therefore passed validations to be
5773
/// updated and rendered this frame, dispatch a new GPU operation to fill the
5774
/// indirect dispatch args of its init pass based on the number of GPU events
5775
/// emitted in the previous frame and stored in its event buffer.
5776
pub fn queue_init_indirect_workgroup_update(
330✔
5777
    q_cached_effects: Query<(
5778
        Entity,
5779
        &CachedChildInfo,
5780
        &CachedEffectEvents,
5781
        &CachedReadyState,
5782
    )>,
5783
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5784
) {
5785
    debug_assert_eq!(
330✔
5786
        GpuChildInfo::min_size().get() % 4,
330✔
5787
        0,
5788
        "Invalid GpuChildInfo alignment."
×
5789
    );
5790

5791
    // Schedule some GPU buffer operation to update the number of workgroups to
5792
    // dispatch during the indirect init pass of this effect based on the number of
5793
    // GPU spawn events written in its buffer.
5794
    for (entity, cached_child_info, cached_effect_events, cached_ready_state) in &q_cached_effects {
330✔
5795
        if !cached_ready_state.is_ready() {
×
5796
            trace!(
×
5797
                "[Effect {:?}] Skipping init_fill_dispatch.enqueue() because effect is not ready.",
×
5798
                entity
5799
            );
5800
            continue;
5801
        }
5802
        let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
×
5803
        let global_child_index = cached_child_info.global_child_index;
×
5804
        trace!(
×
5805
            "[Effect {:?}] init_fill_dispatch.enqueue(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
5806
            entity,
5807
            global_child_index,
5808
            init_indirect_dispatch_index,
5809
        );
5810
        assert!(global_child_index != u32::MAX);
×
5811
        init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
5812
    }
5813
}
5814

5815
/// Prepare GPU resources for effect rendering.
5816
///
5817
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5818
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5819
/// access to the current camera view.
5820
pub(crate) fn prepare_gpu_resources(
330✔
5821
    mut effects_meta: ResMut<EffectsMeta>,
5822
    //mut effect_cache: ResMut<EffectCache>,
5823
    mut event_cache: ResMut<EventCache>,
5824
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5825
    mut sort_bind_groups: ResMut<SortBindGroups>,
5826
    render_device: Res<RenderDevice>,
5827
    render_queue: Res<RenderQueue>,
5828
    view_uniforms: Res<ViewUniforms>,
5829
    render_pipeline: Res<ParticlesRenderPipeline>,
5830
) {
5831
    // Get the binding for the ViewUniform, the uniform data structure containing
5832
    // the Camera data for the current view. If not available, we cannot render
5833
    // anything.
5834
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
660✔
5835
        return;
×
5836
    };
5837

5838
    // Upload simulation parameters for this frame
5839
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
654✔
5840
    effects_meta
5841
        .sim_params_uniforms
5842
        .write_buffer(&render_device, &render_queue);
5843
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
663✔
5844
        // Buffer changed, invalidate bind groups
5845
        effects_meta.update_sim_params_bind_group = None;
9✔
5846
        effects_meta.indirect_sim_params_bind_group = None;
3✔
5847
    }
5848

5849
    // Create the bind group for the camera/view parameters
5850
    // FIXME - Not here!
5851
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5852
        "hanabi:bind_group_camera_view",
5853
        &render_pipeline.view_layout,
5854
        &[
5855
            BindGroupEntry {
5856
                binding: 0,
5857
                resource: view_binding,
5858
            },
5859
            BindGroupEntry {
5860
                binding: 1,
5861
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5862
            },
5863
        ],
5864
    ));
5865

5866
    // Re-/allocate the draw indirect args buffer if needed
5867
    if effects_meta
5868
        .draw_indirect_buffer
5869
        .allocate_gpu(&render_device, &render_queue)
5870
    {
5871
        // All those bind groups use the buffer so need to be re-created
5872
        trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
4✔
5873
        effects_meta.update_sim_params_bind_group = None;
4✔
5874
        effects_meta.indirect_metadata_bind_group = None;
4✔
5875
    }
5876

5877
    // Re-/allocate any GPU buffer if needed
5878
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5879
    // effect_bind_groups);
5880
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5881
    sort_bind_groups.prepare_buffers(&render_device);
5882
    if effects_meta
5883
        .dispatch_indirect_buffer
5884
        .prepare_buffers(&render_device)
5885
    {
5886
        // All those bind groups use the buffer so need to be re-created
5887
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
4✔
5888
        effect_bind_groups.particle_slabs.clear();
4✔
5889
    }
5890
}
5891

5892
/// Update the [`GpuEffectMetadata`] of all the effects queued for update/render
5893
/// this frame.
5894
///
5895
/// By this point, all effects should have a [`CachedEffectMetadata`] with a
5896
/// valid allocation in the GPU table for a [`GpuEffectMetadata`] entry. This
5897
/// system actually synchronize the CPU value with the GPU one in case of
5898
/// change.
5899
pub(crate) fn prepare_effect_metadata(
330✔
5900
    render_device: Res<RenderDevice>,
5901
    render_queue: Res<RenderQueue>,
5902
    mut q_effects: Query<(
5903
        MainEntity,
5904
        Ref<ExtractedEffect>,
5905
        Ref<CachedEffect>,
5906
        Ref<DispatchBufferIndices>,
5907
        Option<Ref<CachedChildInfo>>,
5908
        Option<Ref<CachedParentInfo>>,
5909
        Option<Ref<CachedDrawIndirectArgs>>,
5910
        Option<Ref<CachedEffectEvents>>,
5911
        &mut CachedEffectMetadata,
5912
    )>,
5913
    mut effects_meta: ResMut<EffectsMeta>,
5914
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5915
) {
5916
    #[cfg(feature = "trace")]
5917
    let _span = bevy::log::info_span!("prepare_effect_metadata").entered();
990✔
5918
    trace!("prepare_effect_metadata");
650✔
5919

5920
    for (
5921
        main_entity,
314✔
5922
        extracted_effect,
314✔
5923
        cached_effect,
314✔
5924
        dispatch_buffer_indices,
314✔
5925
        maybe_cached_child_info,
314✔
5926
        maybe_cached_parent_info,
314✔
5927
        maybe_cached_draw_indirect_args,
314✔
5928
        maybe_cached_effect_events,
314✔
5929
        mut cached_effect_metadata,
314✔
5930
    ) in &mut q_effects
644✔
5931
    {
5932
        // Check if anything relevant to GpuEffectMetadata changed this frame; otherwise
5933
        // early out and skip this effect.
5934
        let is_changed_ee = extracted_effect.is_changed();
942✔
5935
        let is_changed_ce = cached_effect.is_changed();
942✔
5936
        let is_changed_dbi = dispatch_buffer_indices.is_changed();
942✔
5937
        let is_changed_cci = maybe_cached_child_info
628✔
5938
            .as_ref()
5939
            .map(|cci| cci.is_changed())
314✔
5940
            .unwrap_or(false);
5941
        let is_changed_cpi = maybe_cached_parent_info
628✔
5942
            .as_ref()
5943
            .map(|cpi| cpi.is_changed())
314✔
5944
            .unwrap_or(false);
5945
        let is_changed_cdia = maybe_cached_draw_indirect_args
628✔
5946
            .as_ref()
5947
            .map(|cdia| cdia.is_changed())
942✔
5948
            .unwrap_or(false);
5949
        let is_changed_cee = maybe_cached_effect_events
628✔
5950
            .as_ref()
5951
            .map(|cee| cee.is_changed())
314✔
5952
            .unwrap_or(false);
5953
        trace!(
314✔
5954
            "Preparting GpuEffectMetadata for effect {:?}: is_changed[] = {} {} {} {} {} {} {}",
314✔
5955
            main_entity,
5956
            is_changed_ee,
5957
            is_changed_ce,
5958
            is_changed_dbi,
5959
            is_changed_cci,
5960
            is_changed_cpi,
5961
            is_changed_cdia,
5962
            is_changed_cee
5963
        );
5964
        if !is_changed_ee
314✔
5965
            && !is_changed_ce
311✔
5966
            && !is_changed_dbi
311✔
5967
            && !is_changed_cci
311✔
5968
            && !is_changed_cpi
311✔
5969
            && !is_changed_cdia
311✔
5970
            && !is_changed_cee
311✔
5971
        {
5972
            continue;
311✔
5973
        }
5974

5975
        let capacity = cached_effect.slice.len();
9✔
5976

5977
        // Global and local indices of this effect as a child of another (parent) effect
5978
        let (global_child_index, local_child_index) = maybe_cached_child_info
9✔
5979
            .map(|cci| (cci.global_child_index, cci.local_child_index))
3✔
5980
            .unwrap_or((u32::MAX, u32::MAX));
6✔
5981

5982
        // Base index of all children of this (parent) effect
5983
        let base_child_index = maybe_cached_parent_info
6✔
5984
            .map(|cpi| {
3✔
5985
                debug_assert_eq!(
×
5986
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
5987
                    0
5988
                );
5989
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
5990
            })
5991
            .unwrap_or(u32::MAX);
3✔
5992

5993
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
9✔
5994
        let sort_key_offset = extracted_effect
6✔
5995
            .particle_layout
3✔
5996
            .byte_offset(Attribute::RIBBON_ID)
3✔
5997
            .map(|byte_offset| byte_offset / 4)
3✔
5998
            .unwrap_or(u32::MAX);
3✔
5999
        let sort_key2_offset = extracted_effect
6✔
6000
            .particle_layout
3✔
6001
            .byte_offset(Attribute::AGE)
3✔
6002
            .map(|byte_offset| byte_offset / 4)
3✔
6003
            .unwrap_or(u32::MAX);
3✔
6004

6005
        let gpu_effect_metadata = GpuEffectMetadata {
6006
            capacity,
6007
            alive_count: 0,
6008
            max_update: 0,
6009
            max_spawn: capacity,
6010
            indirect_write_index: 0,
6011
            indirect_dispatch_index: dispatch_buffer_indices
3✔
6012
                .update_dispatch_indirect_buffer_row_index,
6013
            indirect_draw_index: maybe_cached_draw_indirect_args
3✔
6014
                .map(|cdia| cdia.get_row().0)
6015
                .unwrap_or(u32::MAX),
6016
            init_indirect_dispatch_index: maybe_cached_effect_events
3✔
6017
                .map(|cee| cee.init_indirect_dispatch_index)
6018
                .unwrap_or(u32::MAX),
6019
            local_child_index,
6020
            global_child_index,
6021
            base_child_index,
6022
            particle_stride,
6023
            sort_key_offset,
6024
            sort_key2_offset,
6025
            ..default()
6026
        };
6027

6028
        // Insert of update entry in GPU buffer table
6029
        assert!(cached_effect_metadata.table_id.is_valid());
9✔
6030
        if gpu_effect_metadata != cached_effect_metadata.metadata {
3✔
6031
            effects_meta
2✔
6032
                .effect_metadata_buffer
2✔
6033
                .update(cached_effect_metadata.table_id, gpu_effect_metadata);
6✔
6034

6035
            cached_effect_metadata.metadata = gpu_effect_metadata;
2✔
6036

6037
            // This triggers on all new spawns and annoys everyone; silence until we can at
6038
            // least warn only on non-first-spawn, and ideally split indirect data from that
6039
            // struct so we don't overwrite it and solve the issue.
6040
            debug!(
2✔
6041
                "Updated metadata entry {} for effect {:?}, this will reset it.",
2✔
6042
                cached_effect_metadata.table_id.0, main_entity
2✔
6043
            );
6044
        }
6045
    }
6046

6047
    // Once all EffectMetadata values are written, schedule a GPU upload
6048
    if effects_meta
330✔
6049
        .effect_metadata_buffer
330✔
6050
        .allocate_gpu(render_device.as_ref(), render_queue.as_ref())
6051
    {
6052
        // All those bind groups use the buffer so need to be re-created
6053
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
4✔
6054
        effects_meta.indirect_metadata_bind_group = None;
4✔
6055
        effect_bind_groups.init_metadata_bind_groups.clear();
4✔
6056
        effect_bind_groups.update_metadata_bind_groups.clear();
4✔
6057
    }
6058
}
6059

6060
/// Read the queued init fill dispatch operations, batch them together by
6061
/// contiguous source and destination entries in the buffers, and enqueue
6062
/// corresponding GPU buffer fill dispatch operations for all batches.
6063
///
6064
/// This system runs after the GPU buffers have been (re-)allocated in
6065
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
6066
/// reference them from the generic [`GpuBufferOperationQueue`].
6067
pub(crate) fn queue_init_fill_dispatch_ops(
330✔
6068
    event_cache: Res<EventCache>,
6069
    render_device: Res<RenderDevice>,
6070
    render_queue: Res<RenderQueue>,
6071
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
6072
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
6073
) {
6074
    // Submit all queued init fill dispatch operations with the proper buffers
6075
    if !init_fill_dispatch_queue.is_empty() {
330✔
6076
        let src_buffer = event_cache.child_infos().buffer();
×
6077
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
6078
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
6079
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
6080
        } else {
6081
            if src_buffer.is_none() {
×
6082
                warn!("Event cache has no allocated GpuChildInfo buffer, but there's {} init fill dispatch operation(s) queued. Ignoring those operations. This will prevent child particles from spawning.", init_fill_dispatch_queue.queue.len());
×
6083
            }
6084
            if dst_buffer.is_none() {
×
6085
                warn!("Event cache has no allocated GpuDispatchIndirect buffer, but there's {} init fill dispatch operation(s) queued. Ignoring those operations. This will prevent child particles from spawning.", init_fill_dispatch_queue.queue.len());
×
6086
            }
6087
        }
6088
    }
6089

6090
    // Once all GPU operations for this frame are enqueued, upload them to GPU
6091
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
990✔
6092
}
6093

6094
pub(crate) fn prepare_bind_groups(
330✔
6095
    mut effects_meta: ResMut<EffectsMeta>,
6096
    mut effect_cache: ResMut<EffectCache>,
6097
    mut event_cache: ResMut<EventCache>,
6098
    mut effect_bind_groups: ResMut<EffectBindGroups>,
6099
    mut property_bind_groups: ResMut<PropertyBindGroups>,
6100
    mut sort_bind_groups: ResMut<SortBindGroups>,
6101
    property_cache: Res<PropertyCache>,
6102
    sorted_effect_batched: Res<SortedEffectBatches>,
6103
    render_device: Res<RenderDevice>,
6104
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
6105
    utils_pipeline: Res<UtilsPipeline>,
6106
    init_pipeline: Res<ParticlesInitPipeline>,
6107
    update_pipeline: Res<ParticlesUpdatePipeline>,
6108
    render_pipeline: ResMut<ParticlesRenderPipeline>,
6109
    gpu_images: Res<RenderAssets<GpuImage>>,
6110
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
6111
) {
6112
    // We can't simulate nor render anything without at least the spawner buffer
6113
    if effects_meta.spawner_buffer.is_empty() {
660✔
6114
        return;
18✔
6115
    }
6116
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
312✔
6117
        return;
×
6118
    };
6119

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

6125
    {
6126
        #[cfg(feature = "trace")]
6127
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
6128

6129
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
6130
        // loop below. Also allows earlying out before doing any work in case some
6131
        // buffer is missing.
6132
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
312✔
6133
            return;
×
6134
        };
6135

6136
        // Create the sim_params@0 bind group for the global simulation parameters,
6137
        // which is shared by the init and update passes.
6138
        if effects_meta.update_sim_params_bind_group.is_none() {
6139
            if let Some(draw_indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() {
4✔
6140
                effects_meta.update_sim_params_bind_group = Some(render_device.create_bind_group(
6141
                    "hanabi:bind_group:vfx_update:sim_params@0",
6142
                    &update_pipeline.sim_params_layout,
6143
                    &[
6144
                        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6145
                        BindGroupEntry {
6146
                            binding: 0,
6147
                            resource: effects_meta.sim_params_uniforms.binding().unwrap(),
6148
                        },
6149
                        // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
6150
                        // array<DrawIndexedIndirectArgs>;
6151
                        BindGroupEntry {
6152
                            binding: 1,
6153
                            resource: draw_indirect_buffer.as_entire_binding(),
6154
                        },
6155
                    ],
6156
                ));
6157
            } else {
6158
                debug!("Cannot allocate bind group for vfx_update:sim_params@0 - draw_indirect_buffer not ready");
×
6159
            }
6160
        }
6161
        if effects_meta.indirect_sim_params_bind_group.is_none() {
2✔
6162
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
8✔
6163
                "hanabi:bind_group:vfx_indirect:sim_params@0",
2✔
6164
                &init_pipeline.sim_params_layout, // FIXME - Shared with init
4✔
6165
                &[
2✔
6166
                    // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6167
                    BindGroupEntry {
2✔
6168
                        binding: 0,
2✔
6169
                        resource: effects_meta.sim_params_uniforms.binding().unwrap(),
4✔
6170
                    },
6171
                ],
6172
            ));
6173
        }
6174

6175
        // Create the @1 bind group for the indirect dispatch preparation pass of all
6176
        // effects at once
6177
        effects_meta.indirect_metadata_bind_group = match (
6178
            effects_meta.effect_metadata_buffer.buffer(),
6179
            effects_meta.dispatch_indirect_buffer.buffer(),
6180
            effects_meta.draw_indirect_buffer.buffer(),
6181
        ) {
6182
            (
6183
                Some(effect_metadata_buffer),
312✔
6184
                Some(dispatch_indirect_buffer),
6185
                Some(draw_indirect_buffer),
6186
            ) => {
6187
                // Base bind group for indirect pass
6188
                Some(render_device.create_bind_group(
6189
                    "hanabi:bind_group:vfx_indirect:metadata@1",
6190
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
6191
                    &[
6192
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer :
6193
                        // array<u32>;
6194
                        BindGroupEntry {
6195
                            binding: 0,
6196
                            resource: effect_metadata_buffer.as_entire_binding(),
6197
                        },
6198
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer
6199
                        // : array<DispatchIndirectArgs>;
6200
                        BindGroupEntry {
6201
                            binding: 1,
6202
                            resource: dispatch_indirect_buffer.as_entire_binding(),
6203
                        },
6204
                        // @group(1) @binding(2) var<storage, read_write> draw_indirect_buffer :
6205
                        // array<u32>;
6206
                        BindGroupEntry {
6207
                            binding: 2,
6208
                            resource: draw_indirect_buffer.as_entire_binding(),
6209
                        },
6210
                    ],
6211
                ))
6212
            }
6213

6214
            // Some buffer is not yet available, can't create the bind group
6215
            _ => None,
×
6216
        };
6217

6218
        // Create the @2 bind group for the indirect dispatch preparation pass of all
6219
        // effects at once
6220
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
6221
            let bind_group = render_device.create_bind_group(
10✔
6222
                "hanabi:bind_group:vfx_indirect:spawner@2",
6223
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
6✔
6224
                &[
4✔
6225
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
6226
                    BindGroupEntry {
4✔
6227
                        binding: 0,
4✔
6228
                        resource: BindingResource::Buffer(BufferBinding {
4✔
6229
                            buffer: &spawner_buffer,
4✔
6230
                            offset: 0,
4✔
6231
                            size: None,
4✔
6232
                        }),
6233
                    },
6234
                ],
6235
            );
6236

6237
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
6238
        }
6239
    }
6240

6241
    // Create the per-slab bind groups
6242
    trace!("Create per-slab bind groups...");
312✔
6243
    for (slab_index, particle_slab) in effect_cache.slabs().iter().enumerate() {
312✔
6244
        #[cfg(feature = "trace")]
6245
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
6246

6247
        let Some(particle_slab) = particle_slab else {
312✔
6248
            trace!(
×
6249
                "Particle slab index #{} has no allocated EffectBuffer, skipped.",
×
6250
                slab_index
6251
            );
6252
            continue;
×
6253
        };
6254

6255
        // Ensure all effects in this batch have a bind group for the entire buffer of
6256
        // the group, since the update phase runs on an entire group/buffer at once,
6257
        // with all the effect instances in it batched together.
6258
        trace!("effect particle slab_index=#{}", slab_index);
312✔
6259
        effect_bind_groups
6260
            .particle_slabs
6261
            .entry(SlabId::new(slab_index as u32))
6262
            .or_insert_with(|| {
2✔
6263
                // Bind group particle@1 for render pass
6264
                trace!("Creating particle@1 bind group for buffer #{slab_index} in render pass");
4✔
6265
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
4✔
6266
                    render_device.limits().min_storage_buffer_offset_alignment,
2✔
6267
                );
6268
                let entries = [
4✔
6269
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
6270
                    BindGroupEntry {
4✔
6271
                        binding: 0,
4✔
6272
                        resource: particle_slab.as_entire_binding_particle(),
4✔
6273
                    },
6274
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
6275
                    BindGroupEntry {
4✔
6276
                        binding: 1,
4✔
6277
                        resource: particle_slab.as_entire_binding_indirect(),
4✔
6278
                    },
6279
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
6280
                    BindGroupEntry {
2✔
6281
                        binding: 2,
2✔
6282
                        resource: BindingResource::Buffer(BufferBinding {
2✔
6283
                            buffer: &spawner_buffer,
2✔
6284
                            offset: 0,
2✔
6285
                            size: Some(spawner_min_binding_size),
2✔
6286
                        }),
6287
                    },
6288
                ];
6289
                let render = render_device.create_bind_group(
8✔
6290
                    &format!("hanabi:bind_group:render:particles@1:vfx{slab_index}")[..],
6✔
6291
                    particle_slab.render_particles_buffer_layout(),
4✔
6292
                    &entries[..],
2✔
6293
                );
6294

6295
                BufferBindGroups { render }
2✔
6296
            });
6297
    }
6298

6299
    // Create bind groups for queued GPU buffer operations
6300
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
6301

6302
    // Create the per-effect bind groups
6303
    let spawner_buffer_binding_size =
6304
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
6305
    for effect_batch in sorted_effect_batched.iter() {
312✔
6306
        #[cfg(feature = "trace")]
6307
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
936✔
6308

6309
        // Create the property bind group @2 if needed
6310
        if let Some(property_key) = &effect_batch.property_key {
325✔
6311
            if let Err(err) = property_bind_groups.ensure_exists(
×
6312
                property_key,
6313
                &property_cache,
6314
                &spawner_buffer,
6315
                spawner_buffer_binding_size,
6316
                &render_device,
6317
            ) {
6318
                error!("Failed to create property bind group for effect batch: {err:?}");
×
6319
                continue;
6320
            }
6321
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
897✔
6322
            &property_cache,
598✔
6323
            &spawner_buffer,
598✔
6324
            spawner_buffer_binding_size,
299✔
6325
            &render_device,
299✔
6326
        ) {
6327
            error!("Failed to create property bind group for effect batch: {err:?}");
×
6328
            continue;
6329
        }
6330

6331
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
6332
        // simulate particles.
6333
        if effect_cache
312✔
6334
            .create_particle_sim_bind_group(
6335
                &effect_batch.slab_id,
6336
                &render_device,
6337
                effect_batch.particle_layout.min_binding_size32(),
6338
                effect_batch.parent_min_binding_size,
6339
                effect_batch.parent_binding_source.as_ref(),
6340
            )
6341
            .is_err()
6342
        {
6343
            error!("No particle buffer allocated for effect batch.");
×
6344
            continue;
×
6345
        }
6346

6347
        // Bind group @3 of init pass
6348
        // FIXME - this is instance-dependent, not buffer-dependent
6349
        {
6350
            let consume_gpu_spawn_events = effect_batch
6351
                .layout_flags
6352
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
6353
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
312✔
6354
                effect_batch.spawn_info
6355
            {
6356
                assert!(consume_gpu_spawn_events);
×
6357
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
6358
                Some(ConsumeEventBuffers {
×
6359
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
6360
                    events: BufferSlice {
×
6361
                        buffer: event_cache
×
6362
                            .get_buffer(cached_effect_events.buffer_index)
×
6363
                            .unwrap(),
×
6364
                        // Note: event range is in u32 count, not bytes
6365
                        offset: cached_effect_events.range.start * 4,
×
6366
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
6367
                    },
6368
                })
6369
            } else {
6370
                assert!(!consume_gpu_spawn_events);
624✔
6371
                None
312✔
6372
            };
6373
            let Some(init_metadata_layout) =
312✔
6374
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
6375
            else {
6376
                continue;
×
6377
            };
6378
            if effect_bind_groups
6379
                .get_or_create_init_metadata(
6380
                    effect_batch,
6381
                    &render_device,
6382
                    init_metadata_layout,
6383
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6384
                    consume_event_buffers,
6385
                )
6386
                .is_err()
6387
            {
6388
                continue;
×
6389
            }
6390
        }
6391

6392
        // Bind group @3 of update pass
6393
        // FIXME - this is instance-dependent, not buffer-dependent#
6394
        {
6395
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
6396

6397
            let Some(update_metadata_layout) =
312✔
6398
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
6399
            else {
6400
                continue;
×
6401
            };
6402
            if effect_bind_groups
6403
                .get_or_create_update_metadata(
6404
                    effect_batch,
6405
                    &render_device,
6406
                    update_metadata_layout,
6407
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6408
                    event_cache.child_infos_buffer(),
6409
                    &effect_batch.child_event_buffers[..],
6410
                )
6411
                .is_err()
6412
            {
6413
                continue;
×
6414
            }
6415
        }
6416

6417
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
6418
            let effect_buffer = effect_cache.get_slab(&effect_batch.slab_id).unwrap();
×
6419

6420
            // Bind group @0 of sort-fill pass
6421
            let particle_buffer = effect_buffer.particle_buffer();
×
6422
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6423
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6424
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
6425
                &effect_batch.particle_layout,
×
6426
                particle_buffer,
×
6427
                indirect_index_buffer,
×
6428
                effect_metadata_buffer,
×
NEW
6429
                &spawner_buffer,
×
6430
            ) {
6431
                error!(
6432
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
6433
                    err
6434
                );
6435
                continue;
6436
            }
6437

6438
            // Bind group @0 of sort-copy pass
6439
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
NEW
6440
            if let Err(err) = sort_bind_groups.ensure_sort_copy_bind_group(
×
NEW
6441
                indirect_index_buffer,
×
NEW
6442
                effect_metadata_buffer,
×
NEW
6443
                &spawner_buffer,
×
6444
            ) {
6445
                error!(
6446
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
6447
                    err
6448
                );
6449
                continue;
6450
            }
6451
        }
6452

6453
        // Ensure the particle texture(s) are available as GPU resources and that a bind
6454
        // group for them exists
6455
        // FIXME fix this insert+get below
6456
        if !effect_batch.texture_layout.layout.is_empty() {
312✔
6457
            // This should always be available, as this is cached into the render pipeline
6458
            // just before we start specializing it.
6459
            let Some(material_bind_group_layout) =
×
6460
                render_pipeline.get_material(&effect_batch.texture_layout)
×
6461
            else {
6462
                error!(
×
6463
                    "Failed to find material bind group layout for particle slab #{}",
×
6464
                    effect_batch.slab_id.index()
×
6465
                );
6466
                continue;
×
6467
            };
6468

6469
            // TODO = move
6470
            let material = Material {
6471
                layout: effect_batch.texture_layout.clone(),
6472
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6473
            };
6474
            assert_eq!(material.layout.layout.len(), material.textures.len());
6475

6476
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6477
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6478
                trace!(
×
6479
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
6480
                    material
6481
                );
6482
                continue;
×
6483
            };
6484

6485
            effect_bind_groups
6486
                .material_bind_groups
6487
                .entry(material.clone())
6488
                .or_insert_with(|| {
×
6489
                    debug!("Creating material bind group for material {:?}", material);
×
6490
                    render_device.create_bind_group(
×
6491
                        &format!(
×
6492
                            "hanabi:material_bind_group_{}",
×
6493
                            material.layout.layout.len()
×
6494
                        )[..],
×
6495
                        material_bind_group_layout,
×
6496
                        &bind_group_entries[..],
×
6497
                    )
6498
                });
6499
        }
6500
    }
6501
}
6502

6503
type DrawEffectsSystemState = SystemState<(
6504
    SRes<EffectsMeta>,
6505
    SRes<EffectBindGroups>,
6506
    SRes<PipelineCache>,
6507
    SRes<RenderAssets<RenderMesh>>,
6508
    SRes<MeshAllocator>,
6509
    SQuery<Read<ViewUniformOffset>>,
6510
    SRes<SortedEffectBatches>,
6511
    SQuery<Read<EffectDrawBatch>>,
6512
)>;
6513

6514
/// Draw function for rendering all active effects for the current frame.
6515
///
6516
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6517
/// and the [`Transparent3d`] phase of the main 3D pass.
6518
pub(crate) struct DrawEffects {
6519
    params: DrawEffectsSystemState,
6520
}
6521

6522
impl DrawEffects {
6523
    pub fn new(world: &mut World) -> Self {
12✔
6524
        Self {
6525
            params: SystemState::new(world),
12✔
6526
        }
6527
    }
6528
}
6529

6530
/// Draw all particles of a single effect in view, in 2D or 3D.
6531
///
6532
/// FIXME: use pipeline ID to look up which group index it is.
6533
fn draw<'w>(
311✔
6534
    world: &'w World,
6535
    pass: &mut TrackedRenderPass<'w>,
6536
    view: Entity,
6537
    entity: (Entity, MainEntity),
6538
    pipeline_id: CachedRenderPipelineId,
6539
    params: &mut DrawEffectsSystemState,
6540
) {
6541
    let (
×
6542
        effects_meta,
311✔
6543
        effect_bind_groups,
311✔
6544
        pipeline_cache,
311✔
6545
        meshes,
311✔
6546
        mesh_allocator,
311✔
6547
        views,
311✔
6548
        sorted_effect_batches,
311✔
6549
        effect_draw_batches,
311✔
6550
    ) = params.get(world);
622✔
6551
    let view_uniform = views.get(view).unwrap();
1,555✔
6552
    let effects_meta = effects_meta.into_inner();
933✔
6553
    let effect_bind_groups = effect_bind_groups.into_inner();
933✔
6554
    let meshes = meshes.into_inner();
933✔
6555
    let mesh_allocator = mesh_allocator.into_inner();
933✔
6556
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
1,555✔
6557
    let effect_batch = sorted_effect_batches
933✔
6558
        .get(effect_draw_batch.effect_batch_index)
311✔
6559
        .unwrap();
6560

6561
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
933✔
6562
        return;
×
6563
    };
6564

6565
    trace!("render pass");
311✔
6566

6567
    pass.set_render_pipeline(pipeline);
×
6568

6569
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
311✔
6570
        return;
×
6571
    };
6572
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
311✔
6573
        return;
×
6574
    };
6575

6576
    // Vertex buffer containing the particle model to draw. Generally a quad.
6577
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6578
    // "base_vertex" in the indirect struct...
6579
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6580

6581
    // View properties (camera matrix, etc.)
6582
    pass.set_bind_group(
×
6583
        0,
6584
        effects_meta.view_bind_group.as_ref().unwrap(),
×
6585
        &[view_uniform.offset],
×
6586
    );
6587

6588
    // Particles buffer
6589
    let spawner_base = effect_batch.spawner_base;
×
6590
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
6591
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6592
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
622✔
6593
    pass.set_bind_group(
622✔
6594
        1,
6595
        effect_bind_groups
622✔
6596
            .particle_render(&effect_batch.slab_id)
622✔
6597
            .unwrap(),
311✔
6598
        &[spawner_offset],
311✔
6599
    );
6600

6601
    // Particle texture
6602
    // TODO = move
6603
    let material = Material {
6604
        layout: effect_batch.texture_layout.clone(),
622✔
6605
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
933✔
6606
    };
6607
    if !effect_batch.texture_layout.layout.is_empty() {
311✔
6608
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6609
            pass.set_bind_group(2, bind_group, &[]);
×
6610
        } else {
6611
            // Texture(s) not ready; skip this drawing for now
6612
            trace!(
×
6613
                "Particle material bind group not available for batch slab_id={}. Skipping draw call.",
×
6614
                effect_batch.slab_id.index(),
×
6615
            );
6616
            return;
×
6617
        }
6618
    }
6619

6620
    let draw_indirect_index = effect_batch.draw_indirect_buffer_row_index.0;
311✔
6621
    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
×
6622
    let draw_indirect_offset =
311✔
6623
        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
311✔
6624
    trace!(
311✔
6625
        "Draw up to {} particles with {} vertices per particle for batch from particle slab #{} \
311✔
6626
            (effect_metadata_index={}, draw_indirect_offset={}B).",
311✔
6627
        effect_batch.slice.len(),
622✔
6628
        render_mesh.vertex_count,
×
6629
        effect_batch.slab_id.index(),
622✔
6630
        draw_indirect_index,
×
6631
        draw_indirect_offset,
×
6632
    );
6633

6634
    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
622✔
6635
        trace!(
×
6636
            "The draw indirect buffer containing the indirect draw args is not ready for batch slab_id=#{}. Skipping draw call.",
×
6637
            effect_batch.slab_id.index(),
×
6638
        );
6639
        return;
×
6640
    };
6641

6642
    match render_mesh.buffer_info {
×
6643
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
311✔
6644
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
311✔
6645
            else {
×
6646
                trace!(
×
6647
                    "The index buffer for indexed rendering is not ready for batch slab_id=#{}. Skipping draw call.",
×
6648
                    effect_batch.slab_id.index(),
×
6649
                );
6650
                return;
×
6651
            };
6652

6653
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6654
            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
×
6655
        }
6656
        RenderMeshBufferInfo::NonIndexed => {
×
6657
            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
×
6658
        }
6659
    }
6660
}
6661

6662
#[cfg(feature = "2d")]
6663
impl Draw<Transparent2d> for DrawEffects {
6664
    fn draw<'w>(
×
6665
        &mut self,
6666
        world: &'w World,
6667
        pass: &mut TrackedRenderPass<'w>,
6668
        view: Entity,
6669
        item: &Transparent2d,
6670
    ) -> Result<(), DrawError> {
6671
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6672
        draw(
6673
            world,
×
6674
            pass,
×
6675
            view,
×
6676
            item.entity,
×
6677
            item.pipeline,
×
6678
            &mut self.params,
×
6679
        );
6680
        Ok(())
×
6681
    }
6682
}
6683

6684
#[cfg(feature = "3d")]
6685
impl Draw<Transparent3d> for DrawEffects {
6686
    fn draw<'w>(
311✔
6687
        &mut self,
6688
        world: &'w World,
6689
        pass: &mut TrackedRenderPass<'w>,
6690
        view: Entity,
6691
        item: &Transparent3d,
6692
    ) -> Result<(), DrawError> {
6693
        trace!("Draw<Transparent3d>: view={:?}", view);
622✔
6694
        draw(
6695
            world,
311✔
6696
            pass,
311✔
6697
            view,
311✔
6698
            item.entity,
311✔
6699
            item.pipeline,
311✔
6700
            &mut self.params,
311✔
6701
        );
6702
        Ok(())
311✔
6703
    }
6704
}
6705

6706
#[cfg(feature = "3d")]
6707
impl Draw<AlphaMask3d> for DrawEffects {
6708
    fn draw<'w>(
×
6709
        &mut self,
6710
        world: &'w World,
6711
        pass: &mut TrackedRenderPass<'w>,
6712
        view: Entity,
6713
        item: &AlphaMask3d,
6714
    ) -> Result<(), DrawError> {
6715
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6716
        draw(
6717
            world,
×
6718
            pass,
×
6719
            view,
×
6720
            item.representative_entity,
×
6721
            item.batch_set_key.pipeline,
×
6722
            &mut self.params,
×
6723
        );
6724
        Ok(())
×
6725
    }
6726
}
6727

6728
#[cfg(feature = "3d")]
6729
impl Draw<Opaque3d> for DrawEffects {
6730
    fn draw<'w>(
×
6731
        &mut self,
6732
        world: &'w World,
6733
        pass: &mut TrackedRenderPass<'w>,
6734
        view: Entity,
6735
        item: &Opaque3d,
6736
    ) -> Result<(), DrawError> {
6737
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6738
        draw(
6739
            world,
×
6740
            pass,
×
6741
            view,
×
6742
            item.representative_entity,
×
6743
            item.batch_set_key.pipeline,
×
6744
            &mut self.params,
×
6745
        );
6746
        Ok(())
×
6747
    }
6748
}
6749

6750
/// Render node to run the simulation sub-graph once per frame.
6751
///
6752
/// This node doesn't simulate anything by itself, but instead schedules the
6753
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6754
/// actual simulation.
6755
///
6756
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6757
/// renders all the views, such that rendered views have access to the
6758
/// just-simulated particles to render them.
6759
///
6760
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6761
pub(crate) struct VfxSimulateDriverNode;
6762

6763
impl Node for VfxSimulateDriverNode {
6764
    fn run(
330✔
6765
        &self,
6766
        graph: &mut RenderGraphContext,
6767
        _render_context: &mut RenderContext,
6768
        _world: &World,
6769
    ) -> Result<(), NodeRunError> {
6770
        graph.run_sub_graph(
660✔
6771
            crate::plugin::simulate_graph::HanabiSimulateGraph,
330✔
6772
            vec![],
330✔
6773
            None,
330✔
6774
        )?;
6775
        Ok(())
330✔
6776
    }
6777
}
6778

6779
#[derive(Debug, Clone, PartialEq, Eq)]
6780
enum HanabiPipelineId {
6781
    Invalid,
6782
    Cached(CachedComputePipelineId),
6783
}
6784

6785
#[derive(Debug)]
6786
pub(crate) enum ComputePipelineError {
6787
    Queued,
6788
    Creating,
6789
    Error,
6790
}
6791

6792
impl From<&CachedPipelineState> for ComputePipelineError {
6793
    fn from(value: &CachedPipelineState) -> Self {
×
6794
        match value {
×
6795
            CachedPipelineState::Queued => Self::Queued,
×
6796
            CachedPipelineState::Creating(_) => Self::Creating,
×
6797
            CachedPipelineState::Err(_) => Self::Error,
×
6798
            _ => panic!("Trying to convert Ok state to error."),
×
6799
        }
6800
    }
6801
}
6802

6803
pub(crate) struct HanabiComputePass<'a> {
6804
    /// Pipeline cache to fetch cached compute pipelines by ID.
6805
    pipeline_cache: &'a PipelineCache,
6806
    /// WGPU compute pass.
6807
    compute_pass: ComputePass<'a>,
6808
    /// Current pipeline (cached).
6809
    pipeline_id: HanabiPipelineId,
6810
}
6811

6812
impl<'a> Deref for HanabiComputePass<'a> {
6813
    type Target = ComputePass<'a>;
6814

6815
    fn deref(&self) -> &Self::Target {
×
6816
        &self.compute_pass
×
6817
    }
6818
}
6819

6820
impl DerefMut for HanabiComputePass<'_> {
6821
    fn deref_mut(&mut self) -> &mut Self::Target {
4,308✔
6822
        &mut self.compute_pass
4,308✔
6823
    }
6824
}
6825

6826
impl<'a> HanabiComputePass<'a> {
6827
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
1,248✔
6828
        Self {
6829
            pipeline_cache,
6830
            compute_pass,
6831
            pipeline_id: HanabiPipelineId::Invalid,
6832
        }
6833
    }
6834

6835
    pub fn set_cached_compute_pipeline(
921✔
6836
        &mut self,
6837
        pipeline_id: CachedComputePipelineId,
6838
    ) -> Result<(), ComputePipelineError> {
6839
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
921✔
6840
            trace!("set_cached_compute_pipeline() id={pipeline_id:?} -> already set; skipped");
×
6841
            return Ok(());
×
6842
        }
6843
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
921✔
6844
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
921✔
6845
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6846
            if let CachedPipelineState::Err(err) = state {
×
6847
                error!(
×
6848
                    "Failed to find compute pipeline #{}: {:?}",
×
6849
                    pipeline_id.id(),
×
6850
                    err
×
6851
                );
6852
            } else {
6853
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6854
            }
6855
            return Err(state.into());
×
6856
        };
6857
        self.compute_pass.set_pipeline(pipeline);
×
6858
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6859
        Ok(())
×
6860
    }
6861
}
6862

6863
/// Render node to run the simulation of all effects once per frame.
6864
///
6865
/// Runs inside the simulation sub-graph, looping over all extracted effect
6866
/// batches to simulate them.
6867
pub(crate) struct VfxSimulateNode {}
6868

6869
impl VfxSimulateNode {
6870
    /// Create a new node for simulating the effects of the given world.
6871
    pub fn new(_world: &mut World) -> Self {
3✔
6872
        Self {}
6873
    }
6874

6875
    /// Begin a new compute pass and return a wrapper with extra
6876
    /// functionalities.
6877
    pub fn begin_compute_pass<'encoder>(
1,248✔
6878
        &self,
6879
        label: &str,
6880
        pipeline_cache: &'encoder PipelineCache,
6881
        render_context: &'encoder mut RenderContext,
6882
    ) -> HanabiComputePass<'encoder> {
6883
        let compute_pass =
1,248✔
6884
            render_context
1,248✔
6885
                .command_encoder()
6886
                .begin_compute_pass(&ComputePassDescriptor {
2,496✔
6887
                    label: Some(label),
1,248✔
6888
                    timestamp_writes: None,
1,248✔
6889
                });
6890
        HanabiComputePass::new(pipeline_cache, compute_pass)
3,744✔
6891
    }
6892
}
6893

6894
impl Node for VfxSimulateNode {
6895
    fn input(&self) -> Vec<SlotInfo> {
3✔
6896
        vec![]
3✔
6897
    }
6898

6899
    fn update(&mut self, _world: &mut World) {}
660✔
6900

6901
    fn run(
330✔
6902
        &self,
6903
        _graph: &mut RenderGraphContext,
6904
        render_context: &mut RenderContext,
6905
        world: &World,
6906
    ) -> Result<(), NodeRunError> {
6907
        trace!("VfxSimulateNode::run()");
650✔
6908

6909
        let pipeline_cache = world.resource::<PipelineCache>();
990✔
6910
        let effects_meta = world.resource::<EffectsMeta>();
990✔
6911
        let effect_bind_groups = world.resource::<EffectBindGroups>();
990✔
6912
        let property_bind_groups = world.resource::<PropertyBindGroups>();
990✔
6913
        let sort_bind_groups = world.resource::<SortBindGroups>();
990✔
6914
        let utils_pipeline = world.resource::<UtilsPipeline>();
990✔
6915
        let effect_cache = world.resource::<EffectCache>();
990✔
6916
        let event_cache = world.resource::<EventCache>();
990✔
6917
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
990✔
6918
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
990✔
6919
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
990✔
6920

6921
        // Make sure to schedule any buffer copy before accessing their content later in
6922
        // the GPU commands below.
6923
        {
6924
            let command_encoder = render_context.command_encoder();
1,320✔
6925
            effects_meta
660✔
6926
                .dispatch_indirect_buffer
660✔
6927
                .write_buffers(command_encoder);
990✔
6928
            effects_meta
660✔
6929
                .draw_indirect_buffer
660✔
6930
                .write_buffer(command_encoder);
990✔
6931
            effects_meta
660✔
6932
                .effect_metadata_buffer
660✔
6933
                .write_buffer(command_encoder);
990✔
6934
            event_cache.write_buffers(command_encoder);
1,320✔
6935
            sort_bind_groups.write_buffers(command_encoder);
660✔
6936
        }
6937

6938
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6939
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6940
        // the update pass of their parent effect during the previous frame.
6941
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
330✔
6942
            gpu_buffer_operations.dispatch(
6943
                *queue_index,
6944
                render_context,
6945
                utils_pipeline,
6946
                Some("hanabi:init_indirect_fill_dispatch"),
6947
            );
6948
        }
6949

6950
        // If there's no batch, there's nothing more to do. Avoid continuing because
6951
        // some GPU resources are missing, which is expected when there's no effect but
6952
        // is an error (and will log warnings/errors) otherwise.
6953
        if sorted_effect_batches.is_empty() {
660✔
6954
            return Ok(());
18✔
6955
        }
6956

6957
        // Compute init pass
6958
        {
6959
            trace!("init: loop over effect batches...");
312✔
6960

6961
            let mut compute_pass =
6962
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6963

6964
            // Bind group simparams@0 is common to everything, only set once per init pass
6965
            compute_pass.set_bind_group(
6966
                0,
6967
                effects_meta
6968
                    .indirect_sim_params_bind_group
6969
                    .as_ref()
6970
                    .unwrap(),
6971
                &[],
6972
            );
6973

6974
            // Dispatch init compute jobs for all batches
6975
            for effect_batch in sorted_effect_batches.iter() {
312✔
6976
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6977
                // batch. Note that this hopefully should have been skipped earlier.
6978
                {
6979
                    let use_indirect_dispatch = effect_batch
624✔
6980
                        .layout_flags
312✔
6981
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
312✔
6982
                    match effect_batch.spawn_info {
312✔
6983
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
312✔
6984
                            assert!(!use_indirect_dispatch);
6985
                            if total_spawn_count == 0 {
312✔
6986
                                continue;
15✔
6987
                            }
6988
                        }
6989
                        BatchSpawnInfo::GpuSpawner { .. } => {
6990
                            assert!(use_indirect_dispatch);
×
6991
                        }
6992
                    }
6993
                }
6994

6995
                // Fetch bind group particle@1
6996
                let Some(particle_bind_group) =
297✔
6997
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
297✔
6998
                else {
6999
                    error!(
×
7000
                        "Failed to find init particle@1 bind group for slab #{}",
×
7001
                        effect_batch.slab_id.index()
×
7002
                    );
7003
                    continue;
×
7004
                };
7005

7006
                // Fetch bind group metadata@3
7007
                let Some(metadata_bind_group) = effect_bind_groups
297✔
7008
                    .init_metadata_bind_groups
7009
                    .get(&effect_batch.slab_id)
7010
                else {
7011
                    error!(
×
7012
                        "Failed to find init metadata@3 bind group for slab #{}",
×
7013
                        effect_batch.slab_id.index()
×
7014
                    );
7015
                    continue;
×
7016
                };
7017

7018
                if compute_pass
7019
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
7020
                    .is_err()
7021
                {
7022
                    continue;
×
7023
                }
7024

7025
                // Compute dynamic offsets
7026
                let spawner_base = effect_batch.spawner_base;
7027
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7028
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
7029
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
594✔
7030
                let property_offset = effect_batch.property_offset;
594✔
7031

7032
                // Setup init pass
7033
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
891✔
7034
                let offsets = if let Some(property_offset) = property_offset {
594✔
7035
                    vec![spawner_offset, property_offset]
7036
                } else {
7037
                    vec![spawner_offset]
594✔
7038
                };
7039
                compute_pass.set_bind_group(
891✔
7040
                    2,
7041
                    property_bind_groups
594✔
7042
                        .get(effect_batch.property_key.as_ref())
1,188✔
7043
                        .unwrap(),
594✔
7044
                    &offsets[..],
297✔
7045
                );
7046
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
891✔
7047

7048
                // Dispatch init job
7049
                match effect_batch.spawn_info {
297✔
7050
                    // Indirect dispatch via GPU spawn events
7051
                    BatchSpawnInfo::GpuSpawner {
7052
                        init_indirect_dispatch_index,
×
7053
                        ..
7054
                    } => {
7055
                        assert!(effect_batch
×
7056
                            .layout_flags
×
7057
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
7058

7059
                        // Note: the indirect offset of a dispatch workgroup only needs
7060
                        // 4-byte alignment
7061
                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
×
7062
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
7063

7064
                        trace!(
×
7065
                            "record commands for indirect init pipeline of effect {:?} \
×
7066
                                init_indirect_dispatch_index={} \
×
7067
                                indirect_offset={} \
×
7068
                                spawner_base={} \
×
7069
                                spawner_offset={} \
×
7070
                                property_key={:?}...",
×
7071
                            effect_batch.handle,
7072
                            init_indirect_dispatch_index,
7073
                            indirect_offset,
7074
                            spawner_base,
7075
                            spawner_offset,
7076
                            effect_batch.property_key,
7077
                        );
7078

7079
                        compute_pass.dispatch_workgroups_indirect(
×
7080
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
7081
                            indirect_offset,
×
7082
                        );
7083
                    }
7084

7085
                    // Direct dispatch via CPU spawn count
7086
                    BatchSpawnInfo::CpuSpawner {
7087
                        total_spawn_count: spawn_count,
297✔
7088
                    } => {
7089
                        assert!(!effect_batch
7090
                            .layout_flags
7091
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
7092

7093
                        const WORKGROUP_SIZE: u32 = 64;
7094
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
297✔
7095

7096
                        trace!(
7097
                            "record commands for init pipeline of effect {:?} \
297✔
7098
                                (spawn {} particles => {} workgroups) spawner_base={} \
297✔
7099
                                spawner_offset={} \
297✔
7100
                                property_key={:?}...",
297✔
7101
                            effect_batch.handle,
7102
                            spawn_count,
7103
                            workgroup_count,
7104
                            spawner_base,
7105
                            spawner_offset,
7106
                            effect_batch.property_key,
7107
                        );
7108

7109
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7110
                    }
7111
                }
7112

7113
                trace!("init compute dispatched");
594✔
7114
            }
7115
        }
7116

7117
        // Compute indirect dispatch pass
7118
        if effects_meta.spawner_buffer.buffer().is_some()
312✔
7119
            && !effects_meta.spawner_buffer.is_empty()
312✔
7120
            && effects_meta.indirect_metadata_bind_group.is_some()
312✔
7121
            && effects_meta.indirect_sim_params_bind_group.is_some()
624✔
7122
        {
7123
            // Only start a compute pass if there's an effect; makes things clearer in
7124
            // debugger.
7125
            let mut compute_pass =
312✔
7126
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
1,560✔
7127

7128
            // Dispatch indirect dispatch compute job
7129
            trace!("record commands for indirect dispatch pipeline...");
624✔
7130

7131
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
624✔
7132
            if has_gpu_spawn_events {
312✔
7133
                if let Some(indirect_child_info_buffer_bind_group) =
×
7134
                    event_cache.indirect_child_info_buffer_bind_group()
×
7135
                {
7136
                    assert!(has_gpu_spawn_events);
7137
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
7138
                } else {
7139
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
7140
                    // render_context
7141
                    //     .command_encoder()
7142
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
7143
                    // FIXME - Bevy doesn't allow returning custom errors here...
7144
                    return Ok(());
×
7145
                }
7146
            }
7147

7148
            if compute_pass
312✔
7149
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
7150
                .is_err()
7151
            {
7152
                // FIXME - Bevy doesn't allow returning custom errors here...
7153
                return Ok(());
×
7154
            }
7155

7156
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
7157
            // the size exluding gaps!");
7158
            const WORKGROUP_SIZE: u32 = 64;
7159
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
7160
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
7161
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
7162

7163
            // Setup vfx_indirect pass
7164
            compute_pass.set_bind_group(
7165
                0,
7166
                effects_meta
7167
                    .indirect_sim_params_bind_group
7168
                    .as_ref()
7169
                    .unwrap(),
7170
                &[],
7171
            );
7172
            compute_pass.set_bind_group(
7173
                1,
7174
                // FIXME - got some unwrap() panic here, investigate... possibly race
7175
                // condition!
7176
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
7177
                &[],
7178
            );
7179
            compute_pass.set_bind_group(
7180
                2,
7181
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
7182
                &[],
7183
            );
7184
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7185
            trace!(
7186
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
312✔
7187
                total_effect_count,
7188
                workgroup_count
7189
            );
7190
        }
7191

7192
        // Compute update pass
7193
        {
7194
            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
624✔
7195
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
7196
                render_context
×
7197
                    .command_encoder()
7198
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
7199
                // FIXME - Bevy doesn't allow returning custom errors here...
7200
                return Ok(());
×
7201
            };
7202

7203
            let mut compute_pass =
7204
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
7205

7206
            // Bind group simparams@0 is common to everything, only set once per update pass
7207
            compute_pass.set_bind_group(
7208
                0,
7209
                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
7210
                &[],
7211
            );
7212

7213
            // Dispatch update compute jobs
7214
            for effect_batch in sorted_effect_batches.iter() {
312✔
7215
                // Fetch bind group particle@1
7216
                let Some(particle_bind_group) =
312✔
7217
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
624✔
7218
                else {
7219
                    error!(
×
7220
                        "Failed to find update particle@1 bind group for slab #{}",
×
7221
                        effect_batch.slab_id.index()
×
7222
                    );
7223
                    compute_pass.insert_debug_marker("ERROR:MissingParticleSimBindGroup");
×
7224
                    continue;
×
7225
                };
7226

7227
                // Fetch bind group metadata@3
7228
                let Some(metadata_bind_group) = effect_bind_groups
312✔
7229
                    .update_metadata_bind_groups
7230
                    .get(&effect_batch.slab_id)
7231
                else {
7232
                    error!(
×
7233
                        "Failed to find update metadata@3 bind group for slab #{}",
×
7234
                        effect_batch.slab_id.index()
×
7235
                    );
7236
                    compute_pass.insert_debug_marker("ERROR:MissingMetadataBindGroup");
×
7237
                    continue;
×
7238
                };
7239

7240
                // Fetch compute pipeline
7241
                if let Err(err) = compute_pass
×
7242
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
7243
                {
7244
                    compute_pass.insert_debug_marker(&format!(
7245
                        "ERROR:FailedToSetCachedUpdatePipeline:{:?}",
7246
                        err
7247
                    ));
7248
                    continue;
7249
                }
7250

7251
                // Compute dynamic offsets
7252
                let spawner_base = effect_batch.spawner_base;
624✔
7253
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
936✔
7254
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
936✔
7255
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
312✔
7256
                let property_offset = effect_batch.property_offset;
7257

7258
                trace!(
7259
                    "record commands for update pipeline of effect {:?} spawner_base={}",
312✔
7260
                    effect_batch.handle,
7261
                    spawner_base,
7262
                );
7263

7264
                // Setup update pass
7265
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
7266
                let offsets = if let Some(property_offset) = property_offset {
13✔
7267
                    vec![spawner_offset, property_offset]
7268
                } else {
7269
                    vec![spawner_offset]
598✔
7270
                };
7271
                compute_pass.set_bind_group(
7272
                    2,
7273
                    property_bind_groups
7274
                        .get(effect_batch.property_key.as_ref())
7275
                        .unwrap(),
7276
                    &offsets[..],
7277
                );
7278
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
7279

7280
                // Dispatch update job
7281
                let dispatch_indirect_offset = effect_batch
7282
                    .dispatch_buffer_indices
7283
                    .update_dispatch_indirect_buffer_row_index
7284
                    * 12;
7285
                trace!(
7286
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
312✔
7287
                    indirect_buffer,
7288
                    dispatch_indirect_offset,
7289
                );
7290
                compute_pass
7291
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
7292

7293
                trace!("update compute dispatched");
312✔
7294
            }
7295
        }
7296

7297
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
7298
        // batch of particles which needs sorting, based on the actual number of alive
7299
        // particles in the batch after their update in the compute update pass. Since
7300
        // particles may die during update, this may be different from the number of
7301
        // particles updated.
7302
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
312✔
7303
            gpu_buffer_operations.dispatch(
7304
                *queue_index,
7305
                render_context,
7306
                utils_pipeline,
7307
                Some("hanabi:sort_fill_dispatch"),
7308
            );
7309
        }
7310

7311
        // Compute sort pass
7312
        {
7313
            let mut compute_pass =
7314
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
7315

7316
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
7317
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
7318

7319
            // Loop on batches and find those which need sorting
7320
            for effect_batch in sorted_effect_batches.iter() {
312✔
7321
                trace!("Processing effect batch for sorting...");
624✔
7322
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
312✔
7323
                    continue;
312✔
7324
                }
7325
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
7326
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
7327

7328
                let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
×
7329
                    warn!("Missing sort-fill effect buffer.");
×
7330
                    // render_context
7331
                    //     .command_encoder()
7332
                    //     .insert_debug_marker("ERROR:MissingEffectBatchBuffer");
7333
                    continue;
×
7334
                };
7335

7336
                let indirect_dispatch_index = *effect_batch
7337
                    .sort_fill_indirect_dispatch_index
7338
                    .as_ref()
7339
                    .unwrap();
7340
                let indirect_offset =
7341
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
7342

7343
                // Fill the sort buffer with the key-value pairs to sort
7344
                {
7345
                    compute_pass.push_debug_group("hanabi:sort_fill");
7346

7347
                    // Fetch compute pipeline
7348
                    let Some(pipeline_id) =
×
7349
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
7350
                    else {
7351
                        warn!("Missing sort-fill pipeline.");
×
7352
                        compute_pass.insert_debug_marker("ERROR:MissingSortFillPipeline");
×
7353
                        continue;
×
7354
                    };
7355
                    if compute_pass
7356
                        .set_cached_compute_pipeline(pipeline_id)
7357
                        .is_err()
7358
                    {
7359
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortFillPipeline");
×
7360
                        compute_pass.pop_debug_group();
×
7361
                        // FIXME - Bevy doesn't allow returning custom errors here...
7362
                        return Ok(());
×
7363
                    }
7364

7365
                    let spawner_base = effect_batch.spawner_base;
7366
                    let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7367
                    assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
NEW
7368
                    let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
7369

7370
                    // Bind group sort_fill@0
UNCOV
7371
                    let particle_buffer = effect_buffer.particle_buffer();
×
UNCOV
7372
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
7373
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
UNCOV
7374
                        particle_buffer.id(),
×
UNCOV
7375
                        indirect_index_buffer.id(),
×
UNCOV
7376
                        effect_metadata_buffer.id(),
×
7377
                    ) else {
7378
                        warn!("Missing sort-fill bind group.");
×
7379
                        compute_pass.insert_debug_marker("ERROR:MissingSortFillBindGroup");
×
7380
                        continue;
×
7381
                    };
7382
                    let effect_metadata_offset = effects_meta
7383
                        .gpu_limits
7384
                        .effect_metadata_offset(effect_batch.metadata_table_id.0)
7385
                        as u32;
7386
                    compute_pass.set_bind_group(
7387
                        0,
7388
                        bind_group,
7389
                        &[effect_metadata_offset, spawner_offset],
7390
                    );
7391

7392
                    compute_pass
7393
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7394
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
7395

7396
                    compute_pass.pop_debug_group();
7397
                }
7398

7399
                // Do the actual sort
7400
                {
7401
                    compute_pass.push_debug_group("hanabi:sort");
7402

7403
                    if compute_pass
7404
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
7405
                        .is_err()
7406
                    {
7407
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortPipeline");
×
7408
                        compute_pass.pop_debug_group();
×
7409
                        // FIXME - Bevy doesn't allow returning custom errors here...
7410
                        return Ok(());
×
7411
                    }
7412

7413
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
7414
                    compute_pass
7415
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7416
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7417

7418
                    compute_pass.pop_debug_group();
7419
                }
7420

7421
                // Copy the sorted particle indices back into the indirect index buffer, where
7422
                // the render pass will read them.
7423
                {
7424
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7425

7426
                    // Fetch compute pipeline
7427
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7428
                    if compute_pass
7429
                        .set_cached_compute_pipeline(pipeline_id)
7430
                        .is_err()
7431
                    {
7432
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortCopyPipeline");
×
UNCOV
7433
                        compute_pass.pop_debug_group();
×
7434
                        // FIXME - Bevy doesn't allow returning custom errors here...
UNCOV
7435
                        return Ok(());
×
7436
                    }
7437

7438
                    let spawner_base = effect_batch.spawner_base;
7439
                    let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7440
                    assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
NEW
7441
                    let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
7442

7443
                    // Bind group sort_copy@0
7444
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
7445
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
7446
                        indirect_index_buffer.id(),
7447
                        effect_metadata_buffer.id(),
7448
                    ) else {
7449
                        warn!("Missing sort-copy bind group.");
×
7450
                        compute_pass.insert_debug_marker("ERROR:MissingSortCopyBindGroup");
×
7451
                        continue;
×
7452
                    };
7453
                    let effect_metadata_offset = effects_meta
7454
                        .effect_metadata_buffer
7455
                        .dynamic_offset(effect_batch.metadata_table_id);
7456
                    compute_pass.set_bind_group(
7457
                        0,
7458
                        bind_group,
7459
                        &[effect_metadata_offset, spawner_offset],
7460
                    );
7461

7462
                    compute_pass
7463
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7464
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7465

7466
                    compute_pass.pop_debug_group();
7467
                }
7468
            }
7469
        }
7470

7471
        Ok(())
312✔
7472
    }
7473
}
7474

7475
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7476
    fn from(layout_flags: LayoutFlags) -> Self {
936✔
7477
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
1,872✔
7478
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7479
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
936✔
7480
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7481
        } else {
7482
            ParticleRenderAlphaMaskPipelineKey::Blend
936✔
7483
        }
7484
    }
7485
}
7486

7487
#[cfg(test)]
7488
mod tests {
7489
    use super::*;
7490

7491
    #[test]
7492
    fn layout_flags() {
7493
        let flags = LayoutFlags::default();
7494
        assert_eq!(flags, LayoutFlags::NONE);
7495
    }
7496

7497
    #[cfg(feature = "gpu_tests")]
7498
    #[test]
7499
    fn gpu_limits() {
7500
        use crate::test_utils::MockRenderer;
7501

7502
        let renderer = MockRenderer::new();
7503
        let device = renderer.device();
7504
        let limits = GpuLimits::from_device(&device);
7505

7506
        // assert!(limits.storage_buffer_align().get() >= 1);
7507
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7508
    }
7509

7510
    #[cfg(feature = "gpu_tests")]
7511
    #[test]
7512
    fn gpu_ops_ifda() {
7513
        use crate::test_utils::MockRenderer;
7514

7515
        let renderer = MockRenderer::new();
7516
        let device = renderer.device();
7517
        let render_queue = renderer.queue();
7518

7519
        let mut world = World::new();
7520
        world.insert_resource(device.clone());
7521
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7522

7523
        let src_buffer = device.create_buffer(&BufferDescriptor {
7524
            label: None,
7525
            size: 256,
7526
            usage: BufferUsages::STORAGE,
7527
            mapped_at_creation: false,
7528
        });
7529
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7530
            label: None,
7531
            size: 256,
7532
            usage: BufferUsages::STORAGE,
7533
            mapped_at_creation: false,
7534
        });
7535

7536
        // Two consecutive ops can be merged. This includes having contiguous slices
7537
        // both in source and destination.
7538
        buffer_ops.begin_frame();
7539
        {
7540
            let mut q = InitFillDispatchQueue::default();
7541
            q.enqueue(0, 0);
7542
            assert_eq!(q.queue.len(), 1);
7543
            q.enqueue(1, 1);
7544
            // Ops are not batched yet
7545
            assert_eq!(q.queue.len(), 2);
7546
            // On submit, the ops get batched together
7547
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7548
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7549
        }
7550
        buffer_ops.end_frame(&device, &render_queue);
7551

7552
        // Even if out of order, the init fill dispatch ops are batchable. Here the
7553
        // offsets are enqueued inverted.
7554
        buffer_ops.begin_frame();
7555
        {
7556
            let mut q = InitFillDispatchQueue::default();
7557
            q.enqueue(1, 1);
7558
            assert_eq!(q.queue.len(), 1);
7559
            q.enqueue(0, 0);
7560
            // Ops are not batched yet
7561
            assert_eq!(q.queue.len(), 2);
7562
            // On submit, the ops get batched together
7563
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7564
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7565
        }
7566
        buffer_ops.end_frame(&device, &render_queue);
7567

7568
        // However, both the source and destination need to be contiguous at the same
7569
        // time. Here they are mixed so we can't batch.
7570
        buffer_ops.begin_frame();
7571
        {
7572
            let mut q = InitFillDispatchQueue::default();
7573
            q.enqueue(0, 1);
7574
            assert_eq!(q.queue.len(), 1);
7575
            q.enqueue(1, 0);
7576
            // Ops are not batched yet
7577
            assert_eq!(q.queue.len(), 2);
7578
            // On submit, the ops cannot get batched together
7579
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7580
            assert_eq!(buffer_ops.args_buffer.len(), 2);
7581
        }
7582
        buffer_ops.end_frame(&device, &render_queue);
7583
    }
7584
}
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