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

djeedai / bevy_hanabi / 17710478608

14 Sep 2025 11:21AM UTC coverage: 66.279% (+0.2%) from 66.033%
17710478608

push

github

web-flow
Move indirect draw args to separate buffer (#495)

Move the indirect draw args outside of `EffectMetadata`, and into a
separate buffer of their own. This decouples the indirect draw args,
which are largely driven by GPU, from the effect metadata, which are
largely (and ideally, entirely) driven by CPU. The new indirect draw
args buffer stores both indexed and non-indexed draw args, the latter
padded with an extra `u32`. This ensures all entries are the same size
and simplifies handling, but more importantly allows retaining a single
unified dispatch of `vfx_indirect` for all effects without adding any
extra indirection or having to split into two passes.

The main benefit is that this prevents resetting the effect when Bevy
relocates the mesh, which requires re-uploading the mesh location info
into the draw args (base vertex and/or first index, notably), but
otherwise doesn't affect runtime info like the number of particles
alive. Previously when this happened, the entire `EffectMetadata` was
re-uploaded from CPU with default values for GPU-driven fields,
effectively leading to a "reset" of the effect (alive particle reset to
zero), as the warning in #471 used to highlight.

This change also cleans up the shaders by removing the `dead_count`
atomic particle count, and instead adding the constant `capacity`
particle count, which allows deducing the dead particle count from the
existing `alive_count`. This means `alive_count` becomes the only source
of truth for the number of alive particles. This makes several shaders
much more readable, and saves a couple of atomic instructions.

96 of 122 new or added lines in 5 files covered. (78.69%)

4 existing lines in 1 file now uncovered.

4900 of 7393 relevant lines covered (66.28%)

449.04 hits per line

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

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

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

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

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

101
use self::batch::EffectBatch;
102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1370
            let shader_defs = default();
6✔
1371

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2521
            // 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
2522
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2523
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
6✔
2524
                entity: entity.into(),
×
2525
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2526
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
2527
            });
2528

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2838
            // Allocate an indirect dispatch arguments struct for this instance
2839
            let update_dispatch_indirect_buffer_row_index =
2840
                self.dispatch_indirect_buffer.allocate();
2841

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

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

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

2881
            // Allocate storage for properties if needed
2882
            if !added_effect.property_layout.is_empty() {
1✔
2883
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
1✔
2884
                cmd.insert(cached_effect_properties);
1✔
2885
            } else {
2886
                cmd.remove::<CachedEffectProperties>();
1✔
2887
            }
2888

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

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

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

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

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

2939
            trace!(
2940
                "+ added effect entity {:?}: main_entity={:?} \
2✔
2941
                first_update_group_dispatch_buffer_index={} \
2✔
2942
                render_effect_dispatch_buffer_id={}",
2✔
2943
                added_effect.render_entity,
2944
                added_effect.entity,
2945
                update_dispatch_indirect_buffer_row_index,
2946
                effect_metadata_buffer_table_id.0
2947
            );
2948
        }
2949
    }
2950

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

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

3001
    pub fn free_draw_indirect(&mut self, row_index: DrawIndirectRowIndex) {
1✔
3002
        self.draw_indirect_buffer.remove(row_index.get());
4✔
3003
    }
3004
}
3005

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

3043
impl Default for LayoutFlags {
3044
    fn default() -> Self {
1✔
3045
        Self::NONE
1✔
3046
    }
3047
}
3048

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

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

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

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

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

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

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

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

3168
    // Allocate new effects
3169
    effects_meta.add_effects(
3,090✔
3170
        commands,
2,060✔
3171
        std::mem::take(&mut extracted_effects.added_effects),
3,090✔
3172
        &mut effect_cache,
2,060✔
3173
        &mut property_cache,
1,030✔
3174
        &mut event_cache,
1,030✔
3175
    );
3176

3177
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3178
    // because EffectBuffer already contains a reference to the
3179
    // RenderDevice, so has done so internally. This is not ideal
3180
    // design-wise, but works.
3181
}
3182

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

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

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

3256
    // Build map of render entity from main entity for all cached effects.
3257
    let render_from_main_entity = q_cached_effects
2,060✔
3258
        .iter()
3259
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
3,058✔
3260
        .collect::<HashMap<_, _>>();
3261

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

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

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

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

3331
        // Push the child entity into the children list
3332
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3333
        let local_child_index = child_vec.len() as u32;
3334
        child_vec.push((child_entity, child_buffer_binding_source));
3335
        child_infos.push(GpuChildInfo {
3336
            event_count: 0,
3337
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3338
        });
3339

3340
        // Check if child info changed. Avoid overwriting if no change.
3341
        if let Some(old_cached_child_info) = cached_child_info {
×
3342
            if parent_entity == old_cached_child_info.parent
3343
                && parent_cached_effect.slice.particle_layout
×
3344
                    == old_cached_child_info.parent_particle_layout
×
3345
                && parent_buffer_binding_source
×
3346
                    == old_cached_child_info.parent_buffer_binding_source
×
3347
                // 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.
3348
                && local_child_index == old_cached_child_info.local_child_index
×
3349
                && cached_effect_events.init_indirect_dispatch_index
×
3350
                    == old_cached_child_info.init_indirect_dispatch_index
×
3351
            {
3352
                trace!(
×
3353
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3354
                    child_entity
3355
                );
3356
                continue;
×
3357
            }
3358
        }
3359

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

3373
        // Make a note of the parent entity so that we remember to mark its
3374
        // `CachedParentInfo` as changed below.
3375
        parents_with_dirty_children.insert(parent_entity);
3376
    }
3377

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

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

3399
        // Check if any child changed compared to the existing CachedChildren component
3400
        if !is_child_list_changed(
3401
            parent_entity,
3402
            cached_parent_info
3403
                .children
3404
                .iter()
3405
                .map(|(entity, _)| *entity),
3406
            children.iter().map(|(entity, _)| *entity),
3407
        ) {
3408
            continue;
×
3409
        }
3410

3411
        event_cache.reallocate_child_infos(
3412
            parent_entity,
3413
            children,
3414
            &child_infos[..],
3415
            cached_parent_info.deref_mut(),
3416
        );
3417
    }
3418

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

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

3440
pub fn fixup_parents(
1,030✔
3441
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3442
    mut q_children: Query<&mut CachedChildInfo>,
3443
) {
3444
    #[cfg(feature = "trace")]
3445
    let _span = bevy::log::info_span!("fixup_parents").entered();
3,090✔
3446
    trace!("fixup_parents");
2,050✔
3447

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

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

3503
        // Resolve the render mesh
3504
        let Some(render_mesh) = render_meshes.get(cached_mesh.mesh) else {
1,014✔
3505
            warn!(
×
3506
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3507
                entity, cached_mesh.mesh
3508
            );
3509
            continue;
×
3510
        };
3511

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

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

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

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

3603
            old_mesh_location.set_if_neq(new_mesh_location);
3604
        } else {
3605
            commands.entity(entity).insert(new_mesh_location);
6✔
3606
        }
3607
    }
3608
}
3609

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

3624
/// Render world cached mesh infos for a single effect instance.
3625
#[derive(Debug, Clone, Copy, Component)]
3626
pub(crate) struct CachedMesh {
3627
    /// Asset of the effect mesh to draw.
3628
    pub mesh: AssetId<Mesh>,
3629
}
3630

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

3643
impl PartialEq for MeshIndexSlice {
3644
    fn eq(&self, other: &Self) -> bool {
2,024✔
3645
        self.format == other.format
2,024✔
3646
            && self.buffer.id() == other.buffer.id()
4,048✔
3647
            && self.range == other.range
2,024✔
3648
    }
3649
}
3650

3651
impl Eq for MeshIndexSlice {}
3652

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

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

3685
#[derive(SystemParam)]
3686
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3687
    sim_params: Res<'w, SimParams>,
3688
    render_device: Res<'w, RenderDevice>,
3689
    render_queue: Res<'w, RenderQueue>,
3690
    marker: PhantomData<&'s usize>,
3691
}
3692

3693
#[derive(SystemParam)]
3694
pub struct PipelineSystemParams<'w, 's> {
3695
    pipeline_cache: Res<'w, PipelineCache>,
3696
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3697
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3698
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3699
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3700
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3701
    specialized_indirect_pipelines:
3702
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3703
    marker: PhantomData<&'s usize>,
3704
}
3705

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

3737
    init_fill_dispatch_queue.clear();
1,030✔
3738

3739
    // Workaround for too many params in system (TODO: refactor to split work?)
3740
    let sim_params = read_only_params.sim_params.into_inner();
3,090✔
3741
    let render_device = read_only_params.render_device.into_inner();
3,090✔
3742
    let render_queue = read_only_params.render_queue.into_inner();
3,090✔
3743
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
3,090✔
3744
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
3,090✔
3745
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
3,090✔
3746
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
3,090✔
3747

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

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

3791
    gpu_buffer_operations.begin_frame();
1,030✔
3792

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

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

3837
        let effect_slice = EffectSlice {
3838
            slice: cached_effect.slice.range(),
3839
            buffer_index: cached_effect.buffer_index,
3840
            particle_layout: cached_effect.slice.particle_layout.clone(),
3841
        };
3842

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

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

3864
            // Resolve parent entry
3865
            let Ok((_, _, _, _, _, _, cached_parent_info, _, _)) =
×
3866
                q_cached_effects.get(cached_child_info.parent)
×
3867
            else {
3868
                continue;
×
3869
            };
3870
            let Some(cached_parent_info) = cached_parent_info else {
×
3871
                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);
×
3872
                continue;
×
3873
            };
3874

3875
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3876
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3877
            let global_child_index = cached_child_info.global_child_index;
×
3878

3879
            // Schedule a fill dispatch
3880
            trace!(
×
3881
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3882
                global_child_index,
3883
                init_indirect_dispatch_index,
3884
            );
3885
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3886
        }
3887

3888
        // Create init pipeline key flags.
3889
        let init_pipeline_key_flags = {
1,014✔
3890
            let mut flags = ParticleInitPipelineKeyFlags::empty();
3891
            flags.set(
3892
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3893
                effect_slice.particle_layout.contains(Attribute::PREV),
3894
            );
3895
            flags.set(
3896
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3897
                effect_slice.particle_layout.contains(Attribute::NEXT),
3898
            );
3899
            flags.set(
3900
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3901
                has_event_buffer,
3902
            );
3903
            flags
3904
        };
3905

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

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

3966
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3967
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3968

3969
        // Specialize the init pipeline based on the effect.
3970
        let init_pipeline_id = {
3971
            let consume_gpu_spawn_events = init_pipeline_key_flags
3972
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3973

3974
            // Fetch the metadata@3 bind group layout from the cache
3975
            let metadata_bind_group_layout = effect_cache
3976
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3977
                .unwrap()
3978
                .clone();
3979

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

4009
            init_pipeline_id
4010
        };
4011

4012
        let update_pipeline_id = {
4013
            let num_event_buffers = cached_parent_info
4014
                .map(|p| p.children.len() as u32)
×
4015
                .unwrap_or_default();
4016

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

4024
            // Fetch the bind group layouts from the cache
4025
            let metadata_bind_group_layout = effect_cache
4026
                .metadata_update_bind_group_layout(num_event_buffers)
4027
                .unwrap()
4028
                .clone();
4029

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

4059
            update_pipeline_id
4060
        };
4061

4062
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
4063
            init: init_pipeline_id,
4064
            update: update_pipeline_id,
4065
        };
4066

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

4082
        // Output some debug info
4083
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
2,028✔
4084
        trace!(
4085
            "update_shader = {:?}",
1,014✔
4086
            extracted_effect.effect_shaders.update
4087
        );
4088
        trace!(
4089
            "render_shader = {:?}",
1,014✔
4090
            extracted_effect.effect_shaders.render
4091
        );
4092
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
1,014✔
4093
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
1,014✔
4094

4095
        let spawner_index = effects_meta.allocate_spawner(
4096
            &extracted_effect.transform,
4097
            extracted_effect.spawn_count,
4098
            extracted_effect.prng_seed,
4099
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
4100
            dispatch_buffer_indices.draw_indirect_buffer_row_index,
4101
        );
4102

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

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

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

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

