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

djeedai / bevy_hanabi / 18260856969

05 Oct 2025 03:39PM UTC coverage: 66.606% (+0.03%) from 66.58%
18260856969

push

github

web-flow
Upgrade to Bevy v0.17 (#502)

31 of 38 new or added lines in 11 files covered. (81.58%)

2 existing lines in 1 file now uncovered.

5120 of 7687 relevant lines covered (66.61%)

148.45 hits per line

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

63.42
/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 {
53✔
311
        NonZeroU64::new(T::min_size().get().next_multiple_of(alignment as u64)).unwrap()
318✔
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
}
365

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

383
impl Default for GpuDispatchIndirectArgs {
384
    fn default() -> Self {
×
385
        Self { x: 0, y: 1, z: 1 }
386
    }
387
}
388

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

407
impl Default for GpuDrawIndirectArgs {
408
    fn default() -> Self {
×
409
        Self {
410
            vertex_count: 0,
411
            instance_count: 1,
412
            first_vertex: 0,
413
            first_instance: 0,
414
        }
415
    }
416
}
417

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

437
impl Default for GpuDrawIndexedIndirectArgs {
438
    fn default() -> Self {
×
439
        Self {
440
            index_count: 0,
441
            instance_count: 1,
442
            first_index: 0,
443
            base_vertex: 0,
444
            first_instance: 0,
445
        }
446
    }
447
}
448

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

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

498
    /// Particle stride, in number of u32.
499
    pub particle_stride: u32,
500
    /// Offset from the particle start to the first sort key, in number of u32.
501
    pub sort_key_offset: u32,
502
    /// Offset from the particle start to the second sort key, in number of u32.
503
    pub sort_key2_offset: u32,
504

505
    //
506
    // Again some runtime-only GPU-mutated data
507
    /// Atomic counter incremented each time a particle spawns. Useful for
508
    /// things like RIBBON_ID or any other use where a unique value is needed.
509
    /// The value loops back after some time, but unless some particle lives
510
    /// forever there's little chance of repetition.
511
    pub particle_counter: u32,
512
}
513

514
/// Single init fill dispatch item in an [`InitFillDispatchQueue`].
515
#[derive(Debug)]
516
pub(super) struct InitFillDispatchItem {
517
    /// Index of the source [`GpuChildInfo`] entry to read the event count from.
518
    pub global_child_index: u32,
519
    /// Index of the [`GpuDispatchIndirect`] entry to write the workgroup count
520
    /// to.
521
    pub dispatch_indirect_index: u32,
522
}
523

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

537
impl InitFillDispatchQueue {
538
    /// Clear the queue.
539
    #[inline]
540
    pub fn clear(&mut self) {
330✔
541
        self.queue.clear();
660✔
542
        self.submitted_queue_index = None;
330✔
543
    }
544

545
    /// Check if the queue is empty.
546
    #[inline]
547
    pub fn is_empty(&self) -> bool {
330✔
548
        self.queue.is_empty()
660✔
549
    }
550

551
    /// Enqueue a new operation.
552
    #[inline]
553
    pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
6✔
554
        assert!(global_child_index != u32::MAX);
12✔
555
        self.queue.push(InitFillDispatchItem {
18✔
556
            global_child_index,
6✔
557
            dispatch_indirect_index,
6✔
558
        });
559
    }
560

561
    /// Submit pending operations for this frame.
562
    pub fn submit(
3✔
563
        &mut self,
564
        src_buffer: &Buffer,
565
        dst_buffer: &Buffer,
566
        gpu_buffer_operations: &mut GpuBufferOperations,
567
    ) {
568
        if self.queue.is_empty() {
6✔
569
            return;
×
570
        }
571

572
        // Sort by source. We can only batch if the destination is also contiguous, so
573
        // we can check with a linear walk if the source is already sorted.
574
        self.queue
575
            .sort_unstable_by_key(|item| item.global_child_index);
576

577
        let mut fill_queue = GpuBufferOperationQueue::new();
578

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

659
        debug_assert!(self.submitted_queue_index.is_none());
3✔
660
        if !fill_queue.operation_queue.is_empty() {
6✔
661
            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
3✔
662
        }
663
    }
664
}
665

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

684
impl FromWorld for DispatchIndirectPipeline {
685
    fn from_world(world: &mut World) -> Self {
3✔
686
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
687

688
        // Copy the indirect pipeline shaders to self, because we can't access anything
689
        // else during pipeline specialization.
690
        let (indirect_shader_noevent, indirect_shader_events) = {
9✔
691
            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
15✔
692
            (
693
                effects_meta.indirect_shader_noevent.clone(),
9✔
694
                effects_meta.indirect_shader_events.clone(),
3✔
695
            )
696
        };
697

698
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
6✔
699
        let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
9✔
700
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
9✔
701

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

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

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

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

799
        Self {
800
            sim_params_bind_group_layout,
801
            effect_metadata_bind_group_layout,
802
            spawner_bind_group_layout,
803
            child_infos_bind_group_layout,
804
            indirect_shader_noevent,
805
            indirect_shader_events,
806
        }
807
    }
808
}
809

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

820
impl SpecializedComputePipeline for DispatchIndirectPipeline {
821
    type Key = DispatchIndirectPipelineKey;
822

823
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
6✔
824
        trace!(
6✔
825
            "Specializing indirect pipeline (has_events={})",
4✔
826
            key.has_events
827
        );
828

829
        let mut shader_defs = Vec::with_capacity(2);
12✔
830
        // Spawner struct needs to be defined with padding, because it's bound as an
831
        // array
832
        shader_defs.push("SPAWNER_PADDING".into());
24✔
833
        if key.has_events {
9✔
834
            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
9✔
835
        }
836

837
        let mut layout = Vec::with_capacity(4);
12✔
838
        layout.push(self.sim_params_bind_group_layout.clone());
24✔
839
        layout.push(self.effect_metadata_bind_group_layout.clone());
24✔
840
        layout.push(self.spawner_bind_group_layout.clone());
24✔
841
        if key.has_events {
9✔
842
            layout.push(self.child_infos_bind_group_layout.clone());
9✔
843
        }
844

845
        let label = format!(
12✔
846
            "hanabi:compute_pipeline:dispatch_indirect{}",
847
            if key.has_events {
6✔
848
                "_events"
3✔
849
            } else {
850
                "_noevent"
3✔
851
            }
852
        );
853

854
        ComputePipelineDescriptor {
855
            label: Some(label.into()),
6✔
856
            layout,
857
            shader: if key.has_events {
6✔
858
                self.indirect_shader_events.clone()
859
            } else {
860
                self.indirect_shader_noevent.clone()
861
            },
862
            shader_defs,
863
            entry_point: Some("main".into()),
6✔
864
            push_constant_ranges: vec![],
6✔
865
            zero_initialize_workgroup_memory: false,
866
        }
867
    }
868
}
869

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

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

919
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
920
struct QueuedOperationBindGroupKey {
921
    src_buffer: BufferId,
922
    src_binding_size: Option<NonZeroU32>,
923
    dst_buffer: BufferId,
924
    dst_binding_size: Option<NonZeroU32>,
925
}
926

927
#[derive(Debug, Clone)]
928
struct QueuedOperation {
929
    op: GpuBufferOperationType,
930
    args_index: u32,
931
    src_buffer: Buffer,
932
    src_binding_offset: u32,
933
    src_binding_size: Option<NonZeroU32>,
934
    dst_buffer: Buffer,
935
    dst_binding_offset: u32,
936
    dst_binding_size: Option<NonZeroU32>,
937
}
938

939
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
940
    fn from(value: &QueuedOperation) -> Self {
×
941
        Self {
942
            src_buffer: value.src_buffer.id(),
×
943
            src_binding_size: value.src_binding_size,
×
944
            dst_buffer: value.dst_buffer.id(),
×
945
            dst_binding_size: value.dst_binding_size,
×
946
        }
947
    }
948
}
949

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

963
impl GpuBufferOperationQueue {
964
    /// Create a new empty queue.
965
    pub fn new() -> Self {
333✔
966
        Self {
967
            args: vec![],
333✔
968
            operation_queue: vec![],
333✔
969
        }
970
    }
971

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

1011
/// GPU buffer operations for this frame.
1012
///
1013
/// This resource contains a list of submitted [`GpuBufferOperationQueue`] for
1014
/// the current frame, and ensures the bind groups for those operations are up
1015
/// to date.
1016
#[derive(Resource)]
1017
pub(super) struct GpuBufferOperations {
1018
    /// Arguments for the buffer operations submitted this frame.
1019
    args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
1020

1021
    /// Bind groups for the submitted operations.
1022
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
1023

1024
    /// Submitted queues for this frame.
1025
    queues: Vec<Vec<QueuedOperation>>,
1026
}
1027

1028
impl FromWorld for GpuBufferOperations {
1029
    fn from_world(world: &mut World) -> Self {
4✔
1030
        let render_device = world.get_resource::<RenderDevice>().unwrap();
16✔
1031
        let align = render_device.limits().min_uniform_buffer_offset_alignment;
8✔
1032
        Self::new(align)
8✔
1033
    }
1034
}
1035

1036
impl GpuBufferOperations {
1037
    pub fn new(align: u32) -> Self {
4✔
1038
        let args_buffer = AlignedBufferVec::new(
1039
            BufferUsages::UNIFORM,
1040
            Some(NonZeroU64::new(align as u64).unwrap()),
8✔
1041
            Some("hanabi:buffer:gpu_operation_args".to_string()),
4✔
1042
        );
1043
        Self {
1044
            args_buffer,
1045
            bind_groups: default(),
4✔
1046
            queues: vec![],
4✔
1047
        }
1048
    }
1049

1050
    /// Clear the queue and begin recording operations for a new frame.
1051
    pub fn begin_frame(&mut self) {
333✔
1052
        self.args_buffer.clear();
666✔
1053
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
666✔
1054
        self.queues.clear();
666✔
1055
    }
1056

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

1072
    /// Finish recording operations for this frame, and schedule buffer writes
1073
    /// to GPU.
1074
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
333✔
1075
        assert_eq!(
333✔
1076
            self.args_buffer.len(),
666✔
1077
            self.queues.iter().fold(0, |len, q| len + q.len())
675✔
1078
        );
1079

1080
        // Upload to GPU buffer
1081
        self.args_buffer.write_buffer(device, render_queue);
1,332✔
1082
    }
1083

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

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

1181
        if queue.is_empty() {
×
1182
            return;
×
1183
        }
1184

1185
        let mut compute_pass =
1186
            render_context
1187
                .command_encoder()
1188
                .begin_compute_pass(&ComputePassDescriptor {
1189
                    label: compute_pass_label,
1190
                    timestamp_writes: None,
1191
                });
1192

1193
        let mut prev_op = None;
1194
        for qop in queue {
×
1195
            trace!("qop={:?}", qop);
×
1196

1197
            if Some(qop.op) != prev_op {
×
1198
                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
×
1199
                prev_op = Some(qop.op);
×
1200
            }
1201

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

1223
            // Dispatch the operations for this buffer
1224
            const WORKGROUP_SIZE: u32 = 64;
1225
            let num_ops = 1u32; // TODO - batching!
1226
            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
1227
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
1228
            trace!(
1229
                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
×
1230
                num_ops,
1231
                workgroup_count
1232
            );
1233
        }
1234
    }
1235
}
1236

1237
/// Compute pipeline to run the `vfx_utils` shader.
1238
#[derive(Resource)]
1239
pub(crate) struct UtilsPipeline {
1240
    #[allow(dead_code)]
1241
    bind_group_layout: BindGroupLayout,
1242
    bind_group_layout_dyn: BindGroupLayout,
1243
    bind_group_layout_no_src: BindGroupLayout,
1244
    pipelines: [ComputePipeline; 4],
1245
}
1246

1247
impl FromWorld for UtilsPipeline {
1248
    fn from_world(world: &mut World) -> Self {
3✔
1249
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1250

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

1287
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1288
            label: Some("hanabi:pipeline_layout:utils"),
6✔
1289
            bind_group_layouts: &[&bind_group_layout],
3✔
1290
            push_constant_ranges: &[],
3✔
1291
        });
1292

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

1329
        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1330
            label: Some("hanabi:pipeline_layout:utils_dyn"),
6✔
1331
            bind_group_layouts: &[&bind_group_layout_dyn],
3✔
1332
            push_constant_ranges: &[],
3✔
1333
        });
1334

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

1361
        let pipeline_layout_no_src =
3✔
1362
            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
9✔
1363
                label: Some("hanabi:pipeline_layout:utils_no_src"),
6✔
1364
                bind_group_layouts: &[&bind_group_layout_no_src],
3✔
1365
                push_constant_ranges: &[],
3✔
1366
            });
1367

1368
        let shader_code = include_str!("vfx_utils.wgsl");
6✔
1369

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

1375
            let shader_defs = default();
6✔
1376

1377
            match composer.make_naga_module(NagaModuleDescriptor {
9✔
1378
                source: shader_code,
6✔
1379
                file_path: "vfx_utils.wgsl",
6✔
1380
                shader_defs,
3✔
1381
                ..Default::default()
3✔
1382
            }) {
1383
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
6✔
1384
                Err(compose_error) => panic!(
×
1385
                    "Failed to compose vfx_utils.wgsl, naga_oil returned: {}",
1386
                    compose_error.emit_to_string(&composer)
1387
                ),
1388
            }
1389
        };
1390

1391
        debug!("Create utils shader module:\n{}", shader_code);
6✔
1392
        #[allow(unsafe_code)]
1393
        let shader_module = unsafe {
1394
            render_device.create_shader_module(ShaderModuleDescriptor {
9✔
1395
                label: Some("hanabi:shader:utils"),
3✔
1396
                source: shader_source,
3✔
1397
            })
1398
        };
1399

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

1448
        Self {
1449
            bind_group_layout,
1450
            bind_group_layout_dyn,
1451
            bind_group_layout_no_src,
1452
            pipelines: [
3✔
1453
                zero_pipeline,
1454
                copy_pipeline,
1455
                fill_dispatch_args_pipeline,
1456
                fill_dispatch_args_self_pipeline,
1457
            ],
1458
        }
1459
    }
1460
}
1461

1462
impl UtilsPipeline {
1463
    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
×
1464
        match op {
×
1465
            GpuBufferOperationType::Zero => &self.pipelines[0],
×
1466
            GpuBufferOperationType::Copy => &self.pipelines[1],
×
1467
            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
×
1468
            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
×
1469
        }
1470
    }
1471

1472
    fn bind_group_layout(
×
1473
        &self,
1474
        op: GpuBufferOperationType,
1475
        with_dynamic_offsets: bool,
1476
    ) -> &BindGroupLayout {
1477
        if op == GpuBufferOperationType::FillDispatchArgsSelf {
×
1478
            assert!(
×
1479
                !with_dynamic_offsets,
×
1480
                "FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
×
1481
            );
1482
            &self.bind_group_layout_no_src
×
1483
        } else if with_dynamic_offsets {
×
1484
            &self.bind_group_layout_dyn
×
1485
        } else {
1486
            &self.bind_group_layout
×
1487
        }
1488
    }
1489
}
1490

1491
#[derive(Resource)]
1492
pub(crate) struct ParticlesInitPipeline {
1493
    sim_params_layout: BindGroupLayout,
1494

1495
    // Temporary values passed to specialize()
1496
    // https://github.com/bevyengine/bevy/issues/17132
1497
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1498
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1499
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1500
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1501
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1502
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1503
}
1504

1505
impl FromWorld for ParticlesInitPipeline {
1506
    fn from_world(world: &mut World) -> Self {
3✔
1507
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1508

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

1524
        Self {
1525
            sim_params_layout,
1526
            temp_particle_bind_group_layout: None,
1527
            temp_spawner_bind_group_layout: None,
1528
            temp_metadata_bind_group_layout: None,
1529
        }
1530
    }
1531
}
1532

1533
bitflags! {
1534
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1535
    pub struct ParticleInitPipelineKeyFlags: u8 {
1536
        //const CLONE = (1u8 << 0); // DEPRECATED
1537
        const ATTRIBUTE_PREV = (1u8 << 1);
1538
        const ATTRIBUTE_NEXT = (1u8 << 2);
1539
        const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
1540
    }
1541
}
1542

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

1566
impl SpecializedComputePipeline for ParticlesInitPipeline {
1567
    type Key = ParticleInitPipelineKey;
1568

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

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

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

1621
        let label = format!("hanabi:pipeline:init_{hash:016X}");
6✔
1622
        trace!(
2✔
1623
            "-> creating pipeline '{}' with shader defs:{}",
2✔
1624
            label,
1625
            shader_defs
2✔
1626
                .iter()
2✔
1627
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
1628
        );
1629

1630
        ComputePipelineDescriptor {
1631
            label: Some(label.into()),
4✔
1632
            layout: vec![
4✔
1633
                self.sim_params_layout.clone(),
1634
                particle_bind_group_layout.clone(),
1635
                spawner_bind_group_layout.clone(),
1636
                metadata_bind_group_layout.clone(),
1637
            ],
1638
            shader: key.shader,
4✔
1639
            shader_defs,
1640
            entry_point: Some("main".into()),
2✔
1641
            push_constant_ranges: vec![],
2✔
1642
            zero_initialize_workgroup_memory: false,
1643
        }
1644
    }
1645
}
1646

1647
#[derive(Resource)]
1648
pub(crate) struct ParticlesUpdatePipeline {
1649
    sim_params_layout: BindGroupLayout,
1650

1651
    // Temporary values passed to specialize()
1652
    // https://github.com/bevyengine/bevy/issues/17132
1653
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1654
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1655
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1656
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1657
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1658
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1659
}
1660

1661
impl FromWorld for ParticlesUpdatePipeline {
1662
    fn from_world(world: &mut World) -> Self {
3✔
1663
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1664

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

1695
        Self {
1696
            sim_params_layout,
1697
            temp_particle_bind_group_layout: None,
1698
            temp_spawner_bind_group_layout: None,
1699
            temp_metadata_bind_group_layout: None,
1700
        }
1701
    }
1702
}
1703

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

1727
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1728
    type Key = ParticleUpdatePipelineKey;
1729

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

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

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

1773
        let hash = calc_func_id(&key);
6✔
1774
        let label = format!("hanabi:pipeline:update_{hash:016X}");
6✔
1775
        trace!(
2✔
1776
            "-> creating pipeline '{}' with shader defs:{}",
2✔
1777
            label,
1778
            shader_defs
2✔
1779
                .iter()
2✔
1780
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
12✔
1781
        );
1782

1783
        ComputePipelineDescriptor {
1784
            label: Some(label.into()),
4✔
1785
            layout: vec![
4✔
1786
                self.sim_params_layout.clone(),
1787
                particle_bind_group_layout.clone(),
1788
                spawner_bind_group_layout.clone(),
1789
                metadata_bind_group_layout.clone(),
1790
            ],
1791
            shader: key.shader,
4✔
1792
            shader_defs,
1793
            entry_point: Some("main".into()),
2✔
1794
            push_constant_ranges: Vec::new(),
2✔
1795
            zero_initialize_workgroup_memory: false,
1796
        }
1797
    }
1798
}
1799

1800
#[derive(Resource)]
1801
pub(crate) struct ParticlesRenderPipeline {
1802
    render_device: RenderDevice,
1803
    view_layout: BindGroupLayout,
1804
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
1805
}
1806

1807
impl ParticlesRenderPipeline {
1808
    /// Cache a material, creating its bind group layout based on the texture
1809
    /// layout.
1810
    pub fn cache_material(&mut self, layout: &TextureLayout) {
312✔
1811
        if layout.layout.is_empty() {
624✔
1812
            return;
312✔
1813
        }
1814

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

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

1854
        self.material_layouts
1855
            .insert(layout.clone(), material_bind_group_layout);
1856
    }
1857

1858
    /// Retrieve a bind group layout for a cached material.
1859
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
2✔
1860
        // Prevent a hash and lookup for the trivial case of an empty layout
1861
        if layout.layout.is_empty() {
4✔
1862
            return None;
2✔
1863
        }
1864

1865
        self.material_layouts.get(layout)
1866
    }
1867
}
1868

1869
impl FromWorld for ParticlesRenderPipeline {
1870
    fn from_world(world: &mut World) -> Self {
3✔
1871
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1872

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

1901
        Self {
1902
            render_device: render_device.clone(),
9✔
1903
            view_layout,
1904
            material_layouts: default(),
3✔
1905
        }
1906
    }
1907
}
1908

1909
#[cfg(all(feature = "2d", feature = "3d"))]
1910
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1911
enum PipelineMode {
1912
    Camera2d,
1913
    Camera3d,
1914
}
1915

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

1962
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1963
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1964
    #[default]
1965
    Blend,
1966
    /// Key: USE_ALPHA_MASK
1967
    /// The effect is rendered with alpha masking.
1968
    AlphaMask,
1969
    /// Key: OPAQUE
1970
    /// The effect is rendered fully-opaquely.
1971
    Opaque,
1972
}
1973

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

1997
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1998
    type Key = ParticleRenderPipelineKey;
1999

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

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

2048
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
10✔
2049
        let mut shader_defs = vec![];
4✔
2050

2051
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
10✔
2052
            mesh_layout
4✔
2053
                .0
4✔
2054
                .get_layout(&[
4✔
2055
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
6✔
2056
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
6✔
2057
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
2✔
2058
                ])
2059
                .ok()
2✔
2060
        });
2061

2062
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
4✔
2063
            layout.push(material_bind_group_layout.clone());
2064
        }
2065

2066
        // Key: LOCAL_SPACE_SIMULATION
2067
        if key.local_space_simulation {
2✔
2068
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2069
        }
2070

2071
        match key.alpha_mask {
2✔
2072
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
2✔
2073
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2074
                // Key: USE_ALPHA_MASK
2075
                shader_defs.push("USE_ALPHA_MASK".into())
×
2076
            }
2077
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2078
                // Key: OPAQUE
2079
                shader_defs.push("OPAQUE".into())
×
2080
            }
2081
        }
2082

2083
        // Key: FLIPBOOK
2084
        if key.flipbook {
2✔
2085
            shader_defs.push("FLIPBOOK".into());
×
2086
        }
2087

2088
        // Key: NEEDS_UV
2089
        if key.needs_uv {
2✔
2090
            shader_defs.push("NEEDS_UV".into());
×
2091
        }
2092

2093
        // Key: NEEDS_NORMAL
2094
        if key.needs_normal {
2✔
2095
            shader_defs.push("NEEDS_NORMAL".into());
×
2096
        }
2097

2098
        if key.needs_particle_fragment {
2✔
2099
            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
×
2100
        }
2101

2102
        // Key: RIBBONS
2103
        if key.ribbons {
2✔
2104
            shader_defs.push("RIBBONS".into());
×
2105
        }
2106

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

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

2134
        #[cfg(all(feature = "2d", feature = "3d"))]
2135
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2✔
2136
        #[cfg(all(feature = "2d", feature = "3d"))]
2137
        let depth_stencil = match key.pipeline_mode {
4✔
2138
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2139
            PipelineMode::Camera3d => Some(depth_stencil_3d),
2✔
2140
        };
2141

2142
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2143
        let depth_stencil = Some(depth_stencil_2d);
2144

2145
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2146
        let depth_stencil = Some(depth_stencil_3d);
2147

2148
        let format = if key.hdr {
4✔
2149
            ViewTarget::TEXTURE_FORMAT_HDR
×
2150
        } else {
2151
            TextureFormat::bevy_default()
2✔
2152
        };
2153

2154
        let hash = calc_func_id(&key);
6✔
2155
        let label = format!("hanabi:pipeline:render_{hash:016X}");
6✔
2156
        trace!(
2✔
2157
            "-> creating pipeline '{}' with shader defs:{}",
2✔
2158
            label,
2159
            shader_defs
2✔
2160
                .iter()
2✔
2161
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
2162
        );
2163

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

2204
/// A single effect instance extracted from a [`ParticleEffect`] as a
2205
/// render world item.
2206
///
2207
/// [`ParticleEffect`]: crate::ParticleEffect
2208
#[derive(Debug, Component)]
2209
pub(crate) struct ExtractedEffectLegacy {
2210
    /// Main world entity owning the [`CompiledParticleEffect`] this effect was
2211
    /// extracted from. Mainly used for visibility.
2212
    pub main_entity: MainEntity,
2213
    /// Render world entity, if any, where the [`CachedEffect`] component
2214
    /// caching this extracted effect resides. If this component was never
2215
    /// cached in the render world, this is `None`. In that case a new
2216
    /// [`CachedEffect`] will be spawned automatically.
2217
    pub render_entity: RenderEntity,
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
    #[allow(dead_code)]
2224
    pub particle_layout: ParticleLayout,
2225
    /// Property layout for the effect.
2226
    pub property_layout: PropertyLayout,
2227
    /// Values of properties written in a binary blob according to
2228
    /// [`property_layout`].
2229
    ///
2230
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2231
    /// `None` if nothing needs to be done for this frame.
2232
    ///
2233
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2234
    pub property_data: Option<Vec<u8>>,
2235
    /// Number of particles to spawn this frame.
2236
    ///
2237
    /// This is ignored if the effect is a child effect consuming GPU spawn
2238
    /// events.
2239
    pub spawn_count: u32,
2240
    /// PRNG seed.
2241
    pub prng_seed: u32,
2242
    /// Global transform of the effect origin.
2243
    pub transform: GlobalTransform,
2244
    /// Layout flags.
2245
    pub layout_flags: LayoutFlags,
2246
    /// Texture layout.
2247
    pub texture_layout: TextureLayout,
2248
    /// Textures.
2249
    pub textures: Vec<Handle<Image>>,
2250
    /// Alpha mode.
2251
    pub alpha_mode: AlphaMode,
2252
    /// Effect shaders.
2253
    pub effect_shaders: EffectShader,
2254
}
2255

2256
/// A single effect instance extracted from a [`ParticleEffect`] as a
2257
/// render world item.
2258
///
2259
/// [`ParticleEffect`]: crate::ParticleEffect
2260
#[derive(Debug, Clone, PartialEq, Component)]
2261
#[require(CachedPipelines, CachedReadyState, CachedEffectMetadata)]
2262
pub(crate) struct ExtractedEffect {
2263
    /// Handle to the effect asset this instance is based on.
2264
    /// The handle is weak to prevent refcount cycles and gracefully handle
2265
    /// assets unloaded or destroyed after a draw call has been submitted.
2266
    pub handle: Handle<EffectAsset>,
2267
    /// Particle layout for the effect.
2268
    pub particle_layout: ParticleLayout,
2269
    /// Effect capacity, in number of particles.
2270
    pub capacity: u32,
2271
    /// Layout flags.
2272
    pub layout_flags: LayoutFlags,
2273
    /// Texture layout.
2274
    pub texture_layout: TextureLayout,
2275
    /// Textures.
2276
    pub textures: Vec<Handle<Image>>,
2277
    /// Alpha mode.
2278
    pub alpha_mode: AlphaMode,
2279
    /// Effect shaders.
2280
    pub effect_shaders: EffectShader,
2281
    /// Condition under which the effect is simulated.
2282
    pub simulation_condition: SimulationCondition,
2283
}
2284

2285
/// Extracted data for the [`GpuSpawnerParams`].
2286
///
2287
/// This contains all data which may change each frame during the regular usage
2288
/// of the effect, but doesn't require any particular GPU resource update
2289
/// (except re-uploading that new data to GPU, of course).
2290
#[derive(Debug, Clone, PartialEq, Component)]
2291
pub(crate) struct ExtractedSpawner {
2292
    /// Number of particles to spawn this frame.
2293
    ///
2294
    /// This is ignored if the effect is a child effect consuming GPU spawn
2295
    /// events.
2296
    pub spawn_count: u32,
2297
    /// PRNG seed.
2298
    pub prng_seed: u32,
2299
    /// Global transform of the effect origin.
2300
    pub transform: GlobalTransform,
2301
    /// Is the effect visible this frame?
2302
    pub is_visible: bool,
2303
}
2304

