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

djeedai / bevy_hanabi / 17956962574

23 Sep 2025 07:30PM UTC coverage: 66.455% (+0.2%) from 66.279%
17956962574

Pull #497

github

web-flow
Merge 1e0b6278d into fee4feaf6
Pull Request #497: Check the results of `has_side_effect` for binary expressions.

19 of 22 new or added lines in 1 file covered. (86.36%)

540 existing lines in 2 files now uncovered.

4917 of 7399 relevant lines covered (66.45%)

448.95 hits per line

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

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

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

67
use crate::{
68
    asset::{DefaultMesh, EffectAsset},
69
    calc_func_id,
70
    render::{
71
        batch::{BatchInput, EffectDrawBatch, InitAndUpdatePipelineIds},
72
        effect_cache::{DispatchBufferIndices, DrawIndirectRowIndex},
73
    },
74
    AlphaMode, Attribute, CompiledParticleEffect, EffectProperties, EffectShader, EffectSimulation,
75
    EffectSpawner, EffectVisibilityClass, ParticleLayout, PropertyLayout, SimulationCondition,
76
    TextureLayout,
77
};
78

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

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

102
use self::batch::EffectBatch;
103

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

538
impl InitFillDispatchQueue {
539
    /// Clear the queue.
540
    #[inline]
541
    pub fn clear(&mut self) {
1,030✔
542
        self.queue.clear();
2,060✔
543
        self.submitted_queue_index = None;
1,030✔
544
    }
545

546
    /// Check if the queue is empty.
547
    #[inline]
548
    pub fn is_empty(&self) -> bool {
1,030✔
549
        self.queue.is_empty()
2,060✔
550
    }
551

552
    /// Enqueue a new operation.
553
    #[inline]
554
    pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
6✔
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✔
UNCOV
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
        let mut src_start = self.queue[0].global_child_index;
581
        let mut dst_start = self.queue[0].dispatch_indirect_index;
582
        let mut src_end = src_start + 1;
583
        let mut dst_end = dst_start + 1;
584
        let src_stride = GpuChildInfo::min_size().get() as u32 / 4;
585
        let dst_stride = GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4;
586
        for i in 1..self.queue.len() {
3✔
587
            let InitFillDispatchItem {
588
                global_child_index: src,
3✔
589
                dispatch_indirect_index: dst,
3✔
590
            } = self.queue[i];
3✔
591
            if src != src_end || dst != dst_end {
6✔
592
                let count = src_end - src_start;
1✔
593
                debug_assert_eq!(count, dst_end - dst_start);
594
                let args = GpuBufferOperationArgs {
595
                    src_offset: src_start * src_stride + 1,
596
                    src_stride,
597
                    dst_offset: dst_start * dst_stride,
598
                    dst_stride,
599
                    count,
600
                };
601
                trace!(
UNCOV
602
                "enqueue_init_fill(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
×
603
                src_start,
604
                dst_start,
605
                args,
606
                src_buffer.id(),
×
UNCOV
607
                dst_buffer.id(),
×
608
            );
609
                fill_queue.enqueue(
610
                    GpuBufferOperationType::FillDispatchArgs,
611
                    args,
612
                    src_buffer.clone(),
613
                    0,
614
                    None,
615
                    dst_buffer.clone(),
616
                    0,
617
                    None,
618
                );
619
                src_start = src;
620
                dst_start = dst;
621
            }
622
            src_end = src + 1;
3✔
623
            dst_end = dst + 1;
624
        }
625
        if src_start != src_end || dst_start != dst_end {
3✔
626
            let count = src_end - src_start;
3✔
627
            debug_assert_eq!(count, dst_end - dst_start);
628
            let args = GpuBufferOperationArgs {
629
                src_offset: src_start * src_stride + 1,
6✔
630
                src_stride,
631
                dst_offset: dst_start * dst_stride,
6✔
632
                dst_stride,
633
                count,
634
            };
635
            trace!(
3✔
UNCOV
636
            "IFDA::submit(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
×
637
            src_start,
638
            dst_start,
639
            args,
640
            src_buffer.id(),
×
UNCOV
641
            dst_buffer.id(),
×
642
        );
643
            fill_queue.enqueue(
6✔
644
                GpuBufferOperationType::FillDispatchArgs,
3✔
645
                args,
3✔
646
                src_buffer.clone(),
6✔
647
                0,
648
                None,
3✔
649
                dst_buffer.clone(),
6✔
650
                0,
651
                None,
3✔
652
            );
653
        }
654

655
        debug_assert!(self.submitted_queue_index.is_none());
3✔
656
        if !fill_queue.operation_queue.is_empty() {
6✔
657
            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
3✔
658
        }
659
    }
660
}
661

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

680
impl FromWorld for DispatchIndirectPipeline {
681
    fn from_world(world: &mut World) -> Self {
3✔
682
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
683

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

694
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
6✔
695
        let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
9✔
696
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
9✔
697

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

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

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

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

795
        Self {
796
            sim_params_bind_group_layout,
797
            effect_metadata_bind_group_layout,
798
            spawner_bind_group_layout,
799
            child_infos_bind_group_layout,
800
            indirect_shader_noevent,
801
            indirect_shader_events,
802
        }
803
    }
804
}
805

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

816
impl SpecializedComputePipeline for DispatchIndirectPipeline {
817
    type Key = DispatchIndirectPipelineKey;
818

819
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
6✔
820
        trace!(
6✔
821
            "Specializing indirect pipeline (has_events={})",
4✔
822
            key.has_events
823
        );
824

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

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

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

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

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

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

915
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
916
struct QueuedOperationBindGroupKey {
917
    src_buffer: BufferId,
918
    src_binding_size: Option<NonZeroU32>,
919
    dst_buffer: BufferId,
920
    dst_binding_size: Option<NonZeroU32>,
921
}
922

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

935
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
UNCOV
936
    fn from(value: &QueuedOperation) -> Self {
×
937
        Self {
938
            src_buffer: value.src_buffer.id(),
×
939
            src_binding_size: value.src_binding_size,
×
940
            dst_buffer: value.dst_buffer.id(),
×
UNCOV
941
            dst_binding_size: value.dst_binding_size,
×
942
        }
943
    }
944
}
945

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

959
impl GpuBufferOperationQueue {
960
    /// Create a new empty queue.
961
    pub fn new() -> Self {
1,033✔
962
        Self {
963
            args: vec![],
1,033✔
964
            operation_queue: vec![],
1,033✔
965
        }
966
    }
967

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

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

1017
    /// Bind groups for the submitted operations.
1018
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
1019

1020
    /// Submitted queues for this frame.
1021
    queues: Vec<Vec<QueuedOperation>>,
1022
}
1023

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

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

1046
    /// Clear the queue and begin recording operations for a new frame.
1047
    pub fn begin_frame(&mut self) {
1,033✔
1048
        self.args_buffer.clear();
2,066✔
1049
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
2,066✔
1050
        self.queues.clear();
2,066✔
1051
    }
1052

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

1068
    /// Finish recording operations for this frame, and schedule buffer writes
1069
    /// to GPU.
1070
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
1,033✔
1071
        assert_eq!(
1,033✔
1072
            self.args_buffer.len(),
2,066✔
1073
            self.queues.iter().fold(0, |len, q| len + q.len())
2,075✔
1074
        );
1075

1076
        // Upload to GPU buffer
1077
        self.args_buffer.write_buffer(device, render_queue);
4,132✔
1078
    }
1079

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

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

1177
        if queue.is_empty() {
×
UNCOV
1178
            return;
×
1179
        }
1180

1181
        let mut compute_pass =
1182
            render_context
1183
                .command_encoder()
1184
                .begin_compute_pass(&ComputePassDescriptor {
1185
                    label: compute_pass_label,
1186
                    timestamp_writes: None,
1187
                });
1188

1189
        let mut prev_op = None;
1190
        for qop in queue {
×
UNCOV
1191
            trace!("qop={:?}", qop);
×
1192

1193
            if Some(qop.op) != prev_op {
×
1194
                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
×
UNCOV
1195
                prev_op = Some(qop.op);
×
1196
            }
1197

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

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

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

1243
impl FromWorld for UtilsPipeline {
1244
    fn from_world(world: &mut World) -> Self {
3✔
1245
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1246

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

1283
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1284
            label: Some("hanabi:pipeline_layout:utils"),
6✔
1285
            bind_group_layouts: &[&bind_group_layout],
3✔
1286
            push_constant_ranges: &[],
3✔
1287
        });
1288

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

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

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

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

1364
        let shader_code = include_str!("vfx_utils.wgsl");
6✔
1365

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

1371
            let shader_defs = default();
6✔
1372

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

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

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

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

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

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

1488
#[derive(Resource)]
1489
pub(crate) struct ParticlesInitPipeline {
1490
    sim_params_layout: BindGroupLayout,
1491

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

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

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

1521
        Self {
1522
            sim_params_layout,
1523
            temp_particle_bind_group_layout: None,
1524
            temp_spawner_bind_group_layout: None,
1525
            temp_metadata_bind_group_layout: None,
1526
        }
1527
    }
1528
}
1529

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

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

1563
impl SpecializedComputePipeline for ParticlesInitPipeline {
1564
    type Key = ParticleInitPipelineKey;
1565

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

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

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

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

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

1644
#[derive(Resource)]
1645
pub(crate) struct ParticlesUpdatePipeline {
1646
    sim_params_layout: BindGroupLayout,
1647

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

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

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

1692
        Self {
1693
            sim_params_layout,
1694
            temp_particle_bind_group_layout: None,
1695
            temp_spawner_bind_group_layout: None,
1696
            temp_metadata_bind_group_layout: None,
1697
        }
1698
    }
1699
}
1700

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

1724
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1725
    type Key = ParticleUpdatePipelineKey;
1726

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

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

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

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

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

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

1804
impl ParticlesRenderPipeline {
1805
    /// Cache a material, creating its bind group layout based on the texture
1806
    /// layout.
1807
    pub fn cache_material(&mut self, layout: &TextureLayout) {
1,014✔
1808
        if layout.layout.is_empty() {
2,028✔
1809
            return;
1,014✔
1810
        }
1811

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

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

1851
        self.material_layouts
1852
            .insert(layout.clone(), material_bind_group_layout);
1853
    }
1854

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

1862
        self.material_layouts.get(layout)
1863
    }
1864
}
1865

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

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

1898
        Self {
1899
            render_device: render_device.clone(),
9✔
1900
            view_layout,
1901
            material_layouts: default(),
3✔
1902
        }
1903
    }
1904
}
1905

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

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

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

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

1994
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1995
    type Key = ParticleRenderPipelineKey;
1996

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

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

2045
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
10✔
2046
        let mut shader_defs = vec![];
4✔
2047

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

2059
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
4✔
2060
            layout.push(material_bind_group_layout.clone());
2061
        }
2062

2063
        // Key: LOCAL_SPACE_SIMULATION
2064
        if key.local_space_simulation {
2✔
UNCOV
2065
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2066
        }
2067

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

2080
        // Key: FLIPBOOK
2081
        if key.flipbook {
2✔
UNCOV
2082
            shader_defs.push("FLIPBOOK".into());
×
2083
        }
2084

2085
        // Key: NEEDS_UV
2086
        if key.needs_uv {
2✔
UNCOV
2087
            shader_defs.push("NEEDS_UV".into());
×
2088
        }
2089

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

2095
        if key.needs_particle_fragment {
2✔
UNCOV
2096
            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
×
2097
        }
2098

2099
        // Key: RIBBONS
2100
        if key.ribbons {
2✔
UNCOV
2101
            shader_defs.push("RIBBONS".into());
×
2102
        }
2103

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

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

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

2139
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2140
        let depth_stencil = Some(depth_stencil_2d);
2141

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

2145
        let format = if key.hdr {
4✔
UNCOV
2146
            ViewTarget::TEXTURE_FORMAT_HDR
×
2147
        } else {
2148
            TextureFormat::bevy_default()
2✔
2149
        };
2150

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

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

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

2253
pub struct AddedEffectParent {
2254
    pub entity: MainEntity,
2255
    pub layout: ParticleLayout,
2256
    /// GPU spawn event count to allocate for this effect.
2257
    pub event_count: u32,
2258
}
2259

2260
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
2261
/// GPU allocation.
2262
///
2263
/// [`ParticleEffect`]: crate::ParticleEffect
2264
pub struct AddedEffect {
2265
    /// Entity with a newly-added [`ParticleEffect`] component.
2266
    ///
2267
    /// [`ParticleEffect`]: crate::ParticleEffect
2268
    pub entity: MainEntity,
2269
    #[allow(dead_code)]
2270
    pub render_entity: RenderEntity,
2271
    /// Capacity, in number of particles, of the effect.
2272
    pub capacity: u32,
2273
    /// Resolved particle mesh, either the one provided by the user or the
2274
    /// default one. This should always be valid.
2275
    pub mesh: Handle<Mesh>,
2276
    /// Parent effect, if any.
2277
    pub parent: Option<AddedEffectParent>,
2278
    /// Layout of particle attributes.
2279
    pub particle_layout: ParticleLayout,
2280
    /// Layout of properties for the effect, if properties are used at all, or
2281
    /// an empty layout.
2282
    pub property_layout: PropertyLayout,
2283
    /// Effect flags.
2284
    pub layout_flags: LayoutFlags,
2285
    /// Handle of the effect asset.
2286
    pub handle: Handle<EffectAsset>,
2287
}
2288

2289
/// Collection of all extracted effects for this frame, inserted into the
2290
/// render world as a render resource.
2291
#[derive(Default, Resource)]
2292
pub(crate) struct ExtractedEffects {
2293
    /// Extracted effects this frame.
2294
    pub effects: Vec<ExtractedEffect>,
2295
    /// Newly added effects without a GPU allocation yet.
2296
    pub added_effects: Vec<AddedEffect>,
2297
}
2298

2299
#[derive(Default, Resource)]
2300
pub(crate) struct EffectAssetEvents {
2301
    pub images: Vec<AssetEvent<Image>>,
2302
}
2303

2304
/// System extracting all the asset events for the [`Image`] assets to enable
2305
/// dynamic update of images bound to any effect.
2306
///
2307
/// This system runs in parallel of [`extract_effects`].
2308
pub(crate) fn extract_effect_events(
1,030✔
2309
    mut events: ResMut<EffectAssetEvents>,
2310
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2311
) {
2312
    #[cfg(feature = "trace")]
2313
    let _span = bevy::log::info_span!("extract_effect_events").entered();
3,090✔
2314
    trace!("extract_effect_events()");
2,050✔
2315

2316
    let EffectAssetEvents { ref mut images } = *events;
2,060✔
2317
    *images = image_events.read().copied().collect();
4,120✔
2318
}
2319

2320
/// Debugging settings.
2321
///
2322
/// Settings used to debug Hanabi. These have no effect on the actual behavior
2323
/// of Hanabi, but may affect its performance.
2324
///
2325
/// # Example
2326
///
2327
/// ```
2328
/// # use bevy::prelude::*;
2329
/// # use bevy_hanabi::*;
2330
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2331
///     // Each time a new effect is spawned, capture 2 frames
2332
///     debug_settings.start_capture_on_new_effect = true;
2333
///     debug_settings.capture_frame_count = 2;
2334
/// }
2335
/// ```
2336
#[derive(Debug, Default, Clone, Copy, Resource)]
2337
pub struct DebugSettings {
2338
    /// Enable automatically starting a GPU debugger capture as soon as this
2339
    /// frame starts rendering (extract phase).
2340
    ///
2341
    /// Enable this feature to automatically capture one or more GPU frames when
2342
    /// the `extract_effects()` system runs next. This instructs any attached
2343
    /// GPU debugger to start a capture; this has no effect if no debugger
2344
    /// is attached.
2345
    ///
2346
    /// If a capture is already on-going this has no effect; the on-going
2347
    /// capture needs to be terminated first. Note however that a capture can
2348
    /// stop and another start in the same frame.
2349
    ///
2350
    /// This value is not reset automatically. If you set this to `true`, you
2351
    /// should set it back to `false` on next frame to avoid capturing forever.
2352
    pub start_capture_this_frame: bool,
2353

2354
    /// Enable automatically starting a GPU debugger capture when one or more
2355
    /// effects are spawned.
2356
    ///
2357
    /// Enable this feature to automatically capture one or more GPU frames when
2358
    /// a new effect is spawned (as detected by ECS change detection). This
2359
    /// instructs any attached GPU debugger to start a capture; this has no
2360
    /// effect if no debugger is attached.
2361
    pub start_capture_on_new_effect: bool,
2362

2363
    /// Number of frames to capture with a GPU debugger.
2364
    ///
2365
    /// By default this value is zero, and a GPU debugger capture runs for a
2366
    /// single frame. If a non-zero frame count is specified here, the capture
2367
    /// will instead stop once the specified number of frames has been recorded.
2368
    ///
2369
    /// You should avoid setting this to a value too large, to prevent the
2370
    /// capture size from getting out of control. A typical value is 1 to 3
2371
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2372
    /// debuggers or graphics APIs might further limit this value on their own,
2373
    /// so there's no guarantee the graphics API will honor this value.
2374
    pub capture_frame_count: u32,
2375
}
2376

2377
#[derive(Debug, Default, Clone, Copy, Resource)]
2378
pub(crate) struct RenderDebugSettings {
2379
    /// Is a GPU debugger capture on-going?
2380
    is_capturing: bool,
2381
    /// Start time of any on-going GPU debugger capture.
2382
    capture_start: Duration,
2383
    /// Number of frames captured so far for on-going GPU debugger capture.
2384
    captured_frames: u32,
2385
}
2386

2387
/// System extracting data for rendering of all active [`ParticleEffect`]
2388
/// components.
2389
///
2390
/// Extract rendering data for all [`ParticleEffect`] components in the world
2391
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
2392
/// the data into a new [`ExtractedEffect`] instance added to the
2393
/// [`ExtractedEffects`] resource.
2394
///
2395
/// This system runs in parallel of [`extract_effect_events`].
2396
///
2397
/// If any GPU debug capture is configured to start or stop in
2398
/// [`DebugSettings`], they do so at the beginning of this system. This ensures
2399
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2400
/// from Bevy itself, if another Bevy system runs before this one).
2401
///
2402
/// [`ParticleEffect`]: crate::ParticleEffect
2403
pub(crate) fn extract_effects(
1,030✔
2404
    real_time: Extract<Res<Time<Real>>>,
2405
    virtual_time: Extract<Res<Time<Virtual>>>,
2406
    time: Extract<Res<Time<EffectSimulation>>>,
2407
    effects: Extract<Res<Assets<EffectAsset>>>,
2408
    q_added_effects: Extract<
2409
        Query<
2410
            (Entity, &RenderEntity, &CompiledParticleEffect),
2411
            (Added<CompiledParticleEffect>, With<GlobalTransform>),
2412
        >,
2413
    >,
2414
    q_effects: Extract<
2415
        Query<(
2416
            Entity,
2417
            &RenderEntity,
2418
            Option<&InheritedVisibility>,
2419
            Option<&ViewVisibility>,
2420
            &EffectSpawner,
2421
            &CompiledParticleEffect,
2422
            Option<Ref<EffectProperties>>,
2423
            &GlobalTransform,
2424
        )>,
2425
    >,
2426
    q_all_effects: Extract<Query<(&RenderEntity, &CompiledParticleEffect), With<GlobalTransform>>>,
2427
    mut pending_effects: Local<Vec<MainEntity>>,
2428
    render_device: Res<RenderDevice>,
2429
    debug_settings: Extract<Res<DebugSettings>>,
2430
    default_mesh: Extract<Res<DefaultMesh>>,
2431
    mut sim_params: ResMut<SimParams>,
2432
    mut extracted_effects: ResMut<ExtractedEffects>,
2433
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2434
) {
2435
    #[cfg(feature = "trace")]
2436
    let _span = bevy::log::info_span!("extract_effects").entered();
3,090✔
2437
    trace!("extract_effects()");
2,050✔
2438

2439
    // Manage GPU debug capture
2440
    if render_debug_settings.is_capturing {
1,030✔
UNCOV
2441
        render_debug_settings.captured_frames += 1;
×
2442

2443
        // Stop any pending capture if needed
2444
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2445
            render_device.wgpu_device().stop_capture();
×
2446
            render_debug_settings.is_capturing = false;
×
2447
            warn!(
×
2448
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2449
                render_debug_settings.captured_frames,
×
UNCOV
2450
                real_time.elapsed().as_secs_f64()
×
2451
            );
2452
        }
2453
    }
2454
    if !render_debug_settings.is_capturing {
1,030✔
2455
        // If no pending capture, consider starting a new one
2456
        if debug_settings.start_capture_this_frame
1,030✔
2457
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty())
1,030✔
2458
        {
UNCOV
2459
            render_device.wgpu_device().start_capture();
×
2460
            render_debug_settings.is_capturing = true;
2461
            render_debug_settings.capture_start = real_time.elapsed();
2462
            render_debug_settings.captured_frames = 0;
2463
            warn!(
2464
                "Started GPU debug capture at t={}s.",
×
UNCOV
2465
                render_debug_settings.capture_start.as_secs_f64()
×
2466
            );
2467
        }
2468
    }
2469

2470
    // Save simulation params into render world
2471
    sim_params.time = time.elapsed_secs_f64();
2,060✔
2472
    sim_params.delta_time = time.delta_secs();
2,060✔
2473
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
2,060✔
2474
    sim_params.virtual_delta_time = virtual_time.delta_secs();
2,060✔
2475
    sim_params.real_time = real_time.elapsed_secs_f64();
2,060✔
2476
    sim_params.real_delta_time = real_time.delta_secs();
2,060✔
2477

2478
    // Collect added effects for later GPU data allocation
2479
    extracted_effects.added_effects = q_added_effects
2,060✔
2480
        .iter()
1,030✔
2481
        .chain(mem::take(&mut *pending_effects).into_iter().filter_map(|main_entity| {
5,159✔
2482
            q_all_effects.get(main_entity.id()).ok().map(|(render_entity, compiled_particle_effect)| {
54✔
2483
                (main_entity.id(), render_entity, compiled_particle_effect)
27✔
2484
            })
2485
        }))
2486
        .filter_map(|(entity, render_entity, compiled_effect)| {
1,042✔
2487
            let handle = compiled_effect.asset.clone_weak();
36✔
2488
            let asset = match effects.get(&compiled_effect.asset) {
26✔
2489
                None => {
2490
                    // The effect wasn't ready yet. Retry on subsequent frames.
2491
                    trace!("Failed to find asset for {:?}/{:?}, deferring to next frame", entity, render_entity);
10✔
2492
                    pending_effects.push(entity.into());
2493
                    return None;
2494
                }
2495
                Some(asset) => asset,
4✔
2496
            };
2497
            let particle_layout = asset.particle_layout();
6✔
2498
            assert!(
2✔
2499
                particle_layout.size() > 0,
2✔
UNCOV
2500
                "Invalid empty particle layout for effect '{}' on entity {:?} (render entity {:?}). Did you forget to add some modifier to the asset?",
×
2501
                asset.name,
2502
                entity,
2503
                render_entity.id(),
2504
            );
2505
            let property_layout = asset.property_layout();
6✔
2506
            let mesh = compiled_effect
4✔
2507
                .mesh
2✔
2508
                .clone()
2✔
2509
                .unwrap_or(default_mesh.0.clone());
6✔
2510

2511
            trace!(
2✔
2512
                "Found new effect: entity {:?} | render entity {:?} | capacity {:?} | particle_layout {:?} | \
2✔
2513
                 property_layout {:?} | layout_flags {:?} | mesh {:?}",
2✔
2514
                 entity,
2515
                 render_entity.id(),
4✔
2516
                 asset.capacity(),
4✔
2517
                 particle_layout,
2518
                 property_layout,
2519
                 compiled_effect.layout_flags,
2520
                 mesh);
2521

2522
            // FIXME - fixed 256 events per child (per frame) for now... this neatly avoids any issue with alignment 32/256 byte storage buffer align for bind groups
2523
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2524
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
6✔
2525
                entity: entity.into(),
×
UNCOV
2526
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2527
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
2528
            });