4196
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4197
        // update its GpuEffectMetadata with all those infos.
4198
        // FIXME - should do this only when the below changes (not only the mesh), via
4199
        // some invalidation mechanism and ECS change detection.
4200
        if !cached_mesh.is_changed() && !cached_mesh_location.is_changed() {
1,012✔
4201
            prepared_effect_count += 1;
1,012✔
4202
            continue;
4203
        }
4204

4205
        // Update the draw indirect args.
4206
        if cached_mesh_location.is_changed() {
4207
            let gpu_draw_args = GpuDrawIndexedIndirectArgs {
4208
                index_count: cached_mesh_location.vertex_or_index_count,
4✔
4209
                instance_count: 0,
4210
                first_index: cached_mesh_location.first_index_or_vertex_offset,
2✔
4211
                base_vertex: cached_mesh_location.vertex_offset_or_base_instance,
2✔
4212
                first_instance: 0,
4213
            };
4214
            assert!(dispatch_buffer_indices
4✔
4215
                .draw_indirect_buffer_row_index
2✔
4216
                .is_valid());
2✔
4217
            effects_meta.draw_indirect_buffer.update(
4✔
4218
                dispatch_buffer_indices.draw_indirect_buffer_row_index.get(),
4✔
4219
                gpu_draw_args,
2✔
4220
            );
4221
        }
4222

4223
        let capacity = cached_effect.slice.len();
2✔
4224

4225
        // Global and local indices of this effect as a child of another (parent) effect
4226
        let (global_child_index, local_child_index) = cached_child_info
4227
            .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4228
            .unwrap_or_default();
4229

4230
        // Base index of all children of this (parent) effect
4231
        let base_child_index = cached_parent_info
4232
            .map(|cpi| {
×
4233
                debug_assert_eq!(
×
4234
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4235
                    0
4236
                );
4237
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4238
            })
4239
            .unwrap_or_default();
4240

4241
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
4242
        let sort_key_offset = extracted_effect
4243
            .particle_layout
4244
            .offset(Attribute::RIBBON_ID)
4245
            .unwrap_or_default()
4246
            / 4;
4247
        let sort_key2_offset = extracted_effect
4248
            .particle_layout
4249
            .offset(Attribute::AGE)
4250
            .unwrap_or_default()
4251
            / 4;
4252

4253
        let gpu_effect_metadata = GpuEffectMetadata {
4254
            capacity,
4255
            alive_count: 0,
4256
            max_update: 0,
4257
            max_spawn: capacity,
4258
            indirect_write_index: 0,
4259
            indirect_dispatch_index: dispatch_buffer_indices
4260
                .update_dispatch_indirect_buffer_row_index,
4261
            indirect_draw_index: dispatch_buffer_indices
4262
                .draw_indirect_buffer_row_index
4263
                .get()
4264
                .0,
4265
            init_indirect_dispatch_index: cached_effect_events
4266
                .map(|cee| cee.init_indirect_dispatch_index)
4267
                .unwrap_or_default(),
4268
            local_child_index,
4269
            global_child_index,
4270
            base_child_index,
4271
            particle_stride,
4272
            sort_key_offset,
4273
            sort_key2_offset,
4274
            ..default()
4275
        };
4276

4277
        assert!(dispatch_buffer_indices
4278
            .effect_metadata_buffer_table_id
4279
            .is_valid());
4280
        effects_meta.effect_metadata_buffer.update(
2✔
4281
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
4282
            gpu_effect_metadata,
4283
        );
4284

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

4293
        prepared_effect_count += 1;
4294
    }
4295
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
2,050✔
4296

4297
    // Once all EffectMetadata values are written, schedule a GPU upload
4298
    if effects_meta
4299
        .effect_metadata_buffer
4300
        .allocate_gpu(render_device, render_queue)
4301
    {
4302
        // All those bind groups use the buffer so need to be re-created
4303
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
4✔
4304
        effects_meta.indirect_metadata_bind_group = None;
4✔
4305
        effect_bind_groups.init_metadata_bind_groups.clear();
4✔
4306
        effect_bind_groups.update_metadata_bind_groups.clear();
4✔
4307
    }
4308

4309
    if effects_meta
4310
        .draw_indirect_buffer
4311
        .allocate_gpu(render_device, render_queue)
4312
    {
4313
        // All those bind groups use the buffer so need to be re-created
4314
        trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
4✔
4315
        effects_meta.update_sim_params_bind_group = None;
4✔
4316
        effects_meta.indirect_metadata_bind_group = None;
4✔
4317
    }
4318

4319
    // Write the entire spawner buffer for this frame, for all effects combined
4320
    assert_eq!(
4321
        prepared_effect_count,
4322
        effects_meta.spawner_buffer.len() as u32
4323
    );
4324
    if effects_meta
1,030✔
4325
        .spawner_buffer
1,030✔
4326
        .write_buffer(render_device, render_queue)
3,090✔
4327
    {
4328
        // All property bind groups use the spawner buffer, which was reallocate
4329
        effect_bind_groups.particle_buffers.clear();
6✔
4330
        property_bind_groups.clear(true);
4✔
4331
        effects_meta.indirect_spawner_bind_group = None;
2✔
4332
    }
4333

4334
    // Update simulation parameters
4335
    effects_meta.sim_params_uniforms.set(sim_params.into());
4,120✔
4336
    {
4337
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
3,090✔
4338
        gpu_sim_params.num_effects = prepared_effect_count;
1,030✔
4339

4340
        trace!(
1,030✔
4341
            "Simulation parameters: time={} delta_time={} virtual_time={} \
1,020✔
4342
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
1,020✔
4343
            gpu_sim_params.time,
4344
            gpu_sim_params.delta_time,
4345
            gpu_sim_params.virtual_time,
4346
            gpu_sim_params.virtual_delta_time,
4347
            gpu_sim_params.real_time,
4348
            gpu_sim_params.real_delta_time,
4349
            gpu_sim_params.num_effects,
4350
        );
4351
    }
4352
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
6,174✔
4353
    effects_meta
1,030✔
4354
        .sim_params_uniforms
1,030✔
4355
        .write_buffer(render_device, render_queue);
3,090✔
4356
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
6,183✔
4357
        // Buffer changed, invalidate bind groups
4358
        effects_meta.update_sim_params_bind_group = None;
9✔
4359
        effects_meta.indirect_sim_params_bind_group = None;
3✔
4360
    }
4361
}
4362

4363
pub(crate) fn batch_effects(
1,030✔
4364
    mut commands: Commands,
4365
    effects_meta: Res<EffectsMeta>,
4366
    mut sort_bind_groups: ResMut<SortBindGroups>,
4367
    mut q_cached_effects: Query<(
4368
        Entity,
4369
        &MainEntity,
4370
        &CachedMesh,
4371
        Option<&CachedEffectEvents>,
4372
        Option<&CachedChildInfo>,
4373
        Option<&CachedProperties>,
4374
        &mut DispatchBufferIndices,
4375
        &mut BatchInput,
4376
    )>,
4377
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4378
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4379
) {
4380
    trace!("batch_effects");
2,050✔
4381

4382
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4383
    // inside that buffer. This is critical for batching to work, because
4384
    // batching effects is based on compatible items, which implies same GPU
4385
    // buffer and continuous slice ranges (the next slice start must be equal to
4386
    // the previous start end, without gap). EffectSlice already contains both
4387
    // information, and the proper ordering implementation.
4388
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4389

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

4394
    let mut sort_queue = GpuBufferOperationQueue::new();
2,060✔
4395

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

4428
        let translation = input.position;
2,028✔
4429

4430
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4431
        // most of the data needed to drive rendering. However this doesn't drive
4432
        // rendering; this is just storage.
4433
        let mut effect_batch = EffectBatch::from_input(
4434
            cached_mesh,
1,014✔
4435
            cached_effect_events,
1,014✔
4436
            cached_child_info,
1,014✔
4437
            &mut input,
1,014✔
4438
            *dispatch_buffer_indices.as_ref(),
1,014✔
4439
            cached_properties.map(|cp| PropertyBindGroupKey {
2,028✔
4440
                buffer_index: cp.buffer_index,
9✔
4441
                binding_size: cp.binding_size,
9✔
4442
            }),
4443
            cached_properties.map(|cp| cp.offset),
2,028✔
4444
        );
4445

4446
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4447
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4448
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4449
        // ribbon).
4450
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
2,028✔
4451
            // This buffer is allocated in prepare_effects(), so should always be available
4452
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4453
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4454
                continue;
×
4455
            };
4456

4457
            // Allocate a GpuDispatchIndirect entry
4458
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4459
            effect_batch.sort_fill_indirect_dispatch_index =
4460
                Some(sort_fill_indirect_dispatch_index);
4461

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

4526
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
1,014✔
4527
        trace!(
4528
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
1,014✔
4529
            effect_batch_index,
4530
            entity,
4531
        );
4532

4533
        // Spawn an EffectDrawBatch, to actually drive rendering.
4534
        commands
4535
            .spawn(EffectDrawBatch {
4536
                effect_batch_index,
4537
                translation,
4538
                main_entity: *main_entity,
4539
            })
4540
            .insert(TemporaryRenderEntity);
4541
    }
4542

4543
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
1,030✔
4544
    if !sort_queue.operation_queue.is_empty() {
1,030✔
4545
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4546
    }
4547

4548
    sorted_effect_batches.sort();
1,030✔
4549
}
4550

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

4588
/// Combination of a texture layout and the bound textures.
4589
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4590
struct Material {
4591
    layout: TextureLayout,
4592
    textures: Vec<AssetId<Image>>,
4593
}
4594

4595
impl Material {
4596
    /// Get the bind group entries to create a bind group.
4597
    pub fn make_entries<'a>(
×
4598
        &self,
4599
        gpu_images: &'a RenderAssets<GpuImage>,
4600
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4601
        if self.textures.is_empty() {
×
4602
            return Ok(vec![]);
×
4603
        }
4604

4605
        let entries: Vec<BindGroupEntry<'a>> = self
×
4606
            .textures
×
4607
            .iter()
4608
            .enumerate()
4609
            .flat_map(|(index, id)| {
×
4610
                let base_binding = index as u32 * 2;
×
4611
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4612
                    vec![
×
4613
                        BindGroupEntry {
×
4614
                            binding: base_binding,
×
4615
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4616
                        },
4617
                        BindGroupEntry {
×
4618
                            binding: base_binding + 1,
×
4619
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4620
                        },
4621
                    ]
4622
                } else {
4623
                    vec![]
×
4624
                }
4625
            })
4626
            .collect();
4627
        if entries.len() == self.textures.len() * 2 {
×
4628
            return Ok(entries);
×
4629
        }
4630
        Err(())
×
4631
    }
4632
}
4633

4634
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4635
struct BindingKey {
4636
    pub buffer_id: BufferId,
4637
    pub offset: u32,
4638
    pub size: NonZeroU32,
4639
}
4640

4641
impl<'a> From<BufferSlice<'a>> for BindingKey {
4642
    fn from(value: BufferSlice<'a>) -> Self {
×
4643
        Self {
4644
            buffer_id: value.buffer.id(),
×
4645
            offset: value.offset,
×
4646
            size: value.size,
×
4647
        }
4648
    }
4649
}
4650

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

4661
impl From<&BufferBindingSource> for BindingKey {
4662
    fn from(value: &BufferBindingSource) -> Self {
×
4663
        Self {
4664
            buffer_id: value.buffer.id(),
×
4665
            offset: value.offset,
×
4666
            size: value.size,
×
4667
        }
4668
    }
4669
}
4670

4671
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4672
struct ConsumeEventKey {
4673
    child_infos_buffer_id: BufferId,
4674
    events: BindingKey,
4675
}
4676

4677
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4678
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4679
        Self {
4680
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4681
            events: value.events.into(),
×
4682
        }
4683
    }
4684
}
4685

4686
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4687
struct InitMetadataBindGroupKey {
4688
    pub buffer_index: u32,
4689
    pub effect_metadata_buffer: BufferId,
4690
    pub effect_metadata_offset: u32,
4691
    pub consume_event_key: Option<ConsumeEventKey>,
4692
}
4693