2305
/// Cache info for the metadata of the effect.
2306
///
2307
/// This manages the GPU allocation of the [`GpuEffectMetadata`] for this
2308
/// effect.
2309
#[derive(Debug, Default, Component)]
2310
pub(crate) struct CachedEffectMetadata {
2311
    /// Allocation ID.
2312
    pub table_id: BufferTableId,
2313
    /// Current metadata values, cached on CPU for change detection.
2314
    pub metadata: GpuEffectMetadata,
2315
}
2316

2317
/// Extracted parent information for a child effect.
2318
///
2319
/// This component is present on the [`RenderEntity`] of an extracted effect if
2320
/// the effect has a parent effect. Otherwise, it's removed.
2321
///
2322
/// This components forms an ECS relationship with [`ChildrenEffects`].
2323
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
2324
#[relationship(relationship_target = ChildrenEffects)]
2325
pub(crate) struct ChildEffectOf {
2326
    /// Render entity of the parent.
2327
    pub parent: Entity,
2328
}
2329

2330
/// Extracted children information for a parent effect.
2331
///
2332
/// This component is present on the [`RenderEntity`] of an extracted effect if
2333
/// the effect is a parent effect for one or more child effects. Otherwise, it's
2334
/// removed.
2335
///
2336
/// This components forms an ECS relationship with [`ChildEffectOf`]. Note that
2337
/// we don't use `linked_spawn` because:
2338
/// 1. This would fight with the `SyncToRenderWorld` as the main world
2339
///    parent-child hierarchy is by design not an ECS relationship (it's a lose
2340
///    declarative coupling).
2341
/// 2. The components on the render entity often store GPU resources or other
2342
///    data we need to clean-up manually, and not all of them currently use
2343
///    lifecycle hooks, so we want to manage despawning manually to prevent
2344
///    leaks.
2345
#[derive(Debug, Clone, PartialEq, Eq, Component)]
2346
#[relationship_target(relationship = ChildEffectOf)]
2347
pub(crate) struct ChildrenEffects(Vec<Entity>);
2348

2349
impl<'a> IntoIterator for &'a ChildrenEffects {
2350
    type Item = <Self::IntoIter as Iterator>::Item;
2351

2352
    type IntoIter = std::slice::Iter<'a, Entity>;
2353

2354
    #[inline(always)]
2355
    fn into_iter(self) -> Self::IntoIter {
×
2356
        self.0.iter()
×
2357
    }
2358
}
2359

2360
impl Deref for ChildrenEffects {
2361
    type Target = [Entity];
2362

2363
    fn deref(&self) -> &Self::Target {
×
2364
        &self.0
×
2365
    }
2366
}
2367

2368
/// Extracted data for an effect's properties, if any.
2369
///
2370
/// This component is present on the [`RenderEntity`] of an extracted effect if
2371
/// that effect has properties. It optionally contains new CPU data to
2372
/// (re-)upload this frame. If the effect has no property, this component is
2373
/// removed.
2374
#[derive(Debug, Component)]
2375
pub(crate) struct ExtractedProperties {
2376
    /// Property layout for the effect.
2377
    pub property_layout: PropertyLayout,
2378
    /// Values of properties written in a binary blob according to
2379
    /// [`property_layout`].
2380
    ///
2381
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2382
    /// `None` if nothing needs to be done for this frame.
2383
    ///
2384
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2385
    pub property_data: Option<Vec<u8>>,
2386
}
2387

2388
#[derive(Default, Resource)]
2389
pub(crate) struct EffectAssetEvents {
2390
    pub images: Vec<AssetEvent<Image>>,
2391
}
2392

2393
/// System extracting all the asset events for the [`Image`] assets to enable
2394
/// dynamic update of images bound to any effect.
2395
///
2396
/// This system runs in parallel of [`extract_effects`].
2397
pub(crate) fn extract_effect_events(
330✔
2398
    mut events: ResMut<EffectAssetEvents>,
2399
    mut image_events: Extract<MessageReader<AssetEvent<Image>>>,
2400
) {
2401
    #[cfg(feature = "trace")]
2402
    let _span = bevy::log::info_span!("extract_effect_events").entered();
990✔
2403
    trace!("extract_effect_events()");
650✔
2404

2405
    let EffectAssetEvents { ref mut images } = *events;
660✔
2406
    *images = image_events.read().copied().collect();
1,320✔
2407
}
2408

2409
/// Debugging settings.
2410
///
2411
/// Settings used to debug Hanabi. These have no effect on the actual behavior
2412
/// of Hanabi, but may affect its performance.
2413
///
2414
/// # Example
2415
///
2416
/// ```
2417
/// # use bevy::prelude::*;
2418
/// # use bevy_hanabi::*;
2419
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2420
///     // Each time a new effect is spawned, capture 2 frames
2421
///     debug_settings.start_capture_on_new_effect = true;
2422
///     debug_settings.capture_frame_count = 2;
2423
/// }
2424
/// ```
2425
#[derive(Debug, Default, Clone, Copy, Resource)]
2426
pub struct DebugSettings {
2427
    /// Enable automatically starting a GPU debugger capture as soon as this
2428
    /// frame starts rendering (extract phase).
2429
    ///
2430
    /// Enable this feature to automatically capture one or more GPU frames when
2431
    /// the `extract_effects()` system runs next. This instructs any attached
2432
    /// GPU debugger to start a capture; this has no effect if no debugger
2433
    /// is attached.
2434
    ///
2435
    /// If a capture is already on-going this has no effect; the on-going
2436
    /// capture needs to be terminated first. Note however that a capture can
2437
    /// stop and another start in the same frame.
2438
    ///
2439
    /// This value is not reset automatically. If you set this to `true`, you
2440
    /// should set it back to `false` on next frame to avoid capturing forever.
2441
    pub start_capture_this_frame: bool,
2442

2443
    /// Enable automatically starting a GPU debugger capture when one or more
2444
    /// effects are spawned.
2445
    ///
2446
    /// Enable this feature to automatically capture one or more GPU frames when
2447
    /// a new effect is spawned (as detected by ECS change detection). This
2448
    /// instructs any attached GPU debugger to start a capture; this has no
2449
    /// effect if no debugger is attached.
2450
    pub start_capture_on_new_effect: bool,
2451

2452
    /// Number of frames to capture with a GPU debugger.
2453
    ///
2454
    /// By default this value is zero, and a GPU debugger capture runs for a
2455
    /// single frame. If a non-zero frame count is specified here, the capture
2456
    /// will instead stop once the specified number of frames has been recorded.
2457
    ///
2458
    /// You should avoid setting this to a value too large, to prevent the
2459
    /// capture size from getting out of control. A typical value is 1 to 3
2460
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2461
    /// debuggers or graphics APIs might further limit this value on their own,
2462
    /// so there's no guarantee the graphics API will honor this value.
2463
    pub capture_frame_count: u32,
2464
}
2465

2466
#[derive(Debug, Default, Clone, Copy, Resource)]
2467
pub(crate) struct RenderDebugSettings {
2468
    /// Is a GPU debugger capture on-going?
2469
    is_capturing: bool,
2470
    /// Start time of any on-going GPU debugger capture.
2471
    capture_start: Duration,
2472
    /// Number of frames captured so far for on-going GPU debugger capture.
2473
    captured_frames: u32,
2474
}
2475

2476
/// Manage GPU debug capture start/stop.
2477
///
2478
/// If any GPU debug capture is configured to start or stop in
2479
/// [`DebugSettings`], they do so during this system's run. This ensures
2480
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2481
/// from Bevy itself, if another Bevy system runs before this one).
2482
///
2483
/// We do this during extract to try and capture as close as possible to an
2484
/// entire GPU frame.
2485
pub(crate) fn start_stop_gpu_debug_capture(
330✔
2486
    real_time: Extract<Res<Time<Real>>>,
2487
    render_device: Res<RenderDevice>,
2488
    debug_settings: Extract<Res<DebugSettings>>,
2489
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2490
    q_added_effects: Extract<Query<(), Added<CompiledParticleEffect>>>,
2491
) {
2492
    #[cfg(feature = "trace")]
2493
    let _span = bevy::log::info_span!("start_stop_debug_capture").entered();
990✔
2494
    trace!("start_stop_debug_capture()");
650✔
2495

2496
    // Stop any pending capture if needed
2497
    if render_debug_settings.is_capturing {
330✔
2498
        render_debug_settings.captured_frames += 1;
×
2499

2500
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2501
            #[expect(unsafe_code, reason = "Debugging only")]
2502
            unsafe {
NEW
2503
                render_device.wgpu_device().stop_graphics_debugger_capture();
×
2504
            }
2505
            render_debug_settings.is_capturing = false;
×
2506
            warn!(
×
2507
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2508
                render_debug_settings.captured_frames,
×
2509
                real_time.elapsed().as_secs_f64()
×
2510
            );
2511
        }
2512
    }
2513

2514
    // If no pending capture, consider starting a new one
2515
    if !render_debug_settings.is_capturing
330✔
2516
        && (debug_settings.start_capture_this_frame
330✔
2517
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty()))
330✔
2518
    {
2519
        #[expect(unsafe_code, reason = "Debugging only")]
2520
        unsafe {
NEW
2521
            render_device
×
2522
                .wgpu_device()
2523
                .start_graphics_debugger_capture();
2524
        }
2525
        render_debug_settings.is_capturing = true;
2526
        render_debug_settings.capture_start = real_time.elapsed();
2527
        render_debug_settings.captured_frames = 0;
2528
        warn!(
2529
            "Started GPU debug capture of {} frames at t={}s.",
×
2530
            debug_settings.capture_frame_count,
×
2531
            render_debug_settings.capture_start.as_secs_f64()
×
2532
        );
2533
    }
2534
}
2535

2536
/// Write the ready state of all render world effects back into their source
2537
/// effect in the main world.
2538
pub(crate) fn report_ready_state(
330✔
2539
    mut main_world: ResMut<MainWorld>,
2540
    q_ready_state: Query<&CachedReadyState>,
2541
) {
2542
    let mut q_effects = main_world.query::<(RenderEntity, &mut CompiledParticleEffect)>();
660✔
2543
    for (render_entity, mut compiled_particle_effect) in q_effects.iter_mut(&mut main_world) {
1,314✔
2544
        if let Ok(cached_ready_state) = q_ready_state.get(render_entity) {
312✔
2545
            compiled_particle_effect.is_ready = cached_ready_state.is_ready();
2546
        }
2547
    }
2548
}
2549

2550
/// System extracting data for rendering of all active [`ParticleEffect`]
2551
/// components.
2552
///
2553
/// [`ParticleEffect`]: crate::ParticleEffect
2554
pub(crate) fn extract_effects(
330✔
2555
    mut commands: Commands,
2556
    effects: Extract<Res<Assets<EffectAsset>>>,
2557
    default_mesh: Extract<Res<DefaultMesh>>,
2558
    // Main world effects to extract
2559
    q_effects: Extract<
2560
        Query<(
2561
            Entity,
2562
            RenderEntity,
2563
            Option<&InheritedVisibility>,
2564
            Option<&ViewVisibility>,
2565
            &EffectSpawner,
2566
            &CompiledParticleEffect,
2567
            Option<Ref<EffectProperties>>,
2568
            &GlobalTransform,
2569
        )>,
2570
    >,
2571
    // Render world effects extracted from a previous frame, if any
2572
    mut q_extracted_effects: Query<(
2573
        &mut ExtractedEffect,
2574
        Option<&mut ExtractedSpawner>,
2575
        Option<&ChildEffectOf>, // immutable, because of relationship
2576
        Option<&mut ExtractedEffectMesh>,
2577
        Option<&mut ExtractedProperties>,
2578
    )>,
2579
) {
2580
    #[cfg(feature = "trace")]
2581
    let _span = bevy::log::info_span!("extract_effects").entered();
990✔
2582
    trace!("extract_effects()");
650✔
2583

2584
    // Loop over all existing effects to extract them
2585
    trace!("Extracting {} effects...", q_effects.iter().len());
1,290✔
2586
    for (
2587
        main_entity,
314✔
2588
        render_entity,
2589
        maybe_inherited_visibility,
2590
        maybe_view_visibility,
2591
        effect_spawner,
2592
        compiled_effect,
2593
        maybe_properties,
2594
        transform,
2595
    ) in q_effects.iter()
660✔
2596
    {
2597
        // Check if shaders are configured
2598
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
314✔
2599
            trace!("Effect {:?}: no configured shader, skipped.", main_entity);
×
2600
            continue;
×
2601
        };
2602

2603
        // Check if asset is available, otherwise silently ignore
2604
        let Some(asset) = effects.get(&compiled_effect.asset) else {
314✔
2605
            trace!(
×
2606
                "Effect {:?}: EffectAsset not ready, skipped. asset:{:?}",
×
2607
                main_entity,
2608
                compiled_effect.asset
2609
            );
2610
            continue;
×
2611
        };
2612

2613
        let is_visible = maybe_inherited_visibility
2614
            .map(|cv| cv.get())
628✔
2615
            .unwrap_or(true)
2616
            && maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true);
1,570✔
2617

2618
        let mut cmd = commands.entity(render_entity);
2619

2620
        // Fetch the existing extraction compoennts, if any, which we need to update.
2621
        // Because we use SyncToRenderWorld, there's always a render entity, but it may
2622
        // miss all components. And because we can't query only optional components
2623
        // (that would match all entities in the entire world), we force querying
2624
        // ExtractedEffect, which means we get a miss if it's the first extraction and
2625
        // it's not spawned yet. That's OK, we'll spawn it below.
2626
        let (
2627
            maybe_extracted_effect,
2628
            maybe_extracted_spawner,
2629
            maybe_child_of,
2630
            maybe_extracted_mesh,
2631
            maybe_extracted_properties,
2632
        ) = q_extracted_effects
2633
            .get_mut(render_entity)
2634
            .map(|(extracted_effect, b, c, d, e)| (Some(extracted_effect), b, c, d, e))
1,560✔
2635
            .unwrap_or((None, None, None, None, None));
2636

2637
        // Extract general effect data
2638
        let texture_layout = asset.module().texture_layout();
2639
        let layout_flags = compiled_effect.layout_flags;
2640
        let alpha_mode = compiled_effect.alpha_mode;
2641
        trace!(
2642
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
314✔
2643
            asset.name,
2644
            main_entity,
2645
            render_entity,
2646
            texture_layout.layout.len(),
628✔
2647
            compiled_effect.textures.len(),
628✔
2648
            layout_flags,
2649
        );
2650
        let new_extracted_effect = ExtractedEffect {
2651
            handle: compiled_effect.asset.clone(),
2652
            particle_layout: asset.particle_layout().clone(),
2653
            capacity: asset.capacity(),
2654
            layout_flags,
2655
            texture_layout,
2656
            textures: compiled_effect.textures.clone(),
2657
            alpha_mode,
2658
            effect_shaders: effect_shaders.clone(),
2659
            simulation_condition: asset.simulation_condition,
2660
        };
2661
        if let Some(mut extracted_effect) = maybe_extracted_effect {
312✔
2662
            extracted_effect.set_if_neq(new_extracted_effect);
2663
        } else {
2664
            trace!(
2✔
2665
                "Inserting new ExtractedEffect component on {:?}",
2✔
2666
                render_entity
2667
            );
2668
            cmd.insert(new_extracted_effect);
6✔
2669
        }
2670

2671
        // Extract the spawner data
2672
        let new_spawner = ExtractedSpawner {
2673
            spawn_count: effect_spawner.spawn_count,
2674
            prng_seed: compiled_effect.prng_seed,
2675
            transform: *transform,
2676
            is_visible,
2677
        };
2678
        trace!(
2679
            "[Effect {}] spawn_count={} prng_seed={}",
314✔
2680
            render_entity,
2681
            new_spawner.spawn_count,
2682
            new_spawner.prng_seed
2683
        );
2684
        if let Some(mut extracted_spawner) = maybe_extracted_spawner {
312✔
2685
            extracted_spawner.set_if_neq(new_spawner);
2686
        } else {
2687
            trace!(
2✔
2688
                "Inserting new ExtractedSpawner component on {}",
2✔
2689
                render_entity
2690
            );
2691
            cmd.insert(new_spawner);
6✔
2692
        }
2693

2694
        // Extract the effect mesh
2695
        let mesh = compiled_effect
2696
            .mesh
2697
            .clone()
2698
            .unwrap_or(default_mesh.0.clone());
2699
        let new_mesh = ExtractedEffectMesh { mesh: mesh.id() };
2700
        if let Some(mut extracted_mesh) = maybe_extracted_mesh {
312✔
2701
            extracted_mesh.set_if_neq(new_mesh);
2702
        } else {
2703
            trace!(
2✔
2704
                "Inserting new ExtractedEffectMesh component on {:?}",
2✔
2705
                render_entity
2706
            );
2707
            cmd.insert(new_mesh);
6✔
2708
        }
2709

2710
        // Extract the parent, if any, and resolve its render entity
2711
        let parent_render_entity = if let Some(main_entity) = compiled_effect.parent {
314✔
2712
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2713
                error!(
×
2714
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2715
                    main_entity
2716
                );
2717
                cmd.remove::<ChildEffectOf>();
×
2718
                // TODO - prevent extraction altogether here, instead of just de-parenting?
2719
                continue;
×
2720
            };
2721
            Some(render_entity)
2722
        } else {
2723
            None
314✔
2724
        };
2725
        if let Some(render_entity) = parent_render_entity {
×
2726
            let new_child_of = ChildEffectOf {
2727
                parent: render_entity,
2728
            };
2729
            // If there's already an ExtractedParent component, ensure we overwrite only if
2730
            // different, to not trigger ECS change detection that we rely on.
2731
            if let Some(child_effect_of) = maybe_child_of {
×
2732
                // The relationship makes ChildEffectOf immutable, so re-insert to mutate
2733
                if *child_effect_of != new_child_of {
×
2734
                    cmd.insert(new_child_of);
×
2735
                }
2736
            } else {
2737
                trace!(
×
2738
                    "Inserting new ChildEffectOf component on {:?}",
×
2739
                    render_entity
2740
                );
2741
                cmd.insert(new_child_of);
×
2742
            }
2743
        } else {
2744
            cmd.remove::<ChildEffectOf>();
314✔
2745
        }
2746

2747
        // Extract property data
2748
        let property_layout = asset.property_layout();
2749
        if property_layout.is_empty() {
305✔
2750
            cmd.remove::<ExtractedProperties>();
305✔
2751
        } else {
2752
            // Re-extract CPU property data if any. Note that this data is not a "new value"
2753
            // but instead a "value that must be uploaded this frame", and therefore is
2754
            // empty when there's no change (as opposed to, having a constant value
2755
            // frame-to-frame).
2756
            let property_data = if let Some(properties) = maybe_properties {
9✔
2757
                if properties.is_changed() {
2758
                    trace!("Detected property change, re-serializing...");
×
2759
                    Some(properties.serialize(&property_layout))
×
2760
                } else {
2761
                    None
×
2762
                }
2763
            } else {
2764
                None
9✔
2765
            };
2766

2767
            let new_properties = ExtractedProperties {
2768
                property_layout,
2769
                property_data,
2770
            };
2771
            trace!("new_properties = {new_properties:?}");
9✔
2772

2773
            if let Some(mut extracted_properties) = maybe_extracted_properties {
8✔
2774
                // Always mutate if there's new CPU data to re-upload. Otherwise check for any
2775
                // other change.
2776
                if new_properties.property_data.is_some()
2777
                    || (extracted_properties.property_layout != new_properties.property_layout)
8✔
2778
                {
2779
                    trace!(
×
2780
                        "Updating existing ExtractedProperties (was: {:?})",
×
2781
                        extracted_properties.as_ref()
×
2782
                    );
2783
                    *extracted_properties = new_properties;
2784
                }
2785
            } else {
2786
                trace!(
1✔
2787
                    "Inserting new ExtractedProperties component on {:?}",
1✔
2788
                    render_entity
2789
                );
2790
                cmd.insert(new_properties);
3✔
2791
            }
2792
        }
2793
    }
2794
}
2795

2796
pub(crate) fn extract_sim_params(
330✔
2797
    real_time: Extract<Res<Time<Real>>>,
2798
    virtual_time: Extract<Res<Time<Virtual>>>,
2799
    time: Extract<Res<Time<EffectSimulation>>>,
2800
    mut sim_params: ResMut<SimParams>,
2801
) {
2802
    #[cfg(feature = "trace")]
2803
    let _span = bevy::log::info_span!("extract_sim_params").entered();
990✔
2804
    trace!("extract_sim_params()");
650✔
2805

2806
    // Save simulation params into render world
2807
    sim_params.time = time.elapsed_secs_f64();
660✔
2808
    sim_params.delta_time = time.delta_secs();
660✔
2809
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
660✔
2810
    sim_params.virtual_delta_time = virtual_time.delta_secs();
660✔
2811
    sim_params.real_time = real_time.elapsed_secs_f64();
660✔
2812
    sim_params.real_delta_time = real_time.delta_secs();
660✔
2813
    trace!(
330✔
2814
        "SimParams: time={} delta_time={} vtime={} delta_vtime={} rtime={} delta_rtime={}",
320✔
2815
        sim_params.time,
320✔
2816
        sim_params.delta_time,
320✔
2817
        sim_params.virtual_time,
320✔
2818
        sim_params.virtual_delta_time,
320✔
2819
        sim_params.real_time,
320✔
2820
        sim_params.real_delta_time,
320✔
2821
    );
2822
}
2823

2824
/// Various GPU limits and aligned sizes computed once and cached.
2825
struct GpuLimits {
2826
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2827
    ///
2828
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2829
    storage_buffer_align: NonZeroU32,
2830

2831
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2832
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2833
    ///
2834
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2835
    effect_metadata_aligned_size: NonZeroU32,
2836
}
2837

2838
impl GpuLimits {
2839
    pub fn from_device(render_device: &RenderDevice) -> Self {
4✔
2840
        let storage_buffer_align =
4✔
2841
            render_device.limits().min_storage_buffer_offset_alignment as u64;
4✔
2842

2843
        let effect_metadata_aligned_size = NonZeroU32::new(
2844
            GpuEffectMetadata::min_size()
8✔
2845
                .get()
8✔
2846
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2847
        )
2848
        .unwrap();
2849

2850
        trace!(
4✔
2851
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2✔
2852
            storage_buffer_align,
2853
            GpuEffectMetadata::min_size().get(),
4✔
2854
            effect_metadata_aligned_size.get(),
4✔
2855
        );
2856

2857
        Self {
2858
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
12✔
2859
            effect_metadata_aligned_size,
2860
        }
2861
    }
2862

2863
    /// Byte alignment for any storage buffer binding.
2864
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2865
        self.storage_buffer_align
3✔
2866
    }
2867

2868
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2869
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
625✔
2870
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
625✔
2871
    }
2872

2873
    /// Byte alignment for [`GpuEffectMetadata`].
2874
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
2✔
2875
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
6✔
2876
    }
2877
}
2878

2879
/// Global render world resource containing the GPU data to draw all the
2880
/// particle effects in all views.
2881
///
2882
/// The resource is populated by [`prepare_effects()`] with all the effects to
2883
/// render for the current frame, for all views in the frame, and consumed by
2884
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2885
/// effects.
2886
#[derive(Resource)]
2887
pub struct EffectsMeta {
2888
    /// Bind group for the camera view, containing the camera projection and
2889
    /// other uniform values related to the camera.
2890
    view_bind_group: Option<BindGroup>,
2891
    /// Bind group #0 of the vfx_update shader, for the simulation parameters
2892
    /// like the current time and frame delta time.
2893
    update_sim_params_bind_group: Option<BindGroup>,
2894
    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2895
    /// like the current time and frame delta time. This is shared with the
2896
    /// vfx_init pass too.
2897
    indirect_sim_params_bind_group: Option<BindGroup>,
2898
    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2899
    /// compute dispatch and render buffers.
2900
    indirect_metadata_bind_group: Option<BindGroup>,
2901
    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2902
    indirect_spawner_bind_group: Option<BindGroup>,
2903
    /// Global shared GPU uniform buffer storing the simulation parameters,
2904
    /// uploaded each frame from CPU to GPU.
2905
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2906
    /// Global shared GPU buffer storing the various spawner parameter structs
2907
    /// for the active effect instances.
2908
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2909
    /// Global shared GPU buffer storing the various indirect dispatch structs
2910
    /// for the indirect dispatch of the Update pass.
2911
    dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
2912
    /// Global shared GPU buffer storing the various indirect draw structs
2913
    /// for the indirect Render pass. Note that we use
2914
    /// GpuDrawIndexedIndirectArgs as the largest of the two variants (the
2915
    /// other being GpuDrawIndirectArgs). For non-indexed entries, we ignore
2916
    /// the last `u32` value.
2917
    draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
2918
    /// Global shared GPU buffer storing the various `EffectMetadata`
2919
    /// structs for the active effect instances.
2920
    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2921
    /// Various GPU limits and aligned sizes lazily allocated and cached for
2922
    /// convenience.
2923
    gpu_limits: GpuLimits,
2924
    indirect_shader_noevent: Handle<Shader>,
2925
    indirect_shader_events: Handle<Shader>,
2926
    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2927
    /// -noevent and -events variants).
2928
    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2929
    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2930
    /// is either the -noevent or -events variant depending on whether there's
2931
    /// any child effect with GPU events currently active.
2932
    active_indirect_pipeline_id: CachedComputePipelineId,
2933
}
2934

2935
impl EffectsMeta {
2936
    pub fn new(
3✔
2937
        device: RenderDevice,
2938
        indirect_shader_noevent: Handle<Shader>,
2939
        indirect_shader_events: Handle<Shader>,
2940
    ) -> Self {
2941
        let gpu_limits = GpuLimits::from_device(&device);
9✔
2942

2943
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2944
        // be addressed individually by the computer shaders.
2945
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
9✔
2946
        trace!(
3✔
2947
            "Aligning storage buffers to {} bytes as device limits requires.",
2✔
2948
            item_align
2949
        );
2950

2951
        Self {
2952
            view_bind_group: None,
2953
            update_sim_params_bind_group: None,
2954
            indirect_sim_params_bind_group: None,
2955
            indirect_metadata_bind_group: None,
2956
            indirect_spawner_bind_group: None,
2957
            sim_params_uniforms: UniformBuffer::default(),
6✔
2958
            spawner_buffer: AlignedBufferVec::new(
6✔
2959
                BufferUsages::STORAGE,
2960
                NonZeroU64::new(item_align),
2961
                Some("hanabi:buffer:spawner".to_string()),
2962
            ),
2963
            dispatch_indirect_buffer: GpuBuffer::new(
6✔
2964
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2965
                Some("hanabi:buffer:dispatch_indirect".to_string()),
2966
            ),
2967
            draw_indirect_buffer: BufferTable::new(
6✔
2968
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2969
                Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
2970
                Some("hanabi:buffer:draw_indirect".to_string()),
2971
            ),
2972
            effect_metadata_buffer: BufferTable::new(
6✔
2973
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2974
                Some(NonZeroU64::new(item_align).unwrap()),
2975
                Some("hanabi:buffer:effect_metadata".to_string()),
2976
            ),
2977
            gpu_limits,
2978
            indirect_shader_noevent,
2979
            indirect_shader_events,
2980
            indirect_pipeline_ids: [
3✔
2981
                CachedComputePipelineId::INVALID,
2982
                CachedComputePipelineId::INVALID,
2983
            ],
2984
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2985
        }
2986
    }
2987

2988
    pub fn allocate_spawner(
312✔
2989
        &mut self,
2990
        global_transform: &GlobalTransform,
2991
        spawn_count: u32,
2992
        prng_seed: u32,
2993
        effect_metadata_buffer_table_id: BufferTableId,
2994
        maybe_cached_draw_indirect_args: Option<&CachedDrawIndirectArgs>,
2995
    ) -> u32 {
2996
        let spawner_base = self.spawner_buffer.len() as u32;
624✔
2997
        let transform = global_transform.to_matrix().into();
1,248✔
2998
        let inverse_transform = Mat4::from(
2999
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
3000
            // efficient than inversing the Mat4.
3001
            global_transform.affine().inverse(),
624✔
3002
        )
3003
        .into();
3004
        let spawner_params = GpuSpawnerParams {
3005
            transform,
3006
            inverse_transform,
3007
            spawn: spawn_count as i32,
312✔
3008
            seed: prng_seed,
3009
            effect_metadata_index: effect_metadata_buffer_table_id.0,
312✔
3010
            draw_indirect_index: maybe_cached_draw_indirect_args
312✔
3011
                .map(|cdia| cdia.get_row().0)
3012
                .unwrap_or_default(),
3013
            ..default()
3014
        };
3015
        trace!("spawner params = {:?}", spawner_params);
624✔
3016
        self.spawner_buffer.push(spawner_params);
936✔
3017
        spawner_base
312✔
3018
    }
3019

3020
    pub fn allocate_draw_indirect(
2✔
3021
        &mut self,
3022
        draw_args: &AnyDrawIndirectArgs,
3023
    ) -> CachedDrawIndirectArgs {
3024
        let row = self
4✔
3025
            .draw_indirect_buffer
2✔
3026
            .insert(draw_args.bitcast_to_row_entry());
6✔
3027
        CachedDrawIndirectArgs {
3028
            row,
3029
            args: *draw_args,
2✔
3030
        }
3031
    }
3032

3033
    pub fn update_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
×
3034
        self.draw_indirect_buffer
×
3035
            .update(row_index.get_row(), row_index.args.bitcast_to_row_entry());
×
3036
    }