2529

2530
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
8✔
2531
            Some(AddedEffect {
2✔
2532
                entity: MainEntity::from(entity),
6✔
2533
                render_entity: *render_entity,
4✔
2534
                capacity: asset.capacity(),
6✔
2535
                mesh,
4✔
2536
                parent,
4✔
2537
                particle_layout,
4✔
2538
                property_layout,
4✔
2539
                layout_flags: compiled_effect.layout_flags,
2✔
2540
                handle,
2✔
2541
            })
2542
        })
2543
        .collect();
1,030✔
2544

2545
    // Loop over all existing effects to extract them
2546
    extracted_effects.effects.clear();
2,060✔
2547
    for (
2548
        main_entity,
1,014✔
2549
        render_entity,
2550
        maybe_inherited_visibility,
2551
        maybe_view_visibility,
2552
        effect_spawner,
2553
        compiled_effect,
2554
        maybe_properties,
2555
        transform,
2556
    ) in q_effects.iter()
2,060✔
2557
    {
2558
        // Check if shaders are configured
2559
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
1,014✔
UNCOV
2560
            continue;
×
2561
        };
2562

2563
        // Check if hidden, unless always simulated
2564
        if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
2565
            && !maybe_inherited_visibility
1,014✔
2566
                .map(|cv| cv.get())
3,042✔
2567
                .unwrap_or(true)
1,014✔
UNCOV
2568
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
2569
        {
UNCOV
2570
            continue;
×
2571
        }
2572

2573
        // Check if asset is available, otherwise silently ignore
2574
        let Some(asset) = effects.get(&compiled_effect.asset) else {
1,014✔
2575
            trace!(
×
UNCOV
2576
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
2577
                main_entity
2578
            );
UNCOV
2579
            continue;
×
2580
        };
2581

2582
        // Resolve the render entity of the parent, if any
2583
        let _parent = if let Some(main_entity) = compiled_effect.parent {
1,014✔
2584
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2585
                error!(
×
UNCOV
2586
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2587
                    main_entity
2588
                );
UNCOV
2589
                continue;
×
2590
            };
2591
            Some(*render_entity)
2592
        } else {
2593
            None
1,014✔
2594
        };
2595

2596
        let property_layout = asset.property_layout();
UNCOV
2597
        let property_data = if let Some(properties) = maybe_properties {
×
2598
            // Note: must check that property layout is not empty, because the
2599
            // EffectProperties component is marked as changed when added but contains an
2600
            // empty Vec if there's no property, which would later raise an error if we
2601
            // don't return None here.
2602
            if properties.is_changed() && !property_layout.is_empty() {
×
UNCOV
2603
                trace!("Detected property change, re-serializing...");
×
2604
                Some(properties.serialize(&property_layout))
2605
            } else {
UNCOV
2606
                None
×
2607
            }
2608
        } else {
2609
            None
1,014✔
2610
        };
2611

2612
        let texture_layout = asset.module().texture_layout();
2613
        let layout_flags = compiled_effect.layout_flags;
2614
        // let mesh = compiled_effect
2615
        //     .mesh
2616
        //     .clone()
2617
        //     .unwrap_or(default_mesh.0.clone());
2618
        let alpha_mode = compiled_effect.alpha_mode;
2619

2620
        trace!(
2621
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
1,014✔
2622
            asset.name,
2623
            main_entity,
2624
            render_entity.id(),
2,028✔
2625
            texture_layout.layout.len(),
2,028✔
2626
            compiled_effect.textures.len(),
2,028✔
2627
            layout_flags,
2628
        );
2629

2630
        extracted_effects.effects.push(ExtractedEffect {
2631
            render_entity: *render_entity,
2632
            main_entity: main_entity.into(),
2633
            handle: compiled_effect.asset.clone_weak(),
2634
            particle_layout: asset.particle_layout().clone(),
2635
            property_layout,
2636
            property_data,
2637
            spawn_count: effect_spawner.spawn_count,
2638
            prng_seed: compiled_effect.prng_seed,
2639
            transform: *transform,
2640
            layout_flags,
2641
            texture_layout,
2642
            textures: compiled_effect.textures.clone(),
2643
            alpha_mode,
2644
            effect_shaders: effect_shaders.clone(),
2645
        });
2646
    }
2647
}
2648

2649
/// Various GPU limits and aligned sizes computed once and cached.
2650
struct GpuLimits {
2651
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2652
    ///
2653
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2654
    storage_buffer_align: NonZeroU32,
2655

2656
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2657
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2658
    ///
2659
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2660
    effect_metadata_aligned_size: NonZeroU32,
2661
}
2662

2663
impl GpuLimits {
2664
    pub fn from_device(render_device: &RenderDevice) -> Self {
4✔
2665
        let storage_buffer_align =
4✔
2666
            render_device.limits().min_storage_buffer_offset_alignment as u64;
4✔
2667

2668
        let effect_metadata_aligned_size = NonZeroU32::new(
2669
            GpuEffectMetadata::min_size()
8✔
2670
                .get()
8✔
2671
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2672
        )
2673
        .unwrap();
2674

2675
        trace!(
4✔
2676
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2✔
2677
            storage_buffer_align,
2678
            GpuEffectMetadata::min_size().get(),
4✔
2679
            effect_metadata_aligned_size.get(),
4✔
2680
        );
2681

2682
        Self {
2683
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
12✔
2684
            effect_metadata_aligned_size,
2685
        }
2686
    }
2687

2688
    /// Byte alignment for any storage buffer binding.
2689
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2690
        self.storage_buffer_align
3✔
2691
    }
2692

2693
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2694
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
2,029✔
2695
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
2,029✔
2696
    }
2697

2698
    /// Byte alignment for [`GpuEffectMetadata`].
2699
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
2✔
2700
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
6✔
2701
    }
2702
}
2703

2704
/// Global render world resource containing the GPU data to draw all the
2705
/// particle effects in all views.
2706
///
2707
/// The resource is populated by [`prepare_effects()`] with all the effects to
2708
/// render for the current frame, for all views in the frame, and consumed by
2709
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2710
/// effects.
2711
#[derive(Resource)]
2712
pub struct EffectsMeta {
2713
    /// Bind group for the camera view, containing the camera projection and
2714
    /// other uniform values related to the camera.
2715
    view_bind_group: Option<BindGroup>,
2716
    /// Bind group #0 of the vfx_update shader, for the simulation parameters
2717
    /// like the current time and frame delta time.
2718
    update_sim_params_bind_group: Option<BindGroup>,
2719
    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2720
    /// like the current time and frame delta time. This is shared with the
2721
    /// vfx_init pass too.
2722
    indirect_sim_params_bind_group: Option<BindGroup>,
2723
    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2724
    /// compute dispatch and render buffers.
2725
    indirect_metadata_bind_group: Option<BindGroup>,
2726
    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2727
    indirect_spawner_bind_group: Option<BindGroup>,
2728
    /// Global shared GPU uniform buffer storing the simulation parameters,
2729
    /// uploaded each frame from CPU to GPU.
2730
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2731
    /// Global shared GPU buffer storing the various spawner parameter structs
2732
    /// for the active effect instances.
2733
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2734
    /// Global shared GPU buffer storing the various indirect dispatch structs
2735
    /// for the indirect dispatch of the Update pass.
2736
    dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
2737
    /// Global shared GPU buffer storing the various indirect draw structs
2738
    /// for the indirect Render pass. Note that we use
2739
    /// GpuDrawIndexedIndirectArgs as the largest of the two variants (the
2740
    /// other being GpuDrawIndirectArgs). For non-indexed entries, we ignore
2741
    /// the last `u32` value.
2742
    draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
2743
    /// Global shared GPU buffer storing the various `EffectMetadata`
2744
    /// structs for the active effect instances.
2745
    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2746
    /// Various GPU limits and aligned sizes lazily allocated and cached for
2747
    /// convenience.
2748
    gpu_limits: GpuLimits,
2749
    /// Set of all effects that have initialized [`GpuEffectMetadata`] entries
2750
    /// in the [`Self::effect_metadata_buffer`].
2751
    prepared_effects: MainEntityHashSet,
2752
    indirect_shader_noevent: Handle<Shader>,
2753
    indirect_shader_events: Handle<Shader>,
2754
    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2755
    /// -noevent and -events variants).
2756
    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2757
    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2758
    /// is either the -noevent or -events variant depending on whether there's
2759
    /// any child effect with GPU events currently active.
2760
    active_indirect_pipeline_id: CachedComputePipelineId,
2761
}
2762

2763
impl EffectsMeta {
2764
    pub fn new(
3✔
2765
        device: RenderDevice,
2766
        indirect_shader_noevent: Handle<Shader>,
2767
        indirect_shader_events: Handle<Shader>,
2768
    ) -> Self {
2769
        let gpu_limits = GpuLimits::from_device(&device);
9✔
2770

2771
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2772
        // be addressed individually by the computer shaders.
2773
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
9✔
2774
        trace!(
3✔
2775
            "Aligning storage buffers to {} bytes as device limits requires.",
2✔
2776
            item_align
2777
        );
2778

2779
        Self {
2780
            view_bind_group: None,
2781
            update_sim_params_bind_group: None,
2782
            indirect_sim_params_bind_group: None,
2783
            indirect_metadata_bind_group: None,
2784
            indirect_spawner_bind_group: None,
2785
            sim_params_uniforms: UniformBuffer::default(),
6✔
2786
            spawner_buffer: AlignedBufferVec::new(
6✔
2787
                BufferUsages::STORAGE,
2788
                NonZeroU64::new(item_align),
2789
                Some("hanabi:buffer:spawner".to_string()),
2790
            ),
2791
            dispatch_indirect_buffer: GpuBuffer::new(
6✔
2792
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2793
                Some("hanabi:buffer:dispatch_indirect".to_string()),
2794
            ),
2795
            draw_indirect_buffer: BufferTable::new(
6✔
2796
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2797
                Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
2798
                Some("hanabi:buffer:draw_indirect".to_string()),
2799
            ),
2800
            effect_metadata_buffer: BufferTable::new(
6✔
2801
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2802
                Some(NonZeroU64::new(item_align).unwrap()),
2803
                Some("hanabi:buffer:effect_metadata".to_string()),
2804
            ),
2805
            gpu_limits,
2806
            prepared_effects: MainEntityHashSet::default(),
6✔
2807
            indirect_shader_noevent,
2808
            indirect_shader_events,
2809
            indirect_pipeline_ids: [
3✔
2810
                CachedComputePipelineId::INVALID,
2811
                CachedComputePipelineId::INVALID,
2812
            ],
2813
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2814
        }
2815
    }
2816

2817
    /// Allocate internal resources for newly spawned effects.
2818
    ///
2819
    /// After this system ran, all valid extracted effects from the main world
2820
    /// have a corresponding entity with a [`CachedEffect`] component in the
2821
    /// render world. An extracted effect is considered valid if it passed some
2822
    /// basic checks, like having a valid mesh. Note however that the main
2823
    /// world's entity might still be missing its [`RenderEntity`]
2824
    /// reference, since we cannot yet write into the main world.
2825
    pub fn add_effects(
1,030✔
2826
        &mut self,
2827
        mut commands: Commands,
2828
        mut added_effects: Vec<AddedEffect>,
2829
        effect_cache: &mut ResMut<EffectCache>,
2830
        property_cache: &mut ResMut<PropertyCache>,
2831
        event_cache: &mut ResMut<EventCache>,
2832
    ) {
2833
        // FIXME - We delete a buffer above, and have a chance to immediatly re-create
2834
        // it below. We should keep the GPU buffer around until the end of this method.
2835
        // On the other hand, we should also be careful that allocated buffers need to
2836
        // be tightly packed because 'vfx_indirect.wgsl' index them by buffer index in
2837
        // order, so doesn't support offset.
2838

2839
        trace!("Adding {} newly spawned effects", added_effects.len());
4,090✔
2840
        for added_effect in added_effects.drain(..) {
3,092✔
2841
            trace!("+ added effect: capacity={}", added_effect.capacity);
2✔
2842

2843
            // Allocate an indirect dispatch arguments struct for this instance
2844
            let update_dispatch_indirect_buffer_row_index =
2845
                self.dispatch_indirect_buffer.allocate();
2846

2847
            // We cannot allocate yet an entry for the indirect draw arguments, because we
2848
            // need to know if the mesh is indexed.
2849
            let draw_indirect_buffer_row_index = DrawIndirectRowIndex::default(); // invalid
2850

2851
            // Allocate per-effect metadata.
2852
            let gpu_effect_metadata = GpuEffectMetadata {
2853
                capacity: added_effect.capacity,
2854
                alive_count: 0,
2855
                max_update: 0,
2856
                max_spawn: added_effect.capacity,
2857
                ..default()
2858
            };
2859
            trace!("+ Effect: {:?}", gpu_effect_metadata);
2✔
2860
            let effect_metadata_buffer_table_id =
2861
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2862
            let dispatch_buffer_indices = DispatchBufferIndices {
2863
                update_dispatch_indirect_buffer_row_index,
2864
                draw_indirect_buffer_row_index,
2865
                effect_metadata_buffer_table_id,
2866
            };
2867

2868
            // Insert the effect into the cache. This will allocate all the necessary
2869
            // mandatory GPU resources as needed.
2870
            let cached_effect = effect_cache.insert(
2871
                added_effect.handle,
2872
                added_effect.capacity,
2873
                &added_effect.particle_layout,
2874
                added_effect.layout_flags,
2875
            );
2876
            let mut cmd = commands.entity(added_effect.render_entity.id());
2877
            cmd.insert((
2878
                added_effect.entity,
2879
                cached_effect,
2880
                dispatch_buffer_indices,
2881
                CachedMesh {
2882
                    mesh: added_effect.mesh.id(),
2883
                },
2884
            ));
2885

2886
            // Allocate storage for properties if needed
2887
            if !added_effect.property_layout.is_empty() {
1✔
2888
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
1✔
2889
                cmd.insert(cached_effect_properties);
1✔
2890
            } else {
2891
                cmd.remove::<CachedEffectProperties>();
1✔
2892
            }
2893

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

2910
            // Allocate storage for GPU spawn events if needed
UNCOV
2911
            if let Some(parent) = added_effect.parent.as_ref() {
×
2912
                let cached_events = event_cache.allocate(parent.event_count);
2913
                cmd.insert(cached_events);
2914
            } else {
2915
                cmd.remove::<CachedEffectEvents>();
2✔
2916
            }
2917

2918
            // Ensure the particle@1 bind group layout exists for the given configuration of
2919
            // particle layout and (optionally) parent particle layout.
2920
            {
2921
                let parent_min_binding_size = added_effect
2922
                    .parent
UNCOV
2923
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2924
                effect_cache.ensure_particle_bind_group_layout(
2925
                    added_effect.particle_layout.min_binding_size32(),
2926
                    parent_min_binding_size,
2927
                );
2928
            }
2929

2930
            // Ensure the metadata@3 bind group layout exists for init pass.
2931
            {
2932
                let consume_gpu_spawn_events = added_effect
2933
                    .layout_flags
2934
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2935
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2936
            }
2937

2938
            // We cannot yet determine the layout of the metadata@3 bind group for the
2939
            // update pass, because it depends on the number of children, and
2940
            // this is encoded indirectly via the number of child effects
2941
            // pointing to this parent, and only calculated later in
2942
            // resolve_parents().
2943

2944
            trace!(
2945
                "+ added effect entity {:?}: main_entity={:?} \
2✔
2946
                first_update_group_dispatch_buffer_index={} \
2✔
2947
                render_effect_dispatch_buffer_id={}",
2✔
2948
                added_effect.render_entity,
2949
                added_effect.entity,
2950
                update_dispatch_indirect_buffer_row_index,
2951
                effect_metadata_buffer_table_id.0
2952
            );
2953
        }
2954
    }
2955

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

2986
    pub fn allocate_draw_indirect(
2✔
2987
        &mut self,
2988
        is_indexed: bool,
2989
        mesh_location: &CachedMeshLocation,
2990
    ) -> DrawIndirectRowIndex {
2991
        let draw_args = GpuDrawIndexedIndirectArgs {
2992
            index_count: mesh_location.vertex_or_index_count,
4✔
2993
            instance_count: 0,
2994
            first_index: mesh_location.first_index_or_vertex_offset,
2✔
2995
            base_vertex: mesh_location.vertex_offset_or_base_instance,
2✔
2996
            first_instance: 0,
2997
        };
2998
        let idx = self.draw_indirect_buffer.insert(draw_args);
8✔
2999
        if is_indexed {
2✔
3000
            DrawIndirectRowIndex::Indexed(idx)
2✔
3001
        } else {
UNCOV
3002
            DrawIndirectRowIndex::NonIndexed(idx)
×
3003
        }
3004
    }
3005

3006
    pub fn free_draw_indirect(&mut self, row_index: DrawIndirectRowIndex) {
1✔
3007
        self.draw_indirect_buffer.remove(row_index.get());
4✔
3008
    }
3009
}
3010

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

3048
impl Default for LayoutFlags {
3049
    fn default() -> Self {
1✔
3050
        Self::NONE
1✔
3051
    }
3052
}
3053

3054
/// Observer raised when the [`CachedEffect`] component is removed, which
3055
/// indicates that the effect instance was despawned.
3056
pub(crate) fn on_remove_cached_effect(
1✔
3057
    trigger: Trigger<OnRemove, CachedEffect>,
3058
    query: Query<(
3059
        Entity,
3060
        &MainEntity,
3061
        &CachedEffect,
3062
        &DispatchBufferIndices,
3063
        Option<&CachedEffectProperties>,
3064
        Option<&CachedParentInfo>,
3065
        Option<&CachedEffectEvents>,
3066
    )>,
3067
    mut effect_cache: ResMut<EffectCache>,
3068
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3069
    mut effects_meta: ResMut<EffectsMeta>,
3070
    mut event_cache: ResMut<EventCache>,
3071
) {
3072
    #[cfg(feature = "trace")]
3073
    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
3✔
3074

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

3078
    // Fecth the components of the effect being destroyed. Note that the despawn
3079
    // command above is not yet applied, so this query should always succeed.
3080
    let Ok((
3081
        render_entity,
1✔
3082
        main_entity,
3083
        cached_effect,
3084
        dispatch_buffer_indices,
3085
        _opt_props,
3086
        _opt_parent,
3087
        opt_cached_effect_events,
3088
    )) = query.get(trigger.target())
3✔
3089
    else {
UNCOV
3090
        return;
×
3091
    };
3092

3093
    // Dealllocate the effect slice in the event buffer, if any.
3094
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
3095
        match event_cache.free(cached_effect_events) {
UNCOV
3096
            Err(err) => {
×
3097
                error!("Error while freeing effect event slice: {err:?}");
×
3098
            }
UNCOV
3099
            Ok(buffer_state) => {
×
UNCOV
3100
                if buffer_state != BufferState::Used {
×
3101
                    // Clear bind groups associated with the old buffer
UNCOV
3102
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
UNCOV
3103
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3104
                }
3105
            }
3106
        }
3107
    }
3108

3109
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3110
    // last slice, also deallocate the GPU buffer itself.
3111
    trace!(
3112
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
1✔
3113
        render_entity,
3114
        main_entity,
3115
    );
3116
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
3117
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3118
        // do.
UNCOV
3119
        return;
×
3120
    };
3121

3122
    // Clear bind groups associated with the removed buffer
3123
    trace!(
1✔
3124
        "=> GPU buffer #{} gone, destroying its bind groups...",
1✔
3125
        cached_effect.buffer_index
3126
    );
3127
    effect_bind_groups
3128
        .particle_buffers
3129
        .remove(&cached_effect.buffer_index);
3130
    effects_meta
3131
        .dispatch_indirect_buffer
3132
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
3133
    effects_meta.free_draw_indirect(dispatch_buffer_indices.draw_indirect_buffer_row_index);
3134
    effects_meta
3135
        .effect_metadata_buffer
3136
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
3137
    effects_meta.prepared_effects.remove(main_entity);
3138
}
3139

3140
/// Update the [`CachedEffect`] component for any newly allocated effect.
3141
///
3142
/// After this system ran, and its commands are applied, all valid extracted
3143
/// effects have a corresponding entity in the render world, with a
3144
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3145
pub(crate) fn add_effects(
1,030✔
3146
    commands: Commands,
3147
    mut effects_meta: ResMut<EffectsMeta>,
3148
    mut effect_cache: ResMut<EffectCache>,
3149
    mut property_cache: ResMut<PropertyCache>,
3150
    mut event_cache: ResMut<EventCache>,
3151
    mut extracted_effects: ResMut<ExtractedEffects>,
3152
    mut sort_bind_groups: ResMut<SortBindGroups>,
3153
) {
3154
    #[cfg(feature = "trace")]
3155
    let _span = bevy::log::info_span!("add_effects").entered();
3,090✔
3156
    trace!("add_effects");
2,050✔
3157

3158
    // Clear last frame's buffer resizes which may have occured during last frame,
3159
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3160
    // the first point at which we can do that where we're not blocking the main
3161
    // world (so, excluding the extract system).
3162
    effects_meta
1,030✔
3163
        .dispatch_indirect_buffer
1,030✔
3164
        .clear_previous_frame_resizes();
3165
    effects_meta
1,030✔
3166
        .draw_indirect_buffer
1,030✔
3167
        .clear_previous_frame_resizes();
3168
    effects_meta
1,030✔
3169
        .effect_metadata_buffer
1,030✔
3170
        .clear_previous_frame_resizes();
3171
    sort_bind_groups.clear_previous_frame_resizes();
1,030✔
3172
    event_cache.clear_previous_frame_resizes();
1,030✔
3173

3174
    // Allocate new effects
3175
    effects_meta.add_effects(
3,090✔
3176
        commands,
2,060✔
3177
        std::mem::take(&mut extracted_effects.added_effects),
3,090✔
3178
        &mut effect_cache,
2,060✔
3179
        &mut property_cache,
1,030✔
3180
        &mut event_cache,
1,030✔
3181
    );
3182

3183
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3184
    // because EffectBuffer already contains a reference to the
3185
    // RenderDevice, so has done so internally. This is not ideal
3186
    // design-wise, but works.
3187
}
3188

3189
/// Check if two lists of entities are equal.
3190
fn is_child_list_changed(
×
3191
    parent_entity: Entity,
3192
    old: impl ExactSizeIterator<Item = Entity>,
3193
    new: impl ExactSizeIterator<Item = Entity>,
3194
) -> bool {
UNCOV
3195
    if old.len() != new.len() {
×
3196
        trace!(
×
UNCOV
3197
            "Child list changed for effect {:?}: old #{} != new #{}",
×
UNCOV
3198
            parent_entity,
×
UNCOV
3199
            old.len(),
×
3200
            new.len()
×
3201
        );
3202
        return true;
×
3203
    }
3204

3205
    // TODO - this value is arbitrary
3206
    if old.len() >= 16 {
×
3207
        // For large-ish lists, use a hash set.
3208
        let old = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(old);
×
UNCOV
3209
        let new = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(new);
×
3210
        if old != new {
×
UNCOV
3211
            trace!(
×
UNCOV
3212
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3213
            );
3214
            true
×
3215
        } else {
3216
            false
×
3217
        }
3218
    } else {
3219
        // For small lists, just use a linear array and sort it
3220
        let mut old = old.collect::<Vec<_>>();
×
UNCOV
3221
        let mut new = new.collect::<Vec<_>>();
×
3222
        old.sort_unstable();
×
UNCOV
3223
        new.sort_unstable();
×
3224
        if old != new {
×
UNCOV
3225
            trace!(
×
UNCOV
3226
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3227
            );
UNCOV
3228
            true
×
3229
        } else {
UNCOV
3230
            false
×
3231
        }
3232
    }
3233
}
3234