4694
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4695
struct UpdateMetadataBindGroupKey {
4696
    pub buffer_index: u32,
4697
    pub effect_metadata_buffer: BufferId,
4698
    pub effect_metadata_offset: u32,
4699
    pub child_info_buffer_id: Option<BufferId>,
4700
    pub event_buffers_keys: Vec<BindingKey>,
4701
}
4702

4703
struct CachedBindGroup<K: Eq> {
4704
    /// Key the bind group was created from. Each time the key changes, the bind
4705
    /// group should be re-created.
4706
    key: K,
4707
    /// Bind group created from the key.
4708
    bind_group: BindGroup,
4709
}
4710

4711
#[derive(Debug, Clone, Copy)]
4712
struct BufferSlice<'a> {
4713
    pub buffer: &'a Buffer,
4714
    pub offset: u32,
4715
    pub size: NonZeroU32,
4716
}
4717

4718
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4719
    fn from(value: BufferSlice<'a>) -> Self {
×
4720
        Self {
4721
            buffer: value.buffer,
×
4722
            offset: value.offset.into(),
×
4723
            size: Some(value.size.into()),
×
4724
        }
4725
    }
4726
}
4727

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

4738
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4739
    fn from(value: &'a BufferBindingSource) -> Self {
×
4740
        Self {
4741
            buffer: &value.buffer,
×
4742
            offset: value.offset,
×
4743
            size: value.size,
×
4744
        }
4745
    }
4746
}
4747

4748
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4749
/// the init pass consumes GPU events as a mechanism to spawn particles.
4750
struct ConsumeEventBuffers<'a> {
4751
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4752
    /// This is dynamically indexed inside the shader.
4753
    child_infos_buffer: &'a Buffer,
4754
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4755
    events: BufferSlice<'a>,
4756
}
4757

4758
#[derive(Default, Resource)]
4759
pub struct EffectBindGroups {
4760
    /// Map from buffer index to the bind groups shared among all effects that
4761
    /// use that buffer.
4762
    particle_buffers: HashMap<u32, BufferBindGroups>,
4763
    /// Map of bind groups for image assets used as particle textures.
4764
    images: HashMap<AssetId<Image>, BindGroup>,
4765
    /// Map from buffer index to its metadata bind group (group 3) for the init
4766
    /// pass.
4767
    // FIXME - doesn't work with batching; this should be the instance ID
4768
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4769
    /// Map from buffer index to its metadata bind group (group 3) for the
4770
    /// update pass.
4771
    // FIXME - doesn't work with batching; this should be the instance ID
4772
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4773
    /// Map from an effect material to its bind group.
4774
    material_bind_groups: HashMap<Material, BindGroup>,
4775
}
4776

4777
impl EffectBindGroups {
4778
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
1,013✔
4779
        self.particle_buffers
1,013✔
4780
            .get(&buffer_index)
2,026✔
4781
            .map(|bg| &bg.render)
1,013✔
4782
    }
4783

4784
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4785
    /// needed.
4786
    pub(self) fn get_or_create_init_metadata(
1,014✔
4787
        &mut self,
4788
        effect_batch: &EffectBatch,
4789
        gpu_limits: &GpuLimits,
4790
        render_device: &RenderDevice,
4791
        layout: &BindGroupLayout,
4792
        effect_metadata_buffer: &Buffer,
4793
        consume_event_buffers: Option<ConsumeEventBuffers>,
4794
    ) -> Result<&BindGroup, ()> {
4795
        let DispatchBufferIndices {
4796
            effect_metadata_buffer_table_id,
1,014✔
4797
            ..
4798
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4799

4800
        let effect_metadata_offset =
1,014✔
4801
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
2,028✔
4802
        let key = InitMetadataBindGroupKey {
4803
            buffer_index: effect_batch.buffer_index,
2,028✔
4804
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,042✔
4805
            effect_metadata_offset,
4806
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
2,028✔
4807
        };
4808

4809
        let make_entry = || {
1,016✔
4810
            let mut entries = Vec::with_capacity(3);
4✔
4811
            entries.push(
4✔
4812
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4813
                BindGroupEntry {
2✔
4814
                    binding: 0,
2✔
4815
                    resource: BindingResource::Buffer(BufferBinding {
2✔
4816
                        buffer: effect_metadata_buffer,
4✔
4817
                        offset: key.effect_metadata_offset as u64,
4✔
4818
                        size: Some(gpu_limits.effect_metadata_size()),
2✔
4819
                    }),
4820
                },
4821
            );
4822
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
4823
                entries.push(
4824
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4825
                    // ChildInfoBuffer;
4826
                    BindGroupEntry {
4827
                        binding: 1,
4828
                        resource: BindingResource::Buffer(BufferBinding {
4829
                            buffer: consume_event_buffers.child_infos_buffer,
4830
                            offset: 0,
4831
                            size: None,
4832
                        }),
4833
                    },
4834
                );
4835
                entries.push(
4836
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4837
                    BindGroupEntry {
4838
                        binding: 2,
4839
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4840
                    },
4841
                );
4842
            }
4843

4844
            let bind_group = render_device.create_bind_group(
6✔
4845
                "hanabi:bind_group:init:metadata@3",
4846
                layout,
2✔
4847
                &entries[..],
2✔
4848
            );
4849

4850
            trace!(
2✔
4851
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
4852
                    effect_batch.buffer_index,
4853
                    effect_metadata_buffer_table_id.0,
4854
                );
4855

4856
            bind_group
2✔
4857
        };
4858

4859
        Ok(&self
1,014✔
4860
            .init_metadata_bind_groups
1,014✔
4861
            .entry(effect_batch.buffer_index)
2,028✔
4862
            .and_modify(|cbg| {
2,026✔
4863
                if cbg.key != key {
1,012✔
4864
                    trace!(
×
4865
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4866
                        cbg.key,
4867
                        key
4868
                    );
4869
                    cbg.key = key;
×
4870
                    cbg.bind_group = make_entry();
×
4871
                }
4872
            })
4873
            .or_insert_with(|| {
1,016✔
4874
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
4875
                CachedBindGroup {
2✔
4876
                    key,
2✔
4877
                    bind_group: make_entry(),
2✔
4878
                }
4879
            })
4880
            .bind_group)
4881
    }
4882

4883
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4884
    /// needed.
4885
    pub(self) fn get_or_create_update_metadata(
1,014✔
4886
        &mut self,
4887
        effect_batch: &EffectBatch,
4888
        gpu_limits: &GpuLimits,
4889
        render_device: &RenderDevice,
4890
        layout: &BindGroupLayout,
4891
        effect_metadata_buffer: &Buffer,
4892
        child_info_buffer: Option<&Buffer>,
4893
        event_buffers: &[(Entity, BufferBindingSource)],
4894
    ) -> Result<&BindGroup, ()> {
4895
        let DispatchBufferIndices {
4896
            effect_metadata_buffer_table_id,
1,014✔
4897
            ..
4898
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4899

4900
        // Check arguments consistency
4901
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
5,070✔
4902
        let emits_gpu_spawn_events = !event_buffers.is_empty();
2,028✔
4903
        let child_info_buffer_id = if emits_gpu_spawn_events {
2,028✔
4904
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4905
        } else {
4906
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4907
            // if relevant, that is if the effect emits GPU spawn events.
4908
            None
1,014✔
4909
        };
4910
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
3,042✔
4911

4912
        let event_buffers_keys = event_buffers
2,028✔
4913
            .iter()
4914
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
1,014✔
4915
            .collect::<Vec<_>>();
4916

4917
        let key = UpdateMetadataBindGroupKey {
4918
            buffer_index: effect_batch.buffer_index,
2,028✔
4919
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,042✔
4920
            effect_metadata_offset: gpu_limits
3,042✔
4921
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4922
                as u32,
4923
            child_info_buffer_id,
4924
            event_buffers_keys,
4925
        };
4926

4927
        let make_entry = || {
1,016✔
4928
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
6✔
4929
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4930
            // EffectMetadata;
4931
            entries.push(BindGroupEntry {
6✔
4932
                binding: 0,
2✔
4933
                resource: BindingResource::Buffer(BufferBinding {
2✔
4934
                    buffer: effect_metadata_buffer,
4✔
4935
                    offset: key.effect_metadata_offset as u64,
4✔
4936
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
2✔
4937
                }),
4938
            });
4939
            if emits_gpu_spawn_events {
2✔
4940
                let child_info_buffer = child_info_buffer.unwrap();
×
4941

4942
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4943
                // ChildInfoBuffer;
4944
                entries.push(BindGroupEntry {
×
4945
                    binding: 1,
×
4946
                    resource: BindingResource::Buffer(BufferBinding {
×
4947
                        buffer: child_info_buffer,
×
4948
                        offset: 0,
×
4949
                        size: None,
×
4950
                    }),
4951
                });
4952

4953
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4954
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4955
                    // EventBuffer;
4956
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4957
                    // then moved to counting in bytes, so now need some conversion. Need to review
4958
                    // all of this...
4959
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4960
                    buffer_binding.offset *= 4;
4961
                    buffer_binding.size = buffer_binding
4962
                        .size
4963
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4964
                    entries.push(BindGroupEntry {
4965
                        binding: 2 + index as u32,
4966
                        resource: BindingResource::Buffer(buffer_binding),
4967
                    });
4968
                }
4969
            }
4970

4971
            let bind_group = render_device.create_bind_group(
6✔
4972
                "hanabi:bind_group:update:metadata@3",
4973
                layout,
2✔
4974
                &entries[..],
2✔
4975
            );
4976

4977
            trace!(
2✔
4978
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
2✔
4979
                effect_batch.buffer_index,
4980
                effect_metadata_buffer_table_id.0,
4981
            );
4982

4983
            bind_group
2✔
4984
        };
4985

4986
        Ok(&self
1,014✔
4987
            .update_metadata_bind_groups
1,014✔
4988
            .entry(effect_batch.buffer_index)
2,028✔
4989
            .and_modify(|cbg| {
2,026✔
4990
                if cbg.key != key {
1,012✔
4991
                    trace!(
×
4992
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4993
                        cbg.key,
4994
                        key
4995
                    );
4996
                    cbg.key = key.clone();
×
4997
                    cbg.bind_group = make_entry();
×
4998
                }
4999
            })
5000
            .or_insert_with(|| {
1,016✔
5001
                trace!(
2✔
5002
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
5003
                    key
5004
                );
5005
                CachedBindGroup {
2✔
5006
                    key: key.clone(),
4✔
5007
                    bind_group: make_entry(),
2✔
5008
                }
5009
            })
5010
            .bind_group)
5011
    }
5012
}
5013

5014
#[derive(SystemParam)]
5015
pub struct QueueEffectsReadOnlyParams<'w, 's> {
5016
    #[cfg(feature = "2d")]
5017
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5018
    #[cfg(feature = "3d")]
5019
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5020
    #[cfg(feature = "3d")]
5021
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5022
    #[cfg(feature = "3d")]
5023
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5024
    marker: PhantomData<&'s usize>,
5025
}
5026

5027
fn emit_sorted_draw<T, F>(
2,028✔
5028
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5029
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5030
    view_entities: &mut FixedBitSet,
5031
    sorted_effect_batches: &SortedEffectBatches,
5032
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5033
    render_pipeline: &mut ParticlesRenderPipeline,
5034
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5035
    render_meshes: &RenderAssets<RenderMesh>,
5036
    pipeline_cache: &PipelineCache,
5037
    make_phase_item: F,
5038
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5039
) where
5040
    T: SortedPhaseItem,