3037

3038
    pub fn free_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
1✔
3039
        self.draw_indirect_buffer.remove(row_index.get_row());
4✔
3040
    }
3041
}
3042

3043
bitflags! {
3044
    /// Effect flags.
3045
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3046
    pub struct LayoutFlags: u32 {
3047
        /// No flags.
3048
        const NONE = 0;
3049
        // DEPRECATED - The effect uses an image texture.
3050
        //const PARTICLE_TEXTURE = (1 << 0);
3051
        /// The effect is simulated in local space.
3052
        const LOCAL_SPACE_SIMULATION = (1 << 2);
3053
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
3054
        const USE_ALPHA_MASK = (1 << 3);
3055
        /// The effect is rendered with flipbook texture animation based on the
3056
        /// [`Attribute::SPRITE_INDEX`] of each particle.
3057
        const FLIPBOOK = (1 << 4);
3058
        /// The effect needs UVs.
3059
        const NEEDS_UV = (1 << 5);
3060
        /// The effect has ribbons.
3061
        const RIBBONS = (1 << 6);
3062
        /// The effects needs normals.
3063
        const NEEDS_NORMAL = (1 << 7);
3064
        /// The effect is fully-opaque.
3065
        const OPAQUE = (1 << 8);
3066
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
3067
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
3068
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
3069
        /// a single CPU spawn count.
3070
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
3071
        /// The (init or update) shader needs access to its parent particle. This allows
3072
        /// a particle init or update pass to read the data of a parent particle, for
3073
        /// example to inherit some of the attributes.
3074
        const READ_PARENT_PARTICLE = (1 << 11);
3075
        /// The effect access to the particle data in the fragment shader.
3076
        const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
3077
    }
3078
}
3079

3080
impl Default for LayoutFlags {
3081
    fn default() -> Self {
1✔
3082
        Self::NONE
1✔
3083
    }
3084
}
3085

3086
/// Observer raised when the [`CachedEffect`] component is removed, which
3087
/// indicates that the effect instance was despawned.
3088
pub(crate) fn on_remove_cached_effect(
1✔
3089
    trigger: On<Remove, CachedEffect>,
3090
    query: Query<(
3091
        Entity,
3092
        &MainEntity,
3093
        &CachedEffect,
3094
        &DispatchBufferIndices,
3095
        Option<&CachedEffectProperties>,
3096
        Option<&CachedParentInfo>,
3097
        Option<&CachedEffectEvents>,
3098
    )>,
3099
    mut effect_cache: ResMut<EffectCache>,
3100
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3101
    mut effects_meta: ResMut<EffectsMeta>,
3102
    mut event_cache: ResMut<EventCache>,
3103
) {
3104
    #[cfg(feature = "trace")]
3105
    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
3✔
3106

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

3110
    // Fecth the components of the effect being destroyed. Note that the despawn
3111
    // command above is not yet applied, so this query should always succeed.
3112
    let Ok((
3113
        render_entity,
1✔
3114
        main_entity,
3115
        cached_effect,
3116
        dispatch_buffer_indices,
3117
        _opt_props,
3118
        _opt_parent,
3119
        opt_cached_effect_events,
3120
    )) = query.get(trigger.event().entity)
3✔
3121
    else {
3122
        return;
×
3123
    };
3124

3125
    // Dealllocate the effect slice in the event buffer, if any.
3126
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
3127
        match event_cache.free(cached_effect_events) {
3128
            Err(err) => {
×
3129
                error!("Error while freeing effect event slice: {err:?}");
×
3130
            }
3131
            Ok(buffer_state) => {
×
3132
                if buffer_state != SlabState::Used {
×
3133
                    // Clear bind groups associated with the old buffer
3134
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
3135
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3136
                }
3137
            }
3138
        }
3139
    }
3140

3141
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3142
    // last slice, also deallocate the GPU buffer itself.
3143
    trace!(
3144
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
1✔
3145
        render_entity,
3146
        main_entity,
3147
    );
3148
    let Ok(SlabState::Free) = effect_cache.remove(cached_effect) else {
3149
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3150
        // do.
3151
        return;
×
3152
    };
3153

3154
    // Clear bind groups associated with the removed buffer
3155
    trace!(
1✔
3156
        "=> GPU particle slab #{} gone, destroying its bind groups...",
1✔
3157
        cached_effect.slab_id.index()
2✔
3158
    );
3159
    effect_bind_groups
3160
        .particle_slabs
3161
        .remove(&cached_effect.slab_id);
3162
    effects_meta
3163
        .dispatch_indirect_buffer
3164
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
3165
}
3166

3167
/// Observer raised when the [`CachedEffectMetadata`] component is removed, to
3168
/// deallocate the GPU resources associated with the indirect draw args.
3169
pub(crate) fn on_remove_cached_metadata(
1✔
3170
    trigger: On<Remove, CachedEffectMetadata>,
3171
    query: Query<&CachedEffectMetadata>,
3172
    mut effects_meta: ResMut<EffectsMeta>,
3173
) {
3174
    #[cfg(feature = "trace")]
3175
    let _span = bevy::log::info_span!("on_remove_cached_metadata").entered();
3✔
3176

3177
    if let Ok(cached_metadata) = query.get(trigger.event().entity) {
4✔
3178
        if cached_metadata.table_id.is_valid() {
1✔
3179
            effects_meta
2✔
3180
                .effect_metadata_buffer
2✔
3181
                .remove(cached_metadata.table_id);
1✔
3182
        }
3183
    };
3184
}
3185

3186
/// Observer raised when the [`CachedDrawIndirectArgs`] component is removed, to
3187
/// deallocate the GPU resources associated with the indirect draw args.
3188
pub(crate) fn on_remove_cached_draw_indirect_args(
1✔
3189
    trigger: On<Remove, CachedDrawIndirectArgs>,
3190
    query: Query<&CachedDrawIndirectArgs>,
3191
    mut effects_meta: ResMut<EffectsMeta>,
3192
) {
3193
    #[cfg(feature = "trace")]
3194
    let _span = bevy::log::info_span!("on_remove_cached_draw_indirect_args").entered();
3✔
3195

3196
    if let Ok(cached_draw_args) = query.get(trigger.event().entity) {
4✔
3197
        effects_meta.free_draw_indirect(cached_draw_args);
3198
    };
3199
}
3200

3201
/// Clear pending GPU resources left from previous frame.
3202
///
3203
/// Those generally are source buffers for buffer-to-buffer copies on capacity
3204
/// growth, which need the source buffer to be alive until the copy is done,
3205
/// then can be discarded here.
3206
pub(crate) fn clear_previous_frame_resizes(
330✔
3207
    mut effects_meta: ResMut<EffectsMeta>,
3208
    mut sort_bind_groups: ResMut<SortBindGroups>,
3209
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3210
) {
3211
    #[cfg(feature = "trace")]
3212
    let _span = bevy::log::info_span!("clear_previous_frame_resizes").entered();
990✔
3213
    trace!("clear_previous_frame_resizes");
650✔
3214

3215
    init_fill_dispatch_queue.clear();
330✔
3216

3217
    // Clear last frame's buffer resizes which may have occured during last frame,
3218
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3219
    // the first point at which we can do that where we're not blocking the main
3220
    // world (so, excluding the extract system).
3221
    effects_meta
330✔
3222
        .dispatch_indirect_buffer
330✔
3223
        .clear_previous_frame_resizes();
3224
    effects_meta
330✔
3225
        .draw_indirect_buffer
330✔
3226
        .clear_previous_frame_resizes();
3227
    effects_meta
330✔
3228
        .effect_metadata_buffer
330✔
3229
        .clear_previous_frame_resizes();
3230
    sort_bind_groups.clear_previous_frame_resizes();
330✔
3231
}
3232

3233
// Fixup the [`CachedChildInfo::global_child_index`] once all child infos have
3234
// been allocated.
3235
pub fn fixup_parents(
330✔
3236
    q_changed_parents: Query<(Entity, Ref<CachedParentInfo>)>,
3237
    mut q_children: Query<&mut CachedChildInfo>,
3238
) {
3239
    #[cfg(feature = "trace")]
3240
    let _span = bevy::log::info_span!("fixup_parents").entered();
990✔
3241
    trace!("fixup_parents");
650✔
3242

3243
    // Once all parents are (re-)allocated, fix up the global index of all
3244
    // children if the parent base index changed.
3245
    trace!(
330✔
3246
        "Updating the global index of children of parent effects whose child list just changed..."
320✔
3247
    );
3248
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
660✔
3249
        let base_index =
3250
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3251
        let parent_changed = cached_parent_info.is_changed();
3252
        trace!(
3253
            "Updating {} children of parent effect {:?} with base child index {} (parent_changed:{})...",
×
3254
            cached_parent_info.children.len(),
×
3255
            parent_entity,
3256
            base_index,
3257
            parent_changed
3258
        );
3259
        for (child_entity, _) in &cached_parent_info.children {
×
3260
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3261
                error!(
×
3262
                    "Cannot find child {:?} declared by parent {:?}",
×
3263
                    *child_entity, parent_entity
3264
                );
3265
                continue;
×
3266
            };
3267
            if !cached_child_info.is_changed() && !parent_changed {
×
3268
                continue;
×
3269
            }
3270
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3271
            trace!(
×
3272
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3273
                child_entity,
3274
                parent_entity,
3275
                cached_child_info.local_child_index,
×
3276
                cached_child_info.global_child_index
×
3277
            );
3278
        }
3279
    }
3280
}
3281

3282
/// Allocate the GPU resources for all extracted effects.
3283
///
3284
/// This adds the [`CachedEffect`] component as needed, and update it with the
3285
/// allocation in the [`EffectCache`].
3286
pub fn allocate_effects(
330✔
3287
    mut commands: Commands,
3288
    mut q_extracted_effects: Query<
3289
        (
3290
            Entity,
3291
            &ExtractedEffect,
3292
            Has<ChildEffectOf>,
3293
            Option<&mut CachedEffect>,
3294
            Has<DispatchBufferIndices>,
3295
        ),
3296
        Changed<ExtractedEffect>,
3297
    >,
3298
    mut effect_cache: ResMut<EffectCache>,
3299
    mut effects_meta: ResMut<EffectsMeta>,
3300
) {
3301
    #[cfg(feature = "trace")]
3302
    let _span = bevy::log::info_span!("allocate_effects").entered();
990✔
3303
    trace!("allocate_effects");
650✔
3304

3305
    for (entity, extracted_effect, has_parent, maybe_cached_effect, has_dispatch_buffer_indices) in
3✔
3306
        &mut q_extracted_effects
333✔
3307
    {
3308
        // Insert or update the effect into the EffectCache
3309
        if let Some(mut cached_effect) = maybe_cached_effect {
1✔
3310
            trace!("Updating EffectCache entry for entity {entity:?}...");
1✔
3311
            let _ = effect_cache.remove(cached_effect.as_ref());
3312
            *cached_effect = effect_cache.insert(
3313
                extracted_effect.handle.clone(),
3314
                extracted_effect.capacity,
3315
                &extracted_effect.particle_layout,
3316
            );
3317
        } else {
3318
            trace!("Allocating new entry in EffectCache for entity {entity:?}...");
4✔
3319
            let cached_effect = effect_cache.insert(
8✔
3320
                extracted_effect.handle.clone(),
6✔
3321
                extracted_effect.capacity,
2✔
3322
                &extracted_effect.particle_layout,
2✔
3323
            );
3324
            commands.entity(entity).insert(cached_effect);
8✔
3325
        }
3326

3327
        // Ensure the particle@1 bind group layout exists for the given configuration of
3328
        // particle layout. We do this here only for effects without a parent; for those
3329
        // with a parent, we'll do it after we resolved that parent.
3330
        if !has_parent {
3✔
3331
            let parent_min_binding_size = None;
3✔
3332
            effect_cache.ensure_particle_bind_group_layout(
3✔
3333
                extracted_effect.particle_layout.min_binding_size32(),
3✔
3334
                parent_min_binding_size,
3✔
3335
            );
3336
        }
3337

3338
        // Ensure the metadata@3 bind group layout exists for the init pass.
3339
        {
3340
            let consume_gpu_spawn_events = extracted_effect
3341
                .layout_flags
3342
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
3343
            effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
3344
        }
3345

3346
        // Allocate DispatchBufferIndices if not present yet
3347
        if !has_dispatch_buffer_indices {
2✔
3348
            let update_dispatch_indirect_buffer_row_index =
2✔
3349
                effects_meta.dispatch_indirect_buffer.allocate();
2✔
3350
            commands.entity(entity).insert(DispatchBufferIndices {
2✔
3351
                update_dispatch_indirect_buffer_row_index,
2✔
3352
            });
3353
        }
3354
    }
3355
}
3356

3357
/// Update any cached mesh info based on any relocation done by Bevy itself.
3358
///
3359
/// Bevy will merge small meshes into larger GPU buffers automatically. When
3360
/// this happens, the mesh location changes, and we need to update our
3361
/// references to it in order to know how to issue the draw commands.
3362
///
3363
/// This system updates both the [`CachedMeshLocation`] and the
3364
/// [`CachedIndirectDrawArgs`] components.
3365
pub fn update_mesh_locations(
330✔
3366
    mut commands: Commands,
3367
    mut effects_meta: ResMut<EffectsMeta>,
3368
    mesh_allocator: Res<MeshAllocator>,
3369
    render_meshes: Res<RenderAssets<RenderMesh>>,
3370
    mut q_cached_effects: Query<(
3371
        Entity,
3372
        &ExtractedEffectMesh,
3373
        Option<&mut CachedMeshLocation>,
3374
        Option<&mut CachedDrawIndirectArgs>,
3375
    )>,
3376
) {
3377
    #[cfg(feature = "trace")]
3378
    let _span = bevy::log::info_span!("update_mesh_locations").entered();
990✔
3379
    trace!("update_mesh_locations");
650✔
3380

3381
    for (entity, extracted_mesh, maybe_cached_mesh_location, maybe_cached_draw_indirect_args) in
1,256✔
3382
        &mut q_cached_effects
644✔
3383
    {
3384
        let mut cmds = commands.entity(entity);
1,256✔
3385

3386
        // Resolve the render mesh
3387
        let Some(render_mesh) = render_meshes.get(extracted_mesh.mesh) else {
942✔
3388
            warn!(
×
3389
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3390
                entity, extracted_mesh.mesh
3391
            );
3392
            cmds.remove::<CachedMeshLocation>();
×
3393
            continue;
×
3394
        };
3395

3396
        // Find the location where the render mesh was allocated. This is handled by
3397
        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3398
        // re-batch the vertex and optional index data of meshes together at any point,
3399
        // so we need to confirm that the location data we may have cached is still
3400
        // valid.
3401
        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&extracted_mesh.mesh)
314✔
3402
        else {
3403
            trace!(
×
3404
                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
3405
                entity,
3406
                extracted_mesh.mesh
3407
            );
3408
            cmds.remove::<CachedMeshLocation>();
×
3409
            continue;
×
3410
        };
3411
        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&extracted_mesh.mesh);
3412
        let indexed =
314✔
3413
            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
314✔
3414
                if let Some(ref slice) = mesh_index_buffer_slice {
314✔
3415
                    Some(MeshIndexSlice {
3416
                        format: index_format,
3417
                        buffer: slice.buffer.clone(),
3418
                        range: slice.range.clone(),
3419
                    })
3420
                } else {
3421
                    trace!(
×
3422
                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
3423
                        entity,
3424
                        extracted_mesh.mesh
3425
                    );
3426
                    cmds.remove::<CachedMeshLocation>();
×
3427
                    continue;
×
3428
                }
3429
            } else {
3430
                None
×
3431
            };
3432

3433
        // Calculate the new draw args and mesh location based on Bevy's info
3434
        let new_draw_args = AnyDrawIndirectArgs::from_slices(
3435
            &mesh_vertex_buffer_slice,
3436
            mesh_index_buffer_slice.as_ref(),
3437
        );
3438
        let new_mesh_location = match &mesh_index_buffer_slice {
3439
            // Indexed mesh rendering
3440
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3441
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
942✔
3442
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
628✔
3443
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
628✔
3444
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
314✔
3445
                indexed,
3446
            },
3447
            // Non-indexed mesh rendering
3448
            None => CachedMeshLocation {
3449
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3450
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
3451
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3452
                vertex_offset_or_base_instance: 0,
3453
                indexed: None,
3454
            },
3455
        };
3456

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

3463
            // If the GPU draw args changed, re-upload to GPU.
3464
            if new_draw_args != cached_draw_indirect.args {
312✔
3465
                debug!(
×
3466
                    "Indirect draw args changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3467
                    entity, cached_draw_indirect.args, new_draw_args
×
3468
                );
3469
                cached_draw_indirect.args = new_draw_args;
×
3470
                effects_meta.update_draw_indirect(cached_draw_indirect.as_ref());
×
3471
            }
3472
        } else {
3473
            cmds.insert(effects_meta.allocate_draw_indirect(&new_draw_args));
8✔
3474
        }
3475

3476
        // Compare to any cached data and update if necessary, or insert if missing.
3477
        // This will trigger change detection in the ECS, which will in turn trigger
3478
        // GpuEffectMetadata re-upload.
3479
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
626✔
3480
            if *old_mesh_location != new_mesh_location {
3481
                debug!(
×
3482
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3483
                    entity, old_mesh_location, new_mesh_location
3484
                );
3485
                *old_mesh_location = new_mesh_location;
×
3486
            }
3487
        } else {
3488
            cmds.insert(new_mesh_location);
4✔
3489
        }
3490
    }
3491
}
3492

3493
/// Allocate an entry in the GPU table for any [`CachedEffectMetadata`] missing
3494
/// one.
3495
///
3496
/// This system does NOT take care of (re-)uploading recent CPU data to GPU.
3497
/// This is done much later in the frame, after batching and once all data for
3498
/// it is ready. But it's necessary to ensure the allocation is determined
3499
/// already ahead of time, in order to do batching of contiguous metadata
3500
/// blocks (TODO; not currently used, also may end up using binary search in
3501
/// shader, in which case we won't need continguous-ness and can maybe remove
3502
/// this system).
3503
// TODO - consider using observer OnAdd instead?
3504
pub fn allocate_metadata(
330✔
3505
    mut effects_meta: ResMut<EffectsMeta>,
3506
    mut q_metadata: Query<&mut CachedEffectMetadata>,
3507
) {
3508
    for mut metadata in &mut q_metadata {
958✔
3509
        if !metadata.table_id.is_valid() {
2✔
3510
            metadata.table_id = effects_meta
2✔
3511
                .effect_metadata_buffer
2✔
3512
                .insert(metadata.metadata);
2✔
3513
        } else {
3514
            // Unless this is the first time we allocate the GPU entry (above),
3515
            // we should never reach the beginning of this frame
3516
            // with a changed metadata which has not
3517
            // been re-uploaded last frame.
3518
            // NO! We can only detect the change *since last run of THIS system*
3519
            // so wont' see that a latter system the data.
3520
            // assert!(!metadata.is_changed());
3521
        }
3522
    }
3523
}
3524

3525
/// Update the [`CachedParentInfo`] of parent effects and the
3526
/// [`CachedChildInfo`] of child effects.
3527
pub fn allocate_parent_child_infos(
330✔
3528
    mut commands: Commands,
3529
    mut effect_cache: ResMut<EffectCache>,
3530
    mut event_cache: ResMut<EventCache>,
3531
    // All extracted child effects. May or may not already have a CachedChildInfo. If not, this
3532
    // will be spawned below.
3533
    mut q_child_effects: Query<(
3534
        Entity,
3535
        &ExtractedEffect,
3536
        &ChildEffectOf,
3537
        &CachedEffectEvents,
3538
        Option<&mut CachedChildInfo>,
3539
    )>,
3540
    // All parent effects from a previous frame (already have CachedParentInfo), which can be
3541
    // updated in-place without spawning a new CachedParentInfo.
3542
    mut q_parent_effects: Query<(
3543
        Entity,
3544
        &ExtractedEffect,
3545
        &CachedEffect,
3546
        &ChildrenEffects,
3547
        Option<&mut CachedParentInfo>,
3548
    )>,
3549
) {
3550
    #[cfg(feature = "trace")]
3551
    let _span = bevy::log::info_span!("allocate_child_infos").entered();
990✔
3552
    trace!("allocate_child_infos");
650✔
3553

3554
    // Loop on all child effects and ensure their CachedChildInfo is up-to-date.
3555
    for (child_entity, _, child_effect_of, cached_effect_events, maybe_cached_child_info) in
×
3556
        &mut q_child_effects
330✔
3557
    {
3558
        // Fetch the parent effect
3559
        let parent_entity = child_effect_of.parent;
3560
        let Ok((_, _, parent_cached_effect, children_effects, _)) =
×
3561
            q_parent_effects.get(parent_entity)
3562
        else {
3563
            warn!("Unknown parent #{parent_entity:?} on child entity {child_entity:?}, removing CachedChildInfo.");
×
3564
            if maybe_cached_child_info.is_some() {
×
3565
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3566
            }
3567
            continue;
×
3568
        };
3569

3570
        // Find the index of this child entity in its parent's storage
3571
        let Some(local_child_index) = children_effects.0.iter().position(|e| *e == child_entity)
×
3572
        else {
3573
            warn!("Cannot find child entity {child_entity:?} in the children collection of parent entity {parent_entity:?}. Relationship desync?");
×
3574
            if maybe_cached_child_info.is_some() {
×
3575
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3576
            }
3577
            continue;
×
3578
        };
3579
        let local_child_index = local_child_index as u32;
3580

3581
        // Fetch the effect buffer of the parent effect
3582
        let Some(parent_buffer_binding_source) = effect_cache
×
3583
            .get_slab(&parent_cached_effect.slab_id)
3584
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3585
        else {
3586
            warn!(
×
3587
                "Unknown parent slab #{} on parent entity {:?}, removing CachedChildInfo.",
×
3588
                parent_cached_effect.slab_id.index(),
×
3589
                parent_entity
3590
            );
3591
            if maybe_cached_child_info.is_some() {
×
3592
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3593
            }
3594
            continue;
×
3595
        };
3596

3597
        let new_cached_child_info = CachedChildInfo {
3598
            parent_slab_id: parent_cached_effect.slab_id,
3599
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
3600
            parent_buffer_binding_source,
3601
            local_child_index,
3602
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3603
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3604
        };
3605
        if let Some(mut cached_child_info) = maybe_cached_child_info {
×
3606
            if !cached_child_info.is_locally_equal(&new_cached_child_info) {
×
3607
                *cached_child_info = new_cached_child_info;
×
3608
            }
3609
        } else {
3610
            commands.entity(child_entity).insert(new_cached_child_info);
×
3611
        }
3612
    }
3613

3614
    // Loop on all parent effects and ensure their CachedParentInfo is up-to-date.
3615
    for (parent_entity, parent_extracted_effect, _, children_effects, maybe_cached_parent_info) in
×
3616
        &mut q_parent_effects
330✔
3617
    {
3618
        let parent_min_binding_size = parent_extracted_effect.particle_layout.min_binding_size32();
×
3619

3620
        // Loop over children and gather GpuChildInfo
3621
        let mut new_children = Vec::with_capacity(children_effects.0.len());
×
3622
        let mut new_child_infos = Vec::with_capacity(children_effects.0.len());
×
3623
        for child_entity in children_effects.0.iter() {
×
3624
            // Fetch the child's event buffer allocation info
3625
            let Ok((_, child_extracted_effect, _, cached_effect_events, _)) =
×
3626
                q_child_effects.get(*child_entity)
×
3627
            else {
3628
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} didnt't resolve to a child instance. The parent effect cannot be processed.");
×
3629
                if maybe_cached_parent_info.is_some() {
×
3630
                    commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3631
                }
3632
                break;
×
3633
            };
3634

3635
            // Fetch the GPU event buffer of the child
3636
            let Some(event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3637
            else {
3638
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} doesn't have an allocated GPU event buffer. The parent effect cannot be processed.");
×
3639
                break;
3640
            };
3641

3642
            let buffer_binding_source = BufferBindingSource {
3643
                buffer: event_buffer.clone(),
3644
                offset: cached_effect_events.range.start,
3645
                size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3646
            };
3647
            new_children.push((*child_entity, buffer_binding_source));
3648

3649
            new_child_infos.push(GpuChildInfo {
3650
                event_count: 0,
3651
                init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3652
            });
3653

3654
            // Ensure the particle@1 bind group layout exists for the given configuration of
3655
            // particle layout. We do this here only for effects with a parent; for those
3656
            // without a parent, we already did this in allocate_effects().
3657
            effect_cache.ensure_particle_bind_group_layout(
3658
                child_extracted_effect.particle_layout.min_binding_size32(),
3659
                Some(parent_min_binding_size),
3660
            );
3661
        }
3662

3663
        // If we don't have all children, just abort this effect. We don't try to have
3664
        // partial relationships, this is too complex for shader bindings.
3665
        debug_assert_eq!(new_children.len(), new_child_infos.len());
×
3666
        if (new_children.len() < children_effects.len()) && maybe_cached_parent_info.is_some() {
×
3667
            warn!("One or more child effect(s) on parent effect {parent_entity:?} failed to configure. The parent effect cannot be processed.");
×
3668
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3669
            continue;
×
3670
        }
3671

3672
        // Insert or update the CachedParentInfo component of the parent effect
3673
        if let Some(mut cached_parent_info) = maybe_cached_parent_info {
×
3674
            if cached_parent_info.children != new_children {
×
3675
                // FIXME - missing way to just update in-place without changing the allocation
3676
                // size!
3677
                // if cached_parent_info.children.len() == new_children.len() {
3678
                //} else {
3679
                event_cache.reallocate_child_infos(
×
3680
                    parent_entity,
×
3681
                    new_children,
×
3682
                    &new_child_infos[..],
×
3683
                    cached_parent_info.as_mut(),
×
3684
                );
3685
                //}
3686
            }
3687
        } else {
3688
            let cached_parent_info =
×
3689
                event_cache.allocate_child_infos(parent_entity, new_children, &new_child_infos[..]);
×
3690
            commands.entity(parent_entity).insert(cached_parent_info);
×
3691
        }
3692
    }