3235
/// Resolve parents and children, updating their [`CachedParent`] and
3236
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3237
/// slice for all children of each parent.
3238
pub(crate) fn resolve_parents(
1,030✔
3239
    mut commands: Commands,
3240
    q_child_effects: Query<
3241
        (
3242
            Entity,
3243
            &CachedParentRef,
3244
            &CachedEffectEvents,
3245
            Option<&CachedChildInfo>,
3246
        ),
3247
        With<CachedEffect>,
3248
    >,
3249
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3250
    effect_cache: Res<EffectCache>,
3251
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3252
    mut event_cache: ResMut<EventCache>,
3253
    mut children_from_parent: Local<
3254
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3255
    >,
3256
) {
3257
    #[cfg(feature = "trace")]
3258
    let _span = bevy::log::info_span!("resolve_parents").entered();
3,090✔
3259
    let num_parent_effects = q_parent_effects.iter().len();
3,090✔
3260
    trace!("resolve_parents: num_parents={num_parent_effects}");
2,050✔
3261

3262
    // Build map of render entity from main entity for all cached effects.
3263
    let render_from_main_entity = q_cached_effects
2,060✔
3264
        .iter()
3265
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
3,058✔
3266
        .collect::<HashMap<_, _>>();
3267

3268
    // Record all parents with children that changed so that we can mark those
3269
    // parents' `CachedParentInfo` as changed. See the comment in the
3270
    // `q_parent_effects` loop for more information.
3271
    let mut parents_with_dirty_children = EntityHashSet::default();
2,060✔
3272

3273
    // Group child effects by parent, building a list of children for each parent,
3274
    // solely based on the declaration each child makes of its parent. This doesn't
3275
    // mean yet that the parent exists.
3276
    if children_from_parent.capacity() < num_parent_effects {
1,030✔
UNCOV
3277
        let extra = num_parent_effects - children_from_parent.capacity();
×
UNCOV
3278
        children_from_parent.reserve(extra);
×
3279
    }
3280
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3281
        q_child_effects.iter()
2,060✔
3282
    {
3283
        // Resolve the parent reference into the render world
3284
        let parent_main_entity = cached_parent_ref.entity;
3285
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
UNCOV
3286
            warn!(
×
UNCOV
3287
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3288
                parent_main_entity, child_entity
3289
            );
3290
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
UNCOV
3291
            continue;
×
3292
        };
3293
        let parent_entity = *parent_entity;
3294

3295
        // Resolve the parent
UNCOV
3296
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3297
            // Since we failed to resolve, remove this component so the next systems ignore
3298
            // this effect.
UNCOV
3299
            warn!(
×
3300
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3301
                parent_entity, child_entity
3302
            );
UNCOV
3303
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
UNCOV
3304
            continue;
×
3305
        };
3306
        let Some(parent_buffer_binding_source) = effect_cache
×
3307
            .get_buffer(parent_cached_effect.buffer_index)
UNCOV
3308
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3309
        else {
3310
            // Since we failed to resolve, remove this component so the next systems ignore
3311
            // this effect.
UNCOV
3312
            warn!(
×
UNCOV
3313
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3314
                parent_cached_effect.buffer_index, child_entity
3315
            );
UNCOV
3316
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
UNCOV
3317
            continue;
×
3318
        };
3319

UNCOV
3320
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3321
        else {
3322
            // Since we failed to resolve, remove this component so the next systems ignore
3323
            // this effect.
UNCOV
3324
            warn!(
×
UNCOV
3325
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3326
                cached_effect_events.buffer_index, child_entity
3327
            );
UNCOV
3328
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
UNCOV
3329
            continue;
×
3330
        };
3331
        let child_buffer_binding_source = BufferBindingSource {
3332
            buffer: child_event_buffer.clone(),
3333
            offset: cached_effect_events.range.start,
3334
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3335
        };
3336

3337
        // Push the child entity into the children list
3338
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3339
        let local_child_index = child_vec.len() as u32;
3340
        child_vec.push((child_entity, child_buffer_binding_source));
3341
        child_infos.push(GpuChildInfo {
3342
            event_count: 0,
3343
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3344
        });
3345

3346
        // Check if child info changed. Avoid overwriting if no change.
UNCOV
3347
        if let Some(old_cached_child_info) = cached_child_info {
×
3348
            if parent_entity == old_cached_child_info.parent
3349
                && parent_cached_effect.slice.particle_layout
×
3350
                    == old_cached_child_info.parent_particle_layout
×
UNCOV
3351
                && parent_buffer_binding_source
×
3352
                    == old_cached_child_info.parent_buffer_binding_source
×
3353
                // Note: if local child index didn't change, then keep global one too for now. Chances are the parent didn't change, but anyway we can't know for now without inspecting all its children.
UNCOV
3354
                && local_child_index == old_cached_child_info.local_child_index
×
UNCOV
3355
                && cached_effect_events.init_indirect_dispatch_index
×
3356
                    == old_cached_child_info.init_indirect_dispatch_index
×
3357
            {
UNCOV
3358
                trace!(
×
UNCOV
3359
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3360
                    child_entity
3361
                );
UNCOV
3362
                continue;
×
3363
            }
3364
        }
3365

3366
        // Allocate (or overwrite, if already existing) the child info, now that the
3367
        // parent is resolved.
3368
        let cached_child_info = CachedChildInfo {
3369
            parent: parent_entity,
3370
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
3371
            parent_buffer_binding_source,
3372
            local_child_index,
3373
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3374
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3375
        };
3376
        commands.entity(child_entity).insert(cached_child_info);
UNCOV
3377
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3378

3379
        // Make a note of the parent entity so that we remember to mark its
3380
        // `CachedParentInfo` as changed below.
3381
        parents_with_dirty_children.insert(parent_entity);
3382
    }
3383

3384
    // Once all parents are resolved, diff all children of already-cached parents,
3385
    // and re-allocate their GpuChildInfo if needed.
3386
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
2,060✔
3387
        // Fetch the newly extracted list of children
UNCOV
3388
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3389
        else {
UNCOV
3390
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
UNCOV
3391
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
UNCOV
3392
            continue;
×
3393
        };
3394

3395
        // If we updated `CachedChildInfo` for any of this entity's children,
3396
        // then even if the check below passes, we must still set the change
3397
        // flag on this entity's `CachedParentInfo`. That's because the
3398
        // `fixup_parents` system looks at the change flag for the parent in
3399
        // order to determine which `CachedChildInfo` it needs to update, and
3400
        // that system must process all newly-added `CachedChildInfo`s.
UNCOV
3401
        if parents_with_dirty_children.contains(&parent_entity) {
×
UNCOV
3402
            cached_parent_info.set_changed();
×
3403
        }
3404

3405
        // Check if any child changed compared to the existing CachedChildren component
3406
        if !is_child_list_changed(
3407
            parent_entity,
3408
            cached_parent_info
3409
                .children
3410
                .iter()
3411
                .map(|(entity, _)| *entity),
3412
            children.iter().map(|(entity, _)| *entity),
3413
        ) {
UNCOV
3414
            continue;
×
3415
        }
3416

3417
        event_cache.reallocate_child_infos(
3418
            parent_entity,
3419
            children,
3420
            &child_infos[..],
3421
            cached_parent_info.deref_mut(),
3422
        );
3423
    }
3424

3425
    // Once this is done, the children hash map contains all entries which don't
3426
    // already have a CachedParentInfo component. That is, all entities which are
3427
    // new parents.
3428
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
2,060✔
3429
        let cached_parent_info =
3430
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3431
        commands.entity(parent_entity).insert(cached_parent_info);
3432
    }
3433

3434
    // // Once all changes are applied, immediately schedule any GPU buffer
3435
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3436
    // content // will be written later.
3437
    // if event_cache
3438
    //     .child_infos()
3439
    //     .allocate_gpu(render_device, render_queue)
3440
    // {
3441
    //     // All those bind groups use the buffer so need to be re-created
3442
    //     effect_bind_groups.particle_buffers.clear();
3443
    // }
3444
}
3445

3446
pub fn fixup_parents(
1,030✔
3447
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3448
    mut q_children: Query<&mut CachedChildInfo>,
3449
) {
3450
    #[cfg(feature = "trace")]
3451
    let _span = bevy::log::info_span!("fixup_parents").entered();
3,090✔
3452
    trace!("fixup_parents");
2,050✔
3453

3454
    // Once all parents are (re-)allocated, fix up the global index of all
3455
    // children if the parent base index changed.
3456
    trace!(
1,030✔
3457
        "Updating the global index of children of parent effects whose child list just changed..."
1,020✔
3458
    );
3459
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
2,060✔
3460
        let base_index =
3461
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3462
        trace!(
3463
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3464
            cached_parent_info.children.len(),
×
3465
            parent_entity,
3466
            base_index
3467
        );
3468
        for (child_entity, _) in &cached_parent_info.children {
×
UNCOV
3469
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
UNCOV
3470
                continue;
×
3471
            };
3472
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
UNCOV
3473
            trace!(
×
UNCOV
3474
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3475
                child_entity,
3476
                parent_entity,
UNCOV
3477
                cached_child_info.local_child_index,
×
UNCOV
3478
                cached_child_info.global_child_index
×
3479
            );
3480
        }
3481
    }
3482
}
3483

3484
/// Update any cached mesh info based on any relocation done by Bevy itself.
3485
///
3486
/// Bevy will merge small meshes into larger GPU buffers automatically. When
3487
/// this happens, the mesh location changes, and we need to update our
3488
/// references to it in order to know how to issue the draw commands.
3489
pub fn update_mesh_locations(
1,030✔
3490
    mut commands: Commands,
3491
    mut effects_meta: ResMut<EffectsMeta>,
3492
    mesh_allocator: Res<MeshAllocator>,
3493
    render_meshes: Res<RenderAssets<RenderMesh>>,
3494
    mut q_cached_effects: Query<
3495
        (
3496
            Entity,
3497
            &CachedMesh,
3498
            &mut DispatchBufferIndices,
3499
            Option<&mut CachedMeshLocation>,
3500
        ),
3501
        With<CachedEffect>,
3502
    >,
3503
) {
3504
    for (entity, cached_mesh, mut dispatch_buffer_indices, maybe_cached_mesh_location) in
1,014✔
3505
        &mut q_cached_effects
2,044✔
3506
    {
3507
        // FIXME - clear allocated entries (if any) if we can't resolve the mesh!
3508

3509
        // Resolve the render mesh
3510
        let Some(render_mesh) = render_meshes.get(cached_mesh.mesh) else {
1,014✔
UNCOV
3511
            warn!(
×
UNCOV
3512
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3513
                entity, cached_mesh.mesh
3514
            );
UNCOV
3515
            continue;
×
3516
        };
3517

3518
        // Find the location where the render mesh was allocated. This is handled by
3519
        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3520
        // re-batch the vertex and optional index data of meshes together at any point,
3521
        // so we need to confirm that the location data we may have cached is still
3522
        // valid.
3523
        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&cached_mesh.mesh)
1,014✔
3524
        else {
UNCOV
3525
            trace!(
×
UNCOV
3526
                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
3527
                entity,
3528
                cached_mesh.mesh
3529
            );
UNCOV
3530
            continue;
×
3531
        };
3532
        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&cached_mesh.mesh);
3533
        let indexed =
1,014✔
3534
            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
1,014✔
3535
                if let Some(ref slice) = mesh_index_buffer_slice {
1,014✔
3536
                    Some(MeshIndexSlice {
3537
                        format: index_format,
3538
                        buffer: slice.buffer.clone(),
3539
                        range: slice.range.clone(),
3540
                    })
3541
                } else {
UNCOV
3542
                    trace!(
×
UNCOV
3543
                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
3544
                        entity,
3545
                        cached_mesh.mesh
3546
                    );
UNCOV
3547
                    continue;
×
3548
                }
3549
            } else {
UNCOV
3550
                None
×
3551
            };
3552

3553
        // Calculate the new mesh location as it should be based on Bevy's info
3554
        let is_indexed = indexed.is_some();
3555
        let new_mesh_location = match &mesh_index_buffer_slice {
3556
            // Indexed mesh rendering
3557
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3558
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
3,042✔
3559
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
2,028✔
3560
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
2,028✔
3561
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
1,014✔
3562
                indexed,
3563
            },
3564
            // Non-indexed mesh rendering
3565
            None => CachedMeshLocation {
UNCOV
3566
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
UNCOV
3567
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
UNCOV
3568
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3569
                vertex_offset_or_base_instance: 0,
3570
                indexed: None,
3571
            },
3572
        };
3573

3574
        // We don't allocate the draw indirect args ahead of time because we need to
3575
        // select the indexed vs. non-indexed buffer. Now that we know whether the mesh
3576
        // is indexed, we can allocate it (or reallocate it if indexing mode changed).
3577
        if dispatch_buffer_indices
3578
            .draw_indirect_buffer_row_index
3579
            .is_valid()
3580
        {
3581
            let was_indexed = dispatch_buffer_indices
2,024✔
3582
                .draw_indirect_buffer_row_index
1,012✔
3583
                .is_indexed();
3584
            if was_indexed != is_indexed {
1,012✔
UNCOV
3585
                effects_meta
×
UNCOV
3586
                    .free_draw_indirect(dispatch_buffer_indices.draw_indirect_buffer_row_index);
×
3587
            }
3588
        }
3589
        if !dispatch_buffer_indices
3590
            .draw_indirect_buffer_row_index
3591
            .is_valid()
3592
        {
3593
            dispatch_buffer_indices.draw_indirect_buffer_row_index =
2✔
3594
                effects_meta.allocate_draw_indirect(is_indexed, &new_mesh_location);
2✔
3595
        }
3596

3597
        // Compare to any cached data and update if necessary, or insert if missing.
3598
        // This will trigger change detection in the ECS, which will in turn trigger
3599
        // GpuEffectMetadata re-upload.
3600
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
1,012✔
3601
            #[cfg(debug_assertions)]
3602
            if *old_mesh_location.deref() != new_mesh_location {
UNCOV
3603
                debug!(
×
UNCOV
3604
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3605
                    entity, old_mesh_location, new_mesh_location
3606
                );
3607
            }
3608

3609
            old_mesh_location.set_if_neq(new_mesh_location);
3610
        } else {
3611
            commands.entity(entity).insert(new_mesh_location);
6✔
3612
        }
3613
    }
3614
}
3615

3616
// TEMP - Mark all cached effects as invalid for this frame until another system
3617
// explicitly marks them as valid. Otherwise we early out in some parts, and
3618
// reuse by mistake the previous frame's extraction.
3619
pub fn clear_transient_batch_inputs(
1,030✔
3620
    mut commands: Commands,
3621
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3622
) {
3623
    for entity in &mut q_cached_effects {
3,054✔
3624
        if let Ok(mut cmd) = commands.get_entity(entity) {
1,012✔
3625
            cmd.remove::<BatchInput>();
3626
        }
3627
    }
3628
}
3629

3630
/// Render world cached mesh infos for a single effect instance.
3631
#[derive(Debug, Clone, Copy, Component)]
3632
pub(crate) struct CachedMesh {
3633
    /// Asset of the effect mesh to draw.
3634
    pub mesh: AssetId<Mesh>,
3635
}
3636

3637
/// Indexed mesh metadata for [`CachedMesh`].
3638
#[derive(Debug, Clone)]
3639
#[allow(dead_code)]
3640
pub(crate) struct MeshIndexSlice {
3641
    /// Index format.
3642
    pub format: IndexFormat,
3643
    /// GPU buffer containing the indices.
3644
    pub buffer: Buffer,
3645
    /// Range inside [`Self::buffer`] where the indices are.
3646
    pub range: Range<u32>,
3647
}
3648

3649
impl PartialEq for MeshIndexSlice {
3650
    fn eq(&self, other: &Self) -> bool {
2,024✔
3651
        self.format == other.format
2,024✔
3652
            && self.buffer.id() == other.buffer.id()
4,048✔
3653
            && self.range == other.range
2,024✔
3654
    }
3655
}
3656

3657
impl Eq for MeshIndexSlice {}
3658

3659
/// Cached info about a mesh location in a Bevy buffer. This information is
3660
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
3661
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
3662
/// so we can invalidate that GPU data.
3663
#[derive(Debug, Clone, PartialEq, Eq, Component)]
3664
pub(crate) struct CachedMeshLocation {
3665
    /// Vertex buffer.
3666
    pub vertex_buffer: BufferId,
3667
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
3668
    pub vertex_or_index_count: u32,
3669
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
3670
    pub first_index_or_vertex_offset: u32,
3671
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
3672
    pub vertex_offset_or_base_instance: i32,
3673
    /// Indexed rendering metadata.
3674
    pub indexed: Option<MeshIndexSlice>,
3675
}
3676

3677
/// Render world cached properties info for a single effect instance.
3678
#[allow(unused)]
3679
#[derive(Debug, Component)]
3680
pub(crate) struct CachedProperties {
3681
    /// Layout of the effect properties.
3682
    pub layout: PropertyLayout,
3683
    /// Index of the buffer in the [`EffectCache`].
3684
    pub buffer_index: u32,
3685
    /// Offset in bytes inside the buffer.
3686
    pub offset: u32,
3687
    /// Binding size in bytes of the property struct.
3688
    pub binding_size: u32,
3689
}
3690

3691
#[derive(SystemParam)]
3692
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3693
    sim_params: Res<'w, SimParams>,
3694
    render_device: Res<'w, RenderDevice>,
3695
    render_queue: Res<'w, RenderQueue>,
3696
    marker: PhantomData<&'s usize>,
3697
}
3698

3699
#[derive(SystemParam)]
3700
pub struct PipelineSystemParams<'w, 's> {
3701
    pipeline_cache: Res<'w, PipelineCache>,
3702
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3703
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3704
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3705
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3706
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3707
    specialized_indirect_pipelines:
3708
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3709
    marker: PhantomData<&'s usize>,
3710
}
3711

3712
pub(crate) fn prepare_effects(
1,030✔
3713
    mut commands: Commands,
3714
    read_only_params: PrepareEffectsReadOnlyParams,
3715
    mut pipelines: PipelineSystemParams,
3716
    mut property_cache: ResMut<PropertyCache>,
3717
    event_cache: Res<EventCache>,
3718
    mut effect_cache: ResMut<EffectCache>,
3719
    mut effects_meta: ResMut<EffectsMeta>,
3720
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3721
    mut extracted_effects: ResMut<ExtractedEffects>,
3722
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3723
    q_cached_effects: Query<(
3724
        &MainEntity,
3725
        &CachedEffect,
3726
        Ref<CachedMesh>,
3727
        Ref<CachedMeshLocation>,
3728
        &DispatchBufferIndices,
3729
        Option<&CachedEffectProperties>,
3730
        Option<&CachedParentInfo>,
3731
        Option<&CachedChildInfo>,
3732
        Option<&CachedEffectEvents>,
3733
    )>,
3734
    q_debug_all_entities: Query<MainEntity>,
3735
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
3736
    mut sort_bind_groups: ResMut<SortBindGroups>,
3737
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3738
) {
3739
    #[cfg(feature = "trace")]
3740
    let _span = bevy::log::info_span!("prepare_effects").entered();
3,090✔
3741
    trace!("prepare_effects");
2,050✔
3742

3743
    init_fill_dispatch_queue.clear();
1,030✔
3744

3745
    // Workaround for too many params in system (TODO: refactor to split work?)
3746
    let sim_params = read_only_params.sim_params.into_inner();
3,090✔
3747
    let render_device = read_only_params.render_device.into_inner();
3,090✔
3748
    let render_queue = read_only_params.render_queue.into_inner();
3,090✔
3749
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
3,090✔
3750
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
3,090✔
3751
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
3,090✔
3752
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
3,090✔
3753

3754
    // // sort first by z and then by handle. this ensures that, when possible,
3755
    // batches span multiple z layers // batches won't span z-layers if there is
3756
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3757
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3758
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3759
    //         other => other,
3760
    //     }
3761
    // });
3762

3763
    // Ensure the indirect pipelines are created
3764
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
1,033✔
3765
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
12✔
3766
            pipeline_cache,
6✔
3767
            &pipelines.indirect_pipeline,
3✔
3768
            DispatchIndirectPipelineKey { has_events: false },
3✔
3769
        );
3770
    }
3771
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
1,033✔
3772
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
12✔
3773
            pipeline_cache,
6✔
3774
            &pipelines.indirect_pipeline,
3✔
3775
            DispatchIndirectPipelineKey { has_events: true },
3✔
3776
        );
3777
    }
3778
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
1,033✔
3779
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
6✔
3780
    } else {
3781
        // If this is the first time we insert an event buffer, we need to switch the
3782
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3783
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3784
        // more effect using GPU spawn events, we can deallocate.
3785
        let was_empty =
1,027✔
3786
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
3787
        let is_empty = event_cache.child_infos().is_empty();
3788
        if was_empty && !is_empty {
1,027✔
UNCOV
3789
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
UNCOV
3790
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3791
        } else if is_empty && !was_empty {
2,054✔
UNCOV
3792
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
UNCOV
3793
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3794
        }
3795
    }
3796

3797
    gpu_buffer_operations.begin_frame();
1,030✔
3798

3799
    // Clear per-instance buffers, which are filled below and re-uploaded each frame
3800
    effects_meta.spawner_buffer.clear();
2,060✔
3801

3802
    // Build batcher inputs from extracted effects, updating all cached components
3803
    // for each effect on the fly.
3804
    let effects = std::mem::take(&mut extracted_effects.effects);
3,090✔
3805
    let extracted_effect_count = effects.len();
3,090✔
3806
    let mut prepared_effect_count = 0;
2,060✔
3807
    for extracted_effect in effects.into_iter() {
3,074✔
3808
        // Skip effects not cached. Since we're iterating over the extracted effects
3809
        // instead of the cached ones, it might happen we didn't cache some effect on
3810
        // purpose because they failed earlier validations.
3811
        // FIXME - extract into ECS directly so we don't have to do that?
3812
        let Ok((
3813
            main_entity,
1,014✔
3814
            cached_effect,
3815
            cached_mesh,
3816
            cached_mesh_location,
3817
            dispatch_buffer_indices,
3818
            cached_effect_properties,
3819
            cached_parent_info,
3820
            cached_child_info,
3821
            cached_effect_events,
3822
        )) = q_cached_effects.get(extracted_effect.render_entity.id())
3,042✔
3823
        else {
3824
            warn!(
×
3825
                "Unknown render entity {:?} for extracted effect.",
×
UNCOV
3826
                extracted_effect.render_entity.id()
×
3827
            );
UNCOV
3828
            if let Ok(main_entity) = q_debug_all_entities.get(extracted_effect.render_entity.id()) {
×
3829
                info!(
3830
                    "Render entity {:?} exists with main entity {:?}, some component missing!",
×
3831
                    extracted_effect.render_entity.id(),
×
3832
                    main_entity
3833
                );
3834
            } else {
UNCOV
3835
                info!(
×
UNCOV
3836
                    "Render entity {:?} does not exists with a MainEntity.",
×
UNCOV
3837
                    extracted_effect.render_entity.id()
×
3838
                );
3839
            }
UNCOV
3840
            continue;
×
3841
        };
3842

3843
        let effect_slice = EffectSlice {
3844
            slice: cached_effect.slice.range(),
3845
            buffer_index: cached_effect.buffer_index,
3846
            particle_layout: cached_effect.slice.particle_layout.clone(),
3847
        };
3848

3849
        let has_event_buffer = cached_child_info.is_some();
3850
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3851
        // should be Option<T>, not T)
3852
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3853
            None
1,005✔
3854
        } else {
3855
            Some(extracted_effect.property_layout.min_binding_size())
9✔
3856
        };
3857

3858
        // Schedule some GPU buffer operation to update the number of workgroups to
3859
        // dispatch during the indirect init pass of this effect based on the number of
3860
        // GPU spawn events written in its buffer.
3861
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3862
            (cached_effect_events, cached_child_info)