5041
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5042
{
5043
    trace!("emit_sorted_draw() {} views", views.iter().len());
8,112✔
5044

5045
    for (visible_entities, view, msaa) in views.iter() {
6,084✔
5046
        trace!(
×
5047
            "Process new sorted view with {} visible particle effect entities",
2,028✔
5048
            visible_entities.len::<CompiledParticleEffect>()
4,056✔
5049
        );
5050

5051
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
1,014✔
5052
            continue;
1,014✔
5053
        };
5054

5055
        {
5056
            #[cfg(feature = "trace")]
5057
            let _span = bevy::log::info_span!("collect_view_entities").entered();
3,042✔
5058

5059
            view_entities.clear();
2,028✔
5060
            view_entities.extend(
2,028✔
5061
                visible_entities
1,014✔
5062
                    .iter::<EffectVisibilityClass>()
1,014✔
5063
                    .map(|e| e.1.index() as usize),
2,028✔
5064
            );
5065
        }
5066

5067
        // For each view, loop over all the effect batches to determine if the effect
5068
        // needs to be rendered for that view, and enqueue a view-dependent
5069
        // batch if so.
5070
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
3,042✔
5071
            #[cfg(feature = "trace")]
5072
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5073

5074
            trace!(
×
5075
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
1,014✔
5076
                draw_entity,
×
5077
                draw_batch.effect_batch_index,
×
5078
            );
5079

5080
            // Get the EffectBatches this EffectDrawBatch is part of.
5081
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
1,014✔
5082
            else {
×
5083
                continue;
×
5084
            };
5085

5086
            trace!(
×
5087
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
1,014✔
5088
                effect_batch.buffer_index,
×
5089
                effect_batch.spawner_base,
×
5090
                effect_batch.layout_flags,
×
5091
            );
5092

5093
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5094
            if effect_batch
×
5095
                .layout_flags
×
5096
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5097
            {
5098
                trace!("Non-transparent batch. Skipped.");
×
5099
                continue;
×
5100
            }
5101

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

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

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

5127
            let local_space_simulation = effect_batch
2,028✔
5128
                .layout_flags
1,014✔
5129
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
1,014✔
5130
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
3,042✔
5131
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
3,042✔
5132
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
3,042✔
5133
            let needs_normal = effect_batch
2,028✔
5134
                .layout_flags
1,014✔
5135
                .contains(LayoutFlags::NEEDS_NORMAL);
1,014✔
5136
            let needs_particle_fragment = effect_batch
2,028✔
5137
                .layout_flags
1,014✔
5138
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
1,014✔
5139
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
3,042✔
5140
            let image_count = effect_batch.texture_layout.layout.len() as u8;
2,028✔
5141

5142
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5143
            // re-querying here...?
5144
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
3,042✔
5145
                trace!("Batch has no render mesh, skipped.");
×
5146
                continue;
×
5147
            };
5148
            let mesh_layout = render_mesh.layout.clone();
×
5149

5150
            // Specialize the render pipeline based on the effect batch
5151
            trace!(
×
5152
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
1,014✔
5153
                effect_batch.render_shader,
×
5154
                image_count,
×
5155
                alpha_mask,
×
5156
                flipbook,
×
5157
                view.hdr
×
5158
            );
5159

5160
            // Add a draw pass for the effect batch
5161
            trace!("Emitting individual draw for batch");
1,014✔
5162

5163
            let alpha_mode = effect_batch.alpha_mode;
×
5164

5165
            #[cfg(feature = "trace")]
5166
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5167
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5168
                pipeline_cache,
×
5169
                render_pipeline,
×
5170
                ParticleRenderPipelineKey {
×
5171
                    shader: effect_batch.render_shader.clone(),
×
5172
                    mesh_layout: Some(mesh_layout),
×
5173
                    particle_layout: effect_batch.particle_layout.clone(),
×
5174
                    texture_layout: effect_batch.texture_layout.clone(),
×
5175
                    local_space_simulation,
×
5176
                    alpha_mask,
×
5177
                    alpha_mode,
×
5178
                    flipbook,
×
5179
                    needs_uv,
×
5180
                    needs_normal,
×
5181
                    needs_particle_fragment,
×
5182
                    ribbons,
×
5183
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5184
                    pipeline_mode,
×
5185
                    msaa_samples: msaa.samples(),
×
5186
                    hdr: view.hdr,
×
5187
                },
5188
            );
5189
            #[cfg(feature = "trace")]
5190
            _span_specialize.exit();
×
5191

5192
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
1,014✔
5193
            trace!(
×
5194
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
1,014✔
5195
                spawner_base={} handle={:?}",
1,014✔
5196
                draw_entity,
×
5197
                effect_batch.buffer_index,
×
5198
                effect_batch.spawner_base,
×
5199
                effect_batch.handle
×
5200
            );
5201
            render_phase.add(make_phase_item(
×
5202
                render_pipeline_id,
×
5203
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5204
                draw_batch,
×
5205
                view,
×
5206
            ));
5207
        }
5208
    }
5209
}
5210

5211
#[cfg(feature = "3d")]
5212
fn emit_binned_draw<T, F, G>(
2,028✔
5213
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5214
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5215
    view_entities: &mut FixedBitSet,
5216
    sorted_effect_batches: &SortedEffectBatches,
5217
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5218
    render_pipeline: &mut ParticlesRenderPipeline,
5219
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5220
    pipeline_cache: &PipelineCache,
5221
    render_meshes: &RenderAssets<RenderMesh>,
5222
    make_batch_set_key: F,
5223
    make_bin_key: G,
5224
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5225
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5226
    change_tick: &mut Tick,
5227
) where
5228
    T: BinnedPhaseItem,
5229
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5230
    G: Fn() -> T::BinKey,
5231
{
5232
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5233

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

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

5239
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
2,028✔
5240
            continue;
×
5241
        };
5242

5243
        {
5244
            #[cfg(feature = "trace")]
5245
            let _span = bevy::log::info_span!("collect_view_entities").entered();
6,084✔
5246

5247
            view_entities.clear();
4,056✔
5248
            view_entities.extend(
4,056✔
5249
                visible_entities
2,028✔
5250
                    .iter::<EffectVisibilityClass>()
2,028✔
5251
                    .map(|e| e.1.index() as usize),
4,056✔
5252
            );
5253
        }
5254

5255
        // For each view, loop over all the effect batches to determine if the effect
5256
        // needs to be rendered for that view, and enqueue a view-dependent
5257
        // batch if so.
5258
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
6,084✔
5259
            #[cfg(feature = "trace")]
5260
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5261

5262
            trace!(
×
5263
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
2,028✔
5264
                draw_entity,
×
5265
                draw_batch.effect_batch_index,
×
5266
            );
5267

5268
            // Get the EffectBatches this EffectDrawBatch is part of.
5269
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
2,028✔
5270
            else {
×
5271
                continue;
×
5272
            };
5273

5274
            trace!(
×
5275
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
2,028✔
5276
                effect_batch.buffer_index,
×
5277
                effect_batch.spawner_base,
×
5278
                effect_batch.layout_flags,
×
5279
            );
5280

5281
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5282
                trace!(
2,028✔
5283
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
2,028✔
5284
                    effect_batch.layout_flags,
×
5285
                    alpha_mask
×
5286
                );
5287
                continue;
2,028✔
5288
            }
5289

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

5309
            // Create and cache the bind group layout for this texture layout
5310
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5311

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

5315
            let local_space_simulation = effect_batch
×
5316
                .layout_flags
×
5317
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5318
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5319
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5320
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5321
            let needs_normal = effect_batch
×
5322
                .layout_flags
×
5323
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5324
            let needs_particle_fragment = effect_batch
×
5325
                .layout_flags
×
5326
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5327
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5328
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5329
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5330

5331
            // Specialize the render pipeline based on the effect batch
5332
            trace!(
×
5333
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5334
                effect_batch.render_shader,
×
5335
                image_count,
×
5336
                alpha_mask,
×
5337
                flipbook,
×
5338
                view.hdr
×
5339
            );
5340

5341
            // Add a draw pass for the effect batch
5342
            trace!("Emitting individual draw for batch");
×
5343

5344
            let alpha_mode = effect_batch.alpha_mode;
×
5345

5346
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5347
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5348
                continue;
×
5349
            };
5350

5351
            #[cfg(feature = "trace")]
5352
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5353
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5354
                pipeline_cache,
×
5355
                render_pipeline,
×
5356
                ParticleRenderPipelineKey {
×
5357
                    shader: effect_batch.render_shader.clone(),
×
5358
                    mesh_layout: Some(mesh_layout),
×
5359
                    particle_layout: effect_batch.particle_layout.clone(),
×
5360
                    texture_layout: effect_batch.texture_layout.clone(),
×
5361
                    local_space_simulation,
×
5362
                    alpha_mask,
×
5363
                    alpha_mode,
×
5364
                    flipbook,
×
5365
                    needs_uv,
×
5366
                    needs_normal,
×
5367
                    needs_particle_fragment,
×
5368
                    ribbons,
×
5369
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5370
                    pipeline_mode,
×
5371
                    msaa_samples: msaa.samples(),
×
5372
                    hdr: view.hdr,
×
5373
                },
5374
            );
5375
            #[cfg(feature = "trace")]
5376
            _span_specialize.exit();
×
5377

5378
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5379
            trace!(
×
5380
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5381
                spawner_base={} handle={:?}",
×
5382
                draw_entity,
×
5383
                effect_batch.buffer_index,
×
5384
                effect_batch.spawner_base,
×
5385
                effect_batch.handle
×
5386
            );
5387
            render_phase.add(
×
5388
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
5389
                make_bin_key(),
×
5390
                (draw_entity, draw_batch.main_entity),
×
5391
                InputUniformIndex::default(),
×
5392
                BinnedRenderPhaseType::NonMesh,
×
5393
                *change_tick,
×
5394
            );
5395
        }
5396
    }
5397
}
5398

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

5428
    trace!("queue_effects");
2,050✔
5429

5430
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5431
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5432
    // reuse stale data.
5433
    let next_change_tick = change_tick.get() + 1;
2,060✔
5434
    change_tick.set(next_change_tick);
2,060✔
5435

5436
    // If an image has changed, the GpuImage has (probably) changed
5437
    for event in &events.images {
1,057✔
5438
        match event {
5439
            AssetEvent::Added { .. } => None,
24✔
5440
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5441
            AssetEvent::Unused { .. } => None,
×
5442
            AssetEvent::Modified { id } => {
×
5443
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5444
                effect_bind_groups.images.remove(id)
×
5445
            }
5446
            AssetEvent::Removed { id } => {
3✔
5447
                trace!("Destroy bind group of removed image asset {:?}", id);
5✔
5448
                effect_bind_groups.images.remove(id)
9✔
5449
            }
5450
        };
5451
    }
5452

5453
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
3,080✔
5454
        // No spawners are active
5455
        return;
16✔
5456
    }
5457

5458
    // Loop over all 2D cameras/views that need to render effects
5459
    #[cfg(feature = "2d")]
5460
    {
5461
        #[cfg(feature = "trace")]
5462
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5463

5464
        let draw_effects_function_2d = read_params
5465
            .draw_functions_2d
5466
            .read()
5467
            .get_id::<DrawEffects>()
5468
            .unwrap();
5469

5470
        // Effects with full alpha blending
5471
        if !views.is_empty() {
5472
            trace!("Emit effect draw calls for alpha blended 2D views...");
2,028✔
5473
            emit_sorted_draw(
5474
                &views,
5475
                &mut transparent_2d_render_phases,
5476
                &mut view_entities,
5477
                &sorted_effect_batches,
5478
                &effect_draw_batches,
5479
                &mut render_pipeline,
5480
                specialized_render_pipelines.reborrow(),
5481
                &render_meshes,
5482
                &pipeline_cache,
5483
                |id, entity, draw_batch, _view| Transparent2d {
5484
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5485
                    entity,
×
5486
                    pipeline: id,
×
5487
                    draw_function: draw_effects_function_2d,
×
5488
                    batch_range: 0..1,
×
5489
                    extracted_index: 0, // ???
5490
                    extra_index: PhaseItemExtraIndex::None,
×
5491
                    indexed: true, // ???
5492
                },
5493
                #[cfg(feature = "3d")]
5494
                PipelineMode::Camera2d,
5495
            );
5496
        }
5497
    }
5498

5499
    // Loop over all 3D cameras/views that need to render effects
5500
    #[cfg(feature = "3d")]