3693
}
3694

3695
/// Prepare the init and update compute pipelines for an effect.
3696
///
3697
/// This caches the pipeline IDs once resolved, and their compiling state when
3698
/// it changes, to determine when an effect is ready to be used.
3699
///
3700
/// Note that we do that proactively even if the effect will be skipped this
3701
/// frame (for example because it's not visible). This ensures we queue pipeline
3702
/// compilations ASAP, as they can take a long time (10+ frames). We also use
3703
/// the pipeline compiling state, which we query here, to inform whether the
3704
/// effect is ready for this frame. So in general if this is a new pipeline, it
3705
/// won't be ready this frame.
3706
pub fn prepare_init_update_pipelines(
330✔
3707
    mut q_effects: Query<(
3708
        Entity,
3709
        &ExtractedEffect,
3710
        &CachedEffect,
3711
        Option<&CachedChildInfo>,
3712
        Option<&CachedParentInfo>,
3713
        Option<&CachedEffectProperties>,
3714
        &mut CachedPipelines,
3715
    )>,
3716
    // FIXME - need mut for bind group layout creation; shouldn't be create there though
3717
    mut effect_cache: ResMut<EffectCache>,
3718
    pipeline_cache: Res<PipelineCache>,
3719
    property_cache: ResMut<PropertyCache>,
3720
    mut init_pipeline: ResMut<ParticlesInitPipeline>,
3721
    mut update_pipeline: ResMut<ParticlesUpdatePipeline>,
3722
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
3723
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3724
) {
3725
    #[cfg(feature = "trace")]
3726
    let _span = bevy::log::info_span!("prepare_init_update_pipelines").entered();
990✔
3727
    trace!("prepare_init_update_pipelines");
650✔
3728

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

3732
    for (
3733
        entity,
314✔
3734
        extracted_effect,
3735
        cached_effect,
3736
        maybe_cached_child_info,
3737
        maybe_cached_parent_info,
3738
        maybe_cached_properties,
3739
        mut cached_pipelines,
3740
    ) in &mut q_effects
644✔
3741
    {
3742
        trace!(
3743
            "Preparing pipelines for effect {:?}... (flags: {:?})",
314✔
3744
            entity,
3745
            cached_pipelines.flags
314✔
3746
        );
3747

3748
        let particle_layout = &cached_effect.slice.particle_layout;
3749
        let particle_layout_min_binding_size = particle_layout.min_binding_size32();
3750
        let has_event_buffer = maybe_cached_child_info.is_some();
3751
        let parent_particle_layout_min_binding_size = maybe_cached_child_info
3752
            .as_ref()
3753
            .map(|cci| cci.parent_particle_layout.min_binding_size32());
×
3754

3755
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
314✔
3756
            particle_layout_min_binding_size,
3757
            parent_particle_layout_min_binding_size,
3758
        ) else {
3759
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}",
×
3760
                particle_layout_min_binding_size, parent_particle_layout_min_binding_size);
3761
            continue;
×
3762
        };
3763
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3764

3765
        // This should always exist by the time we reach this point, because we should
3766
        // have inserted any property in the cache, which would have allocated the
3767
        // proper bind group layout (or the default no-property one).
3768
        let property_layout_min_binding_size =
3769
            maybe_cached_properties.map(|cp| cp.property_layout.min_binding_size());
28✔
3770
        let spawner_bind_group_layout = property_cache
3771
            .bind_group_layout(property_layout_min_binding_size)
3772
            .unwrap_or_else(|| {
×
3773
                panic!(
×
3774
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
×
3775
                    property_layout_min_binding_size,
3776
                )
3777
            });
3778
        trace!(
3779
            "Retrieved spawner@2 bind group layout {:?} for property binding size {:?}.",
314✔
3780
            spawner_bind_group_layout.id(),
628✔
3781
            property_layout_min_binding_size
3782
        );
3783

3784
        // Resolve the init pipeline
3785
        let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
312✔
3786
            *init_pipeline_id
3787
        } else {
3788
            // Clear flag just in case, to ensure consistency.
3789
            cached_pipelines
2✔
3790
                .flags
2✔
3791
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3792

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

3799
            let init_pipeline_key_flags = {
2✔
3800
                let mut flags = ParticleInitPipelineKeyFlags::empty();
4✔
3801
                flags.set(
4✔
3802
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3803
                    particle_layout.contains(Attribute::PREV),
4✔
3804
                );
3805
                flags.set(
4✔
3806
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3807
                    particle_layout.contains(Attribute::NEXT),
4✔
3808
                );
3809
                flags.set(
4✔
3810
                    ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3811
                    has_event_buffer,
2✔
3812
                );
3813
                flags
2✔
3814
            };
3815

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

3843
            cached_pipelines.init = Some(init_pipeline_id);
2✔
3844
            init_pipeline_id
2✔
3845
        };
3846

3847
        // Resolve the update pipeline
3848
        let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
312✔
3849
        {
3850
            *update_pipeline_id
3851
        } else {
3852
            // Clear flag just in case, to ensure consistency.
3853
            cached_pipelines
2✔
3854
                .flags
2✔
3855
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
2✔
3856

3857
            let num_event_buffers = maybe_cached_parent_info
4✔
3858
                .as_ref()
3859
                .map(|p| p.children.len() as u32)
2✔
3860
                .unwrap_or_default();
3861

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

3869
            // Fetch the bind group layouts from the cache
3870
            let metadata_bind_group_layout = effect_cache
6✔
3871
                .metadata_update_bind_group_layout(num_event_buffers)
2✔
3872
                .unwrap()
3873
                .clone();
3874

3875
            // https://github.com/bevyengine/bevy/issues/17132
3876
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
6✔
3877
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
6✔
3878
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
6✔
3879
            update_pipeline.temp_particle_bind_group_layout = Some(particle_bind_group_layout);
4✔
3880
            update_pipeline.temp_spawner_bind_group_layout =
2✔
3881
                Some(spawner_bind_group_layout.clone());
2✔
3882
            update_pipeline.temp_metadata_bind_group_layout = Some(metadata_bind_group_layout);
4✔
3883
            let update_pipeline_id = specialized_update_pipelines.specialize(
8✔
3884
                pipeline_cache.as_ref(),
4✔
3885
                &update_pipeline,
4✔
3886
                ParticleUpdatePipelineKey {
2✔
3887
                    shader: extracted_effect.effect_shaders.update.clone(),
6✔
3888
                    particle_layout: particle_layout.clone(),
6✔
3889
                    parent_particle_layout_min_binding_size,
4✔
3890
                    num_event_buffers,
4✔
3891
                    particle_bind_group_layout_id,
4✔
3892
                    spawner_bind_group_layout_id,
2✔
3893
                    metadata_bind_group_layout_id,
2✔
3894
                },
3895
            );
3896
            // keep things tidy; this is just a hack, should not persist
3897
            update_pipeline.temp_particle_bind_group_layout = None;
4✔
3898
            update_pipeline.temp_spawner_bind_group_layout = None;
4✔
3899
            update_pipeline.temp_metadata_bind_group_layout = None;
4✔
3900
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
4✔
3901

3902
            cached_pipelines.update = Some(update_pipeline_id);
2✔
3903
            update_pipeline_id
2✔
3904
        };
3905

3906
        // Never batch an effect with a pipeline not available; this will prevent its
3907
        // init/update pass from running, but the vfx_indirect pass will run
3908
        // nonetheless, which causes desyncs and leads to bugs.
3909
        if pipeline_cache
3910
            .get_compute_pipeline(init_pipeline_id)
3911
            .is_none()
3912
        {
3913
            trace!(
2✔
3914
                "Skipping effect from render entity {:?} due to missing or not ready init pipeline (status: {:?})",
2✔
3915
                entity,
3916
                pipeline_cache.get_compute_pipeline_state(init_pipeline_id)
4✔
3917
            );
3918
            cached_pipelines
2✔
3919
                .flags
2✔
3920
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3921
            continue;
2✔
3922
        }
3923

3924
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3925
        // ready
3926
        cached_pipelines
3927
            .flags
3928
            .insert(CachedPipelineFlags::INIT_PIPELINE_READY);
3929
        trace!("[Effect {:?}] Init pipeline ready.", entity);
312✔
3930

3931
        // Never batch an effect with a pipeline not available; this will prevent its
3932
        // init/update pass from running, but the vfx_indirect pass will run
3933
        // nonetheless, which causes desyncs and leads to bugs.
3934
        if pipeline_cache
3935
            .get_compute_pipeline(update_pipeline_id)
3936
            .is_none()
3937
        {
3938
            trace!(
×
3939
                "Skipping effect from render entity {:?} due to missing or not ready update pipeline (status: {:?})",
×
3940
                entity,
3941
                pipeline_cache.get_compute_pipeline_state(update_pipeline_id)
×
3942
            );
3943
            cached_pipelines
×
3944
                .flags
×
3945
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
×
3946
            continue;
×
3947
        }
3948

3949
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3950
        // ready
3951
        cached_pipelines
3952
            .flags
3953
            .insert(CachedPipelineFlags::UPDATE_PIPELINE_READY);
3954
        trace!("[Effect {:?}] Update pipeline ready.", entity);
312✔
3955
    }
3956
}
3957

3958
pub fn prepare_indirect_pipeline(
330✔
3959
    event_cache: Res<EventCache>,
3960
    mut effects_meta: ResMut<EffectsMeta>,
3961
    pipeline_cache: Res<PipelineCache>,
3962
    indirect_pipeline: Res<DispatchIndirectPipeline>,
3963
    mut specialized_indirect_pipelines: ResMut<
3964
        SpecializedComputePipelines<DispatchIndirectPipeline>,
3965
    >,
3966
) {
3967
    // Ensure the 2 variants of the indirect pipelines are created.
3968
    // TODO - move that elsewhere in some one-time setup?
3969
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
333✔
3970
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
12✔
3971
            pipeline_cache.as_ref(),
6✔
3972
            &indirect_pipeline,
3✔
3973
            DispatchIndirectPipelineKey { has_events: false },
3✔
3974
        );
3975
    }
3976
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
333✔
3977
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
12✔
3978
            pipeline_cache.as_ref(),
6✔
3979
            &indirect_pipeline,
3✔
3980
            DispatchIndirectPipelineKey { has_events: true },
3✔
3981
        );
3982
    }
3983

3984
    // Select the active one depending on whether there's any child info to consume
3985
    let is_empty = event_cache.child_infos().is_empty();
990✔
3986
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
330✔
3987
        if is_empty {
6✔
3988
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
6✔
3989
        } else {
3990
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3991
        }
3992
    } else {
3993
        // If this is the first time we insert an event buffer, we need to switch the
3994
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3995
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3996
        // more effect using GPU spawn events, we can deallocate.
3997
        let was_empty =
327✔
3998
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
3999
        if was_empty && !is_empty {
327✔
4000
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
4001
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
4002
        } else if is_empty && !was_empty {
654✔
4003
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
4004
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
4005
        }
4006
    }
4007
}
4008

4009
// TEMP - Mark all cached effects as invalid for this frame until another system
4010
// explicitly marks them as valid. Otherwise we early out in some parts, and
4011
// reuse by mistake the previous frame's extraction.
4012
pub fn clear_transient_batch_inputs(
330✔
4013
    mut commands: Commands,
4014
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
4015
) {
4016
    for entity in &mut q_cached_effects {
950✔
4017
        if let Ok(mut cmd) = commands.get_entity(entity) {
310✔
4018
            cmd.remove::<BatchInput>();
4019
        }
4020
    }
4021
}
4022

4023
/// Effect mesh extracted from the main world.
4024
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
4025
pub(crate) struct ExtractedEffectMesh {
4026
    /// Asset of the effect mesh to draw.
4027
    pub mesh: AssetId<Mesh>,
4028
}
4029

4030
/// Indexed mesh metadata for [`CachedMesh`].
4031
#[derive(Debug, Clone)]
4032
#[allow(dead_code)]
4033
pub(crate) struct MeshIndexSlice {
4034
    /// Index format.
4035
    pub format: IndexFormat,
4036
    /// GPU buffer containing the indices.
4037
    pub buffer: Buffer,
4038
    /// Range inside [`Self::buffer`] where the indices are.
4039
    pub range: Range<u32>,
4040
}
4041

4042
impl PartialEq for MeshIndexSlice {
4043
    fn eq(&self, other: &Self) -> bool {
312✔
4044
        self.format == other.format
312✔
4045
            && self.buffer.id() == other.buffer.id()
624✔
4046
            && self.range == other.range
312✔
4047
    }
4048
}
4049

4050
impl Eq for MeshIndexSlice {}
4051

4052
/// Cached info about a mesh location in a Bevy buffer. This information is
4053
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
4054
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
4055
/// so we can invalidate that GPU data.
4056
#[derive(Debug, Clone, PartialEq, Eq, Component)]
4057
pub(crate) struct CachedMeshLocation {
4058
    /// Vertex buffer.
4059
    pub vertex_buffer: BufferId,
4060
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
4061
    pub vertex_or_index_count: u32,
4062
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
4063
    pub first_index_or_vertex_offset: u32,
4064
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
4065
    pub vertex_offset_or_base_instance: i32,
4066
    /// Indexed rendering metadata.
4067
    pub indexed: Option<MeshIndexSlice>,
4068
}
4069

4070
/// Cached info about the [`GpuEffectMetadata`] allocation for this effect.
4071
///
4072
/// The component is present when the [`GpuEffectMetadata`] is allocated.
4073
#[derive(Debug, Clone, PartialEq, Eq, Component)]
4074
pub(crate) struct CachedMetadata {
4075
    pub buffer_table_id: BufferTableId,
4076
}
4077

4078
bitflags! {
4079
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4080
    pub struct CachedPipelineFlags: u8 {
4081
        const NONE = 0;
4082
        /// The init pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
4083
        const INIT_PIPELINE_READY = (1u8 << 0);
4084
        /// The update pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
4085
        const UPDATE_PIPELINE_READY = (1u8 << 1);
4086
    }
4087
}
4088

4089
impl Default for CachedPipelineFlags {
4090
    fn default() -> Self {
2✔
4091
        Self::NONE
2✔
4092
    }
4093
}
4094

4095
/// Render world cached shader pipelines for a [`CachedEffect`].
4096
///
4097
/// This is updated with the IDs of the pipelines when they are queued for
4098
/// compiling, and with the state of those pipelines to detect when the effect
4099
/// is ready to be used.
4100
///
4101
/// This component is always auto-inserted alongside [`ExtractedEffect`] as soon
4102
/// as a new effect instance is spawned, because it contains the readiness state
4103
/// of those pipelines, which we want to query each frame. The pipelines are
4104
/// also mandatory, so this component is always needed.
4105
#[derive(Debug, Default, Component)]
4106
pub(crate) struct CachedPipelines {
4107
    /// Caching flags indicating the pipelines readiness.
4108
    pub flags: CachedPipelineFlags,
4109
    /// ID of the cached init pipeline. This is valid once the pipeline is
4110
    /// queued for compilation, but this doesn't mean the pipeline is ready for
4111
    /// use. Readiness is encoded in [`Self::flags`].
4112
    pub init: Option<CachedComputePipelineId>,
4113
    /// ID of the cached update pipeline. This is valid once the pipeline is
4114
    /// queued for compilation, but this doesn't mean the pipeline is ready for
4115
    /// use. Readiness is encoded in [`Self::flags`].
4116
    pub update: Option<CachedComputePipelineId>,
4117
}
4118

4119
impl CachedPipelines {
4120
    /// Check if all pipelines for this effect are ready.
4121
    #[inline]
4122
    pub fn is_ready(&self) -> bool {
628✔
4123
        self.flags.contains(
1,256✔
4124
            CachedPipelineFlags::INIT_PIPELINE_READY | CachedPipelineFlags::UPDATE_PIPELINE_READY,
628✔
4125
        )
4126
    }
4127
}
4128

4129
/// Ready state for this effect.
4130
///
4131
/// An effect is ready if:
4132
/// - Its init and update pipelines are ready, as reported by
4133
///   [`CachedPipelines::is_ready()`].
4134
///
4135
/// This components holds the calculated ready state propagated from all
4136
/// ancestor effects, if any. That propagation is done by the
4137
/// [`propagate_ready_state()`] system.
4138
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
4139
pub(crate) struct CachedReadyState {
4140
    is_ready: bool,
4141
}
4142

4143
impl CachedReadyState {
4144
    #[inline(always)]
4145
    pub fn new(is_ready: bool) -> Self {
314✔
4146
        Self { is_ready }
4147
    }
4148

4149
    #[inline(always)]
4150
    pub fn and(mut self, ancestors_ready: bool) -> Self {
×
4151
        self.and_with(ancestors_ready);
×
4152
        self
×
4153
    }
4154

4155
    #[inline(always)]
4156
    pub fn and_with(&mut self, ancestors_ready: bool) {
×
4157
        self.is_ready = self.is_ready && ancestors_ready;
×
4158
    }
4159

4160
    #[inline(always)]
4161
    pub fn is_ready(&self) -> bool {
628✔
4162
        self.is_ready
628✔
4163
    }
4164
}
4165

4166
#[derive(SystemParam)]
4167
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
4168
    sim_params: Res<'w, SimParams>,
4169
    render_device: Res<'w, RenderDevice>,
4170
    render_queue: Res<'w, RenderQueue>,
4171
    marker: PhantomData<&'s usize>,
4172
}
4173

4174
#[derive(SystemParam)]
4175
pub struct PipelineSystemParams<'w, 's> {
4176
    pipeline_cache: Res<'w, PipelineCache>,
4177
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
4178
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
4179
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
4180
    marker: PhantomData<&'s usize>,
4181
}
4182

4183
/// Update the ready state of all effects, and propagate recursively to
4184
/// children.
4185
pub(crate) fn propagate_ready_state(
330✔
4186
    mut q_root_effects: Query<
4187
        (
4188
            Entity,
4189
            Option<&ChildrenEffects>,
4190
            Ref<CachedPipelines>,
4191
            &mut CachedReadyState,
4192
        ),
4193
        Without<ChildEffectOf>,
4194
    >,
4195
    mut orphaned: RemovedComponents<ChildEffectOf>,
4196
    q_ready_state: Query<
4197
        (
4198
            Ref<CachedPipelines>,
4199
            &mut CachedReadyState,
4200
            Option<&ChildrenEffects>,
4201
        ),
4202
        With<ChildEffectOf>,
4203
    >,
4204
    q_child_effects: Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4205
    mut orphaned_entities: Local<Vec<Entity>>,
4206
) {
4207
    #[cfg(feature = "trace")]
4208
    let _span = bevy::log::info_span!("propagate_ready_state").entered();
990✔
4209
    trace!("propagate_ready_state");
650✔
4210

4211
    // Update orphaned list for this frame, and sort it so we can efficiently binary
4212
    // search it
4213
    orphaned_entities.clear();
330✔
4214
    orphaned_entities.extend(orphaned.read());
990✔
4215
    orphaned_entities.sort_unstable();
330✔
4216

4217
    // Iterate in parallel over all root effects (those without any parent). This is
4218
    // the most common case, so should take care of the heavy lifting of propagating
4219
    // to most effects. For child effects, we then descend recursively.
4220
    q_root_effects.par_iter_mut().for_each(
990✔
4221
        |(entity, maybe_children, cached_pipelines, mut cached_ready_state)| {
314✔
4222
            // Update the ready state of this root effect
4223
            let changed = cached_pipelines.is_changed() || cached_ready_state.is_added() || orphaned_entities.binary_search(&entity).is_ok();
942✔
4224
            trace!("[Entity {}] changed={} cached_pipelines={} ready_state={}", entity, changed, cached_pipelines.is_ready(), cached_ready_state.is_ready);
1,256✔
4225
            if changed {
314✔
4226
                // Root effects by default are ready since they have no ancestors to check. After that we check the ready conditions for this effect alone.
4227
                let new_ready_state = CachedReadyState::new(cached_pipelines.is_ready());
942✔
4228
                if *cached_ready_state != new_ready_state {
314✔
4229
                    debug!(
2✔
4230
                        "[Entity {}] Changed ready to: {}",
2✔
4231
                        entity,
4232
                        new_ready_state.is_ready()
4✔
4233
                    );
4234
                    *cached_ready_state = new_ready_state;
2✔
4235
                }
4236
            }
4237

4238
            // Recursively update the ready state of its descendants
4239
            if let Some(children) = maybe_children {
314✔
4240
                for (child, child_of) in q_child_effects.iter_many(children) {
×
4241
                    assert_eq!(
×
4242
                        child_of.parent, entity,
×
4243
                        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
×
4244
                    );
4245
                    // SAFETY:
4246
                    // - `child` must have consistent parentage, or the above assertion would panic.
4247
                    //   Since `child` is parented to a root entity, the entire hierarchy leading to it
4248
                    //   is consistent.
4249
                    // - We may operate as if all descendants are consistent, since
4250
                    //   `propagate_ready_state_recursive` will panic before continuing to propagate if it
4251
                    //   encounters an entity with inconsistent parentage.
4252
                    // - Since each root entity is unique and the hierarchy is consistent and
4253
                    //   forest-like, other root entities' `propagate_ready_state_recursive` calls will not conflict
4254
                    //   with this one.
4255
                    // - Since this is the only place where `transform_query` gets used, there will be
4256
                    //   no conflicting fetches elsewhere.
4257
                    #[expect(unsafe_code, reason = "`propagate_ready_state_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
4258
                    unsafe {
4259
                        propagate_ready_state_recursive(
×
4260
                            &cached_ready_state,
4261
                            &q_ready_state,
4262
                            &q_child_effects,
4263
                            child,
4264
                            changed || child_of.is_changed(),
×
4265
                        );
4266
                    }
4267
                }
4268
            }
4269
        },
4270
    );
4271
}
4272

4273
#[expect(
4274
    unsafe_code,
4275
    reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
4276
)]
4277
unsafe fn propagate_ready_state_recursive(
×
4278
    parent_state: &CachedReadyState,
4279
    q_ready_state: &Query<
4280
        (
4281
            Ref<CachedPipelines>,
4282
            &mut CachedReadyState,
4283
            Option<&ChildrenEffects>,
4284
        ),
4285
        With<ChildEffectOf>,
4286
    >,
4287
    q_child_of: &Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4288
    entity: Entity,
4289
    mut changed: bool,
4290
) {
4291
    // Update this effect in-place by checking its own state and the state of its
4292
    // parent (which has already been propagated from all the parent's ancestors, so
4293
    // is correct for this frame).
4294
    let (cached_ready_state, maybe_children) = {
×
4295
        let Ok((cached_pipelines, mut cached_ready_state, maybe_children)) =
4296
        // SAFETY: Copied from Bevy's transform propagation, same reasoning
4297
        (unsafe { q_ready_state.get_unchecked(entity) }) else {
×
4298
            return;
×
4299
        };
4300

4301
        changed |= cached_pipelines.is_changed() || cached_ready_state.is_added();
×
4302
        if changed {
4303
            let new_ready_state =
×
4304
                CachedReadyState::new(parent_state.is_ready()).and(cached_pipelines.is_ready());
×
4305
            // Ensure we don't trigger ECS change detection here if state didn't change, so
4306
            // we can avoid this effect branch on next iteration.
4307
            if *cached_ready_state != new_ready_state {
×
4308
                debug!(
×
4309
                    "[Entity {}] Changed ready to: {}",
×
4310
                    entity,
4311
                    new_ready_state.is_ready()
×
4312
                );
4313
                *cached_ready_state = new_ready_state;
×
4314
            }
4315
        }
4316
        (cached_ready_state, maybe_children)
4317
    };
4318

4319
    // Recurse into descendants
4320
    let Some(children) = maybe_children else {
×
4321
        return;
×
4322
    };
4323
    for (child, child_of) in q_child_of.iter_many(children) {
×
4324
        assert_eq!(
×
4325
        child_of.parent, entity,
×
4326
        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
×
4327
    );
4328
        // SAFETY: The caller guarantees that `transform_query` will not be fetched for
4329
        // any descendants of `entity`, so it is safe to call
4330
        // `propagate_recursive` for each child.
4331
        //
4332
        // The above assertion ensures that each child has one and only one unique
4333
        // parent throughout the entire hierarchy.
4334
        unsafe {
4335
            propagate_ready_state_recursive(
4336
                cached_ready_state.as_ref(),
4337
                q_ready_state,
4338
                q_child_of,
4339
                child,
4340
                changed || child_of.is_changed(),
×
4341
            );
4342
        }
4343
    }
4344
}
4345

4346
/// Once all effects are extracted and all cached components are updated, it's
4347
/// time to prepare for sorting and batching. Collect all relevant data and
4348
/// insert/update the [`BatchInput`] for each effect.
4349
pub(crate) fn prepare_batch_inputs(
330✔
4350
    mut commands: Commands,
4351
    read_only_params: PrepareEffectsReadOnlyParams,
4352
    pipelines: PipelineSystemParams,
4353
    mut effects_meta: ResMut<EffectsMeta>,
4354
    mut effect_bind_groups: ResMut<EffectBindGroups>,
4355
    mut property_bind_groups: ResMut<PropertyBindGroups>,
4356
    q_cached_effects: Query<(
4357
        MainEntity,
4358
        Entity,
4359
        &ExtractedEffect,
4360
        &ExtractedSpawner,
4361
        &CachedEffect,
4362
        &CachedEffectMetadata,
4363
        &CachedReadyState,
4364
        &CachedPipelines,
4365
        Option<&CachedDrawIndirectArgs>,
4366
        Option<&CachedParentInfo>,
4367
        Option<&ChildEffectOf>,
4368
        Option<&CachedChildInfo>,
4369
        Option<&CachedEffectEvents>,
4370
    )>,
4371
    mut sort_bind_groups: ResMut<SortBindGroups>,
4372
) {
4373
    #[cfg(feature = "trace")]
4374
    let _span = bevy::log::info_span!("prepare_batch_inputs").entered();
990✔
4375
    trace!("prepare_batch_inputs");
650✔
4376

4377
    // Workaround for too many params in system (TODO: refactor to split work?)
4378
    let sim_params = read_only_params.sim_params.into_inner();
990✔
4379
    let render_device = read_only_params.render_device.into_inner();
990✔
4380
    let render_queue = read_only_params.render_queue.into_inner();
990✔
4381
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
990✔
4382

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

4386
    // Build batcher inputs from extracted effects, updating all cached components
4387
    // for each effect on the fly.
4388
    let mut extracted_effect_count = 0;
660✔
4389
    let mut prepared_effect_count = 0;
660✔
4390
    for (
4391
        main_entity,
314✔
4392
        render_entity,
314✔
4393
        extracted_effect,
314✔
4394
        extracted_spawner,
314✔
4395
        cached_effect,
314✔
4396
        cached_effect_metadata,
314✔
4397
        cached_ready_state,
314✔
4398
        cached_pipelines,
314✔
4399
        maybe_cached_draw_indirect_args,
314✔
4400
        maybe_cached_parent_info,
314✔
4401
        maybe_child_effect_of,
314✔
4402
        maybe_cached_child_info,
314✔
4403
        maybe_cached_effect_events,
314✔
4404
    ) in &q_cached_effects
644✔
4405
    {
4406
        extracted_effect_count += 1;
314✔
4407

4408
        // Skip this effect if not ready
4409
        if !cached_ready_state.is_ready() {
314✔
4410
            trace!("Pipelines not ready for effect {}, skipped.", render_entity);
4✔
4411
            continue;
4412
        }
4413

4414
        // Skip this effect if not visible and not simulating when hidden
4415
        if !extracted_spawner.is_visible
312✔
4416
            && (extracted_effect.simulation_condition == SimulationCondition::WhenVisible)
×
4417
        {
4418
            trace!(
×
4419
                "Effect {} not visible, and simulation condition is WhenVisible, so skipped.",
×
4420
                render_entity
4421
            );
4422
            continue;
×
4423
        }
4424

4425
        // Fetch the init and update pipelines.
4426
        // SAFETY: If is_ready() returns true, this means the pipelines are cached and
4427
        // ready, so the IDs must be valid.
4428
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
4429
            init: cached_pipelines.init.unwrap(),
4430
            update: cached_pipelines.update.unwrap(),
4431
        };
4432

4433
        let effect_slice = EffectSlice {
4434
            slice: cached_effect.slice.range(),
4435
            slab_id: cached_effect.slab_id,
4436
            particle_layout: cached_effect.slice.particle_layout.clone(),
4437
        };
4438

4439
        // Fetch the bind group layouts from the cache
4440
        trace!("child_effect_of={:?}", maybe_child_effect_of);
312✔
4441
        let parent_slab_id = if let Some(child_effect_of) = maybe_child_effect_of {
312✔
4442
            let Ok((_, _, _, _, parent_cached_effect, _, _, _, _, _, _, _, _)) =
×
4443
                q_cached_effects.get(child_effect_of.parent)
4444
            else {
4445
                // At this point we should have discarded invalid effects with a missing parent,
4446
                // so if the parent is not found this is a bug.
4447
                error!(
×
4448
                    "Effect main_entity {:?}: parent render entity {:?} not found.",
×
4449
                    main_entity, child_effect_of.parent
4450
                );
4451
                continue;
×
4452
            };
4453
            Some(parent_cached_effect.slab_id)
4454
        } else {
4455
            None
312✔
4456
        };
4457

4458
        // For ribbons, we need the sorting pipeline to be ready to sort the ribbon's
4459
        // particles by age in order to build a contiguous mesh.
4460
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4461
            // Ensure the bind group layout for sort-fill is ready. This will also ensure
4462
            // the pipeline is created and queued if needed.
4463
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
4464
                pipeline_cache,
×
4465
                &extracted_effect.particle_layout,
×
4466
            ) {
4467
                error!(
4468
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
4469
                    err
4470
                );
4471
                continue;
4472
            }