3863
        {
3864
            debug_assert_eq!(
3865
                GpuChildInfo::min_size().get() % 4,
3866
                0,
UNCOV
3867
                "Invalid GpuChildInfo alignment."
×
3868
            );
3869

3870
            // Resolve parent entry
3871
            let Ok((_, _, _, _, _, _, cached_parent_info, _, _)) =
×
3872
                q_cached_effects.get(cached_child_info.parent)
×
3873
            else {
UNCOV
3874
                continue;
×
3875
            };
UNCOV
3876
            let Some(cached_parent_info) = cached_parent_info else {
×
3877
                error!("Effect {:?} indicates its parent is {:?}, but that parent effect is missing a CachedParentInfo component. This is a bug.", extracted_effect.render_entity.id(), cached_child_info.parent);
×
UNCOV
3878
                continue;
×
3879
            };
3880

3881
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3882
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
UNCOV
3883
            let global_child_index = cached_child_info.global_child_index;
×
3884

3885
            // Schedule a fill dispatch
UNCOV
3886
            trace!(
×
UNCOV
3887
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3888
                global_child_index,
3889
                init_indirect_dispatch_index,
3890
            );
UNCOV
3891
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3892
        }
3893

3894
        // Create init pipeline key flags.
3895
        let init_pipeline_key_flags = {
1,014✔
3896
            let mut flags = ParticleInitPipelineKeyFlags::empty();
3897
            flags.set(
3898
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3899
                effect_slice.particle_layout.contains(Attribute::PREV),
3900
            );
3901
            flags.set(
3902
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3903
                effect_slice.particle_layout.contains(Attribute::NEXT),
3904
            );
3905
            flags.set(
3906
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3907
                has_event_buffer,
3908
            );
3909
            flags
3910
        };
3911

3912
        // This should always exist by the time we reach this point, because we should
3913
        // have inserted any property in the cache, which would have allocated the
3914
        // proper bind group layout (or the default no-property one).
3915
        let spawner_bind_group_layout = property_cache
3916
            .bind_group_layout(property_layout_min_binding_size)
UNCOV
3917
            .unwrap_or_else(|| {
×
UNCOV
3918
                panic!(
×
UNCOV
3919
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
×
3920
                    property_layout_min_binding_size,
3921
                )
3922
            });
3923
        trace!(
3924
            "Retrieved spawner@2 bind group layout {:?} for property binding size {:?}.",
1,014✔
3925
            spawner_bind_group_layout.id(),
2,028✔
3926
            property_layout_min_binding_size
3927
        );
3928

3929
        // Fetch the bind group layouts from the cache
3930
        trace!("cached_child_info={:?}", cached_child_info);
1,014✔
3931
        let (parent_particle_layout_min_binding_size, parent_buffer_index) =
1,014✔
3932
            if let Some(cached_child) = cached_child_info.as_ref() {
×
3933
                let Ok((_, parent_cached_effect, _, _, _, _, _, _, _)) =
×
3934
                    q_cached_effects.get(cached_child.parent)
3935
                else {
3936
                    // At this point we should have discarded invalid effects with a missing parent,
3937
                    // so if the parent is not found this is a bug.
UNCOV
3938
                    error!(
×
UNCOV
3939
                        "Effect main_entity {:?}: parent render entity {:?} not found.",
×
3940
                        main_entity, cached_child.parent
3941
                    );
UNCOV
3942
                    continue;
×
3943
                };
3944
                (
3945
                    Some(
3946
                        parent_cached_effect
3947
                            .slice
3948
                            .particle_layout
3949
                            .min_binding_size32(),
3950
                    ),
3951
                    Some(parent_cached_effect.buffer_index),
3952
                )
3953
            } else {
3954
                (None, None)
1,014✔
3955
            };
3956
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
1,014✔
3957
            effect_slice.particle_layout.min_binding_size32(),
3958
            parent_particle_layout_min_binding_size,
3959
        ) else {
UNCOV
3960
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", 
×
UNCOV
3961
            effect_slice.particle_layout.min_binding_size32(), parent_particle_layout_min_binding_size);
×
UNCOV
3962
            continue;
×
3963
        };
3964
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3965
        trace!(
3966
            "Retrieved particle@1 bind group layout {:?} for particle binding size {:?} and parent binding size {:?}.",
1,014✔
3967
            particle_bind_group_layout.id(),
2,028✔
3968
            effect_slice.particle_layout.min_binding_size32(),
2,028✔
3969
            parent_particle_layout_min_binding_size,
3970
        );
3971

3972
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3973
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3974

3975
        // Specialize the init pipeline based on the effect.
3976
        let init_pipeline_id = {
3977
            let consume_gpu_spawn_events = init_pipeline_key_flags
3978
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3979

3980
            // Fetch the metadata@3 bind group layout from the cache
3981
            let metadata_bind_group_layout = effect_cache
3982
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3983
                .unwrap()
3984
                .clone();
3985

3986
            // https://github.com/bevyengine/bevy/issues/17132
3987
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3988
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3989
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3990
            pipelines.init_pipeline.temp_particle_bind_group_layout =
3991
                Some(particle_bind_group_layout.clone());
3992
            pipelines.init_pipeline.temp_spawner_bind_group_layout =
3993
                Some(spawner_bind_group_layout.clone());
3994
            pipelines.init_pipeline.temp_metadata_bind_group_layout =
3995
                Some(metadata_bind_group_layout);
3996
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3997
                pipeline_cache,
3998
                &pipelines.init_pipeline,
3999
                ParticleInitPipelineKey {
4000
                    shader: extracted_effect.effect_shaders.init.clone(),
4001
                    particle_layout_min_binding_size,
4002
                    parent_particle_layout_min_binding_size,
4003
                    flags: init_pipeline_key_flags,
4004
                    particle_bind_group_layout_id,
4005
                    spawner_bind_group_layout_id,
4006
                    metadata_bind_group_layout_id,
4007
                },
4008
            );
4009
            // keep things tidy; this is just a hack, should not persist
4010
            pipelines.init_pipeline.temp_particle_bind_group_layout = None;
4011
            pipelines.init_pipeline.temp_spawner_bind_group_layout = None;
4012
            pipelines.init_pipeline.temp_metadata_bind_group_layout = None;
4013
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
1,014✔
4014

4015
            init_pipeline_id
4016
        };
4017

4018
        let update_pipeline_id = {
4019
            let num_event_buffers = cached_parent_info
UNCOV
4020
                .map(|p| p.children.len() as u32)
×
4021
                .unwrap_or_default();
4022

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

4030
            // Fetch the bind group layouts from the cache
4031
            let metadata_bind_group_layout = effect_cache
4032
                .metadata_update_bind_group_layout(num_event_buffers)
4033
                .unwrap()
4034
                .clone();
4035

4036
            // https://github.com/bevyengine/bevy/issues/17132
4037
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
4038
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
4039
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
4040
            pipelines.update_pipeline.temp_particle_bind_group_layout =
4041
                Some(particle_bind_group_layout);
4042
            pipelines.update_pipeline.temp_spawner_bind_group_layout =
4043
                Some(spawner_bind_group_layout);
4044
            pipelines.update_pipeline.temp_metadata_bind_group_layout =
4045
                Some(metadata_bind_group_layout);
4046
            let update_pipeline_id = specialized_update_pipelines.specialize(
4047
                pipeline_cache,
4048
                &pipelines.update_pipeline,
4049
                ParticleUpdatePipelineKey {
4050
                    shader: extracted_effect.effect_shaders.update.clone(),
4051
                    particle_layout: effect_slice.particle_layout.clone(),
4052
                    parent_particle_layout_min_binding_size,
4053
                    num_event_buffers,
4054
                    particle_bind_group_layout_id,
4055
                    spawner_bind_group_layout_id,
4056
                    metadata_bind_group_layout_id,
4057
                },
4058
            );
4059
            // keep things tidy; this is just a hack, should not persist
4060
            pipelines.update_pipeline.temp_particle_bind_group_layout = None;
4061
            pipelines.update_pipeline.temp_spawner_bind_group_layout = None;
4062
            pipelines.update_pipeline.temp_metadata_bind_group_layout = None;
4063
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
1,014✔
4064

4065
            update_pipeline_id
4066
        };
4067

4068
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
4069
            init: init_pipeline_id,
4070
            update: update_pipeline_id,
4071
        };
4072

4073
        // For ribbons, which need particle sorting, create a bind group layout for
4074
        // sorting the effect, based on its particle layout.
4075
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
UNCOV
4076
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
UNCOV
4077
                pipeline_cache,
×
UNCOV
4078
                &extracted_effect.particle_layout,
×
4079
            ) {
4080
                error!(
UNCOV
4081
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
4082
                    err
4083
                );
4084
                continue;
4085
            }
4086
        }
4087

4088
        // Output some debug info
4089
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
2,028✔
4090
        trace!(
4091
            "update_shader = {:?}",
1,014✔
4092
            extracted_effect.effect_shaders.update
4093
        );
4094
        trace!(
4095
            "render_shader = {:?}",
1,014✔
4096
            extracted_effect.effect_shaders.render
4097
        );
4098
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
1,014✔
4099
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
1,014✔
4100

4101
        let spawner_index = effects_meta.allocate_spawner(
4102
            &extracted_effect.transform,
4103
            extracted_effect.spawn_count,
4104
            extracted_effect.prng_seed,
4105
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
4106
            dispatch_buffer_indices.draw_indirect_buffer_row_index,
4107
        );
4108

4109
        trace!(
4110
            "Updating cached effect at entity {:?}...",
1,014✔
4111
            extracted_effect.render_entity.id()
2,028✔
4112
        );
4113
        let mut cmd = commands.entity(extracted_effect.render_entity.id());
4114
        cmd.insert(BatchInput {
4115
            handle: extracted_effect.handle,
4116
            entity: extracted_effect.render_entity.id(),
4117
            main_entity: extracted_effect.main_entity,
4118
            effect_slice,
4119
            init_and_update_pipeline_ids,
4120
            parent_buffer_index,
4121
            event_buffer_index: cached_effect_events.map(|cee| cee.buffer_index),
4122
            child_effects: cached_parent_info
UNCOV
4123
                .map(|cp| cp.children.clone())
×
4124
                .unwrap_or_default(),
4125
            layout_flags: extracted_effect.layout_flags,
4126
            texture_layout: extracted_effect.texture_layout.clone(),
4127
            textures: extracted_effect.textures.clone(),
4128
            alpha_mode: extracted_effect.alpha_mode,
4129
            particle_layout: extracted_effect.particle_layout.clone(),
4130
            shaders: extracted_effect.effect_shaders,
4131
            spawner_index,
4132
            spawn_count: extracted_effect.spawn_count,
4133
            position: extracted_effect.transform.translation(),
4134
            init_indirect_dispatch_index: cached_child_info
4135
                .map(|cc| cc.init_indirect_dispatch_index),
4136
        });
4137

4138
        // Update properties
4139
        if let Some(cached_effect_properties) = cached_effect_properties {
10✔
4140
            // Because the component is persisted, it may be there from a previous version
4141
            // of the asset. And add_remove_effects() only add new instances or remove old
4142
            // ones, but doesn't update existing ones. Check if it needs to be removed.
4143
            // FIXME - Dedupe with add_remove_effect(), we shouldn't have 2 codepaths doing
4144
            // the same thing at 2 different times.
4145
            if extracted_effect.property_layout.is_empty() {
4146
                trace!(
1✔
4147
                    "Render entity {:?} had CachedEffectProperties component, but newly extracted property layout is empty. Removing component...",
1✔
4148
                    extracted_effect.render_entity.id(),
2✔
4149
                );
4150
                cmd.remove::<CachedEffectProperties>();
2✔
4151
                // Also remove the other one. FIXME - dedupe those two...
4152
                cmd.remove::<CachedProperties>();
2✔
4153

4154
                if extracted_effect.property_data.is_some() {
2✔
UNCOV
4155
                    warn!(
×
UNCOV
4156
                        "Effect on entity {:?} doesn't declare any property in its Module, but some property values were provided. Those values will be discarded.",
×
UNCOV
4157
                        extracted_effect.main_entity.id(),
×
4158
                    );
4159
                }
4160
            } else {
4161
                // Insert a new component or overwrite the existing one
4162
                cmd.insert(CachedProperties {
9✔
4163
                    layout: extracted_effect.property_layout.clone(),
4164
                    buffer_index: cached_effect_properties.buffer_index,
4165
                    offset: cached_effect_properties.range.start,
4166
                    binding_size: cached_effect_properties.range.len() as u32,
4167
                });
4168

4169
                // Write properties for this effect if they were modified.
4170
                // FIXME - This doesn't work with batching!
4171
                if let Some(property_data) = &extracted_effect.property_data {
×
4172
                    trace!(
4173
                    "Properties changed; (re-)uploading to GPU... New data: {} bytes. Capacity: {} bytes.",
×
UNCOV
4174
                    property_data.len(),
×
UNCOV
4175
                    cached_effect_properties.range.len(),
×
4176
                );
UNCOV
4177
                    if property_data.len() <= cached_effect_properties.range.len() {
×
4178
                        let property_buffer = property_cache.buffers_mut()
×
4179
                            [cached_effect_properties.buffer_index as usize]
×
4180
                            .as_mut()
4181
                            .unwrap();
4182
                        property_buffer.write(cached_effect_properties.range.start, property_data);
×
4183
                    } else {
UNCOV
4184
                        error!(
×
UNCOV
4185
                            "Cannot upload properties: existing property slice in property buffer #{} is too small ({} bytes) for the new data ({} bytes).",
×
4186
                            cached_effect_properties.buffer_index,
UNCOV
4187
                            cached_effect_properties.range.len(),
×
UNCOV
4188
                            property_data.len()
×
4189
                        );
4190
                    }
4191
                }
4192
            }
4193
        } else {
4194
            // No property on the effect; remove the component
4195
            trace!(
1,004✔
4196
                "No CachedEffectProperties on render entity {:?}, remove any CachedProperties component too.",
1,004✔
4197
                extracted_effect.render_entity.id()
2,008✔
4198
            );
4199
            cmd.remove::<CachedProperties>();
2,008✔
4200
        }
4201

4202
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4203
        // update its GpuEffectMetadata with all those infos.
4204
        // FIXME - should do this only when the below changes (not only the mesh), via
4205
        // some invalidation mechanism and ECS change detection.
4206
        if effects_meta.prepared_effects.contains(main_entity)
4207
            && !cached_mesh.is_changed()
1,012✔
4208
            && !cached_mesh_location.is_changed()
1,012✔
4209
        {
4210
            prepared_effect_count += 1;
1,012✔
4211
            continue;
4212
        }
4213

4214
        // Update the draw indirect args.
4215
        if cached_mesh_location.is_changed() {
4216
            let gpu_draw_args = GpuDrawIndexedIndirectArgs {
4217
                index_count: cached_mesh_location.vertex_or_index_count,
4✔
4218
                instance_count: 0,
4219
                first_index: cached_mesh_location.first_index_or_vertex_offset,
2✔
4220
                base_vertex: cached_mesh_location.vertex_offset_or_base_instance,
2✔
4221
                first_instance: 0,
4222
            };
4223
            assert!(dispatch_buffer_indices
4✔
4224
                .draw_indirect_buffer_row_index
2✔
4225
                .is_valid());
2✔
4226
            effects_meta.draw_indirect_buffer.update(
4✔
4227
                dispatch_buffer_indices.draw_indirect_buffer_row_index.get(),
4✔
4228
                gpu_draw_args,
2✔
4229
            );
4230
        }
4231

4232
        let capacity = cached_effect.slice.len();
2✔
4233

4234
        // Global and local indices of this effect as a child of another (parent) effect
4235
        let (global_child_index, local_child_index) = cached_child_info
UNCOV
4236
            .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4237
            .unwrap_or_default();
4238

4239
        // Base index of all children of this (parent) effect
4240
        let base_child_index = cached_parent_info
UNCOV
4241
            .map(|cpi| {
×
UNCOV
4242
                debug_assert_eq!(
×
UNCOV
4243
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4244
                    0
4245
                );
UNCOV
4246
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4247
            })
4248
            .unwrap_or_default();
4249

4250
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
4251
        let sort_key_offset = extracted_effect
4252
            .particle_layout
4253
            .offset(Attribute::RIBBON_ID)
4254
            .unwrap_or_default()
4255
            / 4;
4256
        let sort_key2_offset = extracted_effect
4257
            .particle_layout
4258
            .offset(Attribute::AGE)
4259
            .unwrap_or_default()
4260
            / 4;
4261

4262
        let gpu_effect_metadata = GpuEffectMetadata {
4263
            capacity,
4264
            alive_count: 0,
4265
            max_update: 0,
4266
            max_spawn: capacity,
4267
            indirect_write_index: 0,
4268
            indirect_dispatch_index: dispatch_buffer_indices
4269
                .update_dispatch_indirect_buffer_row_index,
4270
            indirect_draw_index: dispatch_buffer_indices
4271
                .draw_indirect_buffer_row_index
4272
                .get()
4273
                .0,
4274
            init_indirect_dispatch_index: cached_effect_events
4275
                .map(|cee| cee.init_indirect_dispatch_index)
4276
                .unwrap_or_default(),
4277
            local_child_index,
4278
            global_child_index,
4279
            base_child_index,
4280
            particle_stride,
4281
            sort_key_offset,
4282
            sort_key2_offset,
4283
            ..default()
4284
        };
4285

4286
        assert!(dispatch_buffer_indices
4287
            .effect_metadata_buffer_table_id
4288
            .is_valid());
4289
        effects_meta.effect_metadata_buffer.update(
2✔
4290
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
4291
            gpu_effect_metadata,
4292
        );
4293

4294
        // Record that we prepared this entity.
4295
        effects_meta.prepared_effects.insert(*main_entity);
4296

4297
        // This triggers on all new spawns and annoys everyone; silence until we can at
4298
        // least warn only on non-first-spawn, and ideally split indirect data from that
4299
        // struct so we don't overwrite it and solve the issue.
4300
        debug!(
4301
            "Updated metadata entry {} for effect {:?}, this will reset it.",
2✔
4302
            dispatch_buffer_indices.effect_metadata_buffer_table_id.0, main_entity
4303
        );
4304

4305
        prepared_effect_count += 1;
4306
    }
4307
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
2,050✔
4308

4309
    // Once all EffectMetadata values are written, schedule a GPU upload
4310
    if effects_meta
4311
        .effect_metadata_buffer
4312
        .allocate_gpu(render_device, render_queue)
4313
    {
4314
        // All those bind groups use the buffer so need to be re-created
4315
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
4✔
4316
        effects_meta.indirect_metadata_bind_group = None;
4✔
4317
        effect_bind_groups.init_metadata_bind_groups.clear();
4✔
4318
        effect_bind_groups.update_metadata_bind_groups.clear();
4✔
4319
    }
4320

4321
    if effects_meta
4322
        .draw_indirect_buffer
4323
        .allocate_gpu(render_device, render_queue)
4324
    {
4325
        // All those bind groups use the buffer so need to be re-created
4326
        trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
4✔
4327
        effects_meta.update_sim_params_bind_group = None;
4✔
4328
        effects_meta.indirect_metadata_bind_group = None;
4✔
4329
    }
4330

4331
    // Write the entire spawner buffer for this frame, for all effects combined
4332
    assert_eq!(
4333
        prepared_effect_count,
4334
        effects_meta.spawner_buffer.len() as u32
4335
    );
4336
    if effects_meta
1,030✔
4337
        .spawner_buffer
1,030✔
4338
        .write_buffer(render_device, render_queue)
3,090✔
4339
    {
4340
        // All property bind groups use the spawner buffer, which was reallocate
4341
        effect_bind_groups.particle_buffers.clear();
6✔
4342
        property_bind_groups.clear(true);
4✔
4343
        effects_meta.indirect_spawner_bind_group = None;
2✔
4344
    }
4345

4346
    // Update simulation parameters
4347
    effects_meta.sim_params_uniforms.set(sim_params.into());
4,120✔
4348
    {
4349
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
3,090✔
4350
        gpu_sim_params.num_effects = prepared_effect_count;
1,030✔
4351

4352
        trace!(
1,030✔
4353
            "Simulation parameters: time={} delta_time={} virtual_time={} \
1,020✔
4354
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
1,020✔
4355
            gpu_sim_params.time,
4356
            gpu_sim_params.delta_time,
4357
            gpu_sim_params.virtual_time,
4358
            gpu_sim_params.virtual_delta_time,
4359
            gpu_sim_params.real_time,
4360
            gpu_sim_params.real_delta_time,
4361
            gpu_sim_params.num_effects,
4362
        );
4363
    }
4364
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
6,174✔
4365
    effects_meta
1,030✔
4366
        .sim_params_uniforms
1,030✔
4367
        .write_buffer(render_device, render_queue);
3,090✔
4368
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
6,183✔
4369
        // Buffer changed, invalidate bind groups
4370
        effects_meta.update_sim_params_bind_group = None;
9✔
4371
        effects_meta.indirect_sim_params_bind_group = None;
3✔
4372
    }
4373
}
4374

4375
pub(crate) fn batch_effects(
1,030✔
4376
    mut commands: Commands,
4377
    effects_meta: Res<EffectsMeta>,
4378
    mut sort_bind_groups: ResMut<SortBindGroups>,
4379
    mut q_cached_effects: Query<(
4380
        Entity,
4381
        &MainEntity,
4382
        &CachedMesh,
4383
        Option<&CachedEffectEvents>,
4384
        Option<&CachedChildInfo>,
4385
        Option<&CachedProperties>,
4386
        &mut DispatchBufferIndices,
4387
        &mut BatchInput,
4388
    )>,
4389
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4390
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4391
) {
4392
    trace!("batch_effects");
2,050✔
4393

4394
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4395
    // inside that buffer. This is critical for batching to work, because
4396
    // batching effects is based on compatible items, which implies same GPU
4397
    // buffer and continuous slice ranges (the next slice start must be equal to
4398
    // the previous start end, without gap). EffectSlice already contains both
4399
    // information, and the proper ordering implementation.
4400
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4401

4402
    // For now we re-create that buffer each frame. Since there's no CPU -> GPU
4403
    // transfer, this is pretty cheap in practice.
4404
    sort_bind_groups.clear_indirect_dispatch_buffer();
1,030✔
4405

4406
    let mut sort_queue = GpuBufferOperationQueue::new();
2,060✔
4407

4408
    // Loop on all extracted effects in order, and try to batch them together to
4409
    // reduce draw calls. -- currently does nothing, batching was broken and never
4410
    // fixed.
4411
    // FIXME - This is in ECS order, if we re-add the sorting above we need a
4412
    // different order here!
4413
    trace!("Batching {} effects...", q_cached_effects.iter().len());
4,090✔
4414
    sorted_effect_batches.clear();
1,030✔
4415
    for (
4416
        entity,
1,014✔
4417
        main_entity,
1,014✔
4418
        cached_mesh,
1,014✔
4419
        cached_effect_events,
1,014✔
4420
        cached_child_info,
1,014✔
4421
        cached_properties,
1,014✔
4422
        dispatch_buffer_indices,
1,014✔
4423
        mut input,
1,014✔
4424
    ) in &mut q_cached_effects
2,044✔
4425
    {
4426
        // Detect if this cached effect was not updated this frame by a new extracted
4427
        // effect. This happens when e.g. the effect is invisible and not simulated, or
4428
        // some error prevented it from being extracted. We use the pipeline IDs vector
4429
        // as a marker, because each frame we move it out of the CachedGroup
4430
        // component during batching, so if empty this means a new one was not created
4431
        // this frame.
4432
        // if input.init_and_update_pipeline_ids.is_empty() {
4433
        //     trace!(
4434
        //         "Skipped cached effect on render entity {:?}: not extracted this
4435
        // frame.",         entity
4436
        //     );
4437
        //     continue;
4438
        // }
4439

4440
        let translation = input.position;
2,028✔
4441

4442
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4443
        // most of the data needed to drive rendering. However this doesn't drive
4444
        // rendering; this is just storage.
4445
        let mut effect_batch = EffectBatch::from_input(
4446
            cached_mesh,
1,014✔
4447
            cached_effect_events,
1,014✔
4448
            cached_child_info,
1,014✔
4449
            &mut input,
1,014✔
4450
            *dispatch_buffer_indices.as_ref(),
1,014✔
4451
            cached_properties.map(|cp| PropertyBindGroupKey {
2,028✔
4452
                buffer_index: cp.buffer_index,
9✔
4453
                binding_size: cp.binding_size,
9✔
4454
            }),
4455
            cached_properties.map(|cp| cp.offset),
2,028✔
4456
        );
4457

4458
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4459
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4460
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4461
        // ribbon).
4462
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
2,028✔
4463
            // This buffer is allocated in prepare_effects(), so should always be available
UNCOV
4464
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
UNCOV
4465
                error!("Failed to find effect metadata buffer. This is a bug.");
×
UNCOV
4466
                continue;
×
4467
            };
4468

4469
            // Allocate a GpuDispatchIndirect entry
4470
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4471
            effect_batch.sort_fill_indirect_dispatch_index =
4472
                Some(sort_fill_indirect_dispatch_index);
4473

4474
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4475
            // compute a number of workgroups to dispatch based on that particle count, and
4476
            // store the result into a GpuDispatchIndirect struct which will be used to
4477
            // dispatch the fill-sort pass.
4478
            {
4479
                let src_buffer = effect_metadata_buffer.clone();
4480
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4481
                    effect_batch
4482
                        .dispatch_buffer_indices
4483
                        .effect_metadata_buffer_table_id,
4484
                );
4485
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4486
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
UNCOV
4487
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
UNCOV
4488
                    continue;
×
4489
                };