5501
    {
5502
        #[cfg(feature = "trace")]
5503
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5504

5505
        // Effects with full alpha blending
5506
        if !views.is_empty() {
5507
            trace!("Emit effect draw calls for alpha blended 3D views...");
2,028✔
5508

5509
            let draw_effects_function_3d = read_params
5510
                .draw_functions_3d
5511
                .read()
5512
                .get_id::<DrawEffects>()
5513
                .unwrap();
5514

5515
            emit_sorted_draw(
5516
                &views,
5517
                &mut transparent_3d_render_phases,
5518
                &mut view_entities,
5519
                &sorted_effect_batches,
5520
                &effect_draw_batches,
5521
                &mut render_pipeline,
5522
                specialized_render_pipelines.reborrow(),
5523
                &render_meshes,
5524
                &pipeline_cache,
5525
                |id, entity, batch, view| Transparent3d {
5526
                    distance: view
1,014✔
5527
                        .rangefinder3d()
1,014✔
5528
                        .distance_translation(&batch.translation),
2,028✔
5529
                    pipeline: id,
1,014✔
5530
                    entity,
1,014✔
5531
                    draw_function: draw_effects_function_3d,
1,014✔
5532
                    batch_range: 0..1,
1,014✔
5533
                    extra_index: PhaseItemExtraIndex::None,
1,014✔
5534
                    indexed: true, // ???
5535
                },
5536
                #[cfg(feature = "2d")]
5537
                PipelineMode::Camera3d,
5538
            );
5539
        }
5540

5541
        // Effects with alpha mask
5542
        if !views.is_empty() {
5543
            #[cfg(feature = "trace")]
5544
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
1,014✔
5545

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

5548
            let draw_effects_function_alpha_mask = read_params
5549
                .draw_functions_alpha_mask
5550
                .read()
5551
                .get_id::<DrawEffects>()
5552
                .unwrap();
5553

5554
            emit_binned_draw(
5555
                &views,
5556
                &mut alpha_mask_3d_render_phases,
5557
                &mut view_entities,
5558
                &sorted_effect_batches,
5559
                &effect_draw_batches,
5560
                &mut render_pipeline,
5561
                specialized_render_pipelines.reborrow(),
5562
                &pipeline_cache,
5563
                &render_meshes,
5564
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
5565
                    pipeline: id,
×
5566
                    draw_function: draw_effects_function_alpha_mask,
×
5567
                    material_bind_group_index: None,
×
5568
                    vertex_slab: default(),
×
5569
                    index_slab: None,
×
5570
                },
5571
                // Unused for now
5572
                || OpaqueNoLightmap3dBinKey {
5573
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5574
                },
5575
                #[cfg(feature = "2d")]
5576
                PipelineMode::Camera3d,
5577
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5578
                &mut change_tick,
5579
            );
5580
        }
5581

5582
        // Opaque particles
5583
        if !views.is_empty() {
5584
            #[cfg(feature = "trace")]
5585
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
1,014✔
5586

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

5589
            let draw_effects_function_opaque = read_params
5590
                .draw_functions_opaque
5591
                .read()
5592
                .get_id::<DrawEffects>()
5593
                .unwrap();
5594

5595
            emit_binned_draw(
5596
                &views,
5597
                &mut opaque_3d_render_phases,
5598
                &mut view_entities,
5599
                &sorted_effect_batches,
5600
                &effect_draw_batches,
5601
                &mut render_pipeline,
5602
                specialized_render_pipelines.reborrow(),
5603
                &pipeline_cache,
5604
                &render_meshes,
5605
                |id, _batch, _view| Opaque3dBatchSetKey {
5606
                    pipeline: id,
×
5607
                    draw_function: draw_effects_function_opaque,
×
5608
                    material_bind_group_index: None,
×
5609
                    vertex_slab: default(),
×
5610
                    index_slab: None,
×
5611
                    lightmap_slab: None,
×
5612
                },
5613
                // Unused for now
5614
                || Opaque3dBinKey {
5615
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5616
                },
5617
                #[cfg(feature = "2d")]
5618
                PipelineMode::Camera3d,
5619
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5620
                &mut change_tick,
5621
            );
5622
        }
5623
    }
5624
}
5625

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

5649
    // Create the bind group for the camera/view parameters
5650
    // FIXME - Not here!
5651
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5652
        "hanabi:bind_group_camera_view",
5653
        &render_pipeline.view_layout,
5654
        &[
5655
            BindGroupEntry {
5656
                binding: 0,
5657
                resource: view_binding,
5658
            },
5659
            BindGroupEntry {
5660
                binding: 1,
5661
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5662
            },
5663
        ],
5664
    ));
5665

5666
    // Re-/allocate any GPU buffer if needed
5667
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5668
    // effect_bind_groups);
5669
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5670
    sort_bind_groups.prepare_buffers(&render_device);
5671
    if effects_meta
5672
        .dispatch_indirect_buffer
5673
        .prepare_buffers(&render_device)
5674
    {
5675
        // All those bind groups use the buffer so need to be re-created
5676
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
4✔
5677
        effect_bind_groups.particle_buffers.clear();
4✔
5678
    }
5679
}
5680

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

5711
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5712
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
3,090✔
5713
}
5714

5715
pub(crate) fn prepare_bind_groups(
1,030✔
5716
    mut effects_meta: ResMut<EffectsMeta>,
5717
    mut effect_cache: ResMut<EffectCache>,
5718
    mut event_cache: ResMut<EventCache>,
5719
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5720
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5721
    mut sort_bind_groups: ResMut<SortBindGroups>,
5722
    property_cache: Res<PropertyCache>,
5723
    sorted_effect_batched: Res<SortedEffectBatches>,
5724
    render_device: Res<RenderDevice>,
5725
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5726
    utils_pipeline: Res<UtilsPipeline>,
5727
    init_pipeline: Res<ParticlesInitPipeline>,
5728
    update_pipeline: Res<ParticlesUpdatePipeline>,
5729
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5730
    gpu_images: Res<RenderAssets<GpuImage>>,
5731
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5732
) {
5733
    // We can't simulate nor render anything without at least the spawner buffer
5734
    if effects_meta.spawner_buffer.is_empty() {
2,060✔
5735
        return;
16✔
5736
    }
5737
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
5738
        return;
×
5739
    };
5740

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

5746
    {
5747
        #[cfg(feature = "trace")]
5748
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
5749

5750
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5751
        // loop below. Also allows earlying out before doing any work in case some
5752
        // buffer is missing.
5753
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
5754
            return;
×
5755
        };
5756

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

5795
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5796
        // effects at once
5797
        effects_meta.indirect_metadata_bind_group = match (
5798
            effects_meta.effect_metadata_buffer.buffer(),
5799
            effects_meta.dispatch_indirect_buffer.buffer(),
5800
            effects_meta.draw_indirect_buffer.buffer(),
5801
        ) {
5802
            (
5803
                Some(effect_metadata_buffer),
1,014✔
5804
                Some(dispatch_indirect_buffer),
5805
                Some(draw_indirect_buffer),
5806
            ) => {
5807
                // Base bind group for indirect pass
5808
                Some(render_device.create_bind_group(
5809
                    "hanabi:bind_group:vfx_indirect:metadata@1",
5810
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
5811
                    &[
5812
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer :
5813
                        // array<u32>;
5814
                        BindGroupEntry {
5815
                            binding: 0,
5816
                            resource: effect_metadata_buffer.as_entire_binding(),
5817
                        },
5818
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer
5819
                        // : array<u32>;
5820
                        BindGroupEntry {
5821
                            binding: 1,
5822
                            resource: dispatch_indirect_buffer.as_entire_binding(),
5823
                        },
5824
                        // @group(1) @binding(2) var<storage, read_write> draw_indirect_buffer :
5825
                        // array<u32>;
5826
                        BindGroupEntry {
5827
                            binding: 2,
5828
                            resource: draw_indirect_buffer.as_entire_binding(),
5829
                        },
5830
                    ],
5831
                ))
5832
            }
5833

5834
            // Some buffer is not yet available, can't create the bind group
5835
            _ => None,
×
5836
        };
5837

5838
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5839
        // effects at once
5840
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
5841
            let bind_group = render_device.create_bind_group(
10✔
5842
                "hanabi:bind_group:vfx_indirect:spawner@2",
5843
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
6✔
5844
                &[
4✔
5845
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5846
                    BindGroupEntry {
4✔
5847
                        binding: 0,
4✔
5848
                        resource: BindingResource::Buffer(BufferBinding {
4✔
5849
                            buffer: &spawner_buffer,
4✔
5850
                            offset: 0,
4✔
5851
                            size: None,
4✔
5852
                        }),
5853
                    },
5854
                ],
5855
            );
5856

5857
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
5858
        }
5859
    }
5860

5861
    // Create the per-buffer bind groups
5862
    trace!("Create per-buffer bind groups...");
1,014✔
5863
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
1,014✔
5864
        #[cfg(feature = "trace")]
5865
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
5866

5867
        let Some(effect_buffer) = effect_buffer else {
1,014✔
5868
            trace!(
×
5869
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5870
                buffer_index
5871
            );
5872
            continue;
×
5873
        };
5874

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

5915
                BufferBindGroups { render }
2✔
5916
            });
5917
    }
5918

5919
    // Create bind groups for queued GPU buffer operations
5920
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5921

5922
    // Create the per-effect bind groups
5923
    let spawner_buffer_binding_size =
5924
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5925
    for effect_batch in sorted_effect_batched.iter() {
1,014✔
5926
        #[cfg(feature = "trace")]
5927
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
3,042✔
5928

5929
        // Create the property bind group @2 if needed
5930
        if let Some(property_key) = &effect_batch.property_key {
1,023✔
5931
            if let Err(err) = property_bind_groups.ensure_exists(
×
5932
                property_key,
5933
                &property_cache,
5934
                &spawner_buffer,
5935
                spawner_buffer_binding_size,
5936
                &render_device,
5937
            ) {
5938
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5939
                continue;
5940
            }
5941
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
3,015✔
5942
            &property_cache,
2,010✔
5943
            &spawner_buffer,
2,010✔
5944
            spawner_buffer_binding_size,
1,005✔
5945
            &render_device,
1,005✔
5946
        ) {
5947
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5948
            continue;
5949
        }
5950

5951
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5952
        // simulate particles.
5953
        if effect_cache
1,014✔
5954
            .create_particle_sim_bind_group(
5955
                effect_batch.buffer_index,
5956
                &render_device,
5957
                effect_batch.particle_layout.min_binding_size32(),
5958
                effect_batch.parent_min_binding_size,
5959
                effect_batch.parent_binding_source.as_ref(),
5960
            )
5961
            .is_err()
5962
        {
5963
            error!("No particle buffer allocated for effect batch.");
×
5964
            continue;
×
5965
        }
5966

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

6013
        // Bind group @3 of update pass
6014
        // FIXME - this is instance-dependent, not buffer-dependent#
6015
        {
6016
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
6017

6018
            let Some(update_metadata_layout) =
1,014✔
6019
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
6020
            else {
6021
                continue;
×
6022
            };
6023
            if effect_bind_groups
6024
                .get_or_create_update_metadata(
6025
                    effect_batch,
6026
                    &effects_meta.gpu_limits,
6027
                    &render_device,
6028
                    update_metadata_layout,
6029
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6030
                    event_cache.child_infos_buffer(),
6031
                    &effect_batch.child_event_buffers[..],
6032
                )
6033
                .is_err()
6034
            {
6035
                continue;
×
6036
            }
6037
        }
6038

6039
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
6040
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
6041

6042
            // Bind group @0 of sort-fill pass
6043
            let particle_buffer = effect_buffer.particle_buffer();
×
6044
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6045
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6046
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
6047
                &effect_batch.particle_layout,
×
6048
                particle_buffer,
×
6049
                indirect_index_buffer,
×
6050
                effect_metadata_buffer,
×
6051
            ) {
6052
                error!(
6053
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
6054
                    err
6055
                );
6056
                continue;
6057
            }
6058

6059
            // Bind group @0 of sort-copy pass
6060
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6061
            if let Err(err) = sort_bind_groups
×
6062
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
6063
            {
6064
                error!(
6065
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
6066
                    err
6067
                );
6068
                continue;
6069
            }
6070
        }
6071

6072
        // Ensure the particle texture(s) are available as GPU resources and that a bind
6073
        // group for them exists
6074
        // FIXME fix this insert+get below