4473

4474
            // Check sort pipelines are ready, otherwise we might desync some buffers if
4475
            // running only some of them but not all.
4476
            if !sort_bind_groups
×
4477
                .is_pipeline_ready(&extracted_effect.particle_layout, pipeline_cache)
×
4478
            {
4479
                trace!(
×
4480
                    "Sort pipeline not ready for effect on main entity {:?}; skipped.",
×
4481
                    main_entity
4482
                );
4483
                continue;
4484
            }
4485
        }
4486

4487
        // Output some debug info
4488
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
624✔
4489
        trace!(
4490
            "update_shader = {:?}",
312✔
4491
            extracted_effect.effect_shaders.update
4492
        );
4493
        trace!(
4494
            "render_shader = {:?}",
312✔
4495
            extracted_effect.effect_shaders.render
4496
        );
4497
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
312✔
4498
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
312✔
4499

4500
        assert!(cached_effect_metadata.table_id.is_valid());
4501
        let spawner_index = effects_meta.allocate_spawner(
312✔
4502
            &extracted_spawner.transform,
4503
            extracted_spawner.spawn_count,
4504
            extracted_spawner.prng_seed,
4505
            cached_effect_metadata.table_id,
4506
            maybe_cached_draw_indirect_args,
4507
        );
4508

4509
        trace!("Updating cached effect at entity {render_entity:?}...");
312✔
4510
        let mut cmd = commands.entity(render_entity);
4511
        // Inserting the BatchInput component marks the effect as ready for this frame
4512
        cmd.insert(BatchInput {
4513
            effect_slice,
4514
            init_and_update_pipeline_ids,
4515
            parent_slab_id,
4516
            event_buffer_index: maybe_cached_effect_events.map(|cee| cee.buffer_index),
4517
            child_effects: maybe_cached_parent_info
4518
                .as_ref()
4519
                .map(|cp| cp.children.clone())
×
4520
                .unwrap_or_default(),
4521
            spawner_index,
4522
            init_indirect_dispatch_index: maybe_cached_child_info
4523
                .as_ref()
4524
                .map(|cc| cc.init_indirect_dispatch_index),
4525
        });
4526

4527
        prepared_effect_count += 1;
4528
    }
4529
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
650✔
4530

4531
    // Update simulation parameters, including the total effect count for this frame
4532
    {
4533
        let mut gpu_sim_params: GpuSimParams = sim_params.into();
4534
        gpu_sim_params.num_effects = prepared_effect_count;
4535
        trace!(
4536
            "Simulation parameters: time={} delta_time={} virtual_time={} \
320✔
4537
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
320✔
4538
            gpu_sim_params.time,
4539
            gpu_sim_params.delta_time,
4540
            gpu_sim_params.virtual_time,
4541
            gpu_sim_params.virtual_delta_time,
4542
            gpu_sim_params.real_time,
4543
            gpu_sim_params.real_delta_time,
4544
            gpu_sim_params.num_effects,
4545
        );
4546
        effects_meta.sim_params_uniforms.set(gpu_sim_params);
4547
    }
4548

4549
    // Write the entire spawner buffer for this frame, for all effects combined
4550
    assert_eq!(
4551
        prepared_effect_count,
4552
        effects_meta.spawner_buffer.len() as u32
4553
    );
4554
    if effects_meta
330✔
4555
        .spawner_buffer
330✔
4556
        .write_buffer(render_device, render_queue)
990✔
4557
    {
4558
        // All property bind groups use the spawner buffer, which was reallocate
4559
        effect_bind_groups.particle_slabs.clear();
6✔
4560
        property_bind_groups.clear(true);
4✔
4561
        effects_meta.indirect_spawner_bind_group = None;
2✔
4562
    }
4563
}
4564

4565
/// Batch compatible effects together into a single pass.
4566
///
4567
/// For all effects marked as ready for this frame (have a BatchInput
4568
/// component), sort the effects by grouping compatible effects together, then
4569
/// batch those groups together. Each batch can be updated and rendered with a
4570
/// single compute dispatch or draw call.
4571
pub(crate) fn batch_effects(
330✔
4572
    mut commands: Commands,
4573
    effects_meta: Res<EffectsMeta>,
4574
    mut sort_bind_groups: ResMut<SortBindGroups>,
4575
    mut q_cached_effects: Query<(
4576
        Entity,
4577
        &MainEntity,
4578
        &ExtractedEffect,
4579
        &ExtractedSpawner,
4580
        &ExtractedEffectMesh,
4581
        &CachedDrawIndirectArgs,
4582
        &CachedEffectMetadata,
4583
        Option<&CachedEffectEvents>,
4584
        Option<&ChildEffectOf>,
4585
        Option<&CachedChildInfo>,
4586
        Option<&CachedEffectProperties>,
4587
        &mut DispatchBufferIndices,
4588
        // The presence of BatchInput ensure the effect is ready
4589
        &mut BatchInput,
4590
    )>,
4591
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4592
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4593
) {
4594
    #[cfg(feature = "trace")]
4595
    let _span = bevy::log::info_span!("batch_effects").entered();
990✔
4596
    trace!("batch_effects");
650✔
4597

4598
    // Sort effects in batching order, so that we can batch by simply doing a linear
4599
    // scan of the effects in this order. Currently compatible effects mean:
4600
    // - same effect slab (so we can bind the buffers once for all batched effects)
4601
    // - in order of increasing sub-allocation inside those buffers (to make the
4602
    //   sort stable)
4603
    // - with parents before their children, to ensure ???? FIXME don't we need to
4604
    //   opposite?!!!
4605
    let mut effect_sorter = EffectSorter::new();
660✔
4606
    for (entity, _, _, _, _, _, _, _, child_of, _, _, _, input) in &q_cached_effects {
954✔
4607
        effect_sorter.insert(
4608
            entity,
4609
            input.effect_slice.slab_id,
4610
            input.effect_slice.slice.start,
4611
            child_of.map(|co| co.parent),
4612
        );
4613
    }
4614
    effect_sorter.sort();
660✔
4615

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

4620
    let mut sort_queue = GpuBufferOperationQueue::new();
660✔
4621

4622
    // Loop on all extracted effects in sorted order, and try to batch them together
4623
    // to reduce draw calls. -- currently does nothing, batching was broken and
4624
    // never fixed, but at least we minimize the GPU state changes with the sorting!
4625
    trace!("Batching {} effects...", q_cached_effects.iter().len());
1,290✔
4626
    sorted_effect_batches.clear();
330✔
4627
    for entity in effect_sorter.effects.iter().map(|e| e.entity) {
972✔
4628
        let Ok((
4629
            entity,
312✔
4630
            main_entity,
4631
            extracted_effect,
4632
            extracted_spawner,
4633
            extracted_effect_mesh,
4634
            cached_draw_indirect_args,
4635
            cached_effect_metadata,
4636
            cached_effect_events,
4637
            _,
4638
            cached_child_info,
4639
            cached_properties,
4640
            dispatch_buffer_indices,
4641
            mut input,
4642
        )) = q_cached_effects.get_mut(entity)
624✔
4643
        else {
4644
            continue;
×
4645
        };
4646

4647
        let translation = extracted_spawner.transform.translation();
4648

4649
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4650
        // most of the data needed to drive rendering. However this doesn't drive
4651
        // rendering; this is just storage.
4652
        let mut effect_batch = EffectBatch::from_input(
4653
            main_entity.id(),
4654
            extracted_effect,
4655
            extracted_spawner,
4656
            extracted_effect_mesh,
4657
            cached_effect_events,
4658
            cached_child_info,
4659
            &mut input,
4660
            *dispatch_buffer_indices,
4661
            cached_draw_indirect_args.row,
4662
            cached_effect_metadata.table_id,
4663
            cached_properties.map(|cp| PropertyBindGroupKey {
4664
                buffer_index: cp.buffer_index,
13✔
4665
                binding_size: cp.property_layout.min_binding_size().get() as u32,
26✔
4666
            }),
4667
            cached_properties.map(|cp| cp.range.start),
4668
        );
4669

4670
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4671
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4672
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4673
        // ribbon).
4674
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
4675
            // This buffer is allocated in prepare_effects(), so should always be available
4676
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4677
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4678
                continue;
×
4679
            };
4680

4681
            // Allocate a GpuDispatchIndirect entry
4682
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4683
            effect_batch.sort_fill_indirect_dispatch_index =
4684
                Some(sort_fill_indirect_dispatch_index);
4685

4686
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4687
            // compute a number of workgroups to dispatch based on that particle count, and
4688
            // store the result into a GpuDispatchIndirect struct which will be used to
4689
            // dispatch the fill-sort pass.
4690
            {
4691
                let src_buffer = effect_metadata_buffer.clone();
4692
                let src_binding_offset = effects_meta
4693
                    .effect_metadata_buffer
4694
                    .dynamic_offset(effect_batch.metadata_table_id);
4695
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4696
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4697
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4698
                    continue;
×
4699
                };
4700
                let dst_buffer = dst_buffer.clone();
4701
                let dst_binding_offset = 0; // see dst_offset below
4702
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4703
                trace!(
4704
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4705
                    src_buffer.id(),
×
4706
                    src_binding_offset,
4707
                    src_binding_size.get(),
×
4708
                    dst_buffer.id(),
×
4709
                    dst_binding_offset,
4710
                    -1, //dst_binding_size.get(),
4711
                );
4712
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4713
                debug_assert_eq!(
4714
                    src_offset, 1,
4715
                    "GpuEffectMetadata changed, update this assert."
×
4716
                );
4717
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4718
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4719
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4720
                // binding offset of 0 and binds the entire destination buffer,
4721
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4722
                // index the proper offset in the buffer. This requires of
4723
                // course binding the entire buffer, or at least enough to index all operations
4724
                // (hence the None below). This is not really a general solution, so should be
4725
                // reviewed.
4726
                let dst_offset = sort_bind_groups
×
4727
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
4728
                    / 4;
4729
                sort_queue.enqueue(
4730
                    GpuBufferOperationType::FillDispatchArgs,
4731
                    GpuBufferOperationArgs {
4732
                        src_offset,
4733
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
4734
                        dst_offset,
4735
                        dst_stride: GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4,
4736
                        count: 1,
4737
                    },
4738
                    src_buffer,
4739
                    src_binding_offset,
4740
                    Some(src_binding_size),
4741
                    dst_buffer,
4742
                    dst_binding_offset,
4743
                    None, //Some(dst_binding_size),
4744
                );
4745
            }
4746
        }
4747

4748
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
312✔
4749
        trace!(
4750
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
312✔
4751
            effect_batch_index,
4752
            entity,
4753
        );
4754

4755
        // Spawn an EffectDrawBatch, to actually drive rendering.
4756
        commands
4757
            .spawn(EffectDrawBatch {
4758
                effect_batch_index,
4759
                translation,
4760
                main_entity: *main_entity,
4761
            })
4762
            .insert(TemporaryRenderEntity);
4763
    }
4764

4765
    gpu_buffer_operations.begin_frame();
330✔
4766
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
4767
    if !sort_queue.operation_queue.is_empty() {
330✔
4768
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4769
    }
4770
}
4771

4772
/// Per-buffer bind groups for a GPU effect buffer.
4773
///
4774
/// This contains all bind groups specific to a single [`EffectBuffer`].
4775
///
4776
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4777
pub(crate) struct BufferBindGroups {
4778
    /// Bind group for the render shader.
4779
    ///
4780
    /// ```wgsl
4781
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4782
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4783
    /// @binding(2) var<storage, read> spawner : Spawner;
4784
    /// ```
4785
    render: BindGroup,
4786
    // /// Bind group for filling the indirect dispatch arguments of any child init
4787
    // /// pass.
4788
    // ///
4789
    // /// This bind group is optional; it's only created if the current effect has
4790
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4791
    // /// (although normally the event buffer is not created if there's no
4792
    // /// children).
4793
    // ///
4794
    // /// The source buffer is always the current effect's event buffer. The
4795
    // /// destination buffer is the global shared buffer for indirect fill args
4796
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4797
    // /// args contains the data to index the relevant part of the global shared
4798
    // /// buffer for this effect buffer; it may contain multiple entries in case
4799
    // /// multiple effects are batched inside the current effect buffer.
4800
    // ///
4801
    // /// ```wgsl
4802
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4803
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4804
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4805
    // /// ```
4806
    // init_fill_dispatch: Option<BindGroup>,
4807
}
4808

4809
/// Combination of a texture layout and the bound textures.
4810
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4811
struct Material {
4812
    layout: TextureLayout,
4813
    textures: Vec<AssetId<Image>>,
4814
}
4815

4816
impl Material {
4817
    /// Get the bind group entries to create a bind group.
4818
    pub fn make_entries<'a>(
×
4819
        &self,
4820
        gpu_images: &'a RenderAssets<GpuImage>,
4821
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4822
        if self.textures.is_empty() {
×
4823
            return Ok(vec![]);
×
4824
        }
4825

4826
        let entries: Vec<BindGroupEntry<'a>> = self
×
4827
            .textures
×
4828
            .iter()
4829
            .enumerate()
4830
            .flat_map(|(index, id)| {
×
4831
                let base_binding = index as u32 * 2;
×
4832
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4833
                    vec![
×
4834
                        BindGroupEntry {
×
4835
                            binding: base_binding,
×
4836
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4837
                        },
4838
                        BindGroupEntry {
×
4839
                            binding: base_binding + 1,
×
4840
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4841
                        },
4842
                    ]
4843
                } else {
4844
                    vec![]
×
4845
                }
4846
            })
4847
            .collect();
4848
        if entries.len() == self.textures.len() * 2 {
×
4849
            return Ok(entries);
×
4850
        }
4851
        Err(())
×
4852
    }
4853
}
4854

4855
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4856
struct BindingKey {
4857
    pub buffer_id: BufferId,
4858
    pub offset: u32,
4859
    pub size: NonZeroU32,
4860
}
4861

4862
impl<'a> From<BufferSlice<'a>> for BindingKey {
4863
    fn from(value: BufferSlice<'a>) -> Self {
×
4864
        Self {
4865
            buffer_id: value.buffer.id(),
×
4866
            offset: value.offset,
×
4867
            size: value.size,
×
4868
        }
4869
    }
4870
}
4871

4872
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4873
    fn from(value: &BufferSlice<'a>) -> Self {
×
4874
        Self {
4875
            buffer_id: value.buffer.id(),
×
4876
            offset: value.offset,
×
4877
            size: value.size,
×
4878
        }
4879
    }
4880
}
4881

4882
impl From<&BufferBindingSource> for BindingKey {
4883
    fn from(value: &BufferBindingSource) -> Self {
×
4884
        Self {
4885
            buffer_id: value.buffer.id(),
×
4886
            offset: value.offset,
×
4887
            size: value.size,
×
4888
        }
4889
    }
4890
}
4891

4892
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4893
struct ConsumeEventKey {
4894
    child_infos_buffer_id: BufferId,
4895
    events: BindingKey,
4896
}
4897

4898
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4899
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4900
        Self {
4901
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4902
            events: value.events.into(),
×
4903
        }
4904
    }
4905
}
4906

4907
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4908
struct InitMetadataBindGroupKey {
4909
    pub slab_id: SlabId,
4910
    pub effect_metadata_buffer: BufferId,
4911
    pub effect_metadata_offset: u32,
4912
    pub consume_event_key: Option<ConsumeEventKey>,
4913
}
4914

4915
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4916
struct UpdateMetadataBindGroupKey {
4917
    pub slab_id: SlabId,
4918
    pub effect_metadata_buffer: BufferId,
4919
    pub effect_metadata_offset: u32,
4920
    pub child_info_buffer_id: Option<BufferId>,
4921
    pub event_buffers_keys: Vec<BindingKey>,
4922
}
4923

4924
/// Bind group cached with an associated key.
4925
///
4926
/// The cached bind group is associated with the given key representing the
4927
/// inputs that the bind group depends on. When those inputs change, the key
4928
/// should change, indicating the bind group needs to be recreated.
4929
///
4930
/// This object manages a single bind group and its key.
4931
struct CachedBindGroup<K: Eq> {
4932
    /// Key the bind group was created from. Each time the key changes, the bind
4933
    /// group should be re-created.
4934
    key: K,
4935
    /// Bind group created from the key.
4936
    bind_group: BindGroup,
4937
}
4938

4939
#[derive(Debug, Clone, Copy)]
4940
struct BufferSlice<'a> {
4941
    pub buffer: &'a Buffer,
4942
    pub offset: u32,
4943
    pub size: NonZeroU32,
4944
}
4945

4946
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4947
    fn from(value: BufferSlice<'a>) -> Self {
×
4948
        Self {
4949
            buffer: value.buffer,
×
4950
            offset: value.offset.into(),
×
4951
            size: Some(value.size.into()),
×
4952
        }
4953
    }
4954
}
4955

4956
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4957
    fn from(value: &BufferSlice<'a>) -> Self {
×
4958
        Self {
4959
            buffer: value.buffer,
×
4960
            offset: value.offset.into(),
×
4961
            size: Some(value.size.into()),
×
4962
        }
4963
    }
4964
}
4965

4966
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4967
    fn from(value: &'a BufferBindingSource) -> Self {
×
4968
        Self {
4969
            buffer: &value.buffer,
×
4970
            offset: value.offset,
×
4971
            size: value.size,
×
4972
        }
4973
    }
4974
}
4975

4976
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4977
/// the init pass consumes GPU events as a mechanism to spawn particles.
4978
struct ConsumeEventBuffers<'a> {
4979
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4980
    /// This is dynamically indexed inside the shader.
4981
    child_infos_buffer: &'a Buffer,
4982
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4983
    events: BufferSlice<'a>,
4984
}
4985

4986
#[derive(Default, Resource)]
4987
pub struct EffectBindGroups {
4988
    /// Map from a slab ID to the bind groups shared among all effects that
4989
    /// use that particle slab.
4990
    particle_slabs: HashMap<SlabId, BufferBindGroups>,
4991
    /// Map of bind groups for image assets used as particle textures.
4992
    images: HashMap<AssetId<Image>, BindGroup>,
4993
    /// Map from buffer index to its metadata bind group (group 3) for the init
4994
    /// pass.
4995
    // FIXME - doesn't work with batching; this should be the instance ID
4996
    init_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<InitMetadataBindGroupKey>>,
4997
    /// Map from buffer index to its metadata bind group (group 3) for the
4998
    /// update pass.
4999
    // FIXME - doesn't work with batching; this should be the instance ID
5000
    update_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<UpdateMetadataBindGroupKey>>,
5001
    /// Map from an effect material to its bind group.
5002
    material_bind_groups: HashMap<Material, BindGroup>,
5003
}
5004

5005
impl EffectBindGroups {
5006
    pub fn particle_render(&self, slab_id: &SlabId) -> Option<&BindGroup> {
311✔
5007
        self.particle_slabs.get(slab_id).map(|bg| &bg.render)
1,244✔
5008
    }
5009

5010
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
5011
    /// needed.
5012
    pub(self) fn get_or_create_init_metadata(
312✔
5013
        &mut self,
5014
        effect_batch: &EffectBatch,
5015
        gpu_limits: &GpuLimits,
5016
        render_device: &RenderDevice,
5017
        layout: &BindGroupLayout,
5018
        effect_metadata_buffer: &Buffer,
5019
        consume_event_buffers: Option<ConsumeEventBuffers>,
5020
    ) -> Result<&BindGroup, ()> {
5021
        assert!(effect_batch.metadata_table_id.is_valid());
936✔
5022

5023
        let effect_metadata_offset =
312✔
5024
            gpu_limits.effect_metadata_offset(effect_batch.metadata_table_id.0) as u32;
624✔
5025
        let key = InitMetadataBindGroupKey {
5026
            slab_id: effect_batch.slab_id,
624✔
5027
            effect_metadata_buffer: effect_metadata_buffer.id(),
936✔
5028
            effect_metadata_offset,
5029
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
624✔
5030
        };
5031

5032
        let make_entry = || {
314✔
5033
            let mut entries = Vec::with_capacity(3);
4✔
5034
            entries.push(
4✔
5035
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
5036
                BindGroupEntry {
2✔
5037
                    binding: 0,
2✔
5038
                    resource: BindingResource::Buffer(BufferBinding {
2✔
5039
                        buffer: effect_metadata_buffer,
4✔
5040
                        offset: key.effect_metadata_offset as u64,
4✔
5041
                        size: Some(gpu_limits.effect_metadata_size()),
2✔
5042
                    }),
5043
                },
5044
            );
5045
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
5046
                entries.push(
5047
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
5048
                    // ChildInfoBuffer;
5049
                    BindGroupEntry {
5050
                        binding: 1,
5051
                        resource: BindingResource::Buffer(BufferBinding {
5052
                            buffer: consume_event_buffers.child_infos_buffer,
5053
                            offset: 0,
5054
                            size: None,
5055
                        }),
5056
                    },
5057
                );
5058
                entries.push(
5059
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
5060
                    BindGroupEntry {
5061
                        binding: 2,
5062
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
5063
                    },
5064
                );
5065
            }
5066

5067
            let bind_group = render_device.create_bind_group(
6✔
5068
                "hanabi:bind_group:init:metadata@3",
5069
                layout,
2✔
5070
                &entries[..],
2✔
5071
            );
5072

5073
            trace!(
2✔
5074
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
5075
                    effect_batch.slab_id.index(),
4✔
5076
                    effect_batch.metadata_table_id.0,
5077
                );
5078

5079
            bind_group
2✔
5080
        };
5081

5082
        Ok(&self
312✔
5083
            .init_metadata_bind_groups
312✔
5084
            .entry(effect_batch.slab_id)
624✔
5085
            .and_modify(|cbg| {
622✔
5086
                if cbg.key != key {
310✔
5087
                    trace!(
×
5088
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
5089
                        cbg.key,
5090
                        key
5091
                    );
5092
                    cbg.key = key;
×
5093
                    cbg.bind_group = make_entry();
×
5094
                }
5095
            })
5096
            .or_insert_with(|| {
314✔
5097
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
5098
                CachedBindGroup {
2✔
5099
                    key,
2✔
5100
                    bind_group: make_entry(),
2✔
5101
                }
5102
            })
5103
            .bind_group)
5104
    }
5105

5106
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
5107
    /// needed.
5108
    pub(self) fn get_or_create_update_metadata(
312✔
5109
        &mut self,
5110
        effect_batch: &EffectBatch,
5111
        gpu_limits: &GpuLimits,
5112
        render_device: &RenderDevice,
5113
        layout: &BindGroupLayout,
5114
        effect_metadata_buffer: &Buffer,
5115
        child_info_buffer: Option<&Buffer>,
5116
        event_buffers: &[(Entity, BufferBindingSource)],
5117
    ) -> Result<&BindGroup, ()> {
5118
        assert!(effect_batch.metadata_table_id.is_valid());
936✔
5119

5120
        // Check arguments consistency
5121
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
1,560✔
5122
        let emits_gpu_spawn_events = !event_buffers.is_empty();
624✔
5123
        let child_info_buffer_id = if emits_gpu_spawn_events {
624✔
5124
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
5125
        } else {
5126
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
5127
            // if relevant, that is if the effect emits GPU spawn events.
5128
            None
312✔
5129
        };
5130
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
936✔
5131

5132
        let event_buffers_keys = event_buffers
624✔
5133
            .iter()
5134
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
312✔
5135
            .collect::<Vec<_>>();
5136

5137
        let key = UpdateMetadataBindGroupKey {
5138
            slab_id: effect_batch.slab_id,
624✔
5139
            effect_metadata_buffer: effect_metadata_buffer.id(),
936✔
5140
            effect_metadata_offset: gpu_limits
936✔
5141
                .effect_metadata_offset(effect_batch.metadata_table_id.0)
5142
                as u32,
5143
            child_info_buffer_id,
5144
            event_buffers_keys,
5145
        };
5146

5147
        let make_entry = || {
314✔
5148
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
6✔
5149
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
5150
            // EffectMetadata;
5151
            entries.push(BindGroupEntry {
6✔
5152
                binding: 0,
2✔
5153
                resource: BindingResource::Buffer(BufferBinding {
2✔
5154
                    buffer: effect_metadata_buffer,
4✔
5155
                    offset: key.effect_metadata_offset as u64,
4✔
5156
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
2✔
5157
                }),
5158
            });
5159
            if emits_gpu_spawn_events {
2✔
5160
                let child_info_buffer = child_info_buffer.unwrap();
×
5161

5162
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
5163
                // ChildInfoBuffer;
5164
                entries.push(BindGroupEntry {
×
5165
                    binding: 1,
×
5166
                    resource: BindingResource::Buffer(BufferBinding {
×
5167
                        buffer: child_info_buffer,
×
5168
                        offset: 0,
×
5169
                        size: None,
×
5170
                    }),
5171
                });
5172

5173
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
5174
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
5175
                    // EventBuffer;
5176
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
5177
                    // then moved to counting in bytes, so now need some conversion. Need to review
5178
                    // all of this...
5179
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
5180
                    buffer_binding.offset *= 4;
5181
                    buffer_binding.size = buffer_binding
5182
                        .size
5183
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
5184
                    entries.push(BindGroupEntry {
5185
                        binding: 2 + index as u32,
5186
                        resource: BindingResource::Buffer(buffer_binding),
5187
                    });
5188
                }
5189
            }
5190

5191
            let bind_group = render_device.create_bind_group(
6✔
5192
                "hanabi:bind_group:update:metadata@3",
5193
                layout,
2✔
5194
                &entries[..],
2✔
5195
            );
5196

5197
            trace!(
2✔
5198
                "Created new metadata@3 bind group for update pass and slab ID {}: effect_metadata={}",
2✔
5199
                effect_batch.slab_id.index(),
4✔
5200
                effect_batch.metadata_table_id.0,
5201
            );
5202

5203
            bind_group
2✔
5204
        };