4490
                let dst_buffer = dst_buffer.clone();
4491
                let dst_binding_offset = 0; // see dst_offset below
4492
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4493
                trace!(
UNCOV
4494
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
UNCOV
4495
                    src_buffer.id(),
×
4496
                    src_binding_offset,
UNCOV
4497
                    src_binding_size.get(),
×
UNCOV
4498
                    dst_buffer.id(),
×
4499
                    dst_binding_offset,
4500
                    -1, //dst_binding_size.get(),
4501
                );
4502
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4503
                debug_assert_eq!(
4504
                    src_offset, 1,
UNCOV
4505
                    "GpuEffectMetadata changed, update this assert."
×
4506
                );
4507
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4508
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4509
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4510
                // binding offset of 0 and binds the entire destination buffer,
4511
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4512
                // index the proper offset in the buffer. This requires of
4513
                // course binding the entire buffer, or at least enough to index all operations
4514
                // (hence the None below). This is not really a general solution, so should be
4515
                // reviewed.
UNCOV
4516
                let dst_offset = sort_bind_groups
×
4517
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
4518
                    / 4;
4519
                sort_queue.enqueue(
4520
                    GpuBufferOperationType::FillDispatchArgs,
4521
                    GpuBufferOperationArgs {
4522
                        src_offset,
4523
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
4524
                        dst_offset,
4525
                        dst_stride: GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4,
4526
                        count: 1,
4527
                    },
4528
                    src_buffer,
4529
                    src_binding_offset,
4530
                    Some(src_binding_size),
4531
                    dst_buffer,
4532
                    dst_binding_offset,
4533
                    None, //Some(dst_binding_size),
4534
                );
4535
            }
4536
        }
4537

4538
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
1,014✔
4539
        trace!(
4540
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
1,014✔
4541
            effect_batch_index,
4542
            entity,
4543
        );
4544

4545
        // Spawn an EffectDrawBatch, to actually drive rendering.
4546
        commands
4547
            .spawn(EffectDrawBatch {
4548
                effect_batch_index,
4549
                translation,
4550
                main_entity: *main_entity,
4551
            })
4552
            .insert(TemporaryRenderEntity);
4553
    }
4554

4555
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
1,030✔
4556
    if !sort_queue.operation_queue.is_empty() {
1,030✔
UNCOV
4557
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4558
    }
4559

4560
    sorted_effect_batches.sort();
1,030✔
4561
}
4562

4563
/// Per-buffer bind groups for a GPU effect buffer.
4564
///
4565
/// This contains all bind groups specific to a single [`EffectBuffer`].
4566
///
4567
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4568
pub(crate) struct BufferBindGroups {
4569
    /// Bind group for the render shader.
4570
    ///
4571
    /// ```wgsl
4572
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4573
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4574
    /// @binding(2) var<storage, read> spawner : Spawner;
4575
    /// ```
4576
    render: BindGroup,
4577
    // /// Bind group for filling the indirect dispatch arguments of any child init
4578
    // /// pass.
4579
    // ///
4580
    // /// This bind group is optional; it's only created if the current effect has
4581
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4582
    // /// (although normally the event buffer is not created if there's no
4583
    // /// children).
4584
    // ///
4585
    // /// The source buffer is always the current effect's event buffer. The
4586
    // /// destination buffer is the global shared buffer for indirect fill args
4587
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4588
    // /// args contains the data to index the relevant part of the global shared
4589
    // /// buffer for this effect buffer; it may contain multiple entries in case
4590
    // /// multiple effects are batched inside the current effect buffer.
4591
    // ///
4592
    // /// ```wgsl
4593
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4594
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4595
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4596
    // /// ```
4597
    // init_fill_dispatch: Option<BindGroup>,
4598
}
4599

4600
/// Combination of a texture layout and the bound textures.
4601
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4602
struct Material {
4603
    layout: TextureLayout,
4604
    textures: Vec<AssetId<Image>>,
4605
}
4606

4607
impl Material {
4608
    /// Get the bind group entries to create a bind group.
4609
    pub fn make_entries<'a>(
×
4610
        &self,
4611
        gpu_images: &'a RenderAssets<GpuImage>,
4612
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4613
        if self.textures.is_empty() {
×
4614
            return Ok(vec![]);
×
4615
        }
4616

4617
        let entries: Vec<BindGroupEntry<'a>> = self
×
4618
            .textures
×
4619
            .iter()
4620
            .enumerate()
UNCOV
4621
            .flat_map(|(index, id)| {
×
UNCOV
4622
                let base_binding = index as u32 * 2;
×
4623
                if let Some(gpu_image) = gpu_images.get(*id) {
×
UNCOV
4624
                    vec![
×
UNCOV
4625
                        BindGroupEntry {
×
UNCOV
4626
                            binding: base_binding,
×
4627
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4628
                        },
UNCOV
4629
                        BindGroupEntry {
×
4630
                            binding: base_binding + 1,
×
UNCOV
4631
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4632
                        },
4633
                    ]
4634
                } else {
UNCOV
4635
                    vec![]
×
4636
                }
4637
            })
4638
            .collect();
UNCOV
4639
        if entries.len() == self.textures.len() * 2 {
×
UNCOV
4640
            return Ok(entries);
×
4641
        }
4642
        Err(())
×
4643
    }
4644
}
4645

4646
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4647
struct BindingKey {
4648
    pub buffer_id: BufferId,
4649
    pub offset: u32,
4650
    pub size: NonZeroU32,
4651
}
4652

4653
impl<'a> From<BufferSlice<'a>> for BindingKey {
4654
    fn from(value: BufferSlice<'a>) -> Self {
×
4655
        Self {
4656
            buffer_id: value.buffer.id(),
×
UNCOV
4657
            offset: value.offset,
×
UNCOV
4658
            size: value.size,
×
4659
        }
4660
    }
4661
}
4662

4663
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4664
    fn from(value: &BufferSlice<'a>) -> Self {
×
4665
        Self {
4666
            buffer_id: value.buffer.id(),
×
UNCOV
4667
            offset: value.offset,
×
UNCOV
4668
            size: value.size,
×
4669
        }
4670
    }
4671
}
4672

4673
impl From<&BufferBindingSource> for BindingKey {
UNCOV
4674
    fn from(value: &BufferBindingSource) -> Self {
×
4675
        Self {
UNCOV
4676
            buffer_id: value.buffer.id(),
×
UNCOV
4677
            offset: value.offset,
×
4678
            size: value.size,
×
4679
        }
4680
    }
4681
}
4682

4683
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4684
struct ConsumeEventKey {
4685
    child_infos_buffer_id: BufferId,
4686
    events: BindingKey,
4687
}
4688

4689
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
UNCOV
4690
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4691
        Self {
UNCOV
4692
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
UNCOV
4693
            events: value.events.into(),
×
4694
        }
4695
    }
4696
}
4697

4698
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4699
struct InitMetadataBindGroupKey {
4700
    pub buffer_index: u32,
4701
    pub effect_metadata_buffer: BufferId,
4702
    pub effect_metadata_offset: u32,
4703
    pub consume_event_key: Option<ConsumeEventKey>,
4704
}
4705

4706
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4707
struct UpdateMetadataBindGroupKey {
4708
    pub buffer_index: u32,
4709
    pub effect_metadata_buffer: BufferId,
4710
    pub effect_metadata_offset: u32,
4711
    pub child_info_buffer_id: Option<BufferId>,
4712
    pub event_buffers_keys: Vec<BindingKey>,
4713
}
4714

4715
struct CachedBindGroup<K: Eq> {
4716
    /// Key the bind group was created from. Each time the key changes, the bind
4717
    /// group should be re-created.
4718
    key: K,
4719
    /// Bind group created from the key.
4720
    bind_group: BindGroup,
4721
}
4722

4723
#[derive(Debug, Clone, Copy)]
4724
struct BufferSlice<'a> {
4725
    pub buffer: &'a Buffer,
4726
    pub offset: u32,
4727
    pub size: NonZeroU32,
4728
}
4729

4730
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4731
    fn from(value: BufferSlice<'a>) -> Self {
×
4732
        Self {
4733
            buffer: value.buffer,
×
UNCOV
4734
            offset: value.offset.into(),
×
UNCOV
4735
            size: Some(value.size.into()),
×
4736
        }
4737
    }
4738
}
4739

4740
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4741
    fn from(value: &BufferSlice<'a>) -> Self {
×
4742
        Self {
4743
            buffer: value.buffer,
×
UNCOV
4744
            offset: value.offset.into(),
×
UNCOV
4745
            size: Some(value.size.into()),
×
4746
        }
4747
    }
4748
}
4749

4750
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
UNCOV
4751
    fn from(value: &'a BufferBindingSource) -> Self {
×
4752
        Self {
UNCOV
4753
            buffer: &value.buffer,
×
UNCOV
4754
            offset: value.offset,
×
UNCOV
4755
            size: value.size,
×
4756
        }
4757
    }
4758
}
4759

4760
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4761
/// the init pass consumes GPU events as a mechanism to spawn particles.
4762
struct ConsumeEventBuffers<'a> {
4763
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4764
    /// This is dynamically indexed inside the shader.
4765
    child_infos_buffer: &'a Buffer,
4766
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4767
    events: BufferSlice<'a>,
4768
}
4769

4770
#[derive(Default, Resource)]
4771
pub struct EffectBindGroups {
4772
    /// Map from buffer index to the bind groups shared among all effects that
4773
    /// use that buffer.
4774
    particle_buffers: HashMap<u32, BufferBindGroups>,
4775
    /// Map of bind groups for image assets used as particle textures.
4776
    images: HashMap<AssetId<Image>, BindGroup>,
4777
    /// Map from buffer index to its metadata bind group (group 3) for the init
4778
    /// pass.
4779
    // FIXME - doesn't work with batching; this should be the instance ID
4780
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4781
    /// Map from buffer index to its metadata bind group (group 3) for the
4782
    /// update pass.
4783
    // FIXME - doesn't work with batching; this should be the instance ID
4784
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4785
    /// Map from an effect material to its bind group.
4786
    material_bind_groups: HashMap<Material, BindGroup>,
4787
}
4788

4789
impl EffectBindGroups {
4790
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
1,013✔
4791
        self.particle_buffers
1,013✔
4792
            .get(&buffer_index)
2,026✔
4793
            .map(|bg| &bg.render)
1,013✔
4794
    }
4795

4796
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4797
    /// needed.
4798
    pub(self) fn get_or_create_init_metadata(
1,014✔
4799
        &mut self,
4800
        effect_batch: &EffectBatch,
4801
        gpu_limits: &GpuLimits,
4802
        render_device: &RenderDevice,
4803
        layout: &BindGroupLayout,
4804
        effect_metadata_buffer: &Buffer,
4805
        consume_event_buffers: Option<ConsumeEventBuffers>,
4806
    ) -> Result<&BindGroup, ()> {
4807
        let DispatchBufferIndices {
4808
            effect_metadata_buffer_table_id,
1,014✔
4809
            ..
4810
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4811

4812
        let effect_metadata_offset =
1,014✔
4813
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
2,028✔
4814
        let key = InitMetadataBindGroupKey {
4815
            buffer_index: effect_batch.buffer_index,
2,028✔
4816
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,042✔
4817
            effect_metadata_offset,
4818
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
2,028✔
4819
        };
4820

4821
        let make_entry = || {
1,016✔
4822
            let mut entries = Vec::with_capacity(3);
4✔
4823
            entries.push(
4✔
4824
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4825
                BindGroupEntry {
2✔
4826
                    binding: 0,
2✔
4827
                    resource: BindingResource::Buffer(BufferBinding {
2✔
4828
                        buffer: effect_metadata_buffer,
4✔
4829
                        offset: key.effect_metadata_offset as u64,
4✔
4830
                        size: Some(gpu_limits.effect_metadata_size()),
2✔
4831
                    }),
4832
                },
4833
            );
4834
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
4835
                entries.push(
4836
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4837
                    // ChildInfoBuffer;
4838
                    BindGroupEntry {
4839
                        binding: 1,
4840
                        resource: BindingResource::Buffer(BufferBinding {
4841
                            buffer: consume_event_buffers.child_infos_buffer,
4842
                            offset: 0,
4843
                            size: None,
4844
                        }),
4845
                    },
4846
                );
4847
                entries.push(
4848
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4849
                    BindGroupEntry {
4850
                        binding: 2,
4851
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4852
                    },
4853
                );
4854
            }
4855

4856
            let bind_group = render_device.create_bind_group(
6✔
4857
                "hanabi:bind_group:init:metadata@3",
4858
                layout,
2✔
4859
                &entries[..],
2✔
4860
            );
4861

4862
            trace!(
2✔
4863
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
4864
                    effect_batch.buffer_index,
4865
                    effect_metadata_buffer_table_id.0,
4866
                );
4867

4868
            bind_group
2✔
4869
        };
4870

4871
        Ok(&self
1,014✔
4872
            .init_metadata_bind_groups
1,014✔
4873
            .entry(effect_batch.buffer_index)
2,028✔
4874
            .and_modify(|cbg| {
2,026✔
4875
                if cbg.key != key {
1,012✔
UNCOV
4876
                    trace!(
×
UNCOV
4877
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4878
                        cbg.key,
4879
                        key
4880
                    );
UNCOV
4881
                    cbg.key = key;
×
UNCOV
4882
                    cbg.bind_group = make_entry();
×
4883
                }
4884
            })
4885
            .or_insert_with(|| {
1,016✔
4886
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
4887
                CachedBindGroup {
2✔
4888
                    key,
2✔
4889
                    bind_group: make_entry(),
2✔
4890
                }
4891
            })
4892
            .bind_group)
4893
    }
4894

4895
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4896
    /// needed.
4897
    pub(self) fn get_or_create_update_metadata(
1,014✔
4898
        &mut self,
4899
        effect_batch: &EffectBatch,
4900
        gpu_limits: &GpuLimits,
4901
        render_device: &RenderDevice,
4902
        layout: &BindGroupLayout,
4903
        effect_metadata_buffer: &Buffer,
4904
        child_info_buffer: Option<&Buffer>,
4905
        event_buffers: &[(Entity, BufferBindingSource)],
4906
    ) -> Result<&BindGroup, ()> {
4907
        let DispatchBufferIndices {
4908
            effect_metadata_buffer_table_id,
1,014✔
4909
            ..
4910
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4911

4912
        // Check arguments consistency
4913
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
5,070✔
4914
        let emits_gpu_spawn_events = !event_buffers.is_empty();
2,028✔
4915
        let child_info_buffer_id = if emits_gpu_spawn_events {
2,028✔
UNCOV
4916
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4917
        } else {
4918
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4919
            // if relevant, that is if the effect emits GPU spawn events.
4920
            None
1,014✔
4921
        };
4922
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
3,042✔
4923

4924
        let event_buffers_keys = event_buffers
2,028✔
4925
            .iter()
4926
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
1,014✔
4927
            .collect::<Vec<_>>();
4928

4929
        let key = UpdateMetadataBindGroupKey {
4930
            buffer_index: effect_batch.buffer_index,
2,028✔
4931
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,042✔
4932
            effect_metadata_offset: gpu_limits
3,042✔
4933
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4934
                as u32,
4935
            child_info_buffer_id,
4936
            event_buffers_keys,
4937
        };
4938

4939
        let make_entry = || {
1,016✔
4940
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
6✔
4941
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4942
            // EffectMetadata;
4943
            entries.push(BindGroupEntry {
6✔
4944
                binding: 0,
2✔
4945
                resource: BindingResource::Buffer(BufferBinding {
2✔
4946
                    buffer: effect_metadata_buffer,
4✔
4947
                    offset: key.effect_metadata_offset as u64,
4✔
4948
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
2✔
4949
                }),
4950
            });
4951
            if emits_gpu_spawn_events {
2✔
UNCOV
4952
                let child_info_buffer = child_info_buffer.unwrap();
×
4953

4954
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4955
                // ChildInfoBuffer;
UNCOV
4956
                entries.push(BindGroupEntry {
×
UNCOV
4957
                    binding: 1,
×
UNCOV
4958
                    resource: BindingResource::Buffer(BufferBinding {
×
UNCOV
4959
                        buffer: child_info_buffer,
×
UNCOV
4960
                        offset: 0,
×
UNCOV
4961
                        size: None,
×
4962
                    }),
4963
                });
4964

UNCOV
4965
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4966
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4967
                    // EventBuffer;
4968
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4969
                    // then moved to counting in bytes, so now need some conversion. Need to review
4970
                    // all of this...
4971
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4972
                    buffer_binding.offset *= 4;
4973
                    buffer_binding.size = buffer_binding
4974
                        .size
UNCOV
4975
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4976
                    entries.push(BindGroupEntry {
4977
                        binding: 2 + index as u32,
4978
                        resource: BindingResource::Buffer(buffer_binding),
4979
                    });
4980
                }
4981
            }
4982

4983
            let bind_group = render_device.create_bind_group(
6✔
4984
                "hanabi:bind_group:update:metadata@3",
4985
                layout,
2✔
4986
                &entries[..],
2✔
4987
            );
4988

4989
            trace!(
2✔
4990
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
2✔
4991
                effect_batch.buffer_index,
4992
                effect_metadata_buffer_table_id.0,
4993
            );
4994

4995
            bind_group
2✔
4996
        };
4997

4998
        Ok(&self
1,014✔
4999
            .update_metadata_bind_groups
1,014✔
5000
            .entry(effect_batch.buffer_index)
2,028✔
5001
            .and_modify(|cbg| {
2,026✔
5002
                if cbg.key != key {
1,012✔
UNCOV
5003
                    trace!(
×
UNCOV
5004
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
5005
                        cbg.key,
5006
                        key
5007
                    );
UNCOV
5008
                    cbg.key = key.clone();
×
UNCOV
5009
                    cbg.bind_group = make_entry();
×
5010
                }
5011
            })
5012
            .or_insert_with(|| {
1,016✔
5013
                trace!(
2✔
5014
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
5015
                    key
5016
                );
5017
                CachedBindGroup {
2✔
5018
                    key: key.clone(),
4✔
5019
                    bind_group: make_entry(),
2✔
5020
                }
5021
            })
5022
            .bind_group)
5023
    }
5024
}
5025

5026
#[derive(SystemParam)]
5027
pub struct QueueEffectsReadOnlyParams<'w, 's> {
5028
    #[cfg(feature = "2d")]
5029
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5030
    #[cfg(feature = "3d")]
5031
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5032
    #[cfg(feature = "3d")]
5033
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5034
    #[cfg(feature = "3d")]
5035
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5036
    marker: PhantomData<&'s usize>,
5037
}
5038

5039
fn emit_sorted_draw<T, F>(
2,028✔
5040
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5041
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5042
    view_entities: &mut FixedBitSet,
5043
    sorted_effect_batches: &SortedEffectBatches,
5044
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5045
    render_pipeline: &mut ParticlesRenderPipeline,
5046
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5047
    render_meshes: &RenderAssets<RenderMesh>,
5048
    pipeline_cache: &PipelineCache,
5049
    make_phase_item: F,
5050
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5051
) where
5052
    T: SortedPhaseItem,
5053
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5054
{
5055
    trace!("emit_sorted_draw() {} views", views.iter().len());
8,112✔
5056

5057
    for (visible_entities, view, msaa) in views.iter() {
6,084✔
UNCOV
5058
        trace!(
×
5059
            "Process new sorted view with {} visible particle effect entities",
2,028✔
5060
            visible_entities.len::<CompiledParticleEffect>()
4,056✔
5061
        );
5062

5063
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
1,014✔
5064
            continue;
1,014✔
5065
        };
5066

5067
        {
5068
            #[cfg(feature = "trace")]
5069
            let _span = bevy::log::info_span!("collect_view_entities").entered();
3,042✔
5070

5071
            view_entities.clear();
2,028✔
5072
            view_entities.extend(
2,028✔
5073
                visible_entities
1,014✔
5074
                    .iter::<EffectVisibilityClass>()
1,014✔
5075
                    .map(|e| e.1.index() as usize),
2,028✔
5076
            );
5077
        }
5078

5079
        // For each view, loop over all the effect batches to determine if the effect
5080
        // needs to be rendered for that view, and enqueue a view-dependent
5081
        // batch if so.
5082
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
3,042✔
5083
            #[cfg(feature = "trace")]
UNCOV
5084
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5085

5086
            trace!(
×
5087
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
1,014✔
5088
                draw_entity,
×
5089
                draw_batch.effect_batch_index,
×
5090
            );
5091

5092
            // Get the EffectBatches this EffectDrawBatch is part of.
5093
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
1,014✔
5094
            else {
×
5095
                continue;
×
5096
            };
5097

5098
            trace!(
×
5099
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
1,014✔
UNCOV
5100
                effect_batch.buffer_index,
×
UNCOV
5101
                effect_batch.spawner_base,
×
UNCOV
5102
                effect_batch.layout_flags,
×
5103
            );
5104

5105
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
UNCOV
5106
            if effect_batch
×
UNCOV
5107
                .layout_flags
×
UNCOV
5108
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5109
            {
5110
                trace!("Non-transparent batch. Skipped.");
×
5111
                continue;
×
5112
            }
5113

5114
            // Check if batch contains any entity visible in the current view. Otherwise we
5115
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5116
            // the Sprite renderer this is inspired from) we don't expect more than
5117
            // a handful of particle effect instances, so would rather not pay the memory
5118
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5119
            // TODO - Profile to confirm.
5120
            #[cfg(feature = "trace")]
UNCOV
5121
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
UNCOV
5122
            let has_visible_entity = effect_batch
×
UNCOV
5123
                .entities
×
5124
                .iter()
5125
                .any(|index| view_entities.contains(*index as usize));
3,042✔
UNCOV
5126
            if !has_visible_entity {
×
UNCOV
5127
                trace!("No visible entity for view, not emitting any draw call.");
×
UNCOV
5128
                continue;
×
5129
            }
5130
            #[cfg(feature = "trace")]
5131
            _span_check_vis.exit();
2,028✔
5132

5133
            // Create and cache the bind group layout for this texture layout
5134
            render_pipeline.cache_material(&effect_batch.texture_layout);
3,042✔
5135

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

5139
            let local_space_simulation = effect_batch
2,028✔
5140
                .layout_flags
1,014✔
5141
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
1,014✔
5142
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
3,042✔
5143
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
3,042✔
5144
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
3,042✔
5145
            let needs_normal = effect_batch
2,028✔
5146
                .layout_flags
1,014✔
5147
                .contains(LayoutFlags::NEEDS_NORMAL);
1,014✔
5148
            let needs_particle_fragment = effect_batch
2,028✔
5149
                .layout_flags
1,014✔
5150
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
1,014✔
5151
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
3,042✔
5152
            let image_count = effect_batch.texture_layout.layout.len() as u8;
2,028✔
5153

5154
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5155
            // re-querying here...?
5156
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
3,042✔
5157
                trace!("Batch has no render mesh, skipped.");
×
UNCOV
5158
                continue;
×
5159
            };