6075
        if !effect_batch.texture_layout.layout.is_empty() {
1,014✔
6076
            // This should always be available, as this is cached into the render pipeline
6077
            // just before we start specializing it.
6078
            let Some(material_bind_group_layout) =
×
6079
                render_pipeline.get_material(&effect_batch.texture_layout)
×
6080
            else {
6081
                error!(
×
6082
                    "Failed to find material bind group layout for buffer #{}",
×
6083
                    effect_batch.buffer_index
6084
                );
6085
                continue;
×
6086
            };
6087

6088
            // TODO = move
6089
            let material = Material {
6090
                layout: effect_batch.texture_layout.clone(),
6091
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6092
            };
6093
            assert_eq!(material.layout.layout.len(), material.textures.len());
6094

6095
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6096
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6097
                trace!(
×
6098
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
6099
                    material
6100
                );
6101
                continue;
×
6102
            };
6103

6104
            effect_bind_groups
6105
                .material_bind_groups
6106
                .entry(material.clone())
6107
                .or_insert_with(|| {
×
6108
                    debug!("Creating material bind group for material {:?}", material);
×
6109
                    render_device.create_bind_group(
×
6110
                        &format!(
×
6111
                            "hanabi:material_bind_group_{}",
×
6112
                            material.layout.layout.len()
×
6113
                        )[..],
×
6114
                        material_bind_group_layout,
×
6115
                        &bind_group_entries[..],
×
6116
                    )
6117
                });
6118
        }
6119
    }
6120
}
6121

6122
type DrawEffectsSystemState = SystemState<(
6123
    SRes<EffectsMeta>,
6124
    SRes<EffectBindGroups>,
6125
    SRes<PipelineCache>,
6126
    SRes<RenderAssets<RenderMesh>>,
6127
    SRes<MeshAllocator>,
6128
    SQuery<Read<ViewUniformOffset>>,
6129
    SRes<SortedEffectBatches>,
6130
    SQuery<Read<EffectDrawBatch>>,
6131
)>;
6132

6133
/// Draw function for rendering all active effects for the current frame.
6134
///
6135
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6136
/// and the [`Transparent3d`] phase of the main 3D pass.
6137
pub(crate) struct DrawEffects {
6138
    params: DrawEffectsSystemState,
6139
}
6140

6141
impl DrawEffects {
6142
    pub fn new(world: &mut World) -> Self {
12✔
6143
        Self {
6144
            params: SystemState::new(world),
12✔
6145
        }
6146
    }
6147
}
6148

6149
/// Draw all particles of a single effect in view, in 2D or 3D.
6150
///
6151
/// FIXME: use pipeline ID to look up which group index it is.
6152
fn draw<'w>(
1,013✔
6153
    world: &'w World,
6154
    pass: &mut TrackedRenderPass<'w>,
6155
    view: Entity,
6156
    entity: (Entity, MainEntity),
6157
    pipeline_id: CachedRenderPipelineId,
6158
    params: &mut DrawEffectsSystemState,
6159
) {
6160
    let (
×
6161
        effects_meta,
1,013✔
6162
        effect_bind_groups,
1,013✔
6163
        pipeline_cache,
1,013✔
6164
        meshes,
1,013✔
6165
        mesh_allocator,
1,013✔
6166
        views,
1,013✔
6167
        sorted_effect_batches,
1,013✔
6168
        effect_draw_batches,
1,013✔
6169
    ) = params.get(world);
2,026✔
6170
    let view_uniform = views.get(view).unwrap();
5,065✔
6171
    let effects_meta = effects_meta.into_inner();
3,039✔
6172
    let effect_bind_groups = effect_bind_groups.into_inner();
3,039✔
6173
    let meshes = meshes.into_inner();
3,039✔
6174
    let mesh_allocator = mesh_allocator.into_inner();
3,039✔
6175
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
5,065✔
6176
    let effect_batch = sorted_effect_batches
3,039✔
6177
        .get(effect_draw_batch.effect_batch_index)
1,013✔
6178
        .unwrap();
6179

6180
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
3,039✔
UNCOV
6181
        return;
×
6182
    };
6183

6184
    trace!("render pass");
1,013✔
6185

6186
    pass.set_render_pipeline(pipeline);
×
6187

6188
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
1,013✔
6189
        return;
×
6190
    };
6191
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
1,013✔
6192
        return;
×
6193
    };
6194

6195
    // Vertex buffer containing the particle model to draw. Generally a quad.
6196
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6197
    // "base_vertex" in the indirect struct...
6198
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6199

6200
    // View properties (camera matrix, etc.)
6201
    pass.set_bind_group(
×
6202
        0,
6203
        effects_meta.view_bind_group.as_ref().unwrap(),
×
6204
        &[view_uniform.offset],
×
6205
    );
6206

6207
    // Particles buffer
6208
    let spawner_base = effect_batch.spawner_base;
×
6209
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
6210
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6211
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
2,026✔
6212
    pass.set_bind_group(
2,026✔
6213
        1,
6214
        effect_bind_groups
2,026✔
6215
            .particle_render(effect_batch.buffer_index)
2,026✔
6216
            .unwrap(),
1,013✔
6217
        &[spawner_offset],
1,013✔
6218
    );
6219

6220
    // Particle texture
6221
    // TODO = move
6222
    let material = Material {
6223
        layout: effect_batch.texture_layout.clone(),
2,026✔
6224
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
3,039✔
6225
    };
6226
    if !effect_batch.texture_layout.layout.is_empty() {
1,013✔
6227
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6228
            pass.set_bind_group(2, bind_group, &[]);
×
6229
        } else {
6230
            // Texture(s) not ready; skip this drawing for now
6231
            trace!(
×
6232
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
6233
                effect_batch.buffer_index,
×
6234
            );
6235
            return;
×
6236
        }
6237
    }
6238

6239
    let draw_indirect_index = effect_batch
1,013✔
6240
        .dispatch_buffer_indices
×
NEW
6241
        .draw_indirect_buffer_row_index
×
NEW
6242
        .get()
×
6243
        .0;
×
NEW
6244
    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
×
6245
    let draw_indirect_offset =
1,013✔
6246
        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
1,013✔
6247
    trace!(
1,013✔
6248
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
1,013✔
6249
            (effect_metadata_index={}, draw_indirect_offset={}B).",
1,013✔
6250
        effect_batch.slice.len(),
2,026✔
6251
        render_mesh.vertex_count,
×
6252
        effect_batch.buffer_index,
×
NEW
6253
        draw_indirect_index,
×
NEW
6254
        draw_indirect_offset,
×
6255
    );
6256

6257
    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
2,026✔
UNCOV
6258
        trace!(
×
NEW
6259
            "The draw indirect buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6260
            effect_batch.buffer_index,
×
6261
        );
6262
        return;
×
6263
    };
6264

6265
    match render_mesh.buffer_info {
×
6266
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
1,013✔
6267
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
1,013✔
6268
            else {
×
NEW
6269
                trace!(
×
NEW
6270
                    "The index buffer for indexed rendering is not ready for batch buf=#{}. Skipping draw call.",
×
NEW
6271
                    effect_batch.buffer_index,
×
6272
                );
UNCOV
6273
                return;
×
6274
            };
6275

6276
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
NEW
6277
            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
×
6278
        }
6279
        RenderMeshBufferInfo::NonIndexed => {
×
NEW
6280
            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
×
6281
        }
6282
    }
6283
}
6284

6285
#[cfg(feature = "2d")]
6286
impl Draw<Transparent2d> for DrawEffects {
6287
    fn draw<'w>(
×
6288
        &mut self,
6289
        world: &'w World,
6290
        pass: &mut TrackedRenderPass<'w>,
6291
        view: Entity,
6292
        item: &Transparent2d,
6293
    ) -> Result<(), DrawError> {
6294
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6295
        draw(
6296
            world,
×
6297
            pass,
×
6298
            view,
×
6299
            item.entity,
×
6300
            item.pipeline,
×
6301
            &mut self.params,
×
6302
        );
6303
        Ok(())
×
6304
    }
6305
}
6306

6307
#[cfg(feature = "3d")]
6308
impl Draw<Transparent3d> for DrawEffects {
6309
    fn draw<'w>(
1,013✔
6310
        &mut self,
6311
        world: &'w World,
6312
        pass: &mut TrackedRenderPass<'w>,
6313
        view: Entity,
6314
        item: &Transparent3d,
6315
    ) -> Result<(), DrawError> {
6316
        trace!("Draw<Transparent3d>: view={:?}", view);
2,026✔
6317
        draw(
6318
            world,
1,013✔
6319
            pass,
1,013✔
6320
            view,
1,013✔
6321
            item.entity,
1,013✔
6322
            item.pipeline,
1,013✔
6323
            &mut self.params,
1,013✔
6324
        );
6325
        Ok(())
1,013✔
6326
    }
6327
}
6328

6329
#[cfg(feature = "3d")]
6330
impl Draw<AlphaMask3d> for DrawEffects {
6331
    fn draw<'w>(
×
6332
        &mut self,
6333
        world: &'w World,
6334
        pass: &mut TrackedRenderPass<'w>,
6335
        view: Entity,
6336
        item: &AlphaMask3d,
6337
    ) -> Result<(), DrawError> {
6338
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6339
        draw(
6340
            world,
×
6341
            pass,
×
6342
            view,
×
6343
            item.representative_entity,
×
6344
            item.batch_set_key.pipeline,
×
6345
            &mut self.params,
×
6346
        );
6347
        Ok(())
×
6348
    }
6349
}
6350

6351
#[cfg(feature = "3d")]
6352
impl Draw<Opaque3d> for DrawEffects {
6353
    fn draw<'w>(
×
6354
        &mut self,
6355
        world: &'w World,
6356
        pass: &mut TrackedRenderPass<'w>,
6357
        view: Entity,
6358
        item: &Opaque3d,
6359
    ) -> Result<(), DrawError> {
6360
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6361
        draw(
6362
            world,
×
6363
            pass,
×
6364
            view,
×
6365
            item.representative_entity,
×
6366
            item.batch_set_key.pipeline,
×
6367
            &mut self.params,
×
6368
        );
6369
        Ok(())
×
6370
    }
6371
}
6372

6373
/// Render node to run the simulation sub-graph once per frame.
6374
///
6375
/// This node doesn't simulate anything by itself, but instead schedules the
6376
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6377
/// actual simulation.
6378
///
6379
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6380
/// renders all the views, such that rendered views have access to the
6381
/// just-simulated particles to render them.
6382
///
6383
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6384
pub(crate) struct VfxSimulateDriverNode;
6385

6386
impl Node for VfxSimulateDriverNode {
6387
    fn run(
1,030✔
6388
        &self,
6389
        graph: &mut RenderGraphContext,
6390
        _render_context: &mut RenderContext,
6391
        _world: &World,
6392
    ) -> Result<(), NodeRunError> {
6393
        graph.run_sub_graph(
2,060✔
6394
            crate::plugin::simulate_graph::HanabiSimulateGraph,
1,030✔
6395
            vec![],
1,030✔
6396
            None,
1,030✔
6397
        )?;
6398
        Ok(())
1,030✔
6399
    }
6400
}
6401

6402
#[derive(Debug, Clone, PartialEq, Eq)]
6403
enum HanabiPipelineId {
6404
    Invalid,
6405
    Cached(CachedComputePipelineId),
6406
}
6407

6408
pub(crate) enum ComputePipelineError {
6409
    Queued,
6410
    Creating,
6411
    Error,
6412
}
6413

6414
impl From<&CachedPipelineState> for ComputePipelineError {
6415
    fn from(value: &CachedPipelineState) -> Self {
×
6416
        match value {
×
6417
            CachedPipelineState::Queued => Self::Queued,
×
6418
            CachedPipelineState::Creating(_) => Self::Creating,
×
6419
            CachedPipelineState::Err(_) => Self::Error,
×
6420
            _ => panic!("Trying to convert Ok state to error."),
×
6421
        }
6422
    }
6423
}
6424

6425
pub(crate) struct HanabiComputePass<'a> {
6426
    /// Pipeline cache to fetch cached compute pipelines by ID.
6427
    pipeline_cache: &'a PipelineCache,
6428
    /// WGPU compute pass.
6429
    compute_pass: ComputePass<'a>,
6430
    /// Current pipeline (cached).
6431
    pipeline_id: HanabiPipelineId,
6432
}
6433

6434
impl<'a> Deref for HanabiComputePass<'a> {
6435
    type Target = ComputePass<'a>;
6436

6437
    fn deref(&self) -> &Self::Target {
×
6438
        &self.compute_pass
×
6439
    }
6440
}
6441