5205

5206
        Ok(&self
312✔
5207
            .update_metadata_bind_groups
312✔
5208
            .entry(effect_batch.slab_id)
624✔
5209
            .and_modify(|cbg| {
622✔
5210
                if cbg.key != key {
310✔
5211
                    trace!(
×
5212
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
5213
                        cbg.key,
5214
                        key
5215
                    );
5216
                    cbg.key = key.clone();
×
5217
                    cbg.bind_group = make_entry();
×
5218
                }
5219
            })
5220
            .or_insert_with(|| {
314✔
5221
                trace!(
2✔
5222
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
5223
                    key
5224
                );
5225
                CachedBindGroup {
2✔
5226
                    key: key.clone(),
4✔
5227
                    bind_group: make_entry(),
2✔
5228
                }
5229
            })
5230
            .bind_group)
5231
    }
5232
}
5233

5234
#[derive(SystemParam)]
5235
pub struct QueueEffectsReadOnlyParams<'w, 's> {
5236
    #[cfg(feature = "2d")]
5237
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5238
    #[cfg(feature = "3d")]
5239
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5240
    #[cfg(feature = "3d")]
5241
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5242
    #[cfg(feature = "3d")]
5243
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5244
    marker: PhantomData<&'s usize>,
5245
}
5246

5247
fn emit_sorted_draw<T, F>(
624✔
5248
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5249
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5250
    view_entities: &mut FixedBitSet,
5251
    sorted_effect_batches: &SortedEffectBatches,
5252
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5253
    render_pipeline: &mut ParticlesRenderPipeline,
5254
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5255
    render_meshes: &RenderAssets<RenderMesh>,
5256
    pipeline_cache: &PipelineCache,
5257
    make_phase_item: F,
5258
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5259
) where
5260
    T: SortedPhaseItem,
5261
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5262
{
5263
    trace!("emit_sorted_draw() {} views", views.iter().len());
2,496✔
5264

5265
    for (visible_entities, view, msaa) in views.iter() {
1,872✔
5266
        trace!(
×
5267
            "Process new sorted view with {} visible particle effect entities",
624✔
5268
            visible_entities.len::<CompiledParticleEffect>()
1,248✔
5269
        );
5270

5271
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
312✔
5272
            continue;
312✔
5273
        };
5274

5275
        {
5276
            #[cfg(feature = "trace")]
5277
            let _span = bevy::log::info_span!("collect_view_entities").entered();
936✔
5278

5279
            view_entities.clear();
624✔
5280
            view_entities.extend(
624✔
5281
                visible_entities
312✔
5282
                    .iter::<EffectVisibilityClass>()
312✔
5283
                    .map(|e| e.1.index() as usize),
624✔
5284
            );
5285
        }
5286

5287
        // For each view, loop over all the effect batches to determine if the effect
5288
        // needs to be rendered for that view, and enqueue a view-dependent
5289
        // batch if so.
5290
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
936✔
5291
            #[cfg(feature = "trace")]
5292
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5293

5294
            trace!(
×
5295
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
312✔
5296
                draw_entity,
×
5297
                draw_batch.effect_batch_index,
×
5298
            );
5299

5300
            // Get the EffectBatches this EffectDrawBatch is part of.
5301
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
312✔
5302
            else {
×
5303
                continue;
×
5304
            };
5305

5306
            trace!(
×
5307
                "-> EffectBach: slab_id={} spawner_base={} layout_flags={:?}",
312✔
5308
                effect_batch.slab_id.index(),
624✔
5309
                effect_batch.spawner_base,
×
5310
                effect_batch.layout_flags,
×
5311
            );
5312

5313
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5314
            if effect_batch
×
5315
                .layout_flags
×
5316
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5317
            {
5318
                trace!("Non-transparent batch. Skipped.");
×
5319
                continue;
×
5320
            }
5321

5322
            // Check if batch contains any entity visible in the current view. Otherwise we
5323
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5324
            // the Sprite renderer this is inspired from) we don't expect more than
5325
            // a handful of particle effect instances, so would rather not pay the memory
5326
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5327
            // TODO - Profile to confirm.
5328
            #[cfg(feature = "trace")]
5329
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5330
            let has_visible_entity = effect_batch
×
5331
                .entities
×
5332
                .iter()
5333
                .any(|index| view_entities.contains(*index as usize));
936✔
5334
            if !has_visible_entity {
×
5335
                trace!("No visible entity for view, not emitting any draw call.");
×
5336
                continue;
×
5337
            }
5338
            #[cfg(feature = "trace")]
5339
            _span_check_vis.exit();
624✔
5340

5341
            // Create and cache the bind group layout for this texture layout
5342
            render_pipeline.cache_material(&effect_batch.texture_layout);
936✔
5343

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

5347
            let local_space_simulation = effect_batch
624✔
5348
                .layout_flags
312✔
5349
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
312✔
5350
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
936✔
5351
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
936✔
5352
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
936✔
5353
            let needs_normal = effect_batch
624✔
5354
                .layout_flags
312✔
5355
                .contains(LayoutFlags::NEEDS_NORMAL);
312✔
5356
            let needs_particle_fragment = effect_batch
624✔
5357
                .layout_flags
312✔
5358
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
312✔
5359
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
936✔
5360
            let image_count = effect_batch.texture_layout.layout.len() as u8;
624✔
5361

5362
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5363
            // re-querying here...?
5364
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
936✔
5365
                trace!("Batch has no render mesh, skipped.");
×
5366
                continue;
×
5367
            };
5368
            let mesh_layout = render_mesh.layout.clone();
×
5369

5370
            // Specialize the render pipeline based on the effect batch
5371
            trace!(
×
5372
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
312✔
5373
                effect_batch.render_shader,
×
5374
                image_count,
×
5375
                alpha_mask,
×
5376
                flipbook,
×
5377
                view.hdr
×
5378
            );
5379

5380
            // Add a draw pass for the effect batch
5381
            trace!("Emitting individual draw for batch");
312✔
5382

5383
            let alpha_mode = effect_batch.alpha_mode;
×
5384

5385
            #[cfg(feature = "trace")]
5386
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5387
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5388
                pipeline_cache,
×
5389
                render_pipeline,
×
5390
                ParticleRenderPipelineKey {
×
5391
                    shader: effect_batch.render_shader.clone(),
×
5392
                    mesh_layout: Some(mesh_layout),
×
5393
                    particle_layout: effect_batch.particle_layout.clone(),
×
5394
                    texture_layout: effect_batch.texture_layout.clone(),
×
5395
                    local_space_simulation,
×
5396
                    alpha_mask,
×
5397
                    alpha_mode,
×
5398
                    flipbook,
×
5399
                    needs_uv,
×
5400
                    needs_normal,
×
5401
                    needs_particle_fragment,
×
5402
                    ribbons,
×
5403
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5404
                    pipeline_mode,
×
5405
                    msaa_samples: msaa.samples(),
×
5406
                    hdr: view.hdr,
×
5407
                },
5408
            );
5409
            #[cfg(feature = "trace")]
5410
            _span_specialize.exit();
×
5411

5412
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
312✔
5413
            trace!(
×
5414
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
312✔
5415
                spawner_base={} handle={:?}",
312✔
5416
                draw_entity,
×
5417
                effect_batch.slab_id.index(),
624✔
5418
                effect_batch.spawner_base,
×
5419
                effect_batch.handle
×
5420
            );
5421
            render_phase.add(make_phase_item(
×
5422
                render_pipeline_id,
×
5423
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5424
                draw_batch,
×
5425
                view,
×
5426
            ));
5427
        }
5428
    }
5429
}
5430

5431
#[cfg(feature = "3d")]
5432
fn emit_binned_draw<T, F, G>(
624✔
5433
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5434
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5435
    view_entities: &mut FixedBitSet,
5436
    sorted_effect_batches: &SortedEffectBatches,
5437
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5438
    render_pipeline: &mut ParticlesRenderPipeline,
5439
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5440
    pipeline_cache: &PipelineCache,
5441
    render_meshes: &RenderAssets<RenderMesh>,
5442
    make_batch_set_key: F,
5443
    make_bin_key: G,
5444
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5445
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5446
    change_tick: &mut Tick,
5447
) where
5448
    T: BinnedPhaseItem,
5449
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5450
    G: Fn() -> T::BinKey,
5451
{
5452
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5453

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

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

5459
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
624✔
5460
            continue;
×
5461
        };
5462

5463
        {
5464
            #[cfg(feature = "trace")]
5465
            let _span = bevy::log::info_span!("collect_view_entities").entered();
1,872✔
5466

5467
            view_entities.clear();
1,248✔
5468
            view_entities.extend(
1,248✔
5469
                visible_entities
624✔
5470
                    .iter::<EffectVisibilityClass>()
624✔
5471
                    .map(|e| e.1.index() as usize),
1,248✔
5472
            );
5473
        }
5474

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

5482
            trace!(
×
5483
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
624✔
5484
                draw_entity,
×
5485
                draw_batch.effect_batch_index,
×
5486
            );
5487

5488
            // Get the EffectBatches this EffectDrawBatch is part of.
5489
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
624✔
5490
            else {
×
5491
                continue;
×
5492
            };
5493

5494
            trace!(
×
5495
                "-> EffectBaches: slab_id={} spawner_base={} layout_flags={:?}",
624✔
5496
                effect_batch.slab_id.index(),
1,248✔
5497
                effect_batch.spawner_base,
×
5498
                effect_batch.layout_flags,
×
5499
            );
5500

5501
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5502
                trace!(
624✔
5503
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
624✔
5504
                    effect_batch.layout_flags,
×
5505
                    alpha_mask
×
5506
                );
5507
                continue;
624✔
5508
            }
5509

5510
            // Check if batch contains any entity visible in the current view. Otherwise we
5511
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5512
            // the Sprite renderer this is inspired from) we don't expect more than
5513
            // a handful of particle effect instances, so would rather not pay the memory
5514
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5515
            // TODO - Profile to confirm.
5516
            #[cfg(feature = "trace")]
5517
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5518
            let has_visible_entity = effect_batch
×
5519
                .entities
×
5520
                .iter()
5521
                .any(|index| view_entities.contains(*index as usize));
×
5522
            if !has_visible_entity {
×
5523
                trace!("No visible entity for view, not emitting any draw call.");
×
5524
                continue;
×
5525
            }
5526
            #[cfg(feature = "trace")]
5527
            _span_check_vis.exit();
×
5528

5529
            // Create and cache the bind group layout for this texture layout
5530
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5531

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

5535
            let local_space_simulation = effect_batch
×
5536
                .layout_flags
×
5537
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5538
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5539
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5540
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5541
            let needs_normal = effect_batch
×
5542
                .layout_flags
×
5543
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5544
            let needs_particle_fragment = effect_batch
×
5545
                .layout_flags
×
5546
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5547
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5548
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5549
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5550

5551
            // Specialize the render pipeline based on the effect batch
5552
            trace!(
×
5553
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5554
                effect_batch.render_shader,
×
5555
                image_count,
×
5556
                alpha_mask,
×
5557
                flipbook,
×
5558
                view.hdr
×
5559
            );
5560

5561
            // Add a draw pass for the effect batch
5562
            trace!("Emitting individual draw for batch");
×
5563

5564
            let alpha_mode = effect_batch.alpha_mode;
×
5565

5566
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5567
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5568
                continue;
×
5569
            };
5570

5571
            #[cfg(feature = "trace")]
5572
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5573
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5574
                pipeline_cache,
×
5575
                render_pipeline,
×
5576
                ParticleRenderPipelineKey {
×
5577
                    shader: effect_batch.render_shader.clone(),
×
5578
                    mesh_layout: Some(mesh_layout),
×
5579
                    particle_layout: effect_batch.particle_layout.clone(),
×
5580
                    texture_layout: effect_batch.texture_layout.clone(),
×
5581
                    local_space_simulation,
×
5582
                    alpha_mask,
×
5583
                    alpha_mode,
×
5584
                    flipbook,
×
5585
                    needs_uv,
×
5586
                    needs_normal,
×
5587
                    needs_particle_fragment,
×
5588
                    ribbons,
×
5589
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5590
                    pipeline_mode,
×
5591
                    msaa_samples: msaa.samples(),
×
5592
                    hdr: view.hdr,
×
5593
                },
5594
            );
5595
            #[cfg(feature = "trace")]
5596
            _span_specialize.exit();
×
5597

5598
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5599
            trace!(
×
5600
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
×
5601
                spawner_base={} handle={:?}",
×
5602
                draw_entity,
×
5603
                effect_batch.slab_id.index(),
×
5604
                effect_batch.spawner_base,
×
5605
                effect_batch.handle
×
5606
            );
5607
            render_phase.add(
×
5608
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
5609
                make_bin_key(),
×
5610
                (draw_entity, draw_batch.main_entity),
×
5611
                InputUniformIndex::default(),
×
5612
                BinnedRenderPhaseType::NonMesh,
×
5613
                *change_tick,
×
5614
            );
5615
        }
5616
    }
5617
}
5618

5619
#[allow(clippy::too_many_arguments)]
5620
pub(crate) fn queue_effects(
330✔
5621
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5622
    effects_meta: Res<EffectsMeta>,
5623
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5624
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5625
    pipeline_cache: Res<PipelineCache>,
5626
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5627
    sorted_effect_batches: Res<SortedEffectBatches>,
5628
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5629
    events: Res<EffectAssetEvents>,
5630
    render_meshes: Res<RenderAssets<RenderMesh>>,
5631
    read_params: QueueEffectsReadOnlyParams,
5632
    mut view_entities: Local<FixedBitSet>,
5633
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5634
        ViewSortedRenderPhases<Transparent2d>,
5635
    >,
5636
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5637
        ViewSortedRenderPhases<Transparent3d>,
5638
    >,
5639
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5640
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5641
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5642
    ),
5643
    mut change_tick: Local<Tick>,
5644
) {
5645
    #[cfg(feature = "trace")]
5646
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
990✔
5647

5648
    trace!("queue_effects");
650✔
5649

5650
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5651
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5652
    // reuse stale data.
5653
    let next_change_tick = change_tick.get() + 1;
660✔
5654
    change_tick.set(next_change_tick);
660✔
5655

5656
    // If an image has changed, the GpuImage has (probably) changed
5657
    for event in &events.images {
369✔
5658
        match event {
5659
            AssetEvent::Added { .. } => (),
27✔
5660
            AssetEvent::LoadedWithDependencies { .. } => (),
6✔
5661
            AssetEvent::Unused { .. } => (),
×
5662
            AssetEvent::Modified { id } => {
×
5663
                if effect_bind_groups.images.remove(id).is_some() {
×
5664
                    trace!("Destroyed bind group of modified image asset {:?}", id);
×
5665
                }
5666
            }
5667
            AssetEvent::Removed { id } => {
6✔
5668
                if effect_bind_groups.images.remove(id).is_some() {
18✔
5669
                    trace!("Destroyes bind group of removed image asset {:?}", id);
×
5670
                }
5671
            }
5672
        };
5673
    }
5674

5675
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
978✔
5676
        // No spawners are active
5677
        return;
18✔
5678
    }
5679

5680
    // Loop over all 2D cameras/views that need to render effects
5681
    #[cfg(feature = "2d")]
5682
    {
5683
        #[cfg(feature = "trace")]
5684
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5685

5686
        let draw_effects_function_2d = read_params
5687
            .draw_functions_2d
5688
            .read()
5689
            .get_id::<DrawEffects>()
5690
            .unwrap();
5691

5692
        // Effects with full alpha blending
5693
        if !views.is_empty() {
5694
            trace!("Emit effect draw calls for alpha blended 2D views...");
624✔
5695
            emit_sorted_draw(
5696
                &views,
5697
                &mut transparent_2d_render_phases,
5698
                &mut view_entities,
5699
                &sorted_effect_batches,
5700
                &effect_draw_batches,
5701
                &mut render_pipeline,
5702
                specialized_render_pipelines.reborrow(),
5703
                &render_meshes,
5704
                &pipeline_cache,
5705
                |id, entity, draw_batch, _view| Transparent2d {
5706
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5707
                    entity,
×
5708
                    pipeline: id,
×
5709
                    draw_function: draw_effects_function_2d,
×
5710
                    batch_range: 0..1,
×
5711
                    extracted_index: 0, // ???
5712
                    extra_index: PhaseItemExtraIndex::None,
×
5713
                    indexed: true, // ???
5714
                },
5715
                #[cfg(feature = "3d")]
5716
                PipelineMode::Camera2d,
5717
            );
5718
        }
5719
    }
5720

5721
    // Loop over all 3D cameras/views that need to render effects
5722
    #[cfg(feature = "3d")]
5723
    {
5724
        #[cfg(feature = "trace")]
5725
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5726

5727
        // Effects with full alpha blending
5728
        if !views.is_empty() {
5729
            trace!("Emit effect draw calls for alpha blended 3D views...");
624✔
5730

5731
            let draw_effects_function_3d = read_params
5732
                .draw_functions_3d
5733
                .read()
5734
                .get_id::<DrawEffects>()
5735
                .unwrap();
5736

5737
            emit_sorted_draw(
5738
                &views,
5739
                &mut transparent_3d_render_phases,
5740
                &mut view_entities,
5741
                &sorted_effect_batches,
5742
                &effect_draw_batches,
5743
                &mut render_pipeline,
5744
                specialized_render_pipelines.reborrow(),
5745
                &render_meshes,
5746
                &pipeline_cache,
5747
                |id, entity, batch, view| Transparent3d {
5748
                    distance: view
312✔
5749
                        .rangefinder3d()
312✔
5750
                        .distance_translation(&batch.translation),
624✔
5751
                    pipeline: id,
312✔
5752
                    entity,
312✔
5753
                    draw_function: draw_effects_function_3d,
312✔
5754
                    batch_range: 0..1,
312✔
5755
                    extra_index: PhaseItemExtraIndex::None,
312✔
5756
                    indexed: true, // ???
5757
                },
5758
                #[cfg(feature = "2d")]
5759
                PipelineMode::Camera3d,
5760
            );
5761
        }
5762

5763
        // Effects with alpha mask
5764
        if !views.is_empty() {
5765
            #[cfg(feature = "trace")]
5766
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
312✔
5767

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

5770
            let draw_effects_function_alpha_mask = read_params
5771
                .draw_functions_alpha_mask
5772
                .read()
5773
                .get_id::<DrawEffects>()
5774
                .unwrap();
5775

5776
            emit_binned_draw(
5777
                &views,
5778
                &mut alpha_mask_3d_render_phases,
5779
                &mut view_entities,
5780
                &sorted_effect_batches,
5781
                &effect_draw_batches,
5782
                &mut render_pipeline,
5783
                specialized_render_pipelines.reborrow(),
5784
                &pipeline_cache,
5785
                &render_meshes,
5786
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
5787
                    pipeline: id,
×
5788
                    draw_function: draw_effects_function_alpha_mask,
×
5789
                    material_bind_group_index: None,
×
5790
                    vertex_slab: default(),
×
5791
                    index_slab: None,
×
5792
                },
5793
                // Unused for now
5794
                || OpaqueNoLightmap3dBinKey {
5795
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5796
                },
5797
                #[cfg(feature = "2d")]
5798
                PipelineMode::Camera3d,
5799
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5800
                &mut change_tick,
5801
            );
5802
        }
5803

5804
        // Opaque particles
5805
        if !views.is_empty() {
5806
            #[cfg(feature = "trace")]
5807
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
312✔
5808

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

5811
            let draw_effects_function_opaque = read_params
5812
                .draw_functions_opaque
5813
                .read()
5814
                .get_id::<DrawEffects>()
5815
                .unwrap();
5816

5817
            emit_binned_draw(
5818
                &views,
5819
                &mut opaque_3d_render_phases,
5820
                &mut view_entities,
5821
                &sorted_effect_batches,
5822
                &effect_draw_batches,
5823
                &mut render_pipeline,
5824
                specialized_render_pipelines.reborrow(),
5825
                &pipeline_cache,
5826
                &render_meshes,
5827
                |id, _batch, _view| Opaque3dBatchSetKey {
5828
                    pipeline: id,
×
5829
                    draw_function: draw_effects_function_opaque,
×
5830
                    material_bind_group_index: None,
×
5831
                    vertex_slab: default(),
×
5832
                    index_slab: None,
×
5833
                    lightmap_slab: None,
×
5834
                },
5835
                // Unused for now
5836
                || Opaque3dBinKey {
5837
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5838
                },
5839
                #[cfg(feature = "2d")]
5840
                PipelineMode::Camera3d,
5841
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5842
                &mut change_tick,
5843
            );
5844
        }
5845
    }
5846
}
5847

5848
/// Once a child effect is batched, and therefore passed validations to be
5849
/// updated and rendered this frame, dispatch a new GPU operation to fill the
5850
/// indirect dispatch args of its init pass based on the number of GPU events
5851
/// emitted in the previous frame and stored in its event buffer.
5852
pub fn queue_init_indirect_workgroup_update(
330✔
5853
    q_cached_effects: Query<(Entity, &CachedChildInfo, &CachedEffectEvents)>,
5854
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5855
) {
5856
    debug_assert_eq!(
330✔
5857
        GpuChildInfo::min_size().get() % 4,
330✔
5858
        0,
5859
        "Invalid GpuChildInfo alignment."
×
5860
    );
5861

5862
    // Schedule some GPU buffer operation to update the number of workgroups to
5863
    // dispatch during the indirect init pass of this effect based on the number of
5864
    // GPU spawn events written in its buffer.
5865
    for (entity, cached_child_info, cached_effect_events) in &q_cached_effects {
330✔
5866
        let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
×
5867
        let global_child_index = cached_child_info.global_child_index;
×
5868
        trace!(
×
5869
            "[Effect {:?}] init_fill_dispatch.enqueue(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
5870
            entity,
5871
            global_child_index,
5872
            init_indirect_dispatch_index,
5873
        );
5874
        assert!(global_child_index != u32::MAX);
×
5875
        init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
5876
    }
5877
}
5878

5879
/// Prepare GPU resources for effect rendering.
5880
///
5881
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5882
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5883
/// access to the current camera view.
5884
pub(crate) fn prepare_gpu_resources(
330✔
5885
    mut effects_meta: ResMut<EffectsMeta>,
5886
    //mut effect_cache: ResMut<EffectCache>,
5887
    mut event_cache: ResMut<EventCache>,
5888
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5889
    mut sort_bind_groups: ResMut<SortBindGroups>,
5890
    render_device: Res<RenderDevice>,
5891
    render_queue: Res<RenderQueue>,
5892
    view_uniforms: Res<ViewUniforms>,
5893
    render_pipeline: Res<ParticlesRenderPipeline>,
5894
) {
5895
    // Get the binding for the ViewUniform, the uniform data structure containing
5896
    // the Camera data for the current view. If not available, we cannot render
5897
    // anything.
5898
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
660✔
5899
        return;
×
5900
    };
5901

5902
    // Upload simulation parameters for this frame
5903
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
654✔
5904
    effects_meta
5905
        .sim_params_uniforms
5906
        .write_buffer(&render_device, &render_queue);
5907
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
663✔
5908
        // Buffer changed, invalidate bind groups
5909
        effects_meta.update_sim_params_bind_group = None;
9✔
5910
        effects_meta.indirect_sim_params_bind_group = None;
3✔
5911
    }
5912

5913
    // Create the bind group for the camera/view parameters
5914
    // FIXME - Not here!
5915
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5916
        "hanabi:bind_group_camera_view",
5917
        &render_pipeline.view_layout,
5918
        &[
5919
            BindGroupEntry {
5920
                binding: 0,
5921
                resource: view_binding,
5922
            },
5923
            BindGroupEntry {
5924
                binding: 1,
5925
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5926
            },
5927
        ],
5928
    ));
5929

5930
    // Re-/allocate the draw indirect args buffer if needed
5931
    if effects_meta
5932
        .draw_indirect_buffer
5933
        .allocate_gpu(&render_device, &render_queue)
5934
    {
5935
        // All those bind groups use the buffer so need to be re-created
5936
        trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
4✔
5937
        effects_meta.update_sim_params_bind_group = None;
4✔
5938
        effects_meta.indirect_metadata_bind_group = None;
4✔
5939
    }
5940

5941
    // Re-/allocate any GPU buffer if needed
5942
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5943
    // effect_bind_groups);
5944
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5945
    sort_bind_groups.prepare_buffers(&render_device);
5946
    if effects_meta
5947
        .dispatch_indirect_buffer
5948
        .prepare_buffers(&render_device)
5949
    {
5950
        // All those bind groups use the buffer so need to be re-created
5951
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
4✔
5952
        effect_bind_groups.particle_slabs.clear();
4✔
5953
    }
5954
}
5955