UNCOV
5160
            let mesh_layout = render_mesh.layout.clone();
×
5161

5162
            // Specialize the render pipeline based on the effect batch
5163
            trace!(
×
5164
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
1,014✔
UNCOV
5165
                effect_batch.render_shader,
×
5166
                image_count,
×
5167
                alpha_mask,
×
5168
                flipbook,
×
5169
                view.hdr
×
5170
            );
5171

5172
            // Add a draw pass for the effect batch
5173
            trace!("Emitting individual draw for batch");
1,014✔
5174

5175
            let alpha_mode = effect_batch.alpha_mode;
×
5176

5177
            #[cfg(feature = "trace")]
5178
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5179
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5180
                pipeline_cache,
×
5181
                render_pipeline,
×
5182
                ParticleRenderPipelineKey {
×
5183
                    shader: effect_batch.render_shader.clone(),
×
5184
                    mesh_layout: Some(mesh_layout),
×
5185
                    particle_layout: effect_batch.particle_layout.clone(),
×
5186
                    texture_layout: effect_batch.texture_layout.clone(),
×
UNCOV
5187
                    local_space_simulation,
×
UNCOV
5188
                    alpha_mask,
×
UNCOV
5189
                    alpha_mode,
×
5190
                    flipbook,
×
UNCOV
5191
                    needs_uv,
×
UNCOV
5192
                    needs_normal,
×
5193
                    needs_particle_fragment,
×
UNCOV
5194
                    ribbons,
×
UNCOV
5195
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5196
                    pipeline_mode,
×
5197
                    msaa_samples: msaa.samples(),
×
5198
                    hdr: view.hdr,
×
5199
                },
5200
            );
5201
            #[cfg(feature = "trace")]
5202
            _span_specialize.exit();
×
5203

5204
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
1,014✔
5205
            trace!(
×
5206
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
1,014✔
5207
                spawner_base={} handle={:?}",
1,014✔
UNCOV
5208
                draw_entity,
×
UNCOV
5209
                effect_batch.buffer_index,
×
UNCOV
5210
                effect_batch.spawner_base,
×
UNCOV
5211
                effect_batch.handle
×
5212
            );
UNCOV
5213
            render_phase.add(make_phase_item(
×
UNCOV
5214
                render_pipeline_id,
×
UNCOV
5215
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
UNCOV
5216
                draw_batch,
×
UNCOV
5217
                view,
×
5218
            ));
5219
        }
5220
    }
5221
}
5222

5223
#[cfg(feature = "3d")]
5224
fn emit_binned_draw<T, F, G>(
2,028✔
5225
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5226
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5227
    view_entities: &mut FixedBitSet,
5228
    sorted_effect_batches: &SortedEffectBatches,
5229
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5230
    render_pipeline: &mut ParticlesRenderPipeline,
5231
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5232
    pipeline_cache: &PipelineCache,
5233
    render_meshes: &RenderAssets<RenderMesh>,
5234
    make_batch_set_key: F,
5235
    make_bin_key: G,
5236
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5237
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5238
    change_tick: &mut Tick,
5239
) where
5240
    T: BinnedPhaseItem,
5241
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5242
    G: Fn() -> T::BinKey,
5243
{
5244
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5245

5246
    trace!("emit_binned_draw() {} views", views.iter().len());
8,112✔
5247

5248
    for (visible_entities, view, msaa) in views.iter() {
6,084✔
5249
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
2,028✔
5250

5251
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
2,028✔
UNCOV
5252
            continue;
×
5253
        };
5254

5255
        {
5256
            #[cfg(feature = "trace")]
5257
            let _span = bevy::log::info_span!("collect_view_entities").entered();
6,084✔
5258

5259
            view_entities.clear();
4,056✔
5260
            view_entities.extend(
4,056✔
5261
                visible_entities
2,028✔
5262
                    .iter::<EffectVisibilityClass>()
2,028✔
5263
                    .map(|e| e.1.index() as usize),
4,056✔
5264
            );
5265
        }
5266

5267
        // For each view, loop over all the effect batches to determine if the effect
5268
        // needs to be rendered for that view, and enqueue a view-dependent
5269
        // batch if so.
5270
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
6,084✔
5271
            #[cfg(feature = "trace")]
UNCOV
5272
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5273

5274
            trace!(
×
5275
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
2,028✔
5276
                draw_entity,
×
5277
                draw_batch.effect_batch_index,
×
5278
            );
5279

5280
            // Get the EffectBatches this EffectDrawBatch is part of.
5281
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
2,028✔
UNCOV
5282
            else {
×
UNCOV
5283
                continue;
×
5284
            };
5285

UNCOV
5286
            trace!(
×
5287
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
2,028✔
UNCOV
5288
                effect_batch.buffer_index,
×
UNCOV
5289
                effect_batch.spawner_base,
×
UNCOV
5290
                effect_batch.layout_flags,
×
5291
            );
5292

UNCOV
5293
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5294
                trace!(
2,028✔
5295
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
2,028✔
UNCOV
5296
                    effect_batch.layout_flags,
×
5297
                    alpha_mask
×
5298
                );
5299
                continue;
2,028✔
5300
            }
5301

5302
            // Check if batch contains any entity visible in the current view. Otherwise we
5303
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5304
            // the Sprite renderer this is inspired from) we don't expect more than
5305
            // a handful of particle effect instances, so would rather not pay the memory
5306
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5307
            // TODO - Profile to confirm.
5308
            #[cfg(feature = "trace")]
UNCOV
5309
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5310
            let has_visible_entity = effect_batch
×
UNCOV
5311
                .entities
×
5312
                .iter()
UNCOV
5313
                .any(|index| view_entities.contains(*index as usize));
×
UNCOV
5314
            if !has_visible_entity {
×
5315
                trace!("No visible entity for view, not emitting any draw call.");
×
5316
                continue;
×
5317
            }
5318
            #[cfg(feature = "trace")]
5319
            _span_check_vis.exit();
×
5320

5321
            // Create and cache the bind group layout for this texture layout
5322
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5323

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

5327
            let local_space_simulation = effect_batch
×
5328
                .layout_flags
×
5329
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
UNCOV
5330
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
UNCOV
5331
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5332
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5333
            let needs_normal = effect_batch
×
5334
                .layout_flags
×
5335
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5336
            let needs_particle_fragment = effect_batch
×
5337
                .layout_flags
×
5338
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
UNCOV
5339
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
UNCOV
5340
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
UNCOV
5341
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5342

5343
            // Specialize the render pipeline based on the effect batch
5344
            trace!(
×
UNCOV
5345
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5346
                effect_batch.render_shader,
×
5347
                image_count,
×
5348
                alpha_mask,
×
UNCOV
5349
                flipbook,
×
UNCOV
5350
                view.hdr
×
5351
            );
5352

5353
            // Add a draw pass for the effect batch
5354
            trace!("Emitting individual draw for batch");
×
5355

5356
            let alpha_mode = effect_batch.alpha_mode;
×
5357

5358
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5359
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5360
                continue;
×
5361
            };
5362

5363
            #[cfg(feature = "trace")]
5364
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5365
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5366
                pipeline_cache,
×
5367
                render_pipeline,
×
5368
                ParticleRenderPipelineKey {
×
5369
                    shader: effect_batch.render_shader.clone(),
×
5370
                    mesh_layout: Some(mesh_layout),
×
5371
                    particle_layout: effect_batch.particle_layout.clone(),
×
5372
                    texture_layout: effect_batch.texture_layout.clone(),
×
UNCOV
5373
                    local_space_simulation,
×
UNCOV
5374
                    alpha_mask,
×
UNCOV
5375
                    alpha_mode,
×
5376
                    flipbook,
×
UNCOV
5377
                    needs_uv,
×
5378
                    needs_normal,
×
5379
                    needs_particle_fragment,
×
5380
                    ribbons,
×
5381
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5382
                    pipeline_mode,
×
5383
                    msaa_samples: msaa.samples(),
×
5384
                    hdr: view.hdr,
×
5385
                },
5386
            );
5387
            #[cfg(feature = "trace")]
5388
            _span_specialize.exit();
×
5389

5390
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5391
            trace!(
×
5392
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5393
                spawner_base={} handle={:?}",
×
UNCOV
5394
                draw_entity,
×
UNCOV
5395
                effect_batch.buffer_index,
×
UNCOV
5396
                effect_batch.spawner_base,
×
UNCOV
5397
                effect_batch.handle
×
5398
            );
UNCOV
5399
            render_phase.add(
×
UNCOV
5400
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
UNCOV
5401
                make_bin_key(),
×
UNCOV
5402
                (draw_entity, draw_batch.main_entity),
×
UNCOV
5403
                InputUniformIndex::default(),
×
UNCOV
5404
                BinnedRenderPhaseType::NonMesh,
×
UNCOV
5405
                *change_tick,
×
5406
            );
5407
        }
5408
    }
5409
}
5410

5411
#[allow(clippy::too_many_arguments)]
5412
pub(crate) fn queue_effects(
1,030✔
5413
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5414
    effects_meta: Res<EffectsMeta>,
5415
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5416
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5417
    pipeline_cache: Res<PipelineCache>,
5418
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5419
    sorted_effect_batches: Res<SortedEffectBatches>,
5420
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5421
    events: Res<EffectAssetEvents>,
5422
    render_meshes: Res<RenderAssets<RenderMesh>>,
5423
    read_params: QueueEffectsReadOnlyParams,
5424
    mut view_entities: Local<FixedBitSet>,
5425
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5426
        ViewSortedRenderPhases<Transparent2d>,
5427
    >,
5428
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5429
        ViewSortedRenderPhases<Transparent3d>,
5430
    >,
5431
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5432
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5433
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5434
    ),
5435
    mut change_tick: Local<Tick>,
5436
) {
5437
    #[cfg(feature = "trace")]
5438
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
3,090✔
5439

5440
    trace!("queue_effects");
2,050✔
5441

5442
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5443
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5444
    // reuse stale data.
5445
    let next_change_tick = change_tick.get() + 1;
2,060✔
5446
    change_tick.set(next_change_tick);
2,060✔
5447

5448
    // If an image has changed, the GpuImage has (probably) changed
5449
    for event in &events.images {
1,057✔
5450
        match event {
5451
            AssetEvent::Added { .. } => None,
24✔
UNCOV
5452
            AssetEvent::LoadedWithDependencies { .. } => None,
×
UNCOV
5453
            AssetEvent::Unused { .. } => None,
×
UNCOV
5454
            AssetEvent::Modified { id } => {
×
UNCOV
5455
                trace!("Destroy bind group of modified image asset {:?}", id);
×
UNCOV
5456
                effect_bind_groups.images.remove(id)
×
5457
            }
5458
            AssetEvent::Removed { id } => {
3✔
5459
                trace!("Destroy bind group of removed image asset {:?}", id);
5✔
5460
                effect_bind_groups.images.remove(id)
9✔
5461
            }
5462
        };
5463
    }
5464

5465
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
3,080✔
5466
        // No spawners are active
5467
        return;
16✔
5468
    }
5469

5470
    // Loop over all 2D cameras/views that need to render effects
5471
    #[cfg(feature = "2d")]
5472
    {
5473
        #[cfg(feature = "trace")]
5474
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5475

5476
        let draw_effects_function_2d = read_params
5477
            .draw_functions_2d
5478
            .read()
5479
            .get_id::<DrawEffects>()
5480
            .unwrap();
5481

5482
        // Effects with full alpha blending
5483
        if !views.is_empty() {
5484
            trace!("Emit effect draw calls for alpha blended 2D views...");
2,028✔
5485
            emit_sorted_draw(
5486
                &views,
5487
                &mut transparent_2d_render_phases,
5488
                &mut view_entities,
5489
                &sorted_effect_batches,
5490
                &effect_draw_batches,
5491
                &mut render_pipeline,
5492
                specialized_render_pipelines.reborrow(),
5493
                &render_meshes,
5494
                &pipeline_cache,
5495
                |id, entity, draw_batch, _view| Transparent2d {
UNCOV
5496
                    sort_key: FloatOrd(draw_batch.translation.z),
×
UNCOV
5497
                    entity,
×
UNCOV
5498
                    pipeline: id,
×
UNCOV
5499
                    draw_function: draw_effects_function_2d,
×
UNCOV
5500
                    batch_range: 0..1,
×
5501
                    extracted_index: 0, // ???
UNCOV
5502
                    extra_index: PhaseItemExtraIndex::None,
×
5503
                    indexed: true, // ???
5504
                },
5505
                #[cfg(feature = "3d")]
5506
                PipelineMode::Camera2d,
5507
            );
5508
        }
5509
    }
5510

5511
    // Loop over all 3D cameras/views that need to render effects
5512
    #[cfg(feature = "3d")]
5513
    {
5514
        #[cfg(feature = "trace")]
5515
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5516

5517
        // Effects with full alpha blending
5518
        if !views.is_empty() {
5519
            trace!("Emit effect draw calls for alpha blended 3D views...");
2,028✔
5520

5521
            let draw_effects_function_3d = read_params
5522
                .draw_functions_3d
5523
                .read()
5524
                .get_id::<DrawEffects>()
5525
                .unwrap();
5526

5527
            emit_sorted_draw(
5528
                &views,
5529
                &mut transparent_3d_render_phases,
5530
                &mut view_entities,
5531
                &sorted_effect_batches,
5532
                &effect_draw_batches,
5533
                &mut render_pipeline,
5534
                specialized_render_pipelines.reborrow(),
5535
                &render_meshes,
5536
                &pipeline_cache,
5537
                |id, entity, batch, view| Transparent3d {
5538
                    distance: view
1,014✔
5539
                        .rangefinder3d()
1,014✔
5540
                        .distance_translation(&batch.translation),
2,028✔
5541
                    pipeline: id,
1,014✔
5542
                    entity,
1,014✔
5543
                    draw_function: draw_effects_function_3d,
1,014✔
5544
                    batch_range: 0..1,
1,014✔
5545
                    extra_index: PhaseItemExtraIndex::None,
1,014✔
5546
                    indexed: true, // ???
5547
                },
5548
                #[cfg(feature = "2d")]
5549
                PipelineMode::Camera3d,
5550
            );
5551
        }
5552

5553
        // Effects with alpha mask
5554
        if !views.is_empty() {
5555
            #[cfg(feature = "trace")]
5556
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
1,014✔
5557

5558
            trace!("Emit effect draw calls for alpha masked 3D views...");
1,014✔
5559

5560
            let draw_effects_function_alpha_mask = read_params
5561
                .draw_functions_alpha_mask
5562
                .read()
5563
                .get_id::<DrawEffects>()
5564
                .unwrap();
5565

5566
            emit_binned_draw(
5567
                &views,
5568
                &mut alpha_mask_3d_render_phases,
5569
                &mut view_entities,
5570
                &sorted_effect_batches,
5571
                &effect_draw_batches,
5572
                &mut render_pipeline,
5573
                specialized_render_pipelines.reborrow(),
5574
                &pipeline_cache,
5575
                &render_meshes,
5576
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
UNCOV
5577
                    pipeline: id,
×
UNCOV
5578
                    draw_function: draw_effects_function_alpha_mask,
×
UNCOV
5579
                    material_bind_group_index: None,
×
UNCOV
5580
                    vertex_slab: default(),
×
UNCOV
5581
                    index_slab: None,
×
5582
                },
5583
                // Unused for now
5584
                || OpaqueNoLightmap3dBinKey {
UNCOV
5585
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5586
                },
5587
                #[cfg(feature = "2d")]
5588
                PipelineMode::Camera3d,
5589
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5590
                &mut change_tick,
5591
            );
5592
        }
5593

5594
        // Opaque particles
5595
        if !views.is_empty() {
5596
            #[cfg(feature = "trace")]
5597
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
1,014✔
5598

5599
            trace!("Emit effect draw calls for opaque 3D views...");
1,014✔
5600

5601
            let draw_effects_function_opaque = read_params
5602
                .draw_functions_opaque
5603
                .read()
5604
                .get_id::<DrawEffects>()
5605
                .unwrap();
5606

5607
            emit_binned_draw(
5608
                &views,
5609
                &mut opaque_3d_render_phases,
5610
                &mut view_entities,
5611
                &sorted_effect_batches,
5612
                &effect_draw_batches,
5613
                &mut render_pipeline,
5614
                specialized_render_pipelines.reborrow(),
5615
                &pipeline_cache,
5616
                &render_meshes,
5617
                |id, _batch, _view| Opaque3dBatchSetKey {
UNCOV
5618
                    pipeline: id,
×
UNCOV
5619
                    draw_function: draw_effects_function_opaque,
×
UNCOV
5620
                    material_bind_group_index: None,
×
UNCOV
5621
                    vertex_slab: default(),
×
UNCOV
5622
                    index_slab: None,
×
UNCOV
5623
                    lightmap_slab: None,
×
5624
                },
5625
                // Unused for now
5626
                || Opaque3dBinKey {
UNCOV
5627
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5628
                },
5629
                #[cfg(feature = "2d")]
5630
                PipelineMode::Camera3d,
5631
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5632
                &mut change_tick,
5633
            );
5634
        }
5635
    }
5636
}
5637

5638
/// Prepare GPU resources for effect rendering.
5639
///
5640
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5641
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5642
/// access to the current camera view.
5643
pub(crate) fn prepare_gpu_resources(
1,030✔
5644
    mut effects_meta: ResMut<EffectsMeta>,
5645
    //mut effect_cache: ResMut<EffectCache>,
5646
    mut event_cache: ResMut<EventCache>,
5647
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5648
    mut sort_bind_groups: ResMut<SortBindGroups>,
5649
    render_device: Res<RenderDevice>,
5650
    render_queue: Res<RenderQueue>,
5651
    view_uniforms: Res<ViewUniforms>,
5652
    render_pipeline: Res<ParticlesRenderPipeline>,
5653
) {
5654
    // Get the binding for the ViewUniform, the uniform data structure containing
5655
    // the Camera data for the current view. If not available, we cannot render
5656
    // anything.
5657
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
2,060✔
UNCOV
5658
        return;
×
5659
    };
5660

5661
    // Create the bind group for the camera/view parameters
5662
    // FIXME - Not here!
5663
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5664
        "hanabi:bind_group_camera_view",
5665
        &render_pipeline.view_layout,
5666
        &[
5667
            BindGroupEntry {
5668
                binding: 0,
5669
                resource: view_binding,
5670
            },
5671
            BindGroupEntry {
5672
                binding: 1,
5673
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5674
            },
5675
        ],
5676
    ));
5677

5678
    // Re-/allocate any GPU buffer if needed
5679
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5680
    // effect_bind_groups);
5681
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5682
    sort_bind_groups.prepare_buffers(&render_device);
5683
    if effects_meta
5684
        .dispatch_indirect_buffer
5685
        .prepare_buffers(&render_device)
5686
    {
5687
        // All those bind groups use the buffer so need to be re-created
5688
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
4✔
5689
        effect_bind_groups.particle_buffers.clear();
4✔
5690
    }
5691
}
5692

5693
/// Read the queued init fill dispatch operations, batch them together by
5694
/// contiguous source and destination entries in the buffers, and enqueue
5695
/// corresponding GPU buffer fill dispatch operations for all batches.
5696
///
5697
/// This system runs after the GPU buffers have been (re-)allocated in
5698
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5699
/// reference them from the generic [`GpuBufferOperationQueue`].
5700
pub(crate) fn queue_init_fill_dispatch_ops(
1,030✔
5701
    event_cache: Res<EventCache>,
5702
    render_device: Res<RenderDevice>,
5703
    render_queue: Res<RenderQueue>,
5704
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5705
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5706
) {
5707
    // Submit all queued init fill dispatch operations with the proper buffers
5708
    if !init_fill_dispatch_queue.is_empty() {
1,030✔
UNCOV
5709
        let src_buffer = event_cache.child_infos().buffer();
×
5710
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
UNCOV
5711
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
5712
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
5713
        } else {
UNCOV
5714
            if src_buffer.is_none() {
×
UNCOV
5715
                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());
×
5716
            }
UNCOV
5717
            if dst_buffer.is_none() {
×
UNCOV
5718
                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());
×
5719
            }
5720
        }
5721
    }
5722

5723
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5724
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
3,090✔
5725
}
5726

5727
pub(crate) fn prepare_bind_groups(
1,030✔
5728
    mut effects_meta: ResMut<EffectsMeta>,
5729
    mut effect_cache: ResMut<EffectCache>,
5730
    mut event_cache: ResMut<EventCache>,
5731
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5732
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5733
    mut sort_bind_groups: ResMut<SortBindGroups>,
5734
    property_cache: Res<PropertyCache>,
5735
    sorted_effect_batched: Res<SortedEffectBatches>,
5736
    render_device: Res<RenderDevice>,
5737
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5738
    utils_pipeline: Res<UtilsPipeline>,
5739
    init_pipeline: Res<ParticlesInitPipeline>,
5740
    update_pipeline: Res<ParticlesUpdatePipeline>,
5741
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5742
    gpu_images: Res<RenderAssets<GpuImage>>,
5743
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5744
) {
5745
    // We can't simulate nor render anything without at least the spawner buffer
5746
    if effects_meta.spawner_buffer.is_empty() {
2,060✔
5747
        return;
16✔
5748
    }
5749
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
UNCOV
5750
        return;
×
5751
    };
5752

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

5758
    {
5759
        #[cfg(feature = "trace")]
5760
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
5761

5762
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5763
        // loop below. Also allows earlying out before doing any work in case some
5764
        // buffer is missing.
5765
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
UNCOV
5766
            return;
×
5767
        };
5768

5769
        // Create the sim_params@0 bind group for the global simulation parameters,
5770
        // which is shared by the init and update passes.
5771
        if effects_meta.update_sim_params_bind_group.is_none() {
5772
            if let Some(draw_indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() {
4✔
5773
                effects_meta.update_sim_params_bind_group = Some(render_device.create_bind_group(
5774
                    "hanabi:bind_group:vfx_update:sim_params@0",
5775
                    &update_pipeline.sim_params_layout,
5776
                    &[
5777
                        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
5778
                        BindGroupEntry {
5779
                            binding: 0,
5780
                            resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5781
                        },
5782
                        // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer : array<DrawIndexedIndirectArgs>;
5783
                        BindGroupEntry {
5784
                            binding: 1,
5785
                            resource: draw_indirect_buffer.as_entire_binding(),
5786
                        },
5787
                    ],
5788
                ));
5789
            } else {
UNCOV
5790
                debug!("Cannot allocate bind group for vfx_update:sim_params@0 - draw_indirect_buffer not ready");
×
5791
            }
5792
        }
5793
        if effects_meta.indirect_sim_params_bind_group.is_none() {
2✔
5794
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
8✔
5795
                "hanabi:bind_group:vfx_indirect:sim_params@0",
2✔
5796
                &init_pipeline.sim_params_layout, // FIXME - Shared with init
4✔
5797
                &[
2✔
5798
                    // @group(0) @binding(0) var<uniform> sim_params : SimParams;
5799
                    BindGroupEntry {
2✔
5800
                        binding: 0,
2✔
5801
                        resource: effects_meta.sim_params_uniforms.binding().unwrap(),
4✔
5802
                    },
5803
                ],
5804
            ));
5805
        }
5806

5807
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5808
        // effects at once