6442
impl DerefMut for HanabiComputePass<'_> {
6443
    fn deref_mut(&mut self) -> &mut Self::Target {
14,140✔
6444
        &mut self.compute_pass
14,140✔
6445
    }
6446
}
6447

6448
impl<'a> HanabiComputePass<'a> {
6449
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
4,056✔
6450
        Self {
6451
            pipeline_cache,
6452
            compute_pass,
6453
            pipeline_id: HanabiPipelineId::Invalid,
6454
        }
6455
    }
6456

6457
    pub fn set_cached_compute_pipeline(
3,028✔
6458
        &mut self,
6459
        pipeline_id: CachedComputePipelineId,
6460
    ) -> Result<(), ComputePipelineError> {
6461
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
6,056✔
6462
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
3,028✔
6463
            trace!("-> already set; skipped");
×
6464
            return Ok(());
×
6465
        }
6466
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
3,028✔
6467
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6468
            if let CachedPipelineState::Err(err) = state {
×
6469
                error!(
×
6470
                    "Failed to find compute pipeline #{}: {:?}",
×
6471
                    pipeline_id.id(),
×
6472
                    err
×
6473
                );
6474
            } else {
6475
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6476
            }
6477
            return Err(state.into());
×
6478
        };
6479
        self.compute_pass.set_pipeline(pipeline);
×
6480
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6481
        Ok(())
×
6482
    }
6483
}
6484

6485
/// Render node to run the simulation of all effects once per frame.
6486
///
6487
/// Runs inside the simulation sub-graph, looping over all extracted effect
6488
/// batches to simulate them.
6489
pub(crate) struct VfxSimulateNode {}
6490

6491
impl VfxSimulateNode {
6492
    /// Create a new node for simulating the effects of the given world.
6493
    pub fn new(_world: &mut World) -> Self {
3✔
6494
        Self {}
6495
    }
6496

6497
    /// Begin a new compute pass and return a wrapper with extra
6498
    /// functionalities.
6499
    pub fn begin_compute_pass<'encoder>(
4,056✔
6500
        &self,
6501
        label: &str,
6502
        pipeline_cache: &'encoder PipelineCache,
6503
        render_context: &'encoder mut RenderContext,
6504
    ) -> HanabiComputePass<'encoder> {
6505
        let compute_pass =
4,056✔
6506
            render_context
4,056✔
6507
                .command_encoder()
6508
                .begin_compute_pass(&ComputePassDescriptor {
8,112✔
6509
                    label: Some(label),
4,056✔
6510
                    timestamp_writes: None,
4,056✔
6511
                });
6512
        HanabiComputePass::new(pipeline_cache, compute_pass)
12,168✔
6513
    }
6514
}
6515

6516
impl Node for VfxSimulateNode {
6517
    fn input(&self) -> Vec<SlotInfo> {
3✔
6518
        vec![]
3✔
6519
    }
6520

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

6523
    fn run(
1,030✔
6524
        &self,
6525
        _graph: &mut RenderGraphContext,
6526
        render_context: &mut RenderContext,
6527
        world: &World,
6528
    ) -> Result<(), NodeRunError> {
6529
        trace!("VfxSimulateNode::run()");
2,050✔
6530

6531
        let pipeline_cache = world.resource::<PipelineCache>();
3,090✔
6532
        let effects_meta = world.resource::<EffectsMeta>();
3,090✔
6533
        let effect_bind_groups = world.resource::<EffectBindGroups>();
3,090✔
6534
        let property_bind_groups = world.resource::<PropertyBindGroups>();
3,090✔
6535
        let sort_bind_groups = world.resource::<SortBindGroups>();
3,090✔
6536
        let utils_pipeline = world.resource::<UtilsPipeline>();
3,090✔
6537
        let effect_cache = world.resource::<EffectCache>();
3,090✔
6538
        let event_cache = world.resource::<EventCache>();
3,090✔
6539
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
3,090✔
6540
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
3,090✔
6541
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
3,090✔
6542

6543
        // Make sure to schedule any buffer copy before accessing their content later in
6544
        // the GPU commands below.
6545
        {
6546
            let command_encoder = render_context.command_encoder();
4,120✔
6547
            effects_meta
2,060✔
6548
                .dispatch_indirect_buffer
2,060✔
6549
                .write_buffers(command_encoder);
3,090✔
6550
            effects_meta
2,060✔
6551
                .draw_indirect_buffer
2,060✔
6552
                .write_buffer(command_encoder);
3,090✔
6553
            effects_meta
2,060✔
6554
                .effect_metadata_buffer
2,060✔
6555
                .write_buffer(command_encoder);
3,090✔
6556
            event_cache.write_buffers(command_encoder);
4,120✔
6557
            sort_bind_groups.write_buffers(command_encoder);
2,060✔
6558
        }
6559

6560
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6561
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6562
        // the update pass of their parent effect during the previous frame.
6563
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
1,030✔
6564
            gpu_buffer_operations.dispatch(
6565
                *queue_index,
6566
                render_context,
6567
                utils_pipeline,
6568
                Some("hanabi:init_indirect_fill_dispatch"),
6569
            );
6570
        }
6571

6572
        // If there's no batch, there's nothing more to do. Avoid continuing because
6573
        // some GPU resources are missing, which is expected when there's no effect but
6574
        // is an error (and will log warnings/errors) otherwise.
6575
        if sorted_effect_batches.is_empty() {
2,060✔
6576
            return Ok(());
16✔
6577
        }
6578

6579
        // Compute init pass
6580
        {
6581
            trace!("init: loop over effect batches...");
1,014✔
6582

6583
            let mut compute_pass =
6584
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6585

6586
            // Bind group simparams@0 is common to everything, only set once per init pass
6587
            compute_pass.set_bind_group(
6588
                0,
6589
                effects_meta
6590
                    .indirect_sim_params_bind_group
6591
                    .as_ref()
6592
                    .unwrap(),
6593
                &[],
6594
            );
6595

6596
            // Dispatch init compute jobs for all batches
6597
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6598
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6599
                // batch. Note that this hopefully should have been skipped earlier.
6600
                {
6601
                    let use_indirect_dispatch = effect_batch
2,028✔
6602
                        .layout_flags
1,014✔
6603
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
1,014✔
6604
                    match effect_batch.spawn_info {
1,014✔
6605
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
1,014✔
6606
                            assert!(!use_indirect_dispatch);
6607
                            if total_spawn_count == 0 {
1,014✔
6608
                                continue;
14✔
6609
                            }
6610
                        }
6611
                        BatchSpawnInfo::GpuSpawner { .. } => {
6612
                            assert!(use_indirect_dispatch);
×
6613
                        }
6614
                    }
6615
                }
6616

6617
                // Fetch bind group particle@1
6618
                let Some(particle_bind_group) =
1,000✔
6619
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
1,000✔
6620
                else {
6621
                    error!(
×
6622
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6623
                        effect_batch.buffer_index
6624
                    );
6625
                    continue;
×
6626
                };
6627

6628
                // Fetch bind group metadata@3
6629
                let Some(metadata_bind_group) = effect_bind_groups
1,000✔
6630
                    .init_metadata_bind_groups
6631
                    .get(&effect_batch.buffer_index)
6632
                else {
6633
                    error!(
×
6634
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6635
                        effect_batch.buffer_index
6636
                    );
6637
                    continue;
×
6638
                };
6639

6640
                if compute_pass
6641
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6642
                    .is_err()
6643
                {
6644
                    continue;
×
6645
                }
6646

6647
                // Compute dynamic offsets
6648
                let spawner_base = effect_batch.spawner_base;
6649
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6650
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6651
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
2,000✔
6652
                let property_offset = effect_batch.property_offset;
2,000✔
6653

6654
                // Setup init pass
6655
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
3,000✔
6656
                let offsets = if let Some(property_offset) = property_offset {
2,001✔
6657
                    vec![spawner_offset, property_offset]
6658
                } else {
6659
                    vec![spawner_offset]
1,998✔
6660
                };
6661
                compute_pass.set_bind_group(
3,000✔
6662
                    2,
6663
                    property_bind_groups
2,000✔
6664
                        .get(effect_batch.property_key.as_ref())
4,000✔
6665
                        .unwrap(),
2,000✔
6666
                    &offsets[..],
1,000✔
6667
                );
6668
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
3,000✔
6669

6670
                // Dispatch init job
6671
                match effect_batch.spawn_info {
1,000✔
6672
                    // Indirect dispatch via GPU spawn events
6673
                    BatchSpawnInfo::GpuSpawner {
6674
                        init_indirect_dispatch_index,
×
6675
                        ..
6676
                    } => {
6677
                        assert!(effect_batch
×
6678
                            .layout_flags
×
6679
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6680

6681
                        // Note: the indirect offset of a dispatch workgroup only needs
6682
                        // 4-byte alignment
NEW
6683
                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
×
6684
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6685

6686
                        trace!(
×
6687
                            "record commands for indirect init pipeline of effect {:?} \
×
6688
                                init_indirect_dispatch_index={} \
×
6689
                                indirect_offset={} \
×
6690
                                spawner_base={} \
×
6691
                                spawner_offset={} \
×
6692
                                property_key={:?}...",
×
6693
                            effect_batch.handle,
6694
                            init_indirect_dispatch_index,
6695
                            indirect_offset,
6696
                            spawner_base,
6697
                            spawner_offset,
6698
                            effect_batch.property_key,
6699
                        );
6700

6701
                        compute_pass.dispatch_workgroups_indirect(
×
6702
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6703
                            indirect_offset,
×
6704
                        );
6705
                    }
6706

6707
                    // Direct dispatch via CPU spawn count
6708
                    BatchSpawnInfo::CpuSpawner {
6709
                        total_spawn_count: spawn_count,
1,000✔
6710
                    } => {
6711
                        assert!(!effect_batch
6712
                            .layout_flags
6713
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
6714

6715
                        const WORKGROUP_SIZE: u32 = 64;
6716
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
1,000✔
6717

6718
                        trace!(
6719
                            "record commands for init pipeline of effect {:?} \
1,000✔
6720
                                (spawn {} particles => {} workgroups) spawner_base={} \
1,000✔
6721
                                spawner_offset={} \
1,000✔
6722
                                property_key={:?}...",
1,000✔
6723
                            effect_batch.handle,
6724
                            spawn_count,
6725
                            workgroup_count,
6726
                            spawner_base,
6727
                            spawner_offset,
6728
                            effect_batch.property_key,
6729
                        );
6730

6731
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6732
                    }
6733
                }
6734

6735
                trace!("init compute dispatched");
2,000✔
6736
            }
6737
        }
6738

6739
        // Compute indirect dispatch pass
6740
        if effects_meta.spawner_buffer.buffer().is_some()
1,014✔
6741
            && !effects_meta.spawner_buffer.is_empty()
1,014✔
6742
            && effects_meta.indirect_metadata_bind_group.is_some()
1,014✔
6743
            && effects_meta.indirect_sim_params_bind_group.is_some()
2,028✔
6744
        {
6745
            // Only start a compute pass if there's an effect; makes things clearer in
6746
            // debugger.
6747
            let mut compute_pass =
1,014✔
6748
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
5,070✔
6749

6750
            // Dispatch indirect dispatch compute job
6751
            trace!("record commands for indirect dispatch pipeline...");
2,028✔
6752

6753
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
2,028✔
6754
            if has_gpu_spawn_events {
1,014✔
6755
                if let Some(indirect_child_info_buffer_bind_group) =
×
6756
                    event_cache.indirect_child_info_buffer_bind_group()
×
6757
                {
6758
                    assert!(has_gpu_spawn_events);
6759
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6760
                } else {
6761
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6762
                    // render_context
6763
                    //     .command_encoder()
6764
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6765
                    // FIXME - Bevy doesn't allow returning custom errors here...
6766
                    return Ok(());
×
6767
                }
6768
            }
6769

6770
            if compute_pass
1,014✔
6771
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
6772
                .is_err()
6773
            {
6774
                // FIXME - Bevy doesn't allow returning custom errors here...
6775
                return Ok(());
×
6776
            }
6777

6778
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6779
            // the size exluding gaps!");
6780
            const WORKGROUP_SIZE: u32 = 64;
6781
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6782
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6783
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6784

6785
            // Setup vfx_indirect pass
6786
            compute_pass.set_bind_group(
6787
                0,
6788
                effects_meta
6789
                    .indirect_sim_params_bind_group
6790
                    .as_ref()
6791
                    .unwrap(),
6792
                &[],
6793
            );
6794
            compute_pass.set_bind_group(
6795
                1,
6796
                // FIXME - got some unwrap() panic here, investigate... possibly race
6797
                // condition!
6798
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6799
                &[],
6800
            );
6801
            compute_pass.set_bind_group(
6802
                2,
6803
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6804
                &[],
6805
            );
6806
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6807
            trace!(
6808
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
1,014✔
6809
                total_effect_count,
6810
                workgroup_count
6811
            );
6812
        }
6813

6814
        // Compute update pass
6815
        {
6816
            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
2,028✔
UNCOV
6817
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6818
                render_context
×
6819
                    .command_encoder()
6820
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6821
                // FIXME - Bevy doesn't allow returning custom errors here...
6822
                return Ok(());
×
6823
            };
6824

6825
            let mut compute_pass =
6826
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6827

6828
            // Bind group simparams@0 is common to everything, only set once per update pass
6829
            compute_pass.set_bind_group(
6830
                0,
6831
                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
6832
                &[],
6833
            );
6834

6835
            // Dispatch update compute jobs
6836
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6837
                // Fetch bind group particle@1
6838
                let Some(particle_bind_group) =
1,014✔
6839
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
2,028✔
6840
                else {
6841
                    error!(
×
6842
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6843
                        effect_batch.buffer_index
6844
                    );
6845
                    continue;
×
6846
                };
6847

6848
                // Fetch bind group metadata@3
6849
                let Some(metadata_bind_group) = effect_bind_groups
1,014✔
6850
                    .update_metadata_bind_groups
6851
                    .get(&effect_batch.buffer_index)
6852
                else {
6853
                    error!(
×
6854
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6855
                        effect_batch.buffer_index
6856
                    );
6857
                    continue;
×
6858
                };
6859

6860
                // Fetch compute pipeline
6861
                if compute_pass
6862
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6863
                    .is_err()
6864
                {
6865
                    continue;
×
6866
                }
6867

6868
                // Compute dynamic offsets
6869
                let spawner_index = effect_batch.spawner_base;
6870
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6871
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6872
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
1,014✔
6873
                let property_offset = effect_batch.property_offset;
6874

6875
                trace!(
6876
                    "record commands for update pipeline of effect {:?} spawner_base={}",
1,014✔
6877
                    effect_batch.handle,
6878
                    spawner_index,
6879
                );
6880

6881
                // Setup update pass
6882
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6883
                let offsets = if let Some(property_offset) = property_offset {
9✔
6884
                    vec![spawner_offset, property_offset]
6885
                } else {
6886
                    vec![spawner_offset]
2,010✔
6887
                };
6888
                compute_pass.set_bind_group(
6889
                    2,
6890
                    property_bind_groups
6891
                        .get(effect_batch.property_key.as_ref())
6892
                        .unwrap(),
6893
                    &offsets[..],
6894
                );
6895
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6896

6897
                // Dispatch update job
6898
                let dispatch_indirect_offset = effect_batch
6899
                    .dispatch_buffer_indices
6900
                    .update_dispatch_indirect_buffer_row_index
6901
                    * 12;
6902
                trace!(
6903
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
1,014✔
6904
                    indirect_buffer,
6905
                    dispatch_indirect_offset,
6906
                );
6907
                compute_pass
6908
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6909

6910
                trace!("update compute dispatched");
1,014✔
6911
            }