5956
/// Update the [`GpuEffectMetadata`] of all the effects queued for update/render
5957
/// this frame.
5958
///
5959
/// By this point, all effects should have a [`CachedEffectMetadata`] with a
5960
/// valid allocation in the GPU table for a [`GpuEffectMetadata`] entry. This
5961
/// system actually synchronize the CPU value with the GPU one in case of
5962
/// change.
5963
pub(crate) fn prepare_effect_metadata(
330✔
5964
    render_device: Res<RenderDevice>,
5965
    render_queue: Res<RenderQueue>,
5966
    mut q_effects: Query<(
5967
        MainEntity,
5968
        Ref<ExtractedEffect>,
5969
        Ref<CachedEffect>,
5970
        Ref<DispatchBufferIndices>,
5971
        Option<Ref<CachedChildInfo>>,
5972
        Option<Ref<CachedParentInfo>>,
5973
        Option<Ref<CachedDrawIndirectArgs>>,
5974
        Option<Ref<CachedEffectEvents>>,
5975
        &mut CachedEffectMetadata,
5976
    )>,
5977
    mut effects_meta: ResMut<EffectsMeta>,
5978
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5979
) {
5980
    #[cfg(feature = "trace")]
5981
    let _span = bevy::log::info_span!("prepare_effect_metadata").entered();
990✔
5982
    trace!("prepare_effect_metadata");
650✔
5983

5984
    for (
5985
        main_entity,
314✔
5986
        extracted_effect,
314✔
5987
        cached_effect,
314✔
5988
        dispatch_buffer_indices,
314✔
5989
        maybe_cached_child_info,
314✔
5990
        maybe_cached_parent_info,
314✔
5991
        maybe_cached_draw_indirect_args,
314✔
5992
        maybe_cached_effect_events,
314✔
5993
        mut cached_effect_metadata,
314✔
5994
    ) in &mut q_effects
644✔
5995
    {
5996
        // Check if anything relevant to GpuEffectMetadata changed this frame; otherwise
5997
        // early out and skip this effect.
5998
        let is_changed_ee = extracted_effect.is_changed();
942✔
5999
        let is_changed_ce = cached_effect.is_changed();
942✔
6000
        let is_changed_dbi = dispatch_buffer_indices.is_changed();
942✔
6001
        let is_changed_cci = maybe_cached_child_info
628✔
6002
            .as_ref()
6003
            .map(|cci| cci.is_changed())
314✔
6004
            .unwrap_or(false);
6005
        let is_changed_cpi = maybe_cached_parent_info
628✔
6006
            .as_ref()
6007
            .map(|cpi| cpi.is_changed())
314✔
6008
            .unwrap_or(false);
6009
        let is_changed_cdia = maybe_cached_draw_indirect_args
628✔
6010
            .as_ref()
6011
            .map(|cdia| cdia.is_changed())
942✔
6012
            .unwrap_or(false);
6013
        let is_changed_cee = maybe_cached_effect_events
628✔
6014
            .as_ref()
6015
            .map(|cee| cee.is_changed())
314✔
6016
            .unwrap_or(false);
6017
        trace!(
314✔
6018
            "Preparting GpuEffectMetadata for effect {:?}: is_changed[] = {} {} {} {} {} {} {}",
314✔
6019
            main_entity,
6020
            is_changed_ee,
6021
            is_changed_ce,
6022
            is_changed_dbi,
6023
            is_changed_cci,
6024
            is_changed_cpi,
6025
            is_changed_cdia,
6026
            is_changed_cee
6027
        );
6028
        if !is_changed_ee
314✔
6029
            && !is_changed_ce
311✔
6030
            && !is_changed_dbi
311✔
6031
            && !is_changed_cci
311✔
6032
            && !is_changed_cpi
311✔
6033
            && !is_changed_cdia
311✔
6034
            && !is_changed_cee
311✔
6035
        {
6036
            continue;
311✔
6037
        }
6038

6039
        let capacity = cached_effect.slice.len();
9✔
6040

6041
        // Global and local indices of this effect as a child of another (parent) effect
6042
        let (global_child_index, local_child_index) = maybe_cached_child_info
9✔
6043
            .map(|cci| (cci.global_child_index, cci.local_child_index))
3✔
6044
            .unwrap_or((u32::MAX, u32::MAX));
6✔
6045

6046
        // Base index of all children of this (parent) effect
6047
        let base_child_index = maybe_cached_parent_info
6✔
6048
            .map(|cpi| {
3✔
6049
                debug_assert_eq!(
×
6050
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
6051
                    0
6052
                );
6053
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
6054
            })
6055
            .unwrap_or(u32::MAX);
3✔
6056

6057
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
9✔
6058
        let sort_key_offset = extracted_effect
6✔
6059
            .particle_layout
3✔
6060
            .byte_offset(Attribute::RIBBON_ID)
3✔
6061
            .map(|byte_offset| byte_offset / 4)
3✔
6062
            .unwrap_or(u32::MAX);
3✔
6063
        let sort_key2_offset = extracted_effect
6✔
6064
            .particle_layout
3✔
6065
            .byte_offset(Attribute::AGE)
3✔
6066
            .map(|byte_offset| byte_offset / 4)
3✔
6067
            .unwrap_or(u32::MAX);
3✔
6068

6069
        let gpu_effect_metadata = GpuEffectMetadata {
6070
            capacity,
6071
            alive_count: 0,
6072
            max_update: 0,
6073
            max_spawn: capacity,
6074
            indirect_write_index: 0,
6075
            indirect_dispatch_index: dispatch_buffer_indices
3✔
6076
                .update_dispatch_indirect_buffer_row_index,
6077
            indirect_draw_index: maybe_cached_draw_indirect_args
3✔
6078
                .map(|cdia| cdia.get_row().0)
6079
                .unwrap_or(u32::MAX),
6080
            init_indirect_dispatch_index: maybe_cached_effect_events
3✔
6081
                .map(|cee| cee.init_indirect_dispatch_index)
6082
                .unwrap_or(u32::MAX),
6083
            local_child_index,
6084
            global_child_index,
6085
            base_child_index,
6086
            particle_stride,
6087
            sort_key_offset,
6088
            sort_key2_offset,
6089
            ..default()
6090
        };
6091

6092
        // Insert of update entry in GPU buffer table
6093
        assert!(cached_effect_metadata.table_id.is_valid());
9✔
6094
        if gpu_effect_metadata != cached_effect_metadata.metadata {
3✔
6095
            effects_meta
2✔
6096
                .effect_metadata_buffer
2✔
6097
                .update(cached_effect_metadata.table_id, gpu_effect_metadata);
6✔
6098

6099
            cached_effect_metadata.metadata = gpu_effect_metadata;
2✔
6100

6101
            // This triggers on all new spawns and annoys everyone; silence until we can at
6102
            // least warn only on non-first-spawn, and ideally split indirect data from that
6103
            // struct so we don't overwrite it and solve the issue.
6104
            debug!(
2✔
6105
                "Updated metadata entry {} for effect {:?}, this will reset it.",
2✔
6106
                cached_effect_metadata.table_id.0, main_entity
2✔
6107
            );
6108
        }
6109
    }
6110

6111
    // Once all EffectMetadata values are written, schedule a GPU upload
6112
    if effects_meta
330✔
6113
        .effect_metadata_buffer
330✔
6114
        .allocate_gpu(render_device.as_ref(), render_queue.as_ref())
6115
    {
6116
        // All those bind groups use the buffer so need to be re-created
6117
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
4✔
6118
        effects_meta.indirect_metadata_bind_group = None;
4✔
6119
        effect_bind_groups.init_metadata_bind_groups.clear();
4✔
6120
        effect_bind_groups.update_metadata_bind_groups.clear();
4✔
6121
    }
6122
}
6123

6124
/// Read the queued init fill dispatch operations, batch them together by
6125
/// contiguous source and destination entries in the buffers, and enqueue
6126
/// corresponding GPU buffer fill dispatch operations for all batches.
6127
///
6128
/// This system runs after the GPU buffers have been (re-)allocated in
6129
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
6130
/// reference them from the generic [`GpuBufferOperationQueue`].
6131
pub(crate) fn queue_init_fill_dispatch_ops(
330✔
6132
    event_cache: Res<EventCache>,
6133
    render_device: Res<RenderDevice>,
6134
    render_queue: Res<RenderQueue>,
6135
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
6136
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
6137
) {
6138
    // Submit all queued init fill dispatch operations with the proper buffers
6139
    if !init_fill_dispatch_queue.is_empty() {
330✔
6140
        let src_buffer = event_cache.child_infos().buffer();
×
6141
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
6142
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
6143
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
6144
        } else {
6145
            if src_buffer.is_none() {
×
6146
                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());
×
6147
            }
6148
            if dst_buffer.is_none() {
×
6149
                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());
×
6150
            }
6151
        }
6152
    }
6153

6154
    // Once all GPU operations for this frame are enqueued, upload them to GPU
6155
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
990✔
6156
}
6157

6158
pub(crate) fn prepare_bind_groups(
330✔
6159
    mut effects_meta: ResMut<EffectsMeta>,
6160
    mut effect_cache: ResMut<EffectCache>,
6161
    mut event_cache: ResMut<EventCache>,
6162
    mut effect_bind_groups: ResMut<EffectBindGroups>,
6163
    mut property_bind_groups: ResMut<PropertyBindGroups>,
6164
    mut sort_bind_groups: ResMut<SortBindGroups>,
6165
    property_cache: Res<PropertyCache>,
6166
    sorted_effect_batched: Res<SortedEffectBatches>,
6167
    render_device: Res<RenderDevice>,
6168
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
6169
    utils_pipeline: Res<UtilsPipeline>,
6170
    init_pipeline: Res<ParticlesInitPipeline>,
6171
    update_pipeline: Res<ParticlesUpdatePipeline>,
6172
    render_pipeline: ResMut<ParticlesRenderPipeline>,
6173
    gpu_images: Res<RenderAssets<GpuImage>>,
6174
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
6175
) {
6176
    // We can't simulate nor render anything without at least the spawner buffer
6177
    if effects_meta.spawner_buffer.is_empty() {
660✔
6178
        return;
18✔
6179
    }
6180
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
312✔
6181
        return;
×
6182
    };
6183

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

6189
    {
6190
        #[cfg(feature = "trace")]
6191
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
6192

6193
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
6194
        // loop below. Also allows earlying out before doing any work in case some
6195
        // buffer is missing.
6196
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
312✔
6197
            return;
×
6198
        };
6199

6200
        // Create the sim_params@0 bind group for the global simulation parameters,
6201
        // which is shared by the init and update passes.
6202
        if effects_meta.update_sim_params_bind_group.is_none() {
6203
            if let Some(draw_indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() {
4✔
6204
                effects_meta.update_sim_params_bind_group = Some(render_device.create_bind_group(
6205
                    "hanabi:bind_group:vfx_update:sim_params@0",
6206
                    &update_pipeline.sim_params_layout,
6207
                    &[
6208
                        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6209
                        BindGroupEntry {
6210
                            binding: 0,
6211
                            resource: effects_meta.sim_params_uniforms.binding().unwrap(),
6212
                        },
6213
                        // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
6214
                        // array<DrawIndexedIndirectArgs>;
6215
                        BindGroupEntry {
6216
                            binding: 1,
6217
                            resource: draw_indirect_buffer.as_entire_binding(),
6218
                        },
6219
                    ],
6220
                ));
6221
            } else {
6222
                debug!("Cannot allocate bind group for vfx_update:sim_params@0 - draw_indirect_buffer not ready");
×
6223
            }
6224
        }
6225
        if effects_meta.indirect_sim_params_bind_group.is_none() {
2✔
6226
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
8✔
6227
                "hanabi:bind_group:vfx_indirect:sim_params@0",
2✔
6228
                &init_pipeline.sim_params_layout, // FIXME - Shared with init
4✔
6229
                &[
2✔
6230
                    // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6231
                    BindGroupEntry {
2✔
6232
                        binding: 0,
2✔
6233
                        resource: effects_meta.sim_params_uniforms.binding().unwrap(),
4✔
6234
                    },
6235
                ],
6236
            ));
6237
        }
6238

6239
        // Create the @1 bind group for the indirect dispatch preparation pass of all
6240
        // effects at once
6241
        effects_meta.indirect_metadata_bind_group = match (
6242
            effects_meta.effect_metadata_buffer.buffer(),
6243
            effects_meta.dispatch_indirect_buffer.buffer(),
6244
            effects_meta.draw_indirect_buffer.buffer(),
6245
        ) {
6246
            (
6247
                Some(effect_metadata_buffer),
312✔
6248
                Some(dispatch_indirect_buffer),
6249
                Some(draw_indirect_buffer),
6250
            ) => {
6251
                // Base bind group for indirect pass
6252
                Some(render_device.create_bind_group(
6253
                    "hanabi:bind_group:vfx_indirect:metadata@1",
6254
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
6255
                    &[
6256
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer :
6257
                        // array<u32>;
6258
                        BindGroupEntry {
6259
                            binding: 0,
6260
                            resource: effect_metadata_buffer.as_entire_binding(),
6261
                        },
6262
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer
6263
                        // : array<u32>;
6264
                        BindGroupEntry {
6265
                            binding: 1,
6266
                            resource: dispatch_indirect_buffer.as_entire_binding(),
6267
                        },
6268
                        // @group(1) @binding(2) var<storage, read_write> draw_indirect_buffer :
6269
                        // array<u32>;
6270
                        BindGroupEntry {
6271
                            binding: 2,
6272
                            resource: draw_indirect_buffer.as_entire_binding(),
6273
                        },
6274
                    ],
6275
                ))
6276
            }
6277

6278
            // Some buffer is not yet available, can't create the bind group
6279
            _ => None,
×
6280
        };
6281

6282
        // Create the @2 bind group for the indirect dispatch preparation pass of all
6283
        // effects at once
6284
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
6285
            let bind_group = render_device.create_bind_group(
10✔
6286
                "hanabi:bind_group:vfx_indirect:spawner@2",
6287
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
6✔
6288
                &[
4✔
6289
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
6290
                    BindGroupEntry {
4✔
6291
                        binding: 0,
4✔
6292
                        resource: BindingResource::Buffer(BufferBinding {
4✔
6293
                            buffer: &spawner_buffer,
4✔
6294
                            offset: 0,
4✔
6295
                            size: None,
4✔
6296
                        }),
6297
                    },
6298
                ],
6299
            );
6300

6301
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
6302
        }
6303
    }
6304

6305
    // Create the per-slab bind groups
6306
    trace!("Create per-slab bind groups...");
312✔
6307
    for (slab_index, particle_slab) in effect_cache.slabs().iter().enumerate() {
312✔
6308
        #[cfg(feature = "trace")]
6309
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
6310

6311
        let Some(particle_slab) = particle_slab else {
312✔
6312
            trace!(
×
6313
                "Particle slab index #{} has no allocated EffectBuffer, skipped.",
×
6314
                slab_index
6315
            );
6316
            continue;
×
6317
        };
6318

6319
        // Ensure all effects in this batch have a bind group for the entire buffer of
6320
        // the group, since the update phase runs on an entire group/buffer at once,
6321
        // with all the effect instances in it batched together.
6322
        trace!("effect particle slab_index=#{}", slab_index);
312✔
6323
        effect_bind_groups
6324
            .particle_slabs
6325
            .entry(SlabId::new(slab_index as u32))
6326
            .or_insert_with(|| {
2✔
6327
                // Bind group particle@1 for render pass
6328
                trace!("Creating particle@1 bind group for buffer #{slab_index} in render pass");
4✔
6329
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
4✔
6330
                    render_device.limits().min_storage_buffer_offset_alignment,
2✔
6331
                );
6332
                let entries = [
4✔
6333
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
6334
                    BindGroupEntry {
4✔
6335
                        binding: 0,
4✔
6336
                        resource: particle_slab.as_entire_binding_particle(),
4✔
6337
                    },
6338
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
6339
                    BindGroupEntry {
4✔
6340
                        binding: 1,
4✔
6341
                        resource: particle_slab.as_entire_binding_indirect(),
4✔
6342
                    },
6343
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
6344
                    BindGroupEntry {
2✔
6345
                        binding: 2,
2✔
6346
                        resource: BindingResource::Buffer(BufferBinding {
2✔
6347
                            buffer: &spawner_buffer,
2✔
6348
                            offset: 0,
2✔
6349
                            size: Some(spawner_min_binding_size),
2✔
6350
                        }),
6351
                    },
6352
                ];
6353
                let render = render_device.create_bind_group(
8✔
6354
                    &format!("hanabi:bind_group:render:particles@1:vfx{slab_index}")[..],
6✔
6355
                    particle_slab.render_particles_buffer_layout(),
4✔
6356
                    &entries[..],
2✔
6357
                );
6358

6359
                BufferBindGroups { render }
2✔
6360
            });
6361
    }
6362

6363
    // Create bind groups for queued GPU buffer operations
6364
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
6365

6366
    // Create the per-effect bind groups
6367
    let spawner_buffer_binding_size =
6368
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
6369
    for effect_batch in sorted_effect_batched.iter() {
312✔
6370
        #[cfg(feature = "trace")]
6371
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
936✔
6372

6373
        // Create the property bind group @2 if needed
6374
        if let Some(property_key) = &effect_batch.property_key {
325✔
6375
            if let Err(err) = property_bind_groups.ensure_exists(
×
6376
                property_key,
6377
                &property_cache,
6378
                &spawner_buffer,
6379
                spawner_buffer_binding_size,
6380
                &render_device,
6381
            ) {
6382
                error!("Failed to create property bind group for effect batch: {err:?}");
×
6383
                continue;
6384
            }
6385
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
897✔
6386
            &property_cache,
598✔
6387
            &spawner_buffer,
598✔
6388
            spawner_buffer_binding_size,
299✔
6389
            &render_device,
299✔
6390
        ) {
6391
            error!("Failed to create property bind group for effect batch: {err:?}");
×
6392
            continue;
6393
        }
6394

6395
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
6396
        // simulate particles.
6397
        if effect_cache
312✔
6398
            .create_particle_sim_bind_group(
6399
                &effect_batch.slab_id,
6400
                &render_device,
6401
                effect_batch.particle_layout.min_binding_size32(),
6402
                effect_batch.parent_min_binding_size,
6403
                effect_batch.parent_binding_source.as_ref(),
6404
            )
6405
            .is_err()
6406
        {
6407
            error!("No particle buffer allocated for effect batch.");
×
6408
            continue;
×
6409
        }
6410

6411
        // Bind group @3 of init pass
6412
        // FIXME - this is instance-dependent, not buffer-dependent
6413
        {
6414
            let consume_gpu_spawn_events = effect_batch
6415
                .layout_flags
6416
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
6417
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
312✔
6418
                effect_batch.spawn_info
6419
            {
6420
                assert!(consume_gpu_spawn_events);
×
6421
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
6422
                Some(ConsumeEventBuffers {
×
6423
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
6424
                    events: BufferSlice {
×
6425
                        buffer: event_cache
×
6426
                            .get_buffer(cached_effect_events.buffer_index)
×
6427
                            .unwrap(),
×
6428
                        // Note: event range is in u32 count, not bytes
6429
                        offset: cached_effect_events.range.start * 4,
×
6430
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
6431
                    },
6432
                })
6433
            } else {
6434
                assert!(!consume_gpu_spawn_events);
624✔
6435
                None
312✔
6436
            };
6437
            let Some(init_metadata_layout) =
312✔
6438
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
6439
            else {
6440
                continue;
×
6441
            };
6442
            if effect_bind_groups
6443
                .get_or_create_init_metadata(
6444
                    effect_batch,
6445
                    &effects_meta.gpu_limits,
6446
                    &render_device,
6447
                    init_metadata_layout,
6448
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6449
                    consume_event_buffers,
6450
                )
6451
                .is_err()
6452
            {
6453
                continue;
×
6454
            }
6455
        }
6456

6457
        // Bind group @3 of update pass
6458
        // FIXME - this is instance-dependent, not buffer-dependent#
6459
        {
6460
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
6461

6462
            let Some(update_metadata_layout) =
312✔
6463
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
6464
            else {
6465
                continue;
×
6466
            };
6467
            if effect_bind_groups
6468
                .get_or_create_update_metadata(
6469
                    effect_batch,
6470
                    &effects_meta.gpu_limits,
6471
                    &render_device,
6472
                    update_metadata_layout,
6473
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6474
                    event_cache.child_infos_buffer(),
6475
                    &effect_batch.child_event_buffers[..],
6476
                )
6477
                .is_err()
6478
            {
6479
                continue;
×
6480
            }
6481
        }
6482

6483
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
6484
            let effect_buffer = effect_cache.get_slab(&effect_batch.slab_id).unwrap();
×
6485

6486
            // Bind group @0 of sort-fill pass
6487
            let particle_buffer = effect_buffer.particle_buffer();
×
6488
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6489
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6490
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
6491
                &effect_batch.particle_layout,
×
6492
                particle_buffer,
×
6493
                indirect_index_buffer,
×
6494
                effect_metadata_buffer,
×
6495
            ) {
6496
                error!(
6497
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
6498
                    err
6499
                );
6500
                continue;
6501
            }
6502

6503
            // Bind group @0 of sort-copy pass
6504
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6505
            if let Err(err) = sort_bind_groups
×
6506
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
6507
            {
6508
                error!(
6509
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
6510
                    err
6511
                );
6512
                continue;
6513
            }
6514
        }
6515

6516
        // Ensure the particle texture(s) are available as GPU resources and that a bind
6517
        // group for them exists
6518
        // FIXME fix this insert+get below
6519
        if !effect_batch.texture_layout.layout.is_empty() {
312✔
6520
            // This should always be available, as this is cached into the render pipeline
6521
            // just before we start specializing it.
6522
            let Some(material_bind_group_layout) =
×
6523
                render_pipeline.get_material(&effect_batch.texture_layout)
×
6524
            else {
6525
                error!(
×
6526
                    "Failed to find material bind group layout for particle slab #{}",
×
6527
                    effect_batch.slab_id.index()
×
6528
                );
6529
                continue;
×
6530
            };
6531

6532
            // TODO = move
6533
            let material = Material {
6534
                layout: effect_batch.texture_layout.clone(),
6535
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6536
            };
6537
            assert_eq!(material.layout.layout.len(), material.textures.len());
6538

6539
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6540
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6541
                trace!(
×
6542
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
6543
                    material
6544
                );
6545
                continue;
×
6546
            };
6547

6548
            effect_bind_groups
6549
                .material_bind_groups
6550
                .entry(material.clone())
6551
                .or_insert_with(|| {
×
6552
                    debug!("Creating material bind group for material {:?}", material);
×
6553
                    render_device.create_bind_group(
×
6554
                        &format!(
×
6555
                            "hanabi:material_bind_group_{}",
×
6556
                            material.layout.layout.len()
×
6557
                        )[..],
×
6558
                        material_bind_group_layout,
×
6559
                        &bind_group_entries[..],
×
6560
                    )
6561
                });
6562
        }
6563
    }
6564
}
6565

6566
type DrawEffectsSystemState = SystemState<(
6567
    SRes<EffectsMeta>,
6568
    SRes<EffectBindGroups>,
6569
    SRes<PipelineCache>,
6570
    SRes<RenderAssets<RenderMesh>>,
6571
    SRes<MeshAllocator>,
6572
    SQuery<Read<ViewUniformOffset>>,
6573
    SRes<SortedEffectBatches>,
6574
    SQuery<Read<EffectDrawBatch>>,
6575
)>;
6576

6577
/// Draw function for rendering all active effects for the current frame.
6578
///
6579
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6580
/// and the [`Transparent3d`] phase of the main 3D pass.
6581
pub(crate) struct DrawEffects {
6582
    params: DrawEffectsSystemState,
6583
}
6584

6585
impl DrawEffects {
6586
    pub fn new(world: &mut World) -> Self {
12✔
6587
        Self {
6588
            params: SystemState::new(world),
12✔
6589
        }
6590
    }
6591
}
6592

6593
/// Draw all particles of a single effect in view, in 2D or 3D.
6594
///
6595
/// FIXME: use pipeline ID to look up which group index it is.
6596
fn draw<'w>(
311✔
6597
    world: &'w World,
6598
    pass: &mut TrackedRenderPass<'w>,
6599
    view: Entity,
6600
    entity: (Entity, MainEntity),
6601
    pipeline_id: CachedRenderPipelineId,
6602
    params: &mut DrawEffectsSystemState,
6603
) {
6604
    let (
×
6605
        effects_meta,
311✔
6606
        effect_bind_groups,
311✔
6607
        pipeline_cache,
311✔
6608
        meshes,
311✔
6609
        mesh_allocator,
311✔
6610
        views,
311✔
6611
        sorted_effect_batches,
311✔
6612
        effect_draw_batches,
311✔
6613
    ) = params.get(world);
622✔
6614
    let view_uniform = views.get(view).unwrap();
1,555✔
6615
    let effects_meta = effects_meta.into_inner();
933✔
6616
    let effect_bind_groups = effect_bind_groups.into_inner();
933✔
6617
    let meshes = meshes.into_inner();
933✔
6618
    let mesh_allocator = mesh_allocator.into_inner();
933✔
6619
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
1,555✔
6620
    let effect_batch = sorted_effect_batches
933✔
6621
        .get(effect_draw_batch.effect_batch_index)
311✔
6622
        .unwrap();
6623

6624
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
933✔
6625
        return;
×
6626
    };
6627

6628
    trace!("render pass");
311✔
6629

6630
    pass.set_render_pipeline(pipeline);
×
6631

6632
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
311✔
6633
        return;
×
6634
    };
6635
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
311✔
6636
        return;
×
6637
    };
6638

6639
    // Vertex buffer containing the particle model to draw. Generally a quad.
6640
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6641
    // "base_vertex" in the indirect struct...
6642
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6643

6644
    // View properties (camera matrix, etc.)
6645
    pass.set_bind_group(
×
6646
        0,
6647
        effects_meta.view_bind_group.as_ref().unwrap(),
×
6648
        &[view_uniform.offset],
×
6649
    );
6650

6651
    // Particles buffer
6652
    let spawner_base = effect_batch.spawner_base;
×
6653
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
6654
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6655
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
622✔
6656
    pass.set_bind_group(
622✔
6657
        1,
6658
        effect_bind_groups
622✔
6659
            .particle_render(&effect_batch.slab_id)
622✔
6660
            .unwrap(),
311✔
6661
        &[spawner_offset],
311✔
6662
    );
6663

6664
    // Particle texture
6665
    // TODO = move
6666
    let material = Material {
6667
        layout: effect_batch.texture_layout.clone(),
622✔
6668
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
933✔
6669
    };
6670
    if !effect_batch.texture_layout.layout.is_empty() {
311✔
6671
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6672
            pass.set_bind_group(2, bind_group, &[]);
×
6673
        } else {
6674
            // Texture(s) not ready; skip this drawing for now
6675
            trace!(
×
6676
                "Particle material bind group not available for batch slab_id={}. Skipping draw call.",
×
6677
                effect_batch.slab_id.index(),
×
6678
            );
6679
            return;
×
6680
        }
6681
    }
6682

6683
    let draw_indirect_index = effect_batch.draw_indirect_buffer_row_index.0;
311✔
6684
    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
×
6685
    let draw_indirect_offset =
311✔
6686
        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
311✔
6687
    trace!(
311✔
6688
        "Draw up to {} particles with {} vertices per particle for batch from particle slab #{} \
311✔
6689
            (effect_metadata_index={}, draw_indirect_offset={}B).",
311✔
6690
        effect_batch.slice.len(),
622✔
6691
        render_mesh.vertex_count,
×
6692
        effect_batch.slab_id.index(),
622✔
6693
        draw_indirect_index,
×
6694
        draw_indirect_offset,
×
6695
    );
6696

6697
    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
622✔
6698
        trace!(
×
6699
            "The draw indirect buffer containing the indirect draw args is not ready for batch slab_id=#{}. Skipping draw call.",
×
6700
            effect_batch.slab_id.index(),
×
6701
        );
6702
        return;
×
6703
    };
6704

6705
    match render_mesh.buffer_info {
×
6706
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
311✔
6707
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
311✔
6708
            else {
×
6709
                trace!(
×
6710
                    "The index buffer for indexed rendering is not ready for batch slab_id=#{}. Skipping draw call.",
×
6711
                    effect_batch.slab_id.index(),
×
6712
                );
6713
                return;
×
6714
            };
6715

6716
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6717
            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
×
6718
        }
6719
        RenderMeshBufferInfo::NonIndexed => {
×
6720
            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
×
6721
        }
6722
    }
6723
}
6724

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

6747
#[cfg(feature = "3d")]
6748
impl Draw<Transparent3d> for DrawEffects {
6749
    fn draw<'w>(
311✔
6750
        &mut self,
6751
        world: &'w World,
6752
        pass: &mut TrackedRenderPass<'w>,
6753
        view: Entity,
6754
        item: &Transparent3d,
6755
    ) -> Result<(), DrawError> {
6756
        trace!("Draw<Transparent3d>: view={:?}", view);
622✔
6757
        draw(
6758
            world,
311✔
6759
            pass,
311✔
6760
            view,
311✔
6761
            item.entity,
311✔
6762
            item.pipeline,
311✔
6763
            &mut self.params,
311✔
6764
        );
6765
        Ok(())
311✔
6766
    }
6767
}
6768

6769
#[cfg(feature = "3d")]
6770
impl Draw<AlphaMask3d> for DrawEffects {
6771
    fn draw<'w>(
×
6772
        &mut self,
6773
        world: &'w World,
6774
        pass: &mut TrackedRenderPass<'w>,
6775
        view: Entity,
6776
        item: &AlphaMask3d,
6777
    ) -> Result<(), DrawError> {
6778
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6779
        draw(
6780
            world,
×
6781
            pass,
×
6782
            view,
×
6783
            item.representative_entity,
×
6784
            item.batch_set_key.pipeline,
×
6785
            &mut self.params,
×
6786
        );
6787
        Ok(())
×
6788
    }
6789
}
6790

6791
#[cfg(feature = "3d")]
6792
impl Draw<Opaque3d> for DrawEffects {
6793
    fn draw<'w>(
×
6794
        &mut self,
6795
        world: &'w World,
6796
        pass: &mut TrackedRenderPass<'w>,
6797
        view: Entity,
6798
        item: &Opaque3d,
6799
    ) -> Result<(), DrawError> {
6800
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6801
        draw(
6802
            world,
×
6803
            pass,
×
6804
            view,
×
6805
            item.representative_entity,
×
6806
            item.batch_set_key.pipeline,
×
6807
            &mut self.params,
×
6808
        );
6809
        Ok(())
×
6810
    }
6811
}
6812