5809
        effects_meta.indirect_metadata_bind_group = match (
5810
            effects_meta.effect_metadata_buffer.buffer(),
5811
            effects_meta.dispatch_indirect_buffer.buffer(),
5812
            effects_meta.draw_indirect_buffer.buffer(),
5813
        ) {
5814
            (
5815
                Some(effect_metadata_buffer),
1,014✔
5816
                Some(dispatch_indirect_buffer),
5817
                Some(draw_indirect_buffer),
5818
            ) => {
5819
                // Base bind group for indirect pass
5820
                Some(render_device.create_bind_group(
5821
                    "hanabi:bind_group:vfx_indirect:metadata@1",
5822
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
5823
                    &[
5824
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer :
5825
                        // array<u32>;
5826
                        BindGroupEntry {
5827
                            binding: 0,
5828
                            resource: effect_metadata_buffer.as_entire_binding(),
5829
                        },
5830
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer
5831
                        // : array<u32>;
5832
                        BindGroupEntry {
5833
                            binding: 1,
5834
                            resource: dispatch_indirect_buffer.as_entire_binding(),
5835
                        },
5836
                        // @group(1) @binding(2) var<storage, read_write> draw_indirect_buffer :
5837
                        // array<u32>;
5838
                        BindGroupEntry {
5839
                            binding: 2,
5840
                            resource: draw_indirect_buffer.as_entire_binding(),
5841
                        },
5842
                    ],
5843
                ))
5844
            }
5845

5846
            // Some buffer is not yet available, can't create the bind group
UNCOV
5847
            _ => None,
×
5848
        };
5849

5850
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5851
        // effects at once
5852
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
5853
            let bind_group = render_device.create_bind_group(
10✔
5854
                "hanabi:bind_group:vfx_indirect:spawner@2",
5855
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
6✔
5856
                &[
4✔
5857
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5858
                    BindGroupEntry {
4✔
5859
                        binding: 0,
4✔
5860
                        resource: BindingResource::Buffer(BufferBinding {
4✔
5861
                            buffer: &spawner_buffer,
4✔
5862
                            offset: 0,
4✔
5863
                            size: None,
4✔
5864
                        }),
5865
                    },
5866
                ],
5867
            );
5868

5869
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
5870
        }
5871
    }
5872

5873
    // Create the per-buffer bind groups
5874
    trace!("Create per-buffer bind groups...");
1,014✔
5875
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
1,014✔
5876
        #[cfg(feature = "trace")]
5877
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
5878

5879
        let Some(effect_buffer) = effect_buffer else {
1,014✔
UNCOV
5880
            trace!(
×
UNCOV
5881
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5882
                buffer_index
5883
            );
UNCOV
5884
            continue;
×
5885
        };
5886

5887
        // Ensure all effects in this batch have a bind group for the entire buffer of
5888
        // the group, since the update phase runs on an entire group/buffer at once,
5889
        // with all the effect instances in it batched together.
5890
        trace!("effect particle buffer_index=#{}", buffer_index);
1,014✔
5891
        effect_bind_groups
5892
            .particle_buffers
5893
            .entry(buffer_index as u32)
5894
            .or_insert_with(|| {
2✔
5895
                // Bind group particle@1 for render pass
5896
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
4✔
5897
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
4✔
5898
                    render_device.limits().min_storage_buffer_offset_alignment,
2✔
5899
                );
5900
                let entries = [
4✔
5901
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5902
                    BindGroupEntry {
4✔
5903
                        binding: 0,
4✔
5904
                        resource: effect_buffer.max_binding(),
4✔
5905
                    },
5906
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5907
                    BindGroupEntry {
4✔
5908
                        binding: 1,
4✔
5909
                        resource: effect_buffer.indirect_index_max_binding(),
4✔
5910
                    },
5911
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5912
                    BindGroupEntry {
2✔
5913
                        binding: 2,
2✔
5914
                        resource: BindingResource::Buffer(BufferBinding {
2✔
5915
                            buffer: &spawner_buffer,
2✔
5916
                            offset: 0,
2✔
5917
                            size: Some(spawner_min_binding_size),
2✔
5918
                        }),
5919
                    },
5920
                ];
5921
                let render = render_device.create_bind_group(
8✔
5922
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
6✔
5923
                    effect_buffer.render_particles_buffer_layout(),
4✔
5924
                    &entries[..],
2✔
5925
                );
5926

5927
                BufferBindGroups { render }
2✔
5928
            });
5929
    }
5930

5931
    // Create bind groups for queued GPU buffer operations
5932
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5933

5934
    // Create the per-effect bind groups
5935
    let spawner_buffer_binding_size =
5936
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5937
    for effect_batch in sorted_effect_batched.iter() {
1,014✔
5938
        #[cfg(feature = "trace")]
5939
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
3,042✔
5940

5941
        // Create the property bind group @2 if needed
5942
        if let Some(property_key) = &effect_batch.property_key {
1,023✔
UNCOV
5943
            if let Err(err) = property_bind_groups.ensure_exists(
×
5944
                property_key,
5945
                &property_cache,
5946
                &spawner_buffer,
5947
                spawner_buffer_binding_size,
5948
                &render_device,
5949
            ) {
UNCOV
5950
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5951
                continue;
5952
            }
5953
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
3,015✔
5954
            &property_cache,
2,010✔
5955
            &spawner_buffer,
2,010✔
5956
            spawner_buffer_binding_size,
1,005✔
5957
            &render_device,
1,005✔
5958
        ) {
UNCOV
5959
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5960
            continue;
5961
        }
5962

5963
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5964
        // simulate particles.
5965
        if effect_cache
1,014✔
5966
            .create_particle_sim_bind_group(
5967
                effect_batch.buffer_index,
5968
                &render_device,
5969
                effect_batch.particle_layout.min_binding_size32(),
5970
                effect_batch.parent_min_binding_size,
5971
                effect_batch.parent_binding_source.as_ref(),
5972
            )
5973
            .is_err()
5974
        {
UNCOV
5975
            error!("No particle buffer allocated for effect batch.");
×
5976
            continue;
×
5977
        }
5978

5979
        // Bind group @3 of init pass
5980
        // FIXME - this is instance-dependent, not buffer-dependent
5981
        {
5982
            let consume_gpu_spawn_events = effect_batch
5983
                .layout_flags
5984
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
5985
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
1,014✔
5986
                effect_batch.spawn_info
5987
            {
UNCOV
5988
                assert!(consume_gpu_spawn_events);
×
UNCOV
5989
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
UNCOV
5990
                Some(ConsumeEventBuffers {
×
UNCOV
5991
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
UNCOV
5992
                    events: BufferSlice {
×
UNCOV
5993
                        buffer: event_cache
×
UNCOV
5994
                            .get_buffer(cached_effect_events.buffer_index)
×
UNCOV
5995
                            .unwrap(),
×
5996
                        // Note: event range is in u32 count, not bytes
UNCOV
5997
                        offset: cached_effect_events.range.start * 4,
×
UNCOV
5998
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5999
                    },
6000
                })
6001
            } else {
6002
                assert!(!consume_gpu_spawn_events);
2,028✔
6003
                None
1,014✔
6004
            };
6005
            let Some(init_metadata_layout) =
1,014✔
6006
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
6007
            else {
UNCOV
6008
                continue;
×
6009
            };
6010
            if effect_bind_groups
6011
                .get_or_create_init_metadata(
6012
                    effect_batch,
6013
                    &effects_meta.gpu_limits,
6014
                    &render_device,
6015
                    init_metadata_layout,
6016
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6017
                    consume_event_buffers,
6018
                )
6019
                .is_err()
6020
            {
6021
                continue;
×
6022
            }
6023
        }
6024

6025
        // Bind group @3 of update pass
6026
        // FIXME - this is instance-dependent, not buffer-dependent#
6027
        {
6028
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
6029

6030
            let Some(update_metadata_layout) =
1,014✔
6031
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
6032
            else {
UNCOV
6033
                continue;
×
6034
            };
6035
            if effect_bind_groups
6036
                .get_or_create_update_metadata(
6037
                    effect_batch,
6038
                    &effects_meta.gpu_limits,
6039
                    &render_device,
6040
                    update_metadata_layout,
6041
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6042
                    event_cache.child_infos_buffer(),
6043
                    &effect_batch.child_event_buffers[..],
6044
                )
6045
                .is_err()
6046
            {
6047
                continue;
×
6048
            }
6049
        }
6050

6051
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
UNCOV
6052
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
6053

6054
            // Bind group @0 of sort-fill pass
UNCOV
6055
            let particle_buffer = effect_buffer.particle_buffer();
×
UNCOV
6056
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
UNCOV
6057
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
UNCOV
6058
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
UNCOV
6059
                &effect_batch.particle_layout,
×
6060
                particle_buffer,
×
6061
                indirect_index_buffer,
×
6062
                effect_metadata_buffer,
×
6063
            ) {
6064
                error!(
6065
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
6066
                    err
6067
                );
6068
                continue;
6069
            }
6070

6071
            // Bind group @0 of sort-copy pass
UNCOV
6072
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
UNCOV
6073
            if let Err(err) = sort_bind_groups
×
UNCOV
6074
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
6075
            {
6076
                error!(
UNCOV
6077
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
6078
                    err
6079
                );
6080
                continue;
6081
            }
6082
        }
6083

6084
        // Ensure the particle texture(s) are available as GPU resources and that a bind
6085
        // group for them exists
6086
        // FIXME fix this insert+get below
6087
        if !effect_batch.texture_layout.layout.is_empty() {
1,014✔
6088
            // This should always be available, as this is cached into the render pipeline
6089
            // just before we start specializing it.
UNCOV
6090
            let Some(material_bind_group_layout) =
×
6091
                render_pipeline.get_material(&effect_batch.texture_layout)
×
6092
            else {
UNCOV
6093
                error!(
×
UNCOV
6094
                    "Failed to find material bind group layout for buffer #{}",
×
6095
                    effect_batch.buffer_index
6096
                );
6097
                continue;
×
6098
            };
6099

6100
            // TODO = move
6101
            let material = Material {
6102
                layout: effect_batch.texture_layout.clone(),
UNCOV
6103
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6104
            };
6105
            assert_eq!(material.layout.layout.len(), material.textures.len());
6106

6107
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6108
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6109
                trace!(
×
6110
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
6111
                    material
6112
                );
6113
                continue;
×
6114
            };
6115

6116
            effect_bind_groups
6117
                .material_bind_groups
6118
                .entry(material.clone())
UNCOV
6119
                .or_insert_with(|| {
×
UNCOV
6120
                    debug!("Creating material bind group for material {:?}", material);
×
UNCOV
6121
                    render_device.create_bind_group(
×
UNCOV
6122
                        &format!(
×
UNCOV
6123
                            "hanabi:material_bind_group_{}",
×
UNCOV
6124
                            material.layout.layout.len()
×
UNCOV
6125
                        )[..],
×
UNCOV
6126
                        material_bind_group_layout,
×
UNCOV
6127
                        &bind_group_entries[..],
×
6128
                    )
6129
                });
6130
        }
6131
    }
6132
}
6133

6134
type DrawEffectsSystemState = SystemState<(
6135
    SRes<EffectsMeta>,
6136
    SRes<EffectBindGroups>,
6137
    SRes<PipelineCache>,
6138
    SRes<RenderAssets<RenderMesh>>,
6139
    SRes<MeshAllocator>,
6140
    SQuery<Read<ViewUniformOffset>>,
6141
    SRes<SortedEffectBatches>,
6142
    SQuery<Read<EffectDrawBatch>>,
6143
)>;
6144

6145
/// Draw function for rendering all active effects for the current frame.
6146
///
6147
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6148
/// and the [`Transparent3d`] phase of the main 3D pass.
6149
pub(crate) struct DrawEffects {
6150
    params: DrawEffectsSystemState,
6151
}
6152

6153
impl DrawEffects {
6154
    pub fn new(world: &mut World) -> Self {
12✔
6155
        Self {
6156
            params: SystemState::new(world),
12✔
6157
        }
6158
    }
6159
}
6160

6161
/// Draw all particles of a single effect in view, in 2D or 3D.
6162
///
6163
/// FIXME: use pipeline ID to look up which group index it is.
6164
fn draw<'w>(
1,013✔
6165
    world: &'w World,
6166
    pass: &mut TrackedRenderPass<'w>,
6167
    view: Entity,
6168
    entity: (Entity, MainEntity),
6169
    pipeline_id: CachedRenderPipelineId,
6170
    params: &mut DrawEffectsSystemState,
6171
) {
UNCOV
6172
    let (
×
6173
        effects_meta,
1,013✔
6174
        effect_bind_groups,
1,013✔
6175
        pipeline_cache,
1,013✔
6176
        meshes,
1,013✔
6177
        mesh_allocator,
1,013✔
6178
        views,
1,013✔
6179
        sorted_effect_batches,
1,013✔
6180
        effect_draw_batches,
1,013✔
6181
    ) = params.get(world);
2,026✔
6182
    let view_uniform = views.get(view).unwrap();
5,065✔
6183
    let effects_meta = effects_meta.into_inner();
3,039✔
6184
    let effect_bind_groups = effect_bind_groups.into_inner();
3,039✔
6185
    let meshes = meshes.into_inner();
3,039✔
6186
    let mesh_allocator = mesh_allocator.into_inner();
3,039✔
6187
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
5,065✔
6188
    let effect_batch = sorted_effect_batches
3,039✔
6189
        .get(effect_draw_batch.effect_batch_index)
1,013✔
6190
        .unwrap();
6191

6192
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
3,039✔
UNCOV
6193
        return;
×
6194
    };
6195

6196
    trace!("render pass");
1,013✔
6197

6198
    pass.set_render_pipeline(pipeline);
×
6199

6200
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
1,013✔
6201
        return;
×
6202
    };
6203
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
1,013✔
6204
        return;
×
6205
    };
6206

6207
    // Vertex buffer containing the particle model to draw. Generally a quad.
6208
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6209
    // "base_vertex" in the indirect struct...
6210
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6211

6212
    // View properties (camera matrix, etc.)
UNCOV
6213
    pass.set_bind_group(
×
6214
        0,
UNCOV
6215
        effects_meta.view_bind_group.as_ref().unwrap(),
×
UNCOV
6216
        &[view_uniform.offset],
×
6217
    );
6218

6219
    // Particles buffer
UNCOV
6220
    let spawner_base = effect_batch.spawner_base;
×
UNCOV
6221
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
UNCOV
6222
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6223
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
2,026✔
6224
    pass.set_bind_group(
2,026✔
6225
        1,
6226
        effect_bind_groups
2,026✔
6227
            .particle_render(effect_batch.buffer_index)
2,026✔
6228
            .unwrap(),
1,013✔
6229
        &[spawner_offset],
1,013✔
6230
    );
6231

6232
    // Particle texture
6233
    // TODO = move
6234
    let material = Material {
6235
        layout: effect_batch.texture_layout.clone(),
2,026✔
6236
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
3,039✔
6237
    };
6238
    if !effect_batch.texture_layout.layout.is_empty() {
1,013✔
UNCOV
6239
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6240
            pass.set_bind_group(2, bind_group, &[]);
×
6241
        } else {
6242
            // Texture(s) not ready; skip this drawing for now
6243
            trace!(
×
6244
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
UNCOV
6245
                effect_batch.buffer_index,
×
6246
            );
UNCOV
6247
            return;
×
6248
        }
6249
    }
6250

6251
    let draw_indirect_index = effect_batch
1,013✔
6252
        .dispatch_buffer_indices
×
6253
        .draw_indirect_buffer_row_index
×
6254
        .get()
×
UNCOV
6255
        .0;
×
UNCOV
6256
    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
×
6257
    let draw_indirect_offset =
1,013✔
6258
        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
1,013✔
6259
    trace!(
1,013✔
6260
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
1,013✔
6261
            (effect_metadata_index={}, draw_indirect_offset={}B).",
1,013✔
6262
        effect_batch.slice.len(),
2,026✔
UNCOV
6263
        render_mesh.vertex_count,
×
UNCOV
6264
        effect_batch.buffer_index,
×
6265
        draw_indirect_index,
×
UNCOV
6266
        draw_indirect_offset,
×
6267
    );
6268

6269
    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
2,026✔
6270
        trace!(
×
6271
            "The draw indirect buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
UNCOV
6272
            effect_batch.buffer_index,
×
6273
        );
UNCOV
6274
        return;
×
6275
    };
6276

6277
    match render_mesh.buffer_info {
×
6278
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
1,013✔
6279
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
1,013✔
6280
            else {
×
UNCOV
6281
                trace!(
×
UNCOV
6282
                    "The index buffer for indexed rendering is not ready for batch buf=#{}. Skipping draw call.",
×
UNCOV
6283
                    effect_batch.buffer_index,
×
6284
                );
UNCOV
6285
                return;
×
6286
            };
6287

UNCOV
6288
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
UNCOV
6289
            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
×
6290
        }
UNCOV
6291
        RenderMeshBufferInfo::NonIndexed => {
×
UNCOV
6292
            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
×
6293
        }
6294
    }
6295
}
6296

6297
#[cfg(feature = "2d")]
6298
impl Draw<Transparent2d> for DrawEffects {
6299
    fn draw<'w>(
×
6300
        &mut self,
6301
        world: &'w World,
6302
        pass: &mut TrackedRenderPass<'w>,
6303
        view: Entity,
6304
        item: &Transparent2d,
6305
    ) -> Result<(), DrawError> {
UNCOV
6306
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6307
        draw(
UNCOV
6308
            world,
×
UNCOV
6309
            pass,
×
UNCOV
6310
            view,
×
UNCOV
6311
            item.entity,
×
UNCOV
6312
            item.pipeline,
×
UNCOV
6313
            &mut self.params,
×
6314
        );
UNCOV
6315
        Ok(())
×
6316
    }
6317
}
6318

6319
#[cfg(feature = "3d")]
6320
impl Draw<Transparent3d> for DrawEffects {
6321
    fn draw<'w>(
1,013✔
6322
        &mut self,
6323
        world: &'w World,
6324
        pass: &mut TrackedRenderPass<'w>,
6325
        view: Entity,
6326
        item: &Transparent3d,
6327
    ) -> Result<(), DrawError> {
6328
        trace!("Draw<Transparent3d>: view={:?}", view);
2,026✔
6329
        draw(
6330
            world,
1,013✔
6331
            pass,
1,013✔
6332
            view,
1,013✔
6333
            item.entity,
1,013✔
6334
            item.pipeline,
1,013✔
6335
            &mut self.params,
1,013✔
6336
        );
6337
        Ok(())
1,013✔
6338
    }
6339
}
6340

6341
#[cfg(feature = "3d")]
6342
impl Draw<AlphaMask3d> for DrawEffects {
6343
    fn draw<'w>(
×
6344
        &mut self,
6345
        world: &'w World,
6346
        pass: &mut TrackedRenderPass<'w>,
6347
        view: Entity,
6348
        item: &AlphaMask3d,
6349
    ) -> Result<(), DrawError> {
UNCOV
6350
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6351
        draw(
UNCOV
6352
            world,
×
6353
            pass,
×
UNCOV
6354
            view,
×
UNCOV
6355
            item.representative_entity,
×
UNCOV
6356
            item.batch_set_key.pipeline,
×
UNCOV
6357
            &mut self.params,
×
6358
        );
UNCOV
6359
        Ok(())
×
6360
    }
6361
}
6362

6363
#[cfg(feature = "3d")]
6364
impl Draw<Opaque3d> for DrawEffects {
6365
    fn draw<'w>(
×
6366
        &mut self,
6367
        world: &'w World,
6368
        pass: &mut TrackedRenderPass<'w>,
6369
        view: Entity,
6370
        item: &Opaque3d,
6371
    ) -> Result<(), DrawError> {
UNCOV
6372
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6373
        draw(
UNCOV
6374
            world,
×
UNCOV
6375
            pass,
×
UNCOV
6376
            view,
×
UNCOV
6377
            item.representative_entity,
×
UNCOV
6378
            item.batch_set_key.pipeline,
×
UNCOV
6379
            &mut self.params,
×
6380
        );
UNCOV
6381
        Ok(())
×
6382
    }
6383
}
6384

6385
/// Render node to run the simulation sub-graph once per frame.
6386
///
6387
/// This node doesn't simulate anything by itself, but instead schedules the
6388
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6389
/// actual simulation.
6390
///
6391
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6392
/// renders all the views, such that rendered views have access to the
6393
/// just-simulated particles to render them.
6394
///
6395
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6396
pub(crate) struct VfxSimulateDriverNode;
6397

6398
impl Node for VfxSimulateDriverNode {
6399
    fn run(
1,030✔
6400
        &self,
6401
        graph: &mut RenderGraphContext,
6402
        _render_context: &mut RenderContext,
6403
        _world: &World,
6404
    ) -> Result<(), NodeRunError> {
6405
        graph.run_sub_graph(
2,060✔
6406
            crate::plugin::simulate_graph::HanabiSimulateGraph,
1,030✔
6407
            vec![],
1,030✔
6408
            None,
1,030✔
6409
        )?;
6410
        Ok(())
1,030✔
6411
    }
6412
}
6413

6414
#[derive(Debug, Clone, PartialEq, Eq)]
6415
enum HanabiPipelineId {
6416
    Invalid,
6417
    Cached(CachedComputePipelineId),
6418
}
6419

6420
pub(crate) enum ComputePipelineError {
6421
    Queued,
6422
    Creating,
6423
    Error,
6424
}
6425

6426
impl From<&CachedPipelineState> for ComputePipelineError {
UNCOV
6427
    fn from(value: &CachedPipelineState) -> Self {
×
UNCOV
6428
        match value {
×
UNCOV
6429
            CachedPipelineState::Queued => Self::Queued,
×
UNCOV
6430
            CachedPipelineState::Creating(_) => Self::Creating,
×
UNCOV
6431
            CachedPipelineState::Err(_) => Self::Error,
×
UNCOV
6432
            _ => panic!("Trying to convert Ok state to error."),
×
6433
        }
6434
    }
6435
}
6436

6437
pub(crate) struct HanabiComputePass<'a> {
6438
    /// Pipeline cache to fetch cached compute pipelines by ID.
6439
    pipeline_cache: &'a PipelineCache,
6440
    /// WGPU compute pass.
6441
    compute_pass: ComputePass<'a>,
6442
    /// Current pipeline (cached).
6443
    pipeline_id: HanabiPipelineId,
6444
}
6445

6446
impl<'a> Deref for HanabiComputePass<'a> {
6447
    type Target = ComputePass<'a>;
6448

UNCOV
6449
    fn deref(&self) -> &Self::Target {
×
UNCOV
6450
        &self.compute_pass
×
6451
    }
6452
}
6453

6454
impl DerefMut for HanabiComputePass<'_> {
6455
    fn deref_mut(&mut self) -> &mut Self::Target {
14,140✔
6456
        &mut self.compute_pass
14,140✔
6457
    }
6458
}
6459

6460
impl<'a> HanabiComputePass<'a> {
6461
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
4,056✔
6462
        Self {
6463
            pipeline_cache,
6464
            compute_pass,
6465
            pipeline_id: HanabiPipelineId::Invalid,
6466
        }
6467
    }
6468

6469
    pub fn set_cached_compute_pipeline(
3,028✔
6470
        &mut self,
6471
        pipeline_id: CachedComputePipelineId,
6472
    ) -> Result<(), ComputePipelineError> {
6473
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
6,056✔
6474
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
3,028✔
6475
            trace!("-> already set; skipped");
×
UNCOV
6476
            return Ok(());
×
6477
        }
6478
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
3,028✔
6479
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6480
            if let CachedPipelineState::Err(err) = state {
×
6481
                error!(
×
UNCOV
6482
                    "Failed to find compute pipeline #{}: {:?}",
×
UNCOV
6483
                    pipeline_id.id(),
×
UNCOV
6484
                    err
×
6485
                );
6486
            } else {
UNCOV
6487
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6488
            }
UNCOV
6489
            return Err(state.into());