6912
        }
6913

6914
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6915
        // batch of particles which needs sorting, based on the actual number of alive
6916
        // particles in the batch after their update in the compute update pass. Since
6917
        // particles may die during update, this may be different from the number of
6918
        // particles updated.
6919
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
1,014✔
6920
            gpu_buffer_operations.dispatch(
6921
                *queue_index,
6922
                render_context,
6923
                utils_pipeline,
6924
                Some("hanabi:sort_fill_dispatch"),
6925
            );
6926
        }
6927

6928
        // Compute sort pass
6929
        {
6930
            let mut compute_pass =
6931
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6932

6933
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6934
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6935

6936
            // Loop on batches and find those which need sorting
6937
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6938
                trace!("Processing effect batch for sorting...");
2,028✔
6939
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
1,014✔
6940
                    continue;
1,014✔
6941
                }
6942
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6943
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6944

6945
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6946
                    warn!("Missing sort-fill effect buffer.");
×
6947
                    continue;
×
6948
                };
6949

6950
                let indirect_dispatch_index = *effect_batch
6951
                    .sort_fill_indirect_dispatch_index
6952
                    .as_ref()
6953
                    .unwrap();
6954
                let indirect_offset =
6955
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6956

6957
                // Fill the sort buffer with the key-value pairs to sort
6958
                {
6959
                    compute_pass.push_debug_group("hanabi:sort_fill");
6960

6961
                    // Fetch compute pipeline
6962
                    let Some(pipeline_id) =
×
6963
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6964
                    else {
6965
                        warn!("Missing sort-fill pipeline.");
×
6966
                        continue;
×
6967
                    };
6968
                    if compute_pass
6969
                        .set_cached_compute_pipeline(pipeline_id)
6970
                        .is_err()
6971
                    {
6972
                        compute_pass.pop_debug_group();
×
6973
                        // FIXME - Bevy doesn't allow returning custom errors here...
6974
                        return Ok(());
×
6975
                    }
6976

6977
                    // Bind group sort_fill@0
6978
                    let particle_buffer = effect_buffer.particle_buffer();
6979
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
6980
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6981
                        particle_buffer.id(),
6982
                        indirect_index_buffer.id(),
6983
                        effect_metadata_buffer.id(),
6984
                    ) else {
6985
                        warn!("Missing sort-fill bind group.");
×
6986
                        continue;
×
6987
                    };
6988
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6989
                    let indirect_index_offset =
6990
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6991
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6992
                        effect_batch
6993
                            .dispatch_buffer_indices
6994
                            .effect_metadata_buffer_table_id
6995
                            .0,
6996
                    ) as u32;
6997
                    compute_pass.set_bind_group(
6998
                        0,
6999
                        bind_group,
7000
                        &[
7001
                            particle_offset,
7002
                            indirect_index_offset,
7003
                            effect_metadata_offset,
7004
                        ],
7005
                    );
7006

7007
                    compute_pass
7008
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7009
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
7010

7011
                    compute_pass.pop_debug_group();
7012
                }
7013

7014
                // Do the actual sort
7015
                {
7016
                    compute_pass.push_debug_group("hanabi:sort");
7017

7018
                    if compute_pass
7019
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
7020
                        .is_err()
7021
                    {
7022
                        compute_pass.pop_debug_group();
×
7023
                        // FIXME - Bevy doesn't allow returning custom errors here...
7024
                        return Ok(());
×
7025
                    }
7026

7027
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
7028
                    compute_pass
7029
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7030
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7031

7032
                    compute_pass.pop_debug_group();
7033
                }
7034

7035
                // Copy the sorted particle indices back into the indirect index buffer, where
7036
                // the render pass will read them.
7037
                {
7038
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7039

7040
                    // Fetch compute pipeline
7041
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7042
                    if compute_pass
7043
                        .set_cached_compute_pipeline(pipeline_id)
7044
                        .is_err()
7045
                    {
7046
                        compute_pass.pop_debug_group();
×
7047
                        // FIXME - Bevy doesn't allow returning custom errors here...
7048
                        return Ok(());
7049
                    }
7050

7051
                    // Bind group sort_copy@0
7052
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
7053
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
7054
                        indirect_index_buffer.id(),
7055
                        effect_metadata_buffer.id(),
7056
                    ) else {
7057
                        warn!("Missing sort-copy bind group.");
×
7058
                        continue;
×
7059
                    };
7060
                    let indirect_index_offset = effect_batch.slice.start;
7061
                    let effect_metadata_offset =
7062
                        effects_meta.effect_metadata_buffer.dynamic_offset(
7063
                            effect_batch
7064
                                .dispatch_buffer_indices
7065
                                .effect_metadata_buffer_table_id,
7066
                        );
7067
                    compute_pass.set_bind_group(
7068
                        0,
7069
                        bind_group,
7070
                        &[indirect_index_offset, effect_metadata_offset],
7071
                    );
7072

7073
                    compute_pass
7074
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7075
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7076

7077
                    compute_pass.pop_debug_group();
7078
                }
7079
            }
7080
        }
7081

7082
        Ok(())
1,014✔
7083
    }
7084
}
7085

7086
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7087
    fn from(layout_flags: LayoutFlags) -> Self {
3,042✔
7088
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
6,084✔
7089
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7090
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
3,042✔
7091
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7092
        } else {
7093
            ParticleRenderAlphaMaskPipelineKey::Blend
3,042✔
7094
        }
7095
    }
7096
}
7097

7098
#[cfg(test)]
7099
mod tests {
7100
    use super::*;
7101

7102
    #[test]
7103
    fn layout_flags() {
7104
        let flags = LayoutFlags::default();
7105
        assert_eq!(flags, LayoutFlags::NONE);
7106
    }
7107

7108
    #[cfg(feature = "gpu_tests")]
7109
    #[test]
7110
    fn gpu_limits() {
7111
        use crate::test_utils::MockRenderer;
7112

7113
        let renderer = MockRenderer::new();
7114
        let device = renderer.device();
7115
        let limits = GpuLimits::from_device(&device);
7116

7117
        // assert!(limits.storage_buffer_align().get() >= 1);
7118
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7119
    }
7120

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

7126
        let renderer = MockRenderer::new();
7127
        let device = renderer.device();
7128
        let render_queue = renderer.queue();
7129

7130
        let mut world = World::new();
7131
        world.insert_resource(device.clone());
7132
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7133

7134
        let src_buffer = device.create_buffer(&BufferDescriptor {
7135
            label: None,
7136
            size: 256,
7137
            usage: BufferUsages::STORAGE,
7138
            mapped_at_creation: false,
7139
        });
7140
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7141
            label: None,
7142
            size: 256,
7143
            usage: BufferUsages::STORAGE,
7144
            mapped_at_creation: false,
7145
        });
7146

7147
        // Two consecutive ops can be merged. This includes having contiguous slices
7148
        // both in source and destination.
7149
        buffer_ops.begin_frame();
7150
        {
7151
            let mut q = InitFillDispatchQueue::default();
7152
            q.enqueue(0, 0);
7153
            assert_eq!(q.queue.len(), 1);
7154
            q.enqueue(1, 1);
7155
            // Ops are not batched yet
7156
            assert_eq!(q.queue.len(), 2);
7157
            // On submit, the ops get batched together
7158
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7159
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7160
        }
7161
        buffer_ops.end_frame(&device, &render_queue);
7162

7163
        // Even if out of order, the init fill dispatch ops are batchable. Here the
7164
        // offsets are enqueued inverted.
7165
        buffer_ops.begin_frame();
7166
        {
7167
            let mut q = InitFillDispatchQueue::default();
7168
            q.enqueue(1, 1);
7169
            assert_eq!(q.queue.len(), 1);
7170
            q.enqueue(0, 0);
7171
            // Ops are not batched yet
7172
            assert_eq!(q.queue.len(), 2);
7173
            // On submit, the ops get batched together
7174
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7175
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7176
        }
7177
        buffer_ops.end_frame(&device, &render_queue);
7178

7179
        // However, both the source and destination need to be contiguous at the same
7180
        // time. Here they are mixed so we can't batch.
7181
        buffer_ops.begin_frame();
7182
        {
7183
            let mut q = InitFillDispatchQueue::default();
7184
            q.enqueue(0, 1);
7185
            assert_eq!(q.queue.len(), 1);
7186
            q.enqueue(1, 0);
7187
            // Ops are not batched yet
7188
            assert_eq!(q.queue.len(), 2);
7189
            // On submit, the ops cannot get batched together
7190
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7191
            assert_eq!(buffer_ops.args_buffer.len(), 2);
7192
        }
7193
        buffer_ops.end_frame(&device, &render_queue);
7194
    }
7195
}
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