6813
/// Render node to run the simulation sub-graph once per frame.
6814
///
6815
/// This node doesn't simulate anything by itself, but instead schedules the
6816
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6817
/// actual simulation.
6818
///
6819
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6820
/// renders all the views, such that rendered views have access to the
6821
/// just-simulated particles to render them.
6822
///
6823
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6824
pub(crate) struct VfxSimulateDriverNode;
6825

6826
impl Node for VfxSimulateDriverNode {
6827
    fn run(
330✔
6828
        &self,
6829
        graph: &mut RenderGraphContext,
6830
        _render_context: &mut RenderContext,
6831
        _world: &World,
6832
    ) -> Result<(), NodeRunError> {
6833
        graph.run_sub_graph(
660✔
6834
            crate::plugin::simulate_graph::HanabiSimulateGraph,
330✔
6835
            vec![],
330✔
6836
            None,
330✔
6837
        )?;
6838
        Ok(())
330✔
6839
    }
6840
}
6841

6842
#[derive(Debug, Clone, PartialEq, Eq)]
6843
enum HanabiPipelineId {
6844
    Invalid,
6845
    Cached(CachedComputePipelineId),
6846
}
6847

6848
#[derive(Debug)]
6849
pub(crate) enum ComputePipelineError {
6850
    Queued,
6851
    Creating,
6852
    Error,
6853
}
6854

6855
impl From<&CachedPipelineState> for ComputePipelineError {
6856
    fn from(value: &CachedPipelineState) -> Self {
×
6857
        match value {
×
6858
            CachedPipelineState::Queued => Self::Queued,
×
6859
            CachedPipelineState::Creating(_) => Self::Creating,
×
6860
            CachedPipelineState::Err(_) => Self::Error,
×
6861
            _ => panic!("Trying to convert Ok state to error."),
×
6862
        }
6863
    }
6864
}
6865

6866
pub(crate) struct HanabiComputePass<'a> {
6867
    /// Pipeline cache to fetch cached compute pipelines by ID.
6868
    pipeline_cache: &'a PipelineCache,
6869
    /// WGPU compute pass.
6870
    compute_pass: ComputePass<'a>,
6871
    /// Current pipeline (cached).
6872
    pipeline_id: HanabiPipelineId,
6873
}
6874

6875
impl<'a> Deref for HanabiComputePass<'a> {
6876
    type Target = ComputePass<'a>;
6877

6878
    fn deref(&self) -> &Self::Target {
×
6879
        &self.compute_pass
×
6880
    }
6881
}
6882

6883
impl DerefMut for HanabiComputePass<'_> {
6884
    fn deref_mut(&mut self) -> &mut Self::Target {
4,308✔
6885
        &mut self.compute_pass
4,308✔
6886
    }
6887
}
6888

6889
impl<'a> HanabiComputePass<'a> {
6890
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
1,248✔
6891
        Self {
6892
            pipeline_cache,
6893
            compute_pass,
6894
            pipeline_id: HanabiPipelineId::Invalid,
6895
        }
6896
    }
6897

6898
    pub fn set_cached_compute_pipeline(
921✔
6899
        &mut self,
6900
        pipeline_id: CachedComputePipelineId,
6901
    ) -> Result<(), ComputePipelineError> {
6902
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
921✔
6903
            trace!("set_cached_compute_pipeline() id={pipeline_id:?} -> already set; skipped");
×
6904
            return Ok(());
×
6905
        }
6906
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
921✔
6907
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
921✔
6908
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6909
            if let CachedPipelineState::Err(err) = state {
×
6910
                error!(
×
6911
                    "Failed to find compute pipeline #{}: {:?}",
×
6912
                    pipeline_id.id(),
×
6913
                    err
×
6914
                );
6915
            } else {
6916
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6917
            }
6918
            return Err(state.into());
×
6919
        };
6920
        self.compute_pass.set_pipeline(pipeline);
×
6921
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6922
        Ok(())
×
6923
    }
6924
}
6925

6926
/// Render node to run the simulation of all effects once per frame.
6927
///
6928
/// Runs inside the simulation sub-graph, looping over all extracted effect
6929
/// batches to simulate them.
6930
pub(crate) struct VfxSimulateNode {}
6931

6932
impl VfxSimulateNode {
6933
    /// Create a new node for simulating the effects of the given world.
6934
    pub fn new(_world: &mut World) -> Self {
3✔
6935
        Self {}
6936
    }
6937

6938
    /// Begin a new compute pass and return a wrapper with extra
6939
    /// functionalities.
6940
    pub fn begin_compute_pass<'encoder>(
1,248✔
6941
        &self,
6942
        label: &str,
6943
        pipeline_cache: &'encoder PipelineCache,
6944
        render_context: &'encoder mut RenderContext,
6945
    ) -> HanabiComputePass<'encoder> {
6946
        let compute_pass =
1,248✔
6947
            render_context
1,248✔
6948
                .command_encoder()
6949
                .begin_compute_pass(&ComputePassDescriptor {
2,496✔
6950
                    label: Some(label),
1,248✔
6951
                    timestamp_writes: None,
1,248✔
6952
                });
6953
        HanabiComputePass::new(pipeline_cache, compute_pass)
3,744✔
6954
    }
6955
}
6956

6957
impl Node for VfxSimulateNode {
6958
    fn input(&self) -> Vec<SlotInfo> {
3✔
6959
        vec![]
3✔
6960
    }
6961

6962
    fn update(&mut self, _world: &mut World) {}
660✔
6963

6964
    fn run(
330✔
6965
        &self,
6966
        _graph: &mut RenderGraphContext,
6967
        render_context: &mut RenderContext,
6968
        world: &World,
6969
    ) -> Result<(), NodeRunError> {
6970
        trace!("VfxSimulateNode::run()");
650✔
6971

6972
        let pipeline_cache = world.resource::<PipelineCache>();
990✔
6973
        let effects_meta = world.resource::<EffectsMeta>();
990✔
6974
        let effect_bind_groups = world.resource::<EffectBindGroups>();
990✔
6975
        let property_bind_groups = world.resource::<PropertyBindGroups>();
990✔
6976
        let sort_bind_groups = world.resource::<SortBindGroups>();
990✔
6977
        let utils_pipeline = world.resource::<UtilsPipeline>();
990✔
6978
        let effect_cache = world.resource::<EffectCache>();
990✔
6979
        let event_cache = world.resource::<EventCache>();
990✔
6980
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
990✔
6981
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
990✔
6982
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
990✔
6983

6984
        // Make sure to schedule any buffer copy before accessing their content later in
6985
        // the GPU commands below.
6986
        {
6987
            let command_encoder = render_context.command_encoder();
1,320✔
6988
            effects_meta
660✔
6989
                .dispatch_indirect_buffer
660✔
6990
                .write_buffers(command_encoder);
990✔
6991
            effects_meta
660✔
6992
                .draw_indirect_buffer
660✔
6993
                .write_buffer(command_encoder);
990✔
6994
            effects_meta
660✔
6995
                .effect_metadata_buffer
660✔
6996
                .write_buffer(command_encoder);
990✔
6997
            event_cache.write_buffers(command_encoder);
1,320✔
6998
            sort_bind_groups.write_buffers(command_encoder);
660✔
6999
        }
7000

7001
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
7002
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
7003
        // the update pass of their parent effect during the previous frame.
7004
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
330✔
7005
            gpu_buffer_operations.dispatch(
7006
                *queue_index,
7007
                render_context,
7008
                utils_pipeline,
7009
                Some("hanabi:init_indirect_fill_dispatch"),
7010
            );
7011
        }
7012

7013
        // If there's no batch, there's nothing more to do. Avoid continuing because
7014
        // some GPU resources are missing, which is expected when there's no effect but
7015
        // is an error (and will log warnings/errors) otherwise.
7016
        if sorted_effect_batches.is_empty() {
660✔
7017
            return Ok(());
18✔
7018
        }
7019

7020
        // Compute init pass
7021
        {
7022
            trace!("init: loop over effect batches...");
312✔
7023

7024
            let mut compute_pass =
7025
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
7026

7027
            // Bind group simparams@0 is common to everything, only set once per init pass
7028
            compute_pass.set_bind_group(
7029
                0,
7030
                effects_meta
7031
                    .indirect_sim_params_bind_group
7032
                    .as_ref()
7033
                    .unwrap(),
7034
                &[],
7035
            );
7036

7037
            // Dispatch init compute jobs for all batches
7038
            for effect_batch in sorted_effect_batches.iter() {
312✔
7039
                // Do not dispatch any init work if there's nothing to spawn this frame for the
7040
                // batch. Note that this hopefully should have been skipped earlier.
7041
                {
7042
                    let use_indirect_dispatch = effect_batch
624✔
7043
                        .layout_flags
312✔
7044
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
312✔
7045
                    match effect_batch.spawn_info {
312✔
7046
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
312✔
7047
                            assert!(!use_indirect_dispatch);
7048
                            if total_spawn_count == 0 {
312✔
7049
                                continue;
15✔
7050
                            }
7051
                        }
7052
                        BatchSpawnInfo::GpuSpawner { .. } => {
7053
                            assert!(use_indirect_dispatch);
×
7054
                        }
7055
                    }
7056
                }
7057

7058
                // Fetch bind group particle@1
7059
                let Some(particle_bind_group) =
297✔
7060
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
297✔
7061
                else {
7062
                    error!(
×
7063
                        "Failed to find init particle@1 bind group for slab #{}",
×
7064
                        effect_batch.slab_id.index()
×
7065
                    );
7066
                    continue;
×
7067
                };
7068

7069
                // Fetch bind group metadata@3
7070
                let Some(metadata_bind_group) = effect_bind_groups
297✔
7071
                    .init_metadata_bind_groups
7072
                    .get(&effect_batch.slab_id)
7073
                else {
7074
                    error!(
×
7075
                        "Failed to find init metadata@3 bind group for slab #{}",
×
7076
                        effect_batch.slab_id.index()
×
7077
                    );
7078
                    continue;
×
7079
                };
7080

7081
                if compute_pass
7082
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
7083
                    .is_err()
7084
                {
7085
                    continue;
×
7086
                }
7087

7088
                // Compute dynamic offsets
7089
                let spawner_base = effect_batch.spawner_base;
7090
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7091
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
7092
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
594✔
7093
                let property_offset = effect_batch.property_offset;
594✔
7094

7095
                // Setup init pass
7096
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
891✔
7097
                let offsets = if let Some(property_offset) = property_offset {
594✔
7098
                    vec![spawner_offset, property_offset]
7099
                } else {
7100
                    vec![spawner_offset]
594✔
7101
                };
7102
                compute_pass.set_bind_group(
891✔
7103
                    2,
7104
                    property_bind_groups
594✔
7105
                        .get(effect_batch.property_key.as_ref())
1,188✔
7106
                        .unwrap(),
594✔
7107
                    &offsets[..],
297✔
7108
                );
7109
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
891✔
7110

7111
                // Dispatch init job
7112
                match effect_batch.spawn_info {
297✔
7113
                    // Indirect dispatch via GPU spawn events
7114
                    BatchSpawnInfo::GpuSpawner {
7115
                        init_indirect_dispatch_index,
×
7116
                        ..
7117
                    } => {
7118
                        assert!(effect_batch
×
7119
                            .layout_flags
×
7120
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
7121

7122
                        // Note: the indirect offset of a dispatch workgroup only needs
7123
                        // 4-byte alignment
7124
                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
×
7125
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
7126

7127
                        trace!(
×
7128
                            "record commands for indirect init pipeline of effect {:?} \
×
7129
                                init_indirect_dispatch_index={} \
×
7130
                                indirect_offset={} \
×
7131
                                spawner_base={} \
×
7132
                                spawner_offset={} \
×
7133
                                property_key={:?}...",
×
7134
                            effect_batch.handle,
7135
                            init_indirect_dispatch_index,
7136
                            indirect_offset,
7137
                            spawner_base,
7138
                            spawner_offset,
7139
                            effect_batch.property_key,
7140
                        );
7141

7142
                        compute_pass.dispatch_workgroups_indirect(
×
7143
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
7144
                            indirect_offset,
×
7145
                        );
7146
                    }
7147

7148
                    // Direct dispatch via CPU spawn count
7149
                    BatchSpawnInfo::CpuSpawner {
7150
                        total_spawn_count: spawn_count,
297✔
7151
                    } => {
7152
                        assert!(!effect_batch
7153
                            .layout_flags
7154
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
7155

7156
                        const WORKGROUP_SIZE: u32 = 64;
7157
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
297✔
7158

7159
                        trace!(
7160
                            "record commands for init pipeline of effect {:?} \
297✔
7161
                                (spawn {} particles => {} workgroups) spawner_base={} \
297✔
7162
                                spawner_offset={} \
297✔
7163
                                property_key={:?}...",
297✔
7164
                            effect_batch.handle,
7165
                            spawn_count,
7166
                            workgroup_count,
7167
                            spawner_base,
7168
                            spawner_offset,
7169
                            effect_batch.property_key,
7170
                        );
7171

7172
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7173
                    }
7174
                }
7175

7176
                trace!("init compute dispatched");
594✔
7177
            }
7178
        }
7179

7180
        // Compute indirect dispatch pass
7181
        if effects_meta.spawner_buffer.buffer().is_some()
312✔
7182
            && !effects_meta.spawner_buffer.is_empty()
312✔
7183
            && effects_meta.indirect_metadata_bind_group.is_some()
312✔
7184
            && effects_meta.indirect_sim_params_bind_group.is_some()
624✔
7185
        {
7186
            // Only start a compute pass if there's an effect; makes things clearer in
7187
            // debugger.
7188
            let mut compute_pass =
312✔
7189
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
1,560✔
7190

7191
            // Dispatch indirect dispatch compute job
7192
            trace!("record commands for indirect dispatch pipeline...");
624✔
7193

7194
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
624✔
7195
            if has_gpu_spawn_events {
312✔
7196
                if let Some(indirect_child_info_buffer_bind_group) =
×
7197
                    event_cache.indirect_child_info_buffer_bind_group()
×
7198
                {
7199
                    assert!(has_gpu_spawn_events);
7200
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
7201
                } else {
7202
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
7203
                    // render_context
7204
                    //     .command_encoder()
7205
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
7206
                    // FIXME - Bevy doesn't allow returning custom errors here...
7207
                    return Ok(());
×
7208
                }
7209
            }
7210

7211
            if compute_pass
312✔
7212
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
7213
                .is_err()
7214
            {
7215
                // FIXME - Bevy doesn't allow returning custom errors here...
7216
                return Ok(());
×
7217
            }
7218

7219
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
7220
            // the size exluding gaps!");
7221
            const WORKGROUP_SIZE: u32 = 64;
7222
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
7223
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
7224
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
7225

7226
            // Setup vfx_indirect pass
7227
            compute_pass.set_bind_group(
7228
                0,
7229
                effects_meta
7230
                    .indirect_sim_params_bind_group
7231
                    .as_ref()
7232
                    .unwrap(),
7233
                &[],
7234
            );
7235
            compute_pass.set_bind_group(
7236
                1,
7237
                // FIXME - got some unwrap() panic here, investigate... possibly race
7238
                // condition!
7239
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
7240
                &[],
7241
            );
7242
            compute_pass.set_bind_group(
7243
                2,
7244
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
7245
                &[],
7246
            );
7247
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7248
            trace!(
7249
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
312✔
7250
                total_effect_count,
7251
                workgroup_count
7252
            );
7253
        }
7254

7255
        // Compute update pass
7256
        {
7257
            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
624✔
7258
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
7259
                render_context
×
7260
                    .command_encoder()
7261
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
7262
                // FIXME - Bevy doesn't allow returning custom errors here...
7263
                return Ok(());
×
7264
            };
7265

7266
            let mut compute_pass =
7267
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
7268

7269
            // Bind group simparams@0 is common to everything, only set once per update pass
7270
            compute_pass.set_bind_group(
7271
                0,
7272
                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
7273
                &[],
7274
            );
7275

7276
            // Dispatch update compute jobs
7277
            for effect_batch in sorted_effect_batches.iter() {
312✔
7278
                // Fetch bind group particle@1
7279
                let Some(particle_bind_group) =
312✔
7280
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
624✔
7281
                else {
7282
                    error!(
×
7283
                        "Failed to find update particle@1 bind group for slab #{}",
×
7284
                        effect_batch.slab_id.index()
×
7285
                    );
7286
                    compute_pass.insert_debug_marker("ERROR:MissingParticleSimBindGroup");
×
7287
                    continue;
×
7288
                };
7289

7290
                // Fetch bind group metadata@3
7291
                let Some(metadata_bind_group) = effect_bind_groups
312✔
7292
                    .update_metadata_bind_groups
7293
                    .get(&effect_batch.slab_id)
7294
                else {
7295
                    error!(
×
7296
                        "Failed to find update metadata@3 bind group for slab #{}",
×
7297
                        effect_batch.slab_id.index()
×
7298
                    );
7299
                    compute_pass.insert_debug_marker("ERROR:MissingMetadataBindGroup");
×
7300
                    continue;
×
7301
                };
7302

7303
                // Fetch compute pipeline
7304
                if let Err(err) = compute_pass
×
7305
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
7306
                {
7307
                    compute_pass.insert_debug_marker(&format!(
7308
                        "ERROR:FailedToSetCachedUpdatePipeline:{:?}",
7309
                        err
7310
                    ));
7311
                    continue;
7312
                }
7313

7314
                // Compute dynamic offsets
7315
                let spawner_index = effect_batch.spawner_base;
624✔
7316
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
936✔
7317
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
936✔
7318
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
312✔
7319
                let property_offset = effect_batch.property_offset;
7320

7321
                trace!(
7322
                    "record commands for update pipeline of effect {:?} spawner_base={}",
312✔
7323
                    effect_batch.handle,
7324
                    spawner_index,
7325
                );
7326

7327
                // Setup update pass
7328
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
7329
                let offsets = if let Some(property_offset) = property_offset {
13✔
7330
                    vec![spawner_offset, property_offset]
7331
                } else {
7332
                    vec![spawner_offset]
598✔
7333
                };
7334
                compute_pass.set_bind_group(
7335
                    2,
7336
                    property_bind_groups
7337
                        .get(effect_batch.property_key.as_ref())
7338
                        .unwrap(),
7339
                    &offsets[..],
7340
                );
7341
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
7342

7343
                // Dispatch update job
7344
                let dispatch_indirect_offset = effect_batch
7345
                    .dispatch_buffer_indices
7346
                    .update_dispatch_indirect_buffer_row_index
7347
                    * 12;
7348
                trace!(
7349
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
312✔
7350
                    indirect_buffer,
7351
                    dispatch_indirect_offset,
7352
                );
7353
                compute_pass
7354
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
7355

7356
                trace!("update compute dispatched");
312✔
7357
            }
7358
        }
7359

7360
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
7361
        // batch of particles which needs sorting, based on the actual number of alive
7362
        // particles in the batch after their update in the compute update pass. Since
7363
        // particles may die during update, this may be different from the number of
7364
        // particles updated.
7365
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
312✔
7366
            gpu_buffer_operations.dispatch(
7367
                *queue_index,
7368
                render_context,
7369
                utils_pipeline,
7370
                Some("hanabi:sort_fill_dispatch"),
7371
            );
7372
        }
7373

7374
        // Compute sort pass
7375
        {
7376
            let mut compute_pass =
7377
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
7378

7379
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
7380
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
7381

7382
            // Loop on batches and find those which need sorting
7383
            for effect_batch in sorted_effect_batches.iter() {
312✔
7384
                trace!("Processing effect batch for sorting...");
624✔
7385
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
312✔
7386
                    continue;
312✔
7387
                }
7388
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
7389
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
7390

7391
                let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
×
7392
                    warn!("Missing sort-fill effect buffer.");
×
7393
                    // render_context
7394
                    //     .command_encoder()
7395
                    //     .insert_debug_marker("ERROR:MissingEffectBatchBuffer");
7396
                    continue;
×
7397
                };
7398

7399
                let indirect_dispatch_index = *effect_batch
7400
                    .sort_fill_indirect_dispatch_index
7401
                    .as_ref()
7402
                    .unwrap();
7403
                let indirect_offset =
7404
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
7405

7406
                // Fill the sort buffer with the key-value pairs to sort
7407
                {
7408
                    compute_pass.push_debug_group("hanabi:sort_fill");
7409

7410
                    // Fetch compute pipeline
7411
                    let Some(pipeline_id) =
×
7412
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
7413
                    else {
7414
                        warn!("Missing sort-fill pipeline.");
×
7415
                        compute_pass.insert_debug_marker("ERROR:MissingSortFillPipeline");
×
7416
                        continue;
×
7417
                    };
7418
                    if compute_pass
7419
                        .set_cached_compute_pipeline(pipeline_id)
7420
                        .is_err()
7421
                    {
7422
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortFillPipeline");
×
7423
                        compute_pass.pop_debug_group();
×
7424
                        // FIXME - Bevy doesn't allow returning custom errors here...
7425
                        return Ok(());
×
7426
                    }
7427

7428
                    // Bind group sort_fill@0
7429
                    let particle_buffer = effect_buffer.particle_buffer();
7430
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
7431
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
7432
                        particle_buffer.id(),
7433
                        indirect_index_buffer.id(),
7434
                        effect_metadata_buffer.id(),
7435
                    ) else {
7436
                        warn!("Missing sort-fill bind group.");
×
7437
                        compute_pass.insert_debug_marker("ERROR:MissingSortFillBindGroup");
×
7438
                        continue;
×
7439
                    };
7440
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
7441
                    let indirect_index_offset =
7442
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
7443
                    let effect_metadata_offset = effects_meta
7444
                        .gpu_limits
7445
                        .effect_metadata_offset(effect_batch.metadata_table_id.0)
7446
                        as u32;
7447
                    compute_pass.set_bind_group(
7448
                        0,
7449
                        bind_group,
7450
                        &[
7451
                            particle_offset,
7452
                            indirect_index_offset,
7453
                            effect_metadata_offset,
7454
                        ],
7455
                    );
7456

7457
                    compute_pass
7458
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7459
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
7460

7461
                    compute_pass.pop_debug_group();
7462
                }
7463

7464
                // Do the actual sort
7465
                {
7466
                    compute_pass.push_debug_group("hanabi:sort");
7467

7468
                    if compute_pass
7469
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
7470
                        .is_err()
7471
                    {
7472
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortPipeline");
×
7473
                        compute_pass.pop_debug_group();
×
7474
                        // FIXME - Bevy doesn't allow returning custom errors here...
7475
                        return Ok(());
×
7476
                    }
7477

7478
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
7479
                    compute_pass
7480
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7481
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7482

7483
                    compute_pass.pop_debug_group();
7484
                }
7485

7486
                // Copy the sorted particle indices back into the indirect index buffer, where
7487
                // the render pass will read them.
7488
                {
7489
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7490

7491
                    // Fetch compute pipeline
7492
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7493
                    if compute_pass
7494
                        .set_cached_compute_pipeline(pipeline_id)
7495
                        .is_err()
7496
                    {
7497
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortCopyPipeline");
×
7498
                        compute_pass.pop_debug_group();
7499
                        // FIXME - Bevy doesn't allow returning custom errors here...
7500
                        return Ok(());
7501
                    }
7502

7503
                    // Bind group sort_copy@0
7504
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
7505
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
7506
                        indirect_index_buffer.id(),
7507
                        effect_metadata_buffer.id(),
7508
                    ) else {
7509
                        warn!("Missing sort-copy bind group.");
×
7510
                        compute_pass.insert_debug_marker("ERROR:MissingSortCopyBindGroup");
×
7511
                        continue;
×
7512
                    };
7513
                    let indirect_index_offset = effect_batch.slice.start;
7514
                    let effect_metadata_offset = effects_meta
7515
                        .effect_metadata_buffer
7516
                        .dynamic_offset(effect_batch.metadata_table_id);
7517
                    compute_pass.set_bind_group(
7518
                        0,
7519
                        bind_group,
7520
                        &[indirect_index_offset, effect_metadata_offset],
7521
                    );
7522

7523
                    compute_pass
7524
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7525
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7526

7527
                    compute_pass.pop_debug_group();
7528
                }
7529
            }
7530
        }
7531

7532
        Ok(())
312✔
7533
    }
7534
}
7535

7536
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7537
    fn from(layout_flags: LayoutFlags) -> Self {
936✔
7538
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
1,872✔
7539
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7540
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
936✔
7541
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7542
        } else {
7543
            ParticleRenderAlphaMaskPipelineKey::Blend
936✔
7544
        }
7545
    }
7546
}
7547

7548
#[cfg(test)]
7549
mod tests {
7550
    use super::*;
7551

7552
    #[test]
7553
    fn layout_flags() {
7554
        let flags = LayoutFlags::default();
7555
        assert_eq!(flags, LayoutFlags::NONE);
7556
    }
7557

7558
    #[cfg(feature = "gpu_tests")]
7559
    #[test]
7560
    fn gpu_limits() {
7561
        use crate::test_utils::MockRenderer;
7562

7563
        let renderer = MockRenderer::new();
7564
        let device = renderer.device();
7565
        let limits = GpuLimits::from_device(&device);
7566

7567
        // assert!(limits.storage_buffer_align().get() >= 1);
7568
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7569
    }
7570

7571
    #[cfg(feature = "gpu_tests")]
7572
    #[test]
7573
    fn gpu_ops_ifda() {
7574
        use crate::test_utils::MockRenderer;
7575

7576
        let renderer = MockRenderer::new();
7577
        let device = renderer.device();
7578
        let render_queue = renderer.queue();
7579

7580
        let mut world = World::new();
7581
        world.insert_resource(device.clone());
7582
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7583

7584
        let src_buffer = device.create_buffer(&BufferDescriptor {
7585
            label: None,
7586
            size: 256,
7587
            usage: BufferUsages::STORAGE,
7588
            mapped_at_creation: false,
7589
        });
7590
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7591
            label: None,
7592
            size: 256,
7593
            usage: BufferUsages::STORAGE,
7594
            mapped_at_creation: false,
7595
        });
7596

7597
        // Two consecutive ops can be merged. This includes having contiguous slices
7598
        // both in source and destination.
7599
        buffer_ops.begin_frame();
7600
        {
7601
            let mut q = InitFillDispatchQueue::default();
7602
            q.enqueue(0, 0);
7603
            assert_eq!(q.queue.len(), 1);
7604
            q.enqueue(1, 1);
7605
            // Ops are not batched yet
7606
            assert_eq!(q.queue.len(), 2);
7607
            // On submit, the ops get batched together
7608
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7609
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7610
        }
7611
        buffer_ops.end_frame(&device, &render_queue);
7612

7613
        // Even if out of order, the init fill dispatch ops are batchable. Here the
7614
        // offsets are enqueued inverted.
7615
        buffer_ops.begin_frame();
7616
        {
7617
            let mut q = InitFillDispatchQueue::default();
7618
            q.enqueue(1, 1);
7619
            assert_eq!(q.queue.len(), 1);
7620
            q.enqueue(0, 0);
7621
            // Ops are not batched yet
7622
            assert_eq!(q.queue.len(), 2);
7623
            // On submit, the ops get batched together
7624
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7625
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7626
        }
7627
        buffer_ops.end_frame(&device, &render_queue);
7628

7629
        // However, both the source and destination need to be contiguous at the same
7630
        // time. Here they are mixed so we can't batch.
7631
        buffer_ops.begin_frame();
7632
        {
7633
            let mut q = InitFillDispatchQueue::default();
7634
            q.enqueue(0, 1);
7635
            assert_eq!(q.queue.len(), 1);
7636
            q.enqueue(1, 0);
7637
            // Ops are not batched yet
7638
            assert_eq!(q.queue.len(), 2);
7639
            // On submit, the ops cannot get batched together
7640
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7641
            assert_eq!(buffer_ops.args_buffer.len(), 2);
7642
        }
7643
        buffer_ops.end_frame(&device, &render_queue);
7644
    }
7645
}
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