×
6490
        };
UNCOV
6491
        self.compute_pass.set_pipeline(pipeline);
×
UNCOV
6492
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
UNCOV
6493
        Ok(())
×
6494
    }
6495
}
6496

6497
/// Render node to run the simulation of all effects once per frame.
6498
///
6499
/// Runs inside the simulation sub-graph, looping over all extracted effect
6500
/// batches to simulate them.
6501
pub(crate) struct VfxSimulateNode {}
6502

6503
impl VfxSimulateNode {
6504
    /// Create a new node for simulating the effects of the given world.
6505
    pub fn new(_world: &mut World) -> Self {
3✔
6506
        Self {}
6507
    }
6508

6509
    /// Begin a new compute pass and return a wrapper with extra
6510
    /// functionalities.
6511
    pub fn begin_compute_pass<'encoder>(
4,056✔
6512
        &self,
6513
        label: &str,
6514
        pipeline_cache: &'encoder PipelineCache,
6515
        render_context: &'encoder mut RenderContext,
6516
    ) -> HanabiComputePass<'encoder> {
6517
        let compute_pass =
4,056✔
6518
            render_context
4,056✔
6519
                .command_encoder()
6520
                .begin_compute_pass(&ComputePassDescriptor {
8,112✔
6521
                    label: Some(label),
4,056✔
6522
                    timestamp_writes: None,
4,056✔
6523
                });
6524
        HanabiComputePass::new(pipeline_cache, compute_pass)
12,168✔
6525
    }
6526
}
6527

6528
impl Node for VfxSimulateNode {
6529
    fn input(&self) -> Vec<SlotInfo> {
3✔
6530
        vec![]
3✔
6531
    }
6532

6533
    fn update(&mut self, _world: &mut World) {}
2,060✔
6534

6535
    fn run(
1,030✔
6536
        &self,
6537
        _graph: &mut RenderGraphContext,
6538
        render_context: &mut RenderContext,
6539
        world: &World,
6540
    ) -> Result<(), NodeRunError> {
6541
        trace!("VfxSimulateNode::run()");
2,050✔
6542

6543
        let pipeline_cache = world.resource::<PipelineCache>();
3,090✔
6544
        let effects_meta = world.resource::<EffectsMeta>();
3,090✔
6545
        let effect_bind_groups = world.resource::<EffectBindGroups>();
3,090✔
6546
        let property_bind_groups = world.resource::<PropertyBindGroups>();
3,090✔
6547
        let sort_bind_groups = world.resource::<SortBindGroups>();
3,090✔
6548
        let utils_pipeline = world.resource::<UtilsPipeline>();
3,090✔
6549
        let effect_cache = world.resource::<EffectCache>();
3,090✔
6550
        let event_cache = world.resource::<EventCache>();
3,090✔
6551
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
3,090✔
6552
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
3,090✔
6553
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
3,090✔
6554

6555
        // Make sure to schedule any buffer copy before accessing their content later in
6556
        // the GPU commands below.
6557
        {
6558
            let command_encoder = render_context.command_encoder();
4,120✔
6559
            effects_meta
2,060✔
6560
                .dispatch_indirect_buffer
2,060✔
6561
                .write_buffers(command_encoder);
3,090✔
6562
            effects_meta
2,060✔
6563
                .draw_indirect_buffer
2,060✔
6564
                .write_buffer(command_encoder);
3,090✔
6565
            effects_meta
2,060✔
6566
                .effect_metadata_buffer
2,060✔
6567
                .write_buffer(command_encoder);
3,090✔
6568
            event_cache.write_buffers(command_encoder);
4,120✔
6569
            sort_bind_groups.write_buffers(command_encoder);
2,060✔
6570
        }
6571

6572
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6573
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6574
        // the update pass of their parent effect during the previous frame.
6575
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
1,030✔
6576
            gpu_buffer_operations.dispatch(
6577
                *queue_index,
6578
                render_context,
6579
                utils_pipeline,
6580
                Some("hanabi:init_indirect_fill_dispatch"),
6581
            );
6582
        }
6583

6584
        // If there's no batch, there's nothing more to do. Avoid continuing because
6585
        // some GPU resources are missing, which is expected when there's no effect but
6586
        // is an error (and will log warnings/errors) otherwise.
6587
        if sorted_effect_batches.is_empty() {
2,060✔
6588
            return Ok(());
16✔
6589
        }
6590

6591
        // Compute init pass
6592
        {
6593
            trace!("init: loop over effect batches...");
1,014✔
6594

6595
            let mut compute_pass =
6596
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6597

6598
            // Bind group simparams@0 is common to everything, only set once per init pass
6599
            compute_pass.set_bind_group(
6600
                0,
6601
                effects_meta
6602
                    .indirect_sim_params_bind_group
6603
                    .as_ref()
6604
                    .unwrap(),
6605
                &[],
6606
            );
6607

6608
            // Dispatch init compute jobs for all batches
6609
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6610
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6611
                // batch. Note that this hopefully should have been skipped earlier.
6612
                {
6613
                    let use_indirect_dispatch = effect_batch
2,028✔
6614
                        .layout_flags
1,014✔
6615
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
1,014✔
6616
                    match effect_batch.spawn_info {
1,014✔
6617
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
1,014✔
6618
                            assert!(!use_indirect_dispatch);
6619
                            if total_spawn_count == 0 {
1,014✔
6620
                                continue;
14✔
6621
                            }
6622
                        }
6623
                        BatchSpawnInfo::GpuSpawner { .. } => {
UNCOV
6624
                            assert!(use_indirect_dispatch);
×
6625
                        }
6626
                    }
6627
                }
6628

6629
                // Fetch bind group particle@1
6630
                let Some(particle_bind_group) =
1,000✔
6631
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
1,000✔
6632
                else {
6633
                    error!(
×
6634
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6635
                        effect_batch.buffer_index
6636
                    );
6637
                    continue;
×
6638
                };
6639

6640
                // Fetch bind group metadata@3
6641
                let Some(metadata_bind_group) = effect_bind_groups
1,000✔
6642
                    .init_metadata_bind_groups
6643
                    .get(&effect_batch.buffer_index)
6644
                else {
UNCOV
6645
                    error!(
×
UNCOV
6646
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6647
                        effect_batch.buffer_index
6648
                    );
UNCOV
6649
                    continue;
×
6650
                };
6651

6652
                if compute_pass
6653
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6654
                    .is_err()
6655
                {
UNCOV
6656
                    continue;
×
6657
                }
6658

6659
                // Compute dynamic offsets
6660
                let spawner_base = effect_batch.spawner_base;
6661
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6662
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6663
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
2,000✔
6664
                let property_offset = effect_batch.property_offset;
2,000✔
6665

6666
                // Setup init pass
6667
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
3,000✔
6668
                let offsets = if let Some(property_offset) = property_offset {
2,000✔
6669
                    vec![spawner_offset, property_offset]
6670
                } else {
6671
                    vec![spawner_offset]
2,000✔
6672
                };
6673
                compute_pass.set_bind_group(
3,000✔
6674
                    2,
6675
                    property_bind_groups
2,000✔
6676
                        .get(effect_batch.property_key.as_ref())
4,000✔
6677
                        .unwrap(),
2,000✔
6678
                    &offsets[..],
1,000✔
6679
                );
6680
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
3,000✔
6681

6682
                // Dispatch init job
6683
                match effect_batch.spawn_info {
1,000✔
6684
                    // Indirect dispatch via GPU spawn events
6685
                    BatchSpawnInfo::GpuSpawner {
6686
                        init_indirect_dispatch_index,
×
6687
                        ..
6688
                    } => {
6689
                        assert!(effect_batch
×
6690
                            .layout_flags
×
6691
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6692

6693
                        // Note: the indirect offset of a dispatch workgroup only needs
6694
                        // 4-byte alignment
UNCOV
6695
                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
×
UNCOV
6696
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6697

UNCOV
6698
                        trace!(
×
UNCOV
6699
                            "record commands for indirect init pipeline of effect {:?} \
×
UNCOV
6700
                                init_indirect_dispatch_index={} \
×
6701
                                indirect_offset={} \
×
6702
                                spawner_base={} \
×
6703
                                spawner_offset={} \
×
UNCOV
6704
                                property_key={:?}...",
×
6705
                            effect_batch.handle,
6706
                            init_indirect_dispatch_index,
6707
                            indirect_offset,
6708
                            spawner_base,
6709
                            spawner_offset,
6710
                            effect_batch.property_key,
6711
                        );
6712

UNCOV
6713
                        compute_pass.dispatch_workgroups_indirect(
×
UNCOV
6714
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
UNCOV
6715
                            indirect_offset,
×
6716
                        );
6717
                    }
6718

6719
                    // Direct dispatch via CPU spawn count
6720
                    BatchSpawnInfo::CpuSpawner {
6721
                        total_spawn_count: spawn_count,
1,000✔
6722
                    } => {
6723
                        assert!(!effect_batch
6724
                            .layout_flags
6725
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
6726

6727
                        const WORKGROUP_SIZE: u32 = 64;
6728
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
1,000✔
6729

6730
                        trace!(
6731
                            "record commands for init pipeline of effect {:?} \
1,000✔
6732
                                (spawn {} particles => {} workgroups) spawner_base={} \
1,000✔
6733
                                spawner_offset={} \
1,000✔
6734
                                property_key={:?}...",
1,000✔
6735
                            effect_batch.handle,
6736
                            spawn_count,
6737
                            workgroup_count,
6738
                            spawner_base,
6739
                            spawner_offset,
6740
                            effect_batch.property_key,
6741
                        );
6742

6743
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6744
                    }
6745
                }
6746

6747
                trace!("init compute dispatched");
2,000✔
6748
            }
6749
        }
6750

6751
        // Compute indirect dispatch pass
6752
        if effects_meta.spawner_buffer.buffer().is_some()
1,014✔
6753
            && !effects_meta.spawner_buffer.is_empty()
1,014✔
6754
            && effects_meta.indirect_metadata_bind_group.is_some()
1,014✔
6755
            && effects_meta.indirect_sim_params_bind_group.is_some()
2,028✔
6756
        {
6757
            // Only start a compute pass if there's an effect; makes things clearer in
6758
            // debugger.
6759
            let mut compute_pass =
1,014✔
6760
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
5,070✔
6761

6762
            // Dispatch indirect dispatch compute job
6763
            trace!("record commands for indirect dispatch pipeline...");
2,028✔
6764

6765
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
2,028✔
6766
            if has_gpu_spawn_events {
1,014✔
UNCOV
6767
                if let Some(indirect_child_info_buffer_bind_group) =
×
UNCOV
6768
                    event_cache.indirect_child_info_buffer_bind_group()
×
6769
                {
6770
                    assert!(has_gpu_spawn_events);
UNCOV
6771
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6772
                } else {
UNCOV
6773
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6774
                    // render_context
6775
                    //     .command_encoder()
6776
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6777
                    // FIXME - Bevy doesn't allow returning custom errors here...
UNCOV
6778
                    return Ok(());
×
6779
                }
6780
            }
6781

6782
            if compute_pass
1,014✔
6783
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
6784
                .is_err()
6785
            {
6786
                // FIXME - Bevy doesn't allow returning custom errors here...
UNCOV
6787
                return Ok(());
×
6788
            }
6789

6790
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6791
            // the size exluding gaps!");
6792
            const WORKGROUP_SIZE: u32 = 64;
6793
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6794
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6795
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6796

6797
            // Setup vfx_indirect pass
6798
            compute_pass.set_bind_group(
6799
                0,
6800
                effects_meta
6801
                    .indirect_sim_params_bind_group
6802
                    .as_ref()
6803
                    .unwrap(),
6804
                &[],
6805
            );
6806
            compute_pass.set_bind_group(
6807
                1,
6808
                // FIXME - got some unwrap() panic here, investigate... possibly race
6809
                // condition!
6810
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6811
                &[],
6812
            );
6813
            compute_pass.set_bind_group(
6814
                2,
6815
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6816
                &[],
6817
            );
6818
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6819
            trace!(
6820
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
1,014✔
6821
                total_effect_count,
6822
                workgroup_count
6823
            );
6824
        }
6825

6826
        // Compute update pass
6827
        {
6828
            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
2,028✔
UNCOV
6829
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
UNCOV
6830
                render_context
×
6831
                    .command_encoder()
6832
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6833
                // FIXME - Bevy doesn't allow returning custom errors here...
UNCOV
6834
                return Ok(());
×
6835
            };
6836

6837
            let mut compute_pass =
6838
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6839

6840
            // Bind group simparams@0 is common to everything, only set once per update pass
6841
            compute_pass.set_bind_group(
6842
                0,
6843
                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
6844
                &[],
6845
            );
6846

6847
            // Dispatch update compute jobs
6848
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6849
                // Fetch bind group particle@1
6850
                let Some(particle_bind_group) =
1,014✔
6851
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
2,028✔
6852
                else {
6853
                    error!(
×
6854
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6855
                        effect_batch.buffer_index
6856
                    );
6857
                    continue;
×
6858
                };
6859

6860
                // Fetch bind group metadata@3
6861
                let Some(metadata_bind_group) = effect_bind_groups
1,014✔
6862
                    .update_metadata_bind_groups
6863
                    .get(&effect_batch.buffer_index)
6864
                else {
6865
                    error!(
×
UNCOV
6866
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6867
                        effect_batch.buffer_index
6868
                    );
UNCOV
6869
                    continue;
×
6870
                };
6871

6872
                // Fetch compute pipeline
6873
                if compute_pass
6874
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6875
                    .is_err()
6876
                {
UNCOV
6877
                    continue;
×
6878
                }
6879

6880
                // Compute dynamic offsets
6881
                let spawner_index = effect_batch.spawner_base;
6882
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6883
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6884
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
1,014✔
6885
                let property_offset = effect_batch.property_offset;
6886

6887
                trace!(
6888
                    "record commands for update pipeline of effect {:?} spawner_base={}",
1,014✔
6889
                    effect_batch.handle,
6890
                    spawner_index,
6891
                );
6892

6893
                // Setup update pass
6894
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6895
                let offsets = if let Some(property_offset) = property_offset {
9✔
6896
                    vec![spawner_offset, property_offset]
6897
                } else {
6898
                    vec![spawner_offset]
2,010✔
6899
                };
6900
                compute_pass.set_bind_group(
6901
                    2,
6902
                    property_bind_groups
6903
                        .get(effect_batch.property_key.as_ref())
6904
                        .unwrap(),
6905
                    &offsets[..],
6906
                );
6907
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6908

6909
                // Dispatch update job
6910
                let dispatch_indirect_offset = effect_batch
6911
                    .dispatch_buffer_indices
6912
                    .update_dispatch_indirect_buffer_row_index
6913
                    * 12;
6914
                trace!(
6915
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
1,014✔
6916
                    indirect_buffer,
6917
                    dispatch_indirect_offset,
6918
                );
6919
                compute_pass
6920
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6921

6922
                trace!("update compute dispatched");
1,014✔
6923
            }
6924
        }
6925

6926
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6927
        // batch of particles which needs sorting, based on the actual number of alive
6928
        // particles in the batch after their update in the compute update pass. Since
6929
        // particles may die during update, this may be different from the number of
6930
        // particles updated.
6931
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
1,014✔
6932
            gpu_buffer_operations.dispatch(
6933
                *queue_index,
6934
                render_context,
6935
                utils_pipeline,
6936
                Some("hanabi:sort_fill_dispatch"),
6937
            );
6938
        }
6939

6940
        // Compute sort pass
6941
        {
6942
            let mut compute_pass =
6943
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6944

6945
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6946
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6947

6948
            // Loop on batches and find those which need sorting
6949
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6950
                trace!("Processing effect batch for sorting...");
2,028✔
6951
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
1,014✔
6952
                    continue;
1,014✔
6953
                }
UNCOV
6954
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
UNCOV
6955
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6956

UNCOV
6957
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
UNCOV
6958
                    warn!("Missing sort-fill effect buffer.");
×
UNCOV
6959
                    continue;
×
6960
                };
6961

6962
                let indirect_dispatch_index = *effect_batch
6963
                    .sort_fill_indirect_dispatch_index
6964
                    .as_ref()
6965
                    .unwrap();
6966
                let indirect_offset =
6967
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6968

6969
                // Fill the sort buffer with the key-value pairs to sort
6970
                {
6971
                    compute_pass.push_debug_group("hanabi:sort_fill");
6972

6973
                    // Fetch compute pipeline
6974
                    let Some(pipeline_id) =
×
6975
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6976
                    else {
UNCOV
6977
                        warn!("Missing sort-fill pipeline.");
×
UNCOV
6978
                        continue;
×
6979
                    };
6980
                    if compute_pass
6981
                        .set_cached_compute_pipeline(pipeline_id)
6982
                        .is_err()
6983
                    {
UNCOV
6984
                        compute_pass.pop_debug_group();
×
6985
                        // FIXME - Bevy doesn't allow returning custom errors here...
6986
                        return Ok(());
×
6987
                    }
6988

6989
                    // Bind group sort_fill@0
6990
                    let particle_buffer = effect_buffer.particle_buffer();
6991
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
UNCOV
6992
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6993
                        particle_buffer.id(),
6994
                        indirect_index_buffer.id(),
6995
                        effect_metadata_buffer.id(),
6996
                    ) else {
UNCOV
6997
                        warn!("Missing sort-fill bind group.");
×
UNCOV
6998
                        continue;
×
6999
                    };
7000
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
7001
                    let indirect_index_offset =
7002
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
7003
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
7004
                        effect_batch
7005
                            .dispatch_buffer_indices
7006
                            .effect_metadata_buffer_table_id
7007
                            .0,
7008
                    ) as u32;
7009
                    compute_pass.set_bind_group(
7010
                        0,
7011
                        bind_group,
7012
                        &[
7013
                            particle_offset,
7014
                            indirect_index_offset,
7015
                            effect_metadata_offset,
7016
                        ],
7017
                    );
7018

7019
                    compute_pass
7020
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
UNCOV
7021
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
7022

7023
                    compute_pass.pop_debug_group();
7024
                }
7025

7026
                // Do the actual sort
7027
                {
7028
                    compute_pass.push_debug_group("hanabi:sort");
7029

7030
                    if compute_pass
7031
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
7032
                        .is_err()
7033
                    {
UNCOV
7034
                        compute_pass.pop_debug_group();
×
7035
                        // FIXME - Bevy doesn't allow returning custom errors here...
UNCOV
7036
                        return Ok(());
×
7037
                    }
7038

7039
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
7040
                    compute_pass
7041
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
UNCOV
7042
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7043

7044
                    compute_pass.pop_debug_group();
7045
                }
7046

7047
                // Copy the sorted particle indices back into the indirect index buffer, where
7048
                // the render pass will read them.
7049
                {
7050
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7051

7052
                    // Fetch compute pipeline
7053
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7054
                    if compute_pass
7055
                        .set_cached_compute_pipeline(pipeline_id)
7056
                        .is_err()
7057
                    {
7058
                        compute_pass.pop_debug_group();
×
7059
                        // FIXME - Bevy doesn't allow returning custom errors here...
7060
                        return Ok(());
7061
                    }
7062

7063
                    // Bind group sort_copy@0
7064
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
UNCOV
7065
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
7066
                        indirect_index_buffer.id(),
7067
                        effect_metadata_buffer.id(),
7068
                    ) else {
UNCOV
7069
                        warn!("Missing sort-copy bind group.");
×
UNCOV
7070
                        continue;
×
7071
                    };
7072
                    let indirect_index_offset = effect_batch.slice.start;
7073
                    let effect_metadata_offset =
7074
                        effects_meta.effect_metadata_buffer.dynamic_offset(
7075
                            effect_batch
7076
                                .dispatch_buffer_indices
7077
                                .effect_metadata_buffer_table_id,
7078
                        );
7079
                    compute_pass.set_bind_group(
7080
                        0,
7081
                        bind_group,
7082
                        &[indirect_index_offset, effect_metadata_offset],
7083
                    );
7084

7085
                    compute_pass
7086
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
UNCOV
7087
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7088

7089
                    compute_pass.pop_debug_group();
7090
                }
7091
            }
7092
        }
7093

7094
        Ok(())
1,014✔
7095
    }
7096
}
7097

7098
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7099
    fn from(layout_flags: LayoutFlags) -> Self {
3,042✔
7100
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
6,084✔
UNCOV
7101
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7102
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
3,042✔
UNCOV
7103
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7104
        } else {
7105
            ParticleRenderAlphaMaskPipelineKey::Blend
3,042✔
7106
        }
7107
    }
7108
}
7109

7110
#[cfg(test)]
7111
mod tests {
7112
    use super::*;
7113

7114
    #[test]
7115
    fn layout_flags() {
7116
        let flags = LayoutFlags::default();
7117
        assert_eq!(flags, LayoutFlags::NONE);
7118
    }
7119

7120
    #[cfg(feature = "gpu_tests")]
7121
    #[test]
7122
    fn gpu_limits() {
7123
        use crate::test_utils::MockRenderer;
7124

7125
        let renderer = MockRenderer::new();
7126
        let device = renderer.device();
7127
        let limits = GpuLimits::from_device(&device);
7128

7129
        // assert!(limits.storage_buffer_align().get() >= 1);
7130
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7131
    }
7132

7133
    #[cfg(feature = "gpu_tests")]
7134
    #[test]
7135
    fn gpu_ops_ifda() {
7136
        use crate::test_utils::MockRenderer;
7137

7138
        let renderer = MockRenderer::new();
7139
        let device = renderer.device();
7140
        let render_queue = renderer.queue();
7141

7142
        let mut world = World::new();
7143
        world.insert_resource(device.clone());
7144
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7145

7146
        let src_buffer = device.create_buffer(&BufferDescriptor {
7147
            label: None,
7148
            size: 256,
7149
            usage: BufferUsages::STORAGE,
7150
            mapped_at_creation: false,
7151
        });
7152
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7153
            label: None,
7154
            size: 256,
7155
            usage: BufferUsages::STORAGE,
7156
            mapped_at_creation: false,
7157
        });
7158

7159
        // Two consecutive ops can be merged. This includes having contiguous slices
7160
        // both in source and destination.
7161
        buffer_ops.begin_frame();
7162
        {
7163
            let mut q = InitFillDispatchQueue::default();
7164
            q.enqueue(0, 0);
7165
            assert_eq!(q.queue.len(), 1);
7166
            q.enqueue(1, 1);
7167
            // Ops are not batched yet
7168
            assert_eq!(q.queue.len(), 2);
7169
            // On submit, the ops get batched together
7170
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7171
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7172
        }
7173
        buffer_ops.end_frame(&device, &render_queue);
7174

7175
        // Even if out of order, the init fill dispatch ops are batchable. Here the
7176
        // offsets are enqueued inverted.
7177
        buffer_ops.begin_frame();
7178
        {
7179
            let mut q = InitFillDispatchQueue::default();
7180
            q.enqueue(1, 1);
7181
            assert_eq!(q.queue.len(), 1);
7182
            q.enqueue(0, 0);
7183
            // Ops are not batched yet
7184
            assert_eq!(q.queue.len(), 2);
7185
            // On submit, the ops get batched together
7186
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7187
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7188
        }
7189
        buffer_ops.end_frame(&device, &render_queue);
7190

7191
        // However, both the source and destination need to be contiguous at the same
7192
        // time. Here they are mixed so we can't batch.
7193
        buffer_ops.begin_frame();
7194
        {
7195
            let mut q = InitFillDispatchQueue::default();
7196
            q.enqueue(0, 1);
7197
            assert_eq!(q.queue.len(), 1);
7198
            q.enqueue(1, 0);
7199
            // Ops are not batched yet
7200
            assert_eq!(q.queue.len(), 2);
7201
            // On submit, the ops cannot get batched together
7202
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7203
            assert_eq!(buffer_ops.args_buffer.len(), 2);
7204
        }
7205
        buffer_ops.end_frame(&device, &render_queue);
7206
    }
7207
}
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