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

djeedai / bevy_hanabi / 17622440846

10 Sep 2025 05:55PM UTC coverage: 66.033% (-0.6%) from 66.641%
17622440846

push

github

web-flow
Fixes for rustc v1.89 (#494)

Works around a bug in `encase` :
https://github.com/teoxoy/encase/issues/95

9 of 17 new or added lines in 7 files covered. (52.94%)

133 existing lines in 10 files now uncovered.

4829 of 7313 relevant lines covered (66.03%)

437.02 hits per line

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

60.22
/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,
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.
NEW
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✔
UNCOV
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
}
362

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

380
impl Default for GpuDispatchIndirect {
381
    fn default() -> Self {
×
382
        Self { x: 0, y: 1, z: 1 }
383
    }
384
}
385

386
/// Stores metadata about each particle effect.
387
///
388
/// This is written by the CPU and read by the GPU.
389
#[repr(C)]
390
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
391
pub struct GpuEffectMetadata {
392
    /// The number of vertices in the mesh, if non-indexed; if indexed, the
393
    /// number of indices in the mesh.
394
    pub vertex_or_index_count: u32,
395
    /// The number of instances to render.
396
    pub instance_count: u32,
397
    /// The first index to render, if the mesh is indexed; the offset of the
398
    /// first vertex, if the mesh is non-indexed.
399
    pub first_index_or_vertex_offset: u32,
400
    /// The offset of the first vertex, if the mesh is indexed; the first
401
    /// instance to render, if the mesh is non-indexed.
402
    pub vertex_offset_or_base_instance: i32,
403
    /// The first instance to render, if indexed; unused if non-indexed.
404
    pub base_instance: u32,
405

406
    // Additional data not part of the required draw indirect args
407
    /// Number of alive particles.
408
    pub alive_count: u32,
409
    /// Cached value of `alive_count` to cap threads in update pass.
410
    pub max_update: u32,
411
    /// Number of dead particles.
412
    pub dead_count: u32,
413
    /// Cached value of `dead_count` to cap threads in init pass.
414
    pub max_spawn: u32,
415
    /// Index of the ping buffer for particle indices. Init and update compute
416
    /// passes always write into the ping buffer and read from the pong buffer.
417
    /// The buffers are swapped (ping = 1 - ping) during the indirect dispatch.
418
    pub ping: u32,
419
    /// Index of the [`GpuDispatchIndirect`] struct inside the global
420
    /// [`EffectsMeta::dispatch_indirect_buffer`].
421
    pub indirect_dispatch_index: u32,
422
    /// Index of the [`GpuRenderIndirect`] struct inside the global
423
    /// [`EffectsMeta::render_group_dispatch_buffer`].
424
    pub indirect_render_index: u32,
425
    /// Offset (in u32 count) of the init indirect dispatch struct inside its
426
    /// buffer. This avoids having to align those 16-byte structs to the GPU
427
    /// alignment (at least 32 bytes, even 256 bytes on some).
428
    pub init_indirect_dispatch_index: u32,
429
    /// Index of this effect into its parent's ChildInfo array
430
    /// ([`EffectChildren::effect_cache_ids`] and its associated GPU
431
    /// array). This starts at zero for the first child of each effect, and is
432
    /// only unique per parent, not globally. Only available if this effect is a
433
    /// child of another effect (i.e. if it has a parent).
434
    pub local_child_index: u32,
435
    /// For children, global index of the ChildInfo into the shared array.
436
    pub global_child_index: u32,
437
    /// For parents, base index of the their first ChildInfo into the shared
438
    /// array.
439
    pub base_child_index: u32,
440

441
    /// Particle stride, in number of u32.
442
    pub particle_stride: u32,
443
    /// Offset from the particle start to the first sort key, in number of u32.
444
    pub sort_key_offset: u32,
445
    /// Offset from the particle start to the second sort key, in number of u32.
446
    pub sort_key2_offset: u32,
447

448
    /// Atomic counter incremented each time a particle spawns. Useful for
449
    /// things like RIBBON_ID or any other use where a unique value is needed.
450
    /// The value loops back after some time, but unless some particle lives
451
    /// forever there's little chance of repetition.
452
    pub particle_counter: u32,
453
}
454

455
/// Single init fill dispatch item in an [`InitFillDispatchQueue`].
456
#[derive(Debug)]
457
pub(super) struct InitFillDispatchItem {
458
    /// Index of the source [`GpuChildInfo`] entry to read the event count from.
459
    pub global_child_index: u32,
460
    /// Index of the [`GpuDispatchIndirect`] entry to write the workgroup count
461
    /// to.
462
    pub dispatch_indirect_index: u32,
463
}
464

465
/// Queue of fill dispatch operations for the init indirect pass.
466
///
467
/// The queue stores the init fill dispatch operations for the current frame,
468
/// without the reference to the source and destination buffers, which may be
469
/// reallocated later in the frame. This allows enqueuing operations during the
470
/// prepare rendering phase, while deferring GPU buffer (re-)allocation to a
471
/// later stage.
472
#[derive(Debug, Default, Resource)]
473
pub(super) struct InitFillDispatchQueue {
474
    queue: Vec<InitFillDispatchItem>,
475
    submitted_queue_index: Option<u32>,
476
}
477

478
impl InitFillDispatchQueue {
479
    /// Clear the queue.
480
    #[inline]
481
    pub fn clear(&mut self) {
1,030✔
482
        self.queue.clear();
2,060✔
483
        self.submitted_queue_index = None;
1,030✔
484
    }
485

486
    /// Check if the queue is empty.
487
    #[inline]
488
    pub fn is_empty(&self) -> bool {
1,030✔
489
        self.queue.is_empty()
2,060✔
490
    }
491

492
    /// Enqueue a new operation.
493
    #[inline]
494
    pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
6✔
495
        self.queue.push(InitFillDispatchItem {
18✔
496
            global_child_index,
6✔
497
            dispatch_indirect_index,
6✔
498
        });
499
    }
500

501
    /// Submit pending operations for this frame.
502
    pub fn submit(
3✔
503
        &mut self,
504
        src_buffer: &Buffer,
505
        dst_buffer: &Buffer,
506
        gpu_buffer_operations: &mut GpuBufferOperations,
507
    ) {
508
        if self.queue.is_empty() {
6✔
509
            return;
×
510
        }
511

512
        // Sort by source. We can only batch if the destination is also contiguous, so
513
        // we can check with a linear walk if the source is already sorted.
514
        self.queue
515
            .sort_unstable_by_key(|item| item.global_child_index);
516

517
        let mut fill_queue = GpuBufferOperationQueue::new();
518

519
        // Batch and schedule all init indirect dispatch operations
520
        let mut src_start = self.queue[0].global_child_index;
521
        let mut dst_start = self.queue[0].dispatch_indirect_index;
522
        let mut src_end = src_start + 1;
523
        let mut dst_end = dst_start + 1;
524
        let src_stride = GpuChildInfo::min_size().get() as u32 / 4;
525
        let dst_stride = GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4;
526
        for i in 1..self.queue.len() {
3✔
527
            let InitFillDispatchItem {
528
                global_child_index: src,
3✔
529
                dispatch_indirect_index: dst,
3✔
530
            } = self.queue[i];
3✔
531
            if src != src_end || dst != dst_end {
6✔
532
                let count = src_end - src_start;
1✔
533
                debug_assert_eq!(count, dst_end - dst_start);
534
                let args = GpuBufferOperationArgs {
535
                    src_offset: src_start * src_stride + 1,
536
                    src_stride,
537
                    dst_offset: dst_start * dst_stride,
538
                    dst_stride,
539
                    count,
540
                };
541
                trace!(
542
                "enqueue_init_fill(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
×
543
                src_start,
544
                dst_start,
545
                args,
546
                src_buffer.id(),
×
547
                dst_buffer.id(),
×
548
            );
549
                fill_queue.enqueue(
550
                    GpuBufferOperationType::FillDispatchArgs,
551
                    args,
552
                    src_buffer.clone(),
553
                    0,
554
                    None,
555
                    dst_buffer.clone(),
556
                    0,
557
                    None,
558
                );
559
                src_start = src;
560
                dst_start = dst;
561
            }
562
            src_end = src + 1;
3✔
563
            dst_end = dst + 1;
564
        }
565
        if src_start != src_end || dst_start != dst_end {
3✔
566
            let count = src_end - src_start;
3✔
567
            debug_assert_eq!(count, dst_end - dst_start);
568
            let args = GpuBufferOperationArgs {
569
                src_offset: src_start * src_stride + 1,
6✔
570
                src_stride,
571
                dst_offset: dst_start * dst_stride,
6✔
572
                dst_stride,
573
                count,
574
            };
575
            trace!(
3✔
576
            "IFDA::submit(): src:global_child_index={} dst:init_indirect_dispatch_index={} args={:?} src_buffer={:?} dst_buffer={:?}",
×
577
            src_start,
578
            dst_start,
579
            args,
580
            src_buffer.id(),
×
581
            dst_buffer.id(),
×
582
        );
583
            fill_queue.enqueue(
6✔
584
                GpuBufferOperationType::FillDispatchArgs,
3✔
585
                args,
3✔
586
                src_buffer.clone(),
6✔
587
                0,
588
                None,
3✔
589
                dst_buffer.clone(),
6✔
590
                0,
591
                None,
3✔
592
            );
593
        }
594

595
        debug_assert!(self.submitted_queue_index.is_none());
3✔
596
        if !fill_queue.operation_queue.is_empty() {
6✔
597
            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
3✔
598
        }
599
    }
600
}
601

602
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
603
/// shader.
604
#[derive(Resource)]
605
pub(crate) struct DispatchIndirectPipeline {
606
    /// Layout of bind group sim_params@0.
607
    sim_params_bind_group_layout: BindGroupLayout,
608
    /// Layout of bind group effect_metadata@1.
609
    effect_metadata_bind_group_layout: BindGroupLayout,
610
    /// Layout of bind group spawner@2.
611
    spawner_bind_group_layout: BindGroupLayout,
612
    /// Layout of bind group child_infos@3.
613
    child_infos_bind_group_layout: BindGroupLayout,
614
    /// Shader when no GPU events are used (no bind group @3).
615
    indirect_shader_noevent: Handle<Shader>,
616
    /// Shader when GPU events are used (bind group @3 present).
617
    indirect_shader_events: Handle<Shader>,
618
}
619

620
impl FromWorld for DispatchIndirectPipeline {
621
    fn from_world(world: &mut World) -> Self {
3✔
622
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
623

624
        // Copy the indirect pipeline shaders to self, because we can't access anything
625
        // else during pipeline specialization.
626
        let (indirect_shader_noevent, indirect_shader_events) = {
9✔
627
            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
15✔
628
            (
629
                effects_meta.indirect_shader_noevent.clone(),
9✔
630
                effects_meta.indirect_shader_events.clone(),
3✔
631
            )
632
        };
633

634
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
6✔
635
        let render_effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
9✔
636
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
9✔
637

638
        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
639
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
7✔
640
        let sim_params_bind_group_layout = render_device.create_bind_group_layout(
9✔
641
            "hanabi:bind_group_layout:dispatch_indirect:sim_params",
642
            &[BindGroupLayoutEntry {
3✔
643
                binding: 0,
3✔
644
                visibility: ShaderStages::COMPUTE,
3✔
645
                ty: BindingType::Buffer {
3✔
646
                    ty: BufferBindingType::Uniform,
3✔
647
                    has_dynamic_offset: false,
3✔
648
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
649
                },
650
                count: None,
3✔
651
            }],
652
        );
653

654
        trace!(
3✔
655
            "GpuEffectMetadata: min_size={} padded_size={}",
2✔
656
            GpuEffectMetadata::min_size(),
2✔
657
            render_effect_metadata_size,
658
        );
659
        let effect_metadata_bind_group_layout = render_device.create_bind_group_layout(
9✔
660
            "hanabi:bind_group_layout:dispatch_indirect:effect_metadata@1",
661
            &[
3✔
662
                // @group(0) @binding(0) var<storage, read_write> effect_metadata_buffer :
663
                // array<u32>;
664
                BindGroupLayoutEntry {
6✔
665
                    binding: 0,
6✔
666
                    visibility: ShaderStages::COMPUTE,
6✔
667
                    ty: BindingType::Buffer {
6✔
668
                        ty: BufferBindingType::Storage { read_only: false },
6✔
669
                        has_dynamic_offset: false,
6✔
670
                        min_binding_size: Some(render_effect_metadata_size),
6✔
671
                    },
672
                    count: None,
6✔
673
                },
674
                // @group(0) @binding(2) var<storage, read_write> dispatch_indirect_buffer :
675
                // array<u32>;
676
                BindGroupLayoutEntry {
3✔
677
                    binding: 1,
3✔
678
                    visibility: ShaderStages::COMPUTE,
3✔
679
                    ty: BindingType::Buffer {
3✔
680
                        ty: BufferBindingType::Storage { read_only: false },
6✔
681
                        has_dynamic_offset: false,
3✔
682
                        min_binding_size: Some(
3✔
683
                            NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap(),
6✔
684
                        ),
685
                    },
686
                    count: None,
3✔
687
                },
688
            ],
689
        );
690

691
        // @group(2) @binding(0) var<storage, read_write> spawner_buffer :
692
        // array<Spawner>;
693
        let spawner_bind_group_layout = render_device.create_bind_group_layout(
9✔
694
            "hanabi:bind_group_layout:dispatch_indirect:spawner@2",
695
            &[BindGroupLayoutEntry {
3✔
696
                binding: 0,
3✔
697
                visibility: ShaderStages::COMPUTE,
3✔
698
                ty: BindingType::Buffer {
3✔
699
                    ty: BufferBindingType::Storage { read_only: false },
3✔
700
                    has_dynamic_offset: false,
3✔
701
                    min_binding_size: Some(spawner_min_binding_size),
3✔
702
                },
703
                count: None,
3✔
704
            }],
705
        );
706

707
        // @group(3) @binding(0) var<storage, read_write> child_info_buffer :
708
        // ChildInfoBuffer;
709
        let child_infos_bind_group_layout = render_device.create_bind_group_layout(
9✔
710
            "hanabi:bind_group_layout:dispatch_indirect:child_infos",
711
            &[BindGroupLayoutEntry {
3✔
712
                binding: 0,
3✔
713
                visibility: ShaderStages::COMPUTE,
3✔
714
                ty: BindingType::Buffer {
3✔
715
                    ty: BufferBindingType::Storage { read_only: false },
3✔
716
                    has_dynamic_offset: false,
3✔
717
                    min_binding_size: Some(GpuChildInfo::min_size()),
3✔
718
                },
719
                count: None,
3✔
720
            }],
721
        );
722

723
        Self {
724
            sim_params_bind_group_layout,
725
            effect_metadata_bind_group_layout,
726
            spawner_bind_group_layout,
727
            child_infos_bind_group_layout,
728
            indirect_shader_noevent,
729
            indirect_shader_events,
730
        }
731
    }
732
}
733

734
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
735
pub(crate) struct DispatchIndirectPipelineKey {
736
    /// True if any allocated effect uses GPU spawn events. In that case, the
737
    /// pipeline is specialized to clear all GPU events each frame after the
738
    /// indirect init pass consumed them to spawn particles, and before the
739
    /// update pass optionally produce more events.
740
    /// Key: HAS_GPU_SPAWN_EVENTS
741
    has_events: bool,
742
}
743

744
impl SpecializedComputePipeline for DispatchIndirectPipeline {
745
    type Key = DispatchIndirectPipelineKey;
746

747
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
6✔
748
        trace!(
6✔
749
            "Specializing indirect pipeline (has_events={})",
4✔
750
            key.has_events
751
        );
752

753
        let mut shader_defs = Vec::with_capacity(2);
12✔
754
        // Spawner struct needs to be defined with padding, because it's bound as an
755
        // array
756
        shader_defs.push("SPAWNER_PADDING".into());
24✔
757
        if key.has_events {
9✔
758
            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
9✔
759
        }
760

761
        let mut layout = Vec::with_capacity(4);
12✔
762
        layout.push(self.sim_params_bind_group_layout.clone());
24✔
763
        layout.push(self.effect_metadata_bind_group_layout.clone());
24✔
764
        layout.push(self.spawner_bind_group_layout.clone());
24✔
765
        if key.has_events {
9✔
766
            layout.push(self.child_infos_bind_group_layout.clone());
9✔
767
        }
768

769
        let label = format!(
12✔
770
            "hanabi:compute_pipeline:dispatch_indirect{}",
771
            if key.has_events {
6✔
772
                "_events"
3✔
773
            } else {
774
                "_noevent"
3✔
775
            }
776
        );
777

778
        ComputePipelineDescriptor {
779
            label: Some(label.into()),
6✔
780
            layout,
781
            shader: if key.has_events {
6✔
782
                self.indirect_shader_events.clone()
783
            } else {
784
                self.indirect_shader_noevent.clone()
785
            },
786
            shader_defs,
787
            entry_point: "main".into(),
12✔
788
            push_constant_ranges: vec![],
6✔
789
            zero_initialize_workgroup_memory: false,
790
        }
791
    }
792
}
793

794
/// Type of GPU buffer operation.
795
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
796
pub(super) enum GpuBufferOperationType {
797
    /// Clear the destination buffer to zero.
798
    ///
799
    /// The source parameters [`src_offset`] and [`src_stride`] are ignored.
800
    ///
801
    /// [`src_offset`]: crate::GpuBufferOperationArgs::src_offset
802
    /// [`src_stride`]: crate::GpuBufferOperationArgs::src_stride
803
    #[allow(dead_code)]
804
    Zero,
805
    /// Copy a source buffer into a destination buffer.
806
    ///
807
    /// The source can have a stride between each `u32` copied. The destination
808
    /// is always a contiguous buffer.
809
    #[allow(dead_code)]
810
    Copy,
811
    /// Fill the arguments for a later indirect dispatch call.
812
    ///
813
    /// This is similar to a copy, but will round up the source value to the
814
    /// number of threads per workgroup (64) before writing it into the
815
    /// destination.
816
    FillDispatchArgs,
817
    /// Fill the arguments for a later indirect dispatch call.
818
    ///
819
    /// This is the same as [`FillDispatchArgs`], but the source element count
820
    /// is read from the fourth entry in the destination buffer directly,
821
    /// and the source buffer and source arguments are unused.
822
    #[allow(dead_code)]
823
    FillDispatchArgsSelf,
824
}
825

826
/// GPU representation of the arguments of a block operation on a buffer.
827
#[repr(C)]
828
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod, Zeroable, ShaderType)]
829
pub(super) struct GpuBufferOperationArgs {
830
    /// Offset, as u32 count, where the operation starts in the source buffer.
831
    src_offset: u32,
832
    /// Stride, as u32 count, between elements in the source buffer.
833
    src_stride: u32,
834
    /// Offset, as u32 count, where the operation starts in the destination
835
    /// buffer.
836
    dst_offset: u32,
837
    /// Stride, as u32 count, between elements in the destination buffer.
838
    dst_stride: u32,
839
    /// Number of u32 elements to process for this operation.
840
    count: u32,
841
}
842

843
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
844
struct QueuedOperationBindGroupKey {
845
    src_buffer: BufferId,
846
    src_binding_size: Option<NonZeroU32>,
847
    dst_buffer: BufferId,
848
    dst_binding_size: Option<NonZeroU32>,
849
}
850

851
#[derive(Debug, Clone)]
852
struct QueuedOperation {
853
    op: GpuBufferOperationType,
854
    args_index: u32,
855
    src_buffer: Buffer,
856
    src_binding_offset: u32,
857
    src_binding_size: Option<NonZeroU32>,
858
    dst_buffer: Buffer,
859
    dst_binding_offset: u32,
860
    dst_binding_size: Option<NonZeroU32>,
861
}
862

863
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
864
    fn from(value: &QueuedOperation) -> Self {
×
865
        Self {
866
            src_buffer: value.src_buffer.id(),
×
867
            src_binding_size: value.src_binding_size,
×
868
            dst_buffer: value.dst_buffer.id(),
×
869
            dst_binding_size: value.dst_binding_size,
×
870
        }
871
    }
872
}
873

874
/// Queue of GPU buffer operations.
875
///
876
/// The queue records a series of ordered operations on GPU buffers. It can be
877
/// submitted for this frame via [`GpuBufferOperations::submit()`], and
878
/// subsequently dispatched as a compute pass via
879
/// [`GpuBufferOperations::dispatch()`].
880
pub struct GpuBufferOperationQueue {
881
    /// Operation arguments.
882
    args: Vec<GpuBufferOperationArgs>,
883
    /// Queued operations.
884
    operation_queue: Vec<QueuedOperation>,
885
}
886

887
impl GpuBufferOperationQueue {
888
    /// Create a new empty queue.
889
    pub fn new() -> Self {
1,033✔
890
        Self {
891
            args: vec![],
1,033✔
892
            operation_queue: vec![],
1,033✔
893
        }
894
    }
895

896
    /// Enqueue a generic operation.
897
    pub fn enqueue(
4✔
898
        &mut self,
899
        op: GpuBufferOperationType,
900
        args: GpuBufferOperationArgs,
901
        src_buffer: Buffer,
902
        src_binding_offset: u32,
903
        src_binding_size: Option<NonZeroU32>,
904
        dst_buffer: Buffer,
905
        dst_binding_offset: u32,
906
        dst_binding_size: Option<NonZeroU32>,
907
    ) -> u32 {
908
        trace!(
4✔
909
            "Queue {:?} op: args={:?} src_buffer={:?} src_binding_offset={} src_binding_size={:?} dst_buffer={:?} dst_binding_offset={} dst_binding_size={:?}",
×
910
            op,
911
            args,
912
            src_buffer,
913
            src_binding_offset,
914
            src_binding_size,
915
            dst_buffer,
916
            dst_binding_offset,
917
            dst_binding_size,
918
        );
919
        let args_index = self.args.len() as u32;
8✔
920
        self.args.push(args);
12✔
921
        self.operation_queue.push(QueuedOperation {
12✔
922
            op,
8✔
923
            args_index,
8✔
924
            src_buffer,
8✔
925
            src_binding_offset,
8✔
926
            src_binding_size,
8✔
927
            dst_buffer,
8✔
928
            dst_binding_offset,
4✔
929
            dst_binding_size,
4✔
930
        });
931
        args_index
4✔
932
    }
933
}
934

935
/// GPU buffer operations for this frame.
936
///
937
/// This resource contains a list of submitted [`GpuBufferOperationQueue`] for
938
/// the current frame, and ensures the bind groups for those operations are up
939
/// to date.
940
#[derive(Resource)]
941
pub(super) struct GpuBufferOperations {
942
    /// Arguments for the buffer operations submitted this frame.
943
    args_buffer: AlignedBufferVec<GpuBufferOperationArgs>,
944

945
    /// Bind groups for the submitted operations.
946
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
947

948
    /// Submitted queues for this frame.
949
    queues: Vec<Vec<QueuedOperation>>,
950
}
951

952
impl FromWorld for GpuBufferOperations {
953
    fn from_world(world: &mut World) -> Self {
4✔
954
        let render_device = world.get_resource::<RenderDevice>().unwrap();
16✔
955
        let align = render_device.limits().min_uniform_buffer_offset_alignment;
8✔
956
        Self::new(align)
8✔
957
    }
958
}
959

960
impl GpuBufferOperations {
961
    pub fn new(align: u32) -> Self {
4✔
962
        let args_buffer = AlignedBufferVec::new(
963
            BufferUsages::UNIFORM,
964
            Some(NonZeroU64::new(align as u64).unwrap()),
8✔
965
            Some("hanabi:buffer:gpu_operation_args".to_string()),
4✔
966
        );
967
        Self {
968
            args_buffer,
969
            bind_groups: default(),
4✔
970
            queues: vec![],
4✔
971
        }
972
    }
973

974
    /// Clear the queue and begin recording operations for a new frame.
975
    pub fn begin_frame(&mut self) {
1,033✔
976
        self.args_buffer.clear();
2,066✔
977
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
2,066✔
978
        self.queues.clear();
2,066✔
979
    }
980

981
    /// Submit a recorded queue.
982
    ///
983
    /// # Panics
984
    ///
985
    /// Panics if the queue submitted is empty.
986
    pub fn submit(&mut self, mut queue: GpuBufferOperationQueue) -> u32 {
3✔
987
        assert!(!queue.operation_queue.is_empty());
6✔
988
        let queue_index = self.queues.len() as u32;
6✔
989
        for qop in &mut queue.operation_queue {
11✔
990
            qop.args_index = self.args_buffer.push(queue.args[qop.args_index as usize]) as u32;
991
        }
992
        self.queues.push(queue.operation_queue);
9✔
993
        queue_index
3✔
994
    }
995

996
    /// Finish recording operations for this frame, and schedule buffer writes
997
    /// to GPU.
998
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
1,033✔
999
        assert_eq!(
1,033✔
1000
            self.args_buffer.len(),
2,066✔
1001
            self.queues.iter().fold(0, |len, q| len + q.len())
2,075✔
1002
        );
1003

1004
        // Upload to GPU buffer
1005
        self.args_buffer.write_buffer(device, render_queue);
4,132✔
1006
    }
1007

1008
    /// Create all necessary bind groups for all queued operations.
1009
    pub fn create_bind_groups(
1,014✔
1010
        &mut self,
1011
        render_device: &RenderDevice,
1012
        utils_pipeline: &UtilsPipeline,
1013
    ) {
1014
        trace!(
1,014✔
1015
            "Creating bind groups for {} operation queues...",
1,014✔
1016
            self.queues.len()
2,028✔
1017
        );
1018
        for queue in &self.queues {
1,014✔
1019
            for qop in queue {
×
1020
                let key: QueuedOperationBindGroupKey = qop.into();
1021
                self.bind_groups.entry(key).or_insert_with(|| {
×
1022
                    let src_id: NonZeroU32 = qop.src_buffer.id().into();
×
1023
                    let dst_id: NonZeroU32 = qop.dst_buffer.id().into();
×
1024
                    let label = format!("hanabi:bind_group:util_{}_{}", src_id.get(), dst_id.get());
×
1025
                    let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
×
1026
                    let bind_group_layout =
×
1027
                        utils_pipeline.bind_group_layout(qop.op, use_dynamic_offset);
×
1028
                    let (src_offset, dst_offset) = if use_dynamic_offset {
×
1029
                        (0, 0)
×
1030
                    } else {
1031
                        (qop.src_binding_offset as u64, qop.dst_binding_offset as u64)
×
1032
                    };
1033
                    trace!(
×
1034
                        "-> Creating new bind group '{}': src#{} (@+{}B:{:?}B) dst#{} (@+{}B:{:?}B)",
×
1035
                        label,
1036
                        src_id,
1037
                        src_offset,
1038
                        qop.src_binding_size,
1039
                        dst_id,
1040
                        dst_offset,
1041
                        qop.dst_binding_size,
1042
                    );
1043
                    render_device.create_bind_group(
×
1044
                        Some(&label[..]),
×
1045
                        bind_group_layout,
×
1046
                        &[
×
1047
                            BindGroupEntry {
×
1048
                                binding: 0,
×
1049
                                resource: BindingResource::Buffer(BufferBinding {
×
1050
                                    buffer: self.args_buffer.buffer().unwrap(),
×
1051
                                    offset: 0,
×
1052
                                    // We always bind exactly 1 row of arguments
1053
                                    size: Some(
×
1054
                                        NonZeroU64::new(self.args_buffer.aligned_size() as u64)
×
1055
                                            .unwrap(),
×
1056
                                    ),
1057
                                }),
1058
                            },
1059
                            BindGroupEntry {
×
1060
                                binding: 1,
×
1061
                                resource: BindingResource::Buffer(BufferBinding {
×
1062
                                    buffer: &qop.src_buffer,
×
1063
                                    offset: src_offset,
×
1064
                                    size: qop.src_binding_size.map(Into::into),
×
1065
                                }),
1066
                            },
1067
                            BindGroupEntry {
×
1068
                                binding: 2,
×
1069
                                resource: BindingResource::Buffer(BufferBinding {
×
1070
                                    buffer: &qop.dst_buffer,
×
1071
                                    offset: dst_offset,
×
1072
                                    size: qop.dst_binding_size.map(Into::into),
×
1073
                                }),
1074
                            },
1075
                        ],
1076
                    )
1077
                });
1078
            }
1079
        }
1080
    }
1081

1082
    /// Dispatch a submitted queue by index.
1083
    ///
1084
    /// This creates a new, optionally labelled, compute pass, and records to
1085
    /// the render context a series of compute workgroup dispatch, one for each
1086
    /// enqueued operation.
1087
    ///
1088
    /// The compute pipeline(s) used for each operation are fetched from the
1089
    /// [`UtilsPipeline`], and the associated bind groups are used from a
1090
    /// previous call to [`Self::create_bind_groups()`].
1091
    pub fn dispatch(
×
1092
        &self,
1093
        index: u32,
1094
        render_context: &mut RenderContext,
1095
        utils_pipeline: &UtilsPipeline,
1096
        compute_pass_label: Option<&str>,
1097
    ) {
1098
        let queue = &self.queues[index as usize];
×
1099
        trace!(
×
1100
            "Recording GPU commands for queue #{} ({} ops)...",
×
1101
            index,
1102
            queue.len(),
×
1103
        );
1104

1105
        if queue.is_empty() {
×
1106
            return;
×
1107
        }
1108

1109
        let mut compute_pass =
1110
            render_context
1111
                .command_encoder()
1112
                .begin_compute_pass(&ComputePassDescriptor {
1113
                    label: compute_pass_label,
1114
                    timestamp_writes: None,
1115
                });
1116

1117
        let mut prev_op = None;
1118
        for qop in queue {
×
1119
            trace!("qop={:?}", qop);
×
1120

1121
            if Some(qop.op) != prev_op {
×
1122
                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
×
1123
                prev_op = Some(qop.op);
×
1124
            }
1125

1126
            let key: QueuedOperationBindGroupKey = qop.into();
1127
            if let Some(bind_group) = self.bind_groups.get(&key) {
×
1128
                let args_offset = self.args_buffer.dynamic_offset(qop.args_index as usize);
1129
                let use_dynamic_offset = matches!(qop.op, GpuBufferOperationType::FillDispatchArgs);
×
1130
                let (src_offset, dst_offset) = if use_dynamic_offset {
1131
                    (qop.src_binding_offset, qop.dst_binding_offset)
×
1132
                } else {
1133
                    (0, 0)
×
1134
                };
1135
                compute_pass.set_bind_group(0, bind_group, &[args_offset, src_offset, dst_offset]);
1136
                trace!(
1137
                    "set bind group with args_offset=+{}B src_offset=+{}B dst_offset=+{}B",
×
1138
                    args_offset,
1139
                    src_offset,
1140
                    dst_offset
1141
                );
1142
            } else {
1143
                error!("GPU fill dispatch buffer operation bind group not found for buffers src#{:?} dst#{:?}", qop.src_buffer.id(), qop.dst_buffer.id());
×
1144
                continue;
×
1145
            }
1146

1147
            // Dispatch the operations for this buffer
1148
            const WORKGROUP_SIZE: u32 = 64;
1149
            let num_ops = 1u32; // TODO - batching!
1150
            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
1151
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
1152
            trace!(
1153
                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
×
1154
                num_ops,
1155
                workgroup_count
1156
            );
1157
        }
1158
    }
1159
}
1160

1161
/// Compute pipeline to run the `vfx_utils` shader.
1162
#[derive(Resource)]
1163
pub(crate) struct UtilsPipeline {
1164
    #[allow(dead_code)]
1165
    bind_group_layout: BindGroupLayout,
1166
    bind_group_layout_dyn: BindGroupLayout,
1167
    bind_group_layout_no_src: BindGroupLayout,
1168
    pipelines: [ComputePipeline; 4],
1169
}
1170

1171
impl FromWorld for UtilsPipeline {
1172
    fn from_world(world: &mut World) -> Self {
3✔
1173
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1174

1175
        let bind_group_layout = render_device.create_bind_group_layout(
9✔
1176
            "hanabi:bind_group_layout:utils",
1177
            &[
3✔
1178
                BindGroupLayoutEntry {
6✔
1179
                    binding: 0,
6✔
1180
                    visibility: ShaderStages::COMPUTE,
6✔
1181
                    ty: BindingType::Buffer {
6✔
1182
                        ty: BufferBindingType::Uniform,
6✔
1183
                        has_dynamic_offset: false,
6✔
1184
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
6✔
1185
                    },
1186
                    count: None,
6✔
1187
                },
1188
                BindGroupLayoutEntry {
6✔
1189
                    binding: 1,
6✔
1190
                    visibility: ShaderStages::COMPUTE,
6✔
1191
                    ty: BindingType::Buffer {
6✔
1192
                        ty: BufferBindingType::Storage { read_only: true },
6✔
1193
                        has_dynamic_offset: false,
6✔
1194
                        min_binding_size: NonZeroU64::new(4),
6✔
1195
                    },
1196
                    count: None,
6✔
1197
                },
1198
                BindGroupLayoutEntry {
3✔
1199
                    binding: 2,
3✔
1200
                    visibility: ShaderStages::COMPUTE,
3✔
1201
                    ty: BindingType::Buffer {
3✔
1202
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1203
                        has_dynamic_offset: false,
3✔
1204
                        min_binding_size: NonZeroU64::new(4),
3✔
1205
                    },
1206
                    count: None,
3✔
1207
                },
1208
            ],
1209
        );
1210

1211
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1212
            label: Some("hanabi:pipeline_layout:utils"),
6✔
1213
            bind_group_layouts: &[&bind_group_layout],
3✔
1214
            push_constant_ranges: &[],
3✔
1215
        });
1216

1217
        let bind_group_layout_dyn = render_device.create_bind_group_layout(
9✔
1218
            "hanabi:bind_group_layout:utils_dyn",
1219
            &[
3✔
1220
                BindGroupLayoutEntry {
6✔
1221
                    binding: 0,
6✔
1222
                    visibility: ShaderStages::COMPUTE,
6✔
1223
                    ty: BindingType::Buffer {
6✔
1224
                        ty: BufferBindingType::Uniform,
6✔
1225
                        has_dynamic_offset: true,
6✔
1226
                        min_binding_size: Some(GpuBufferOperationArgs::min_size()),
6✔
1227
                    },
1228
                    count: None,
6✔
1229
                },
1230
                BindGroupLayoutEntry {
6✔
1231
                    binding: 1,
6✔
1232
                    visibility: ShaderStages::COMPUTE,
6✔
1233
                    ty: BindingType::Buffer {
6✔
1234
                        ty: BufferBindingType::Storage { read_only: true },
6✔
1235
                        has_dynamic_offset: true,
6✔
1236
                        min_binding_size: NonZeroU64::new(4),
6✔
1237
                    },
1238
                    count: None,
6✔
1239
                },
1240
                BindGroupLayoutEntry {
3✔
1241
                    binding: 2,
3✔
1242
                    visibility: ShaderStages::COMPUTE,
3✔
1243
                    ty: BindingType::Buffer {
3✔
1244
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1245
                        has_dynamic_offset: true,
3✔
1246
                        min_binding_size: NonZeroU64::new(4),
3✔
1247
                    },
1248
                    count: None,
3✔
1249
                },
1250
            ],
1251
        );
1252

1253
        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1254
            label: Some("hanabi:pipeline_layout:utils_dyn"),
6✔
1255
            bind_group_layouts: &[&bind_group_layout_dyn],
3✔
1256
            push_constant_ranges: &[],
3✔
1257
        });
1258

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

1285
        let pipeline_layout_no_src =
3✔
1286
            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
9✔
1287
                label: Some("hanabi:pipeline_layout:utils_no_src"),
6✔
1288
                bind_group_layouts: &[&bind_group_layout_no_src],
3✔
1289
                push_constant_ranges: &[],
3✔
1290
            });
1291

1292
        let shader_code = include_str!("vfx_utils.wgsl");
6✔
1293

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

1299
            let shader_defs = default();
6✔
1300

1301
            match composer.make_naga_module(NagaModuleDescriptor {
9✔
1302
                source: shader_code,
6✔
1303
                file_path: "vfx_utils.wgsl",
6✔
1304
                shader_defs,
3✔
1305
                ..Default::default()
3✔
1306
            }) {
1307
                Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
6✔
1308
                Err(compose_error) => panic!(
×
1309
                    "Failed to compose vfx_utils.wgsl, naga_oil returned: {}",
1310
                    compose_error.emit_to_string(&composer)
1311
                ),
1312
            }
1313
        };
1314

1315
        debug!("Create utils shader module:\n{}", shader_code);
6✔
1316
        #[allow(unsafe_code)]
1317
        let shader_module = unsafe {
1318
            render_device.create_shader_module(ShaderModuleDescriptor {
9✔
1319
                label: Some("hanabi:shader:utils"),
3✔
1320
                source: shader_source,
3✔
1321
            })
1322
        };
1323

1324
        trace!("Create vfx_utils pipelines...");
5✔
1325
        let dummy = std::collections::HashMap::<String, f64>::new();
6✔
1326
        let zero_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
12✔
1327
            label: Some("hanabi:compute_pipeline:zero_buffer"),
6✔
1328
            layout: Some(&pipeline_layout),
6✔
1329
            module: &shader_module,
6✔
1330
            entry_point: Some("zero_buffer"),
6✔
1331
            compilation_options: PipelineCompilationOptions {
3✔
1332
                constants: &dummy,
3✔
1333
                zero_initialize_workgroup_memory: false,
3✔
1334
            },
1335
            cache: None,
3✔
1336
        });
1337
        let copy_pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
12✔
1338
            label: Some("hanabi:compute_pipeline:copy_buffer"),
6✔
1339
            layout: Some(&pipeline_layout_dyn),
6✔
1340
            module: &shader_module,
6✔
1341
            entry_point: Some("copy_buffer"),
6✔
1342
            compilation_options: PipelineCompilationOptions {
3✔
1343
                constants: &dummy,
3✔
1344
                zero_initialize_workgroup_memory: false,
3✔
1345
            },
1346
            cache: None,
3✔
1347
        });
1348
        let fill_dispatch_args_pipeline =
3✔
1349
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
9✔
1350
                label: Some("hanabi:compute_pipeline:fill_dispatch_args"),
6✔
1351
                layout: Some(&pipeline_layout_dyn),
6✔
1352
                module: &shader_module,
6✔
1353
                entry_point: Some("fill_dispatch_args"),
6✔
1354
                compilation_options: PipelineCompilationOptions {
3✔
1355
                    constants: &dummy,
3✔
1356
                    zero_initialize_workgroup_memory: false,
3✔
1357
                },
1358
                cache: None,
3✔
1359
            });
1360
        let fill_dispatch_args_self_pipeline =
3✔
1361
            render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
9✔
1362
                label: Some("hanabi:compute_pipeline:fill_dispatch_args_self"),
6✔
1363
                layout: Some(&pipeline_layout_no_src),
6✔
1364
                module: &shader_module,
6✔
1365
                entry_point: Some("fill_dispatch_args_self"),
6✔
1366
                compilation_options: PipelineCompilationOptions {
3✔
1367
                    constants: &dummy,
3✔
1368
                    zero_initialize_workgroup_memory: false,
3✔
1369
                },
1370
                cache: None,
3✔
1371
            });
1372

1373
        Self {
1374
            bind_group_layout,
1375
            bind_group_layout_dyn,
1376
            bind_group_layout_no_src,
1377
            pipelines: [
3✔
1378
                zero_pipeline,
1379
                copy_pipeline,
1380
                fill_dispatch_args_pipeline,
1381
                fill_dispatch_args_self_pipeline,
1382
            ],
1383
        }
1384
    }
1385
}
1386

1387
impl UtilsPipeline {
1388
    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
×
1389
        match op {
×
1390
            GpuBufferOperationType::Zero => &self.pipelines[0],
×
1391
            GpuBufferOperationType::Copy => &self.pipelines[1],
×
1392
            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
×
1393
            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
×
1394
        }
1395
    }
1396

1397
    fn bind_group_layout(
×
1398
        &self,
1399
        op: GpuBufferOperationType,
1400
        with_dynamic_offsets: bool,
1401
    ) -> &BindGroupLayout {
1402
        if op == GpuBufferOperationType::FillDispatchArgsSelf {
×
1403
            assert!(
×
1404
                !with_dynamic_offsets,
×
1405
                "FillDispatchArgsSelf op cannot use dynamic offset (not implemented)"
×
1406
            );
1407
            &self.bind_group_layout_no_src
×
1408
        } else if with_dynamic_offsets {
×
1409
            &self.bind_group_layout_dyn
×
1410
        } else {
1411
            &self.bind_group_layout
×
1412
        }
1413
    }
1414
}
1415

1416
#[derive(Resource)]
1417
pub(crate) struct ParticlesInitPipeline {
1418
    sim_params_layout: BindGroupLayout,
1419

1420
    // Temporary values passed to specialize()
1421
    // https://github.com/bevyengine/bevy/issues/17132
1422
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1423
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1424
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1425
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1426
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1427
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1428
}
1429

1430
impl FromWorld for ParticlesInitPipeline {
1431
    fn from_world(world: &mut World) -> Self {
3✔
1432
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1433

1434
        let sim_params_layout = render_device.create_bind_group_layout(
9✔
1435
            "hanabi:bind_group_layout:update_sim_params",
1436
            // @group(0) @binding(0) var<uniform> sim_params: SimParams;
1437
            &[BindGroupLayoutEntry {
3✔
1438
                binding: 0,
3✔
1439
                visibility: ShaderStages::COMPUTE,
3✔
1440
                ty: BindingType::Buffer {
3✔
1441
                    ty: BufferBindingType::Uniform,
3✔
1442
                    has_dynamic_offset: false,
3✔
1443
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
1444
                },
1445
                count: None,
3✔
1446
            }],
1447
        );
1448

1449
        Self {
1450
            sim_params_layout,
1451
            temp_particle_bind_group_layout: None,
1452
            temp_spawner_bind_group_layout: None,
1453
            temp_metadata_bind_group_layout: None,
1454
        }
1455
    }
1456
}
1457

1458
bitflags! {
1459
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1460
    pub struct ParticleInitPipelineKeyFlags: u8 {
1461
        //const CLONE = (1u8 << 0); // DEPRECATED
1462
        const ATTRIBUTE_PREV = (1u8 << 1);
1463
        const ATTRIBUTE_NEXT = (1u8 << 2);
1464
        const CONSUME_GPU_SPAWN_EVENTS = (1u8 << 3);
1465
    }
1466
}
1467

1468
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1469
pub(crate) struct ParticleInitPipelineKey {
1470
    /// Compute shader, with snippets applied, but not preprocessed yet.
1471
    shader: Handle<Shader>,
1472
    /// Minimum binding size in bytes for the particle layout buffer.
1473
    particle_layout_min_binding_size: NonZeroU32,
1474
    /// Minimum binding size in bytes for the particle layout buffer of the
1475
    /// parent effect, if any.
1476
    /// Key: READ_PARENT_PARTICLE
1477
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1478
    /// Pipeline flags.
1479
    flags: ParticleInitPipelineKeyFlags,
1480
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1481
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1482
    particle_bind_group_layout_id: BindGroupLayoutId,
1483
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1484
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1485
    spawner_bind_group_layout_id: BindGroupLayoutId,
1486
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1487
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1488
    metadata_bind_group_layout_id: BindGroupLayoutId,
1489
}
1490

1491
impl SpecializedComputePipeline for ParticlesInitPipeline {
1492
    type Key = ParticleInitPipelineKey;
1493

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

1499
        let mut shader_defs = Vec::with_capacity(4);
6✔
1500
        if key
3✔
1501
            .flags
3✔
1502
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV)
3✔
1503
        {
1504
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1505
        }
1506
        if key
3✔
1507
            .flags
3✔
1508
            .contains(ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT)
3✔
1509
        {
1510
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1511
        }
1512
        let consume_gpu_spawn_events = key
6✔
1513
            .flags
3✔
1514
            .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3✔
1515
        if consume_gpu_spawn_events {
3✔
1516
            shader_defs.push("CONSUME_GPU_SPAWN_EVENTS".into());
×
1517
        }
1518
        // FIXME - for now this needs to keep in sync with consume_gpu_spawn_events
1519
        if key.parent_particle_layout_min_binding_size.is_some() {
6✔
1520
            assert!(consume_gpu_spawn_events);
×
1521
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1522
        } else {
1523
            assert!(!consume_gpu_spawn_events);
3✔
1524
        }
1525

1526
        // This should always be valid when specialize() is called, by design. This is
1527
        // how we pass the value to specialize() to work around the lack of access to
1528
        // external data.
1529
        // https://github.com/bevyengine/bevy/issues/17132
1530
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
3✔
1531
        assert_eq!(
1532
            particle_bind_group_layout.id(),
1533
            key.particle_bind_group_layout_id
1534
        );
1535
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
12✔
1536
        assert_eq!(
3✔
1537
            spawner_bind_group_layout.id(),
6✔
1538
            key.spawner_bind_group_layout_id
1539
        );
1540
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
12✔
1541
        assert_eq!(
3✔
1542
            metadata_bind_group_layout.id(),
6✔
1543
            key.metadata_bind_group_layout_id
1544
        );
1545

1546
        let label = format!("hanabi:pipeline:init_{hash:016X}");
9✔
1547
        trace!(
3✔
1548
            "-> creating pipeline '{}' with shader defs:{}",
3✔
1549
            label,
1550
            shader_defs
3✔
1551
                .iter()
3✔
1552
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
6✔
1553
        );
1554

1555
        ComputePipelineDescriptor {
1556
            label: Some(label.into()),
6✔
1557
            layout: vec![
6✔
1558
                self.sim_params_layout.clone(),
1559
                particle_bind_group_layout.clone(),
1560
                spawner_bind_group_layout.clone(),
1561
                metadata_bind_group_layout.clone(),
1562
            ],
1563
            shader: key.shader,
6✔
1564
            shader_defs,
1565
            entry_point: "main".into(),
6✔
1566
            push_constant_ranges: vec![],
3✔
1567
            zero_initialize_workgroup_memory: false,
1568
        }
1569
    }
1570
}
1571

1572
#[derive(Resource)]
1573
pub(crate) struct ParticlesUpdatePipeline {
1574
    sim_params_layout: BindGroupLayout,
1575

1576
    // Temporary values passed to specialize()
1577
    // https://github.com/bevyengine/bevy/issues/17132
1578
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1579
    temp_particle_bind_group_layout: Option<BindGroupLayout>,
1580
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1581
    temp_spawner_bind_group_layout: Option<BindGroupLayout>,
1582
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1583
    temp_metadata_bind_group_layout: Option<BindGroupLayout>,
1584
}
1585

1586
impl FromWorld for ParticlesUpdatePipeline {
1587
    fn from_world(world: &mut World) -> Self {
3✔
1588
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1589

1590
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
7✔
1591
        let sim_params_layout = render_device.create_bind_group_layout(
9✔
1592
            "hanabi:bind_group_layout:update:particle",
1593
            &[BindGroupLayoutEntry {
3✔
1594
                binding: 0,
3✔
1595
                visibility: ShaderStages::COMPUTE,
3✔
1596
                ty: BindingType::Buffer {
3✔
1597
                    ty: BufferBindingType::Uniform,
3✔
1598
                    has_dynamic_offset: false,
3✔
1599
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
1600
                },
1601
                count: None,
3✔
1602
            }],
1603
        );
1604

1605
        Self {
1606
            sim_params_layout,
1607
            temp_particle_bind_group_layout: None,
1608
            temp_spawner_bind_group_layout: None,
1609
            temp_metadata_bind_group_layout: None,
1610
        }
1611
    }
1612
}
1613

1614
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1615
pub(crate) struct ParticleUpdatePipelineKey {
1616
    /// Compute shader, with snippets applied, but not preprocessed yet.
1617
    shader: Handle<Shader>,
1618
    /// Particle layout.
1619
    particle_layout: ParticleLayout,
1620
    /// Minimum binding size in bytes for the particle layout buffer of the
1621
    /// parent effect, if any.
1622
    /// Key: READ_PARENT_PARTICLE
1623
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1624
    /// Key: EMITS_GPU_SPAWN_EVENTS
1625
    num_event_buffers: u32,
1626
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1627
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1628
    particle_bind_group_layout_id: BindGroupLayoutId,
1629
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1630
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1631
    spawner_bind_group_layout_id: BindGroupLayoutId,
1632
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1633
    // Note: can't directly store BindGroupLayout because it's not Eq nor Hash
1634
    metadata_bind_group_layout_id: BindGroupLayoutId,
1635
}
1636

1637
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1638
    type Key = ParticleUpdatePipelineKey;
1639

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

1645
        let mut shader_defs = Vec::with_capacity(6);
6✔
1646
        shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
12✔
1647
        // ChildInfo needs atomic event_count because all threads append to the event
1648
        // buffer(s) in parallel.
1649
        shader_defs.push("CHILD_INFO_EVENT_COUNT_IS_ATOMIC".into());
12✔
1650
        if key.particle_layout.contains(Attribute::PREV) {
6✔
1651
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1652
        }
1653
        if key.particle_layout.contains(Attribute::NEXT) {
6✔
1654
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1655
        }
1656
        if key.parent_particle_layout_min_binding_size.is_some() {
6✔
1657
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1658
        }
1659
        if key.num_event_buffers > 0 {
3✔
1660
            shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
×
1661
        }
1662

1663
        // This should always be valid when specialize() is called, by design. This is
1664
        // how we pass the value to specialize() to work around the lack of access to
1665
        // external data.
1666
        // https://github.com/bevyengine/bevy/issues/17132
1667
        let particle_bind_group_layout = self.temp_particle_bind_group_layout.as_ref().unwrap();
12✔
1668
        assert_eq!(
3✔
1669
            particle_bind_group_layout.id(),
6✔
1670
            key.particle_bind_group_layout_id
1671
        );
1672
        let spawner_bind_group_layout = self.temp_spawner_bind_group_layout.as_ref().unwrap();
12✔
1673
        assert_eq!(
3✔
1674
            spawner_bind_group_layout.id(),
6✔
1675
            key.spawner_bind_group_layout_id
1676
        );
1677
        let metadata_bind_group_layout = self.temp_metadata_bind_group_layout.as_ref().unwrap();
12✔
1678
        assert_eq!(
3✔
1679
            metadata_bind_group_layout.id(),
6✔
1680
            key.metadata_bind_group_layout_id
1681
        );
1682

1683
        let hash = calc_func_id(&key);
9✔
1684
        let label = format!("hanabi:pipeline:update_{hash:016X}");
9✔
1685
        trace!(
3✔
1686
            "-> creating pipeline '{}' with shader defs:{}",
3✔
1687
            label,
1688
            shader_defs
3✔
1689
                .iter()
3✔
1690
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
18✔
1691
        );
1692

1693
        ComputePipelineDescriptor {
1694
            label: Some(label.into()),
6✔
1695
            layout: vec![
6✔
1696
                self.sim_params_layout.clone(),
1697
                particle_bind_group_layout.clone(),
1698
                spawner_bind_group_layout.clone(),
1699
                metadata_bind_group_layout.clone(),
1700
            ],
1701
            shader: key.shader,
6✔
1702
            shader_defs,
1703
            entry_point: "main".into(),
6✔
1704
            push_constant_ranges: Vec::new(),
3✔
1705
            zero_initialize_workgroup_memory: false,
1706
        }
1707
    }
1708
}
1709

1710
#[derive(Resource)]
1711
pub(crate) struct ParticlesRenderPipeline {
1712
    render_device: RenderDevice,
1713
    view_layout: BindGroupLayout,
1714
    material_layouts: HashMap<TextureLayout, BindGroupLayout>,
1715
}
1716

1717
impl ParticlesRenderPipeline {
1718
    /// Cache a material, creating its bind group layout based on the texture
1719
    /// layout.
1720
    pub fn cache_material(&mut self, layout: &TextureLayout) {
1,014✔
1721
        if layout.layout.is_empty() {
2,028✔
1722
            return;
1,014✔
1723
        }
1724

1725
        // FIXME - no current stable API to insert an entry into a HashMap only if it
1726
        // doesn't exist, and without having to build a key (as opposed to a reference).
1727
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
1728
        // cached (which should be the common case).
1729
        if self.material_layouts.contains_key(layout) {
1730
            return;
×
1731
        }
1732

1733
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
1734
        let mut index = 0;
1735
        for _slot in &layout.layout {
×
1736
            entries.push(BindGroupLayoutEntry {
1737
                binding: index,
1738
                visibility: ShaderStages::FRAGMENT,
1739
                ty: BindingType::Texture {
1740
                    multisampled: false,
1741
                    sample_type: TextureSampleType::Float { filterable: true },
1742
                    view_dimension: TextureViewDimension::D2,
1743
                },
1744
                count: None,
1745
            });
1746
            entries.push(BindGroupLayoutEntry {
1747
                binding: index + 1,
1748
                visibility: ShaderStages::FRAGMENT,
1749
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
1750
                count: None,
1751
            });
1752
            index += 2;
1753
        }
1754
        debug!(
1755
            "Creating material bind group with {} entries [{:?}] for layout {:?}",
×
1756
            entries.len(),
×
1757
            entries,
1758
            layout
1759
        );
1760
        let material_bind_group_layout = self
1761
            .render_device
1762
            .create_bind_group_layout("hanabi:material_layout_render", &entries[..]);
1763

1764
        self.material_layouts
1765
            .insert(layout.clone(), material_bind_group_layout);
1766
    }
1767

1768
    /// Retrieve a bind group layout for a cached material.
1769
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayout> {
2✔
1770
        // Prevent a hash and lookup for the trivial case of an empty layout
1771
        if layout.layout.is_empty() {
4✔
1772
            return None;
2✔
1773
        }
1774

1775
        self.material_layouts.get(layout)
1776
    }
1777
}
1778

1779
impl FromWorld for ParticlesRenderPipeline {
1780
    fn from_world(world: &mut World) -> Self {
3✔
1781
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1782

1783
        let view_layout = render_device.create_bind_group_layout(
9✔
1784
            "hanabi:bind_group_layout:render:view@0",
1785
            &[
3✔
1786
                // @group(0) @binding(0) var<uniform> view: View;
1787
                BindGroupLayoutEntry {
6✔
1788
                    binding: 0,
6✔
1789
                    visibility: ShaderStages::VERTEX_FRAGMENT,
6✔
1790
                    ty: BindingType::Buffer {
6✔
1791
                        ty: BufferBindingType::Uniform,
6✔
1792
                        has_dynamic_offset: true,
6✔
1793
                        min_binding_size: Some(ViewUniform::min_size()),
6✔
1794
                    },
1795
                    count: None,
6✔
1796
                },
1797
                // @group(0) @binding(1) var<uniform> sim_params : SimParams;
1798
                BindGroupLayoutEntry {
3✔
1799
                    binding: 1,
3✔
1800
                    visibility: ShaderStages::VERTEX_FRAGMENT,
3✔
1801
                    ty: BindingType::Buffer {
3✔
1802
                        ty: BufferBindingType::Uniform,
3✔
1803
                        has_dynamic_offset: false,
3✔
1804
                        min_binding_size: Some(GpuSimParams::min_size()),
3✔
1805
                    },
1806
                    count: None,
3✔
1807
                },
1808
            ],
1809
        );
1810

1811
        Self {
1812
            render_device: render_device.clone(),
9✔
1813
            view_layout,
1814
            material_layouts: default(),
3✔
1815
        }
1816
    }
1817
}
1818

1819
#[cfg(all(feature = "2d", feature = "3d"))]
1820
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1821
enum PipelineMode {
1822
    Camera2d,
1823
    Camera3d,
1824
}
1825

1826
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1827
pub(crate) struct ParticleRenderPipelineKey {
1828
    /// Render shader, with snippets applied, but not preprocessed yet.
1829
    shader: Handle<Shader>,
1830
    /// Particle layout.
1831
    particle_layout: ParticleLayout,
1832
    mesh_layout: Option<MeshVertexBufferLayoutRef>,
1833
    /// Texture layout.
1834
    texture_layout: TextureLayout,
1835
    /// Key: LOCAL_SPACE_SIMULATION
1836
    /// The effect is simulated in local space, and during rendering all
1837
    /// particles are transformed by the effect's [`GlobalTransform`].
1838
    local_space_simulation: bool,
1839
    /// Key: USE_ALPHA_MASK, OPAQUE
1840
    /// The particle's alpha masking behavior.
1841
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
1842
    /// The effect needs Alpha blend.
1843
    alpha_mode: AlphaMode,
1844
    /// Key: FLIPBOOK
1845
    /// The effect is rendered with flipbook texture animation based on the
1846
    /// sprite index of each particle.
1847
    flipbook: bool,
1848
    /// Key: NEEDS_UV
1849
    /// The effect needs UVs.
1850
    needs_uv: bool,
1851
    /// Key: NEEDS_NORMAL
1852
    /// The effect needs normals.
1853
    needs_normal: bool,
1854
    /// Key: NEEDS_PARTICLE_IN_FRAGMENT
1855
    /// The effect needs access to the particle index and buffer in the fragment
1856
    /// shader.
1857
    needs_particle_fragment: bool,
1858
    /// Key: RIBBONS
1859
    /// The effect has ribbons.
1860
    ribbons: bool,
1861
    /// For dual-mode configurations only, the actual mode of the current render
1862
    /// pipeline. Otherwise the mode is implicitly determined by the active
1863
    /// feature.
1864
    #[cfg(all(feature = "2d", feature = "3d"))]
1865
    pipeline_mode: PipelineMode,
1866
    /// MSAA sample count.
1867
    msaa_samples: u32,
1868
    /// Is the camera using an HDR render target?
1869
    hdr: bool,
1870
}
1871

1872
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1873
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1874
    #[default]
1875
    Blend,
1876
    /// Key: USE_ALPHA_MASK
1877
    /// The effect is rendered with alpha masking.
1878
    AlphaMask,
1879
    /// Key: OPAQUE
1880
    /// The effect is rendered fully-opaquely.
1881
    Opaque,
1882
}
1883

1884
impl Default for ParticleRenderPipelineKey {
1885
    fn default() -> Self {
×
1886
        Self {
1887
            shader: Handle::default(),
×
1888
            particle_layout: ParticleLayout::empty(),
×
1889
            mesh_layout: None,
1890
            texture_layout: default(),
×
1891
            local_space_simulation: false,
1892
            alpha_mask: default(),
×
1893
            alpha_mode: AlphaMode::Blend,
1894
            flipbook: false,
1895
            needs_uv: false,
1896
            needs_normal: false,
1897
            needs_particle_fragment: false,
1898
            ribbons: false,
1899
            #[cfg(all(feature = "2d", feature = "3d"))]
1900
            pipeline_mode: PipelineMode::Camera3d,
1901
            msaa_samples: Msaa::default().samples(),
×
1902
            hdr: false,
1903
        }
1904
    }
1905
}
1906

1907
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1908
    type Key = ParticleRenderPipelineKey;
1909

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

1913
        trace!("Creating layout for bind group particle@1 of render pass");
4✔
1914
        let alignment = self
4✔
1915
            .render_device
2✔
1916
            .limits()
2✔
1917
            .min_storage_buffer_offset_alignment;
2✔
1918
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(alignment);
6✔
1919
        let entries = [
4✔
1920
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
1921
            BindGroupLayoutEntry {
4✔
1922
                binding: 0,
4✔
1923
                visibility: ShaderStages::VERTEX_FRAGMENT,
4✔
1924
                ty: BindingType::Buffer {
4✔
1925
                    ty: BufferBindingType::Storage { read_only: true },
6✔
1926
                    has_dynamic_offset: false,
4✔
1927
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
4✔
1928
                },
1929
                count: None,
4✔
1930
            },
1931
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
1932
            BindGroupLayoutEntry {
4✔
1933
                binding: 1,
4✔
1934
                visibility: ShaderStages::VERTEX,
4✔
1935
                ty: BindingType::Buffer {
4✔
1936
                    ty: BufferBindingType::Storage { read_only: true },
6✔
1937
                    has_dynamic_offset: false,
4✔
1938
                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
6✔
1939
                },
1940
                count: None,
4✔
1941
            },
1942
            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
1943
            BindGroupLayoutEntry {
2✔
1944
                binding: 2,
2✔
1945
                visibility: ShaderStages::VERTEX,
2✔
1946
                ty: BindingType::Buffer {
2✔
1947
                    ty: BufferBindingType::Storage { read_only: true },
2✔
1948
                    has_dynamic_offset: true,
2✔
1949
                    min_binding_size: Some(spawner_min_binding_size),
2✔
1950
                },
1951
                count: None,
2✔
1952
            },
1953
        ];
1954
        let particle_bind_group_layout = self
4✔
1955
            .render_device
2✔
1956
            .create_bind_group_layout("hanabi:bind_group_layout:render:particle@1", &entries[..]);
4✔
1957

1958
        let mut layout = vec![self.view_layout.clone(), particle_bind_group_layout];
10✔
1959
        let mut shader_defs = vec![];
4✔
1960

1961
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
10✔
1962
            mesh_layout
4✔
1963
                .0
4✔
1964
                .get_layout(&[
4✔
1965
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
6✔
1966
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
6✔
1967
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
2✔
1968
                ])
1969
                .ok()
2✔
1970
        });
1971

1972
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
4✔
1973
            layout.push(material_bind_group_layout.clone());
1974
        }
1975

1976
        // Key: LOCAL_SPACE_SIMULATION
1977
        if key.local_space_simulation {
2✔
1978
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
1979
        }
1980

1981
        match key.alpha_mask {
2✔
1982
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
2✔
1983
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
1984
                // Key: USE_ALPHA_MASK
1985
                shader_defs.push("USE_ALPHA_MASK".into())
×
1986
            }
1987
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
1988
                // Key: OPAQUE
1989
                shader_defs.push("OPAQUE".into())
×
1990
            }
1991
        }
1992

1993
        // Key: FLIPBOOK
1994
        if key.flipbook {
2✔
1995
            shader_defs.push("FLIPBOOK".into());
×
1996
        }
1997

1998
        // Key: NEEDS_UV
1999
        if key.needs_uv {
2✔
2000
            shader_defs.push("NEEDS_UV".into());
×
2001
        }
2002

2003
        // Key: NEEDS_NORMAL
2004
        if key.needs_normal {
2✔
2005
            shader_defs.push("NEEDS_NORMAL".into());
×
2006
        }
2007

2008
        if key.needs_particle_fragment {
2✔
2009
            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
×
2010
        }
2011

2012
        // Key: RIBBONS
2013
        if key.ribbons {
2✔
2014
            shader_defs.push("RIBBONS".into());
×
2015
        }
2016

2017
        #[cfg(feature = "2d")]
2018
        let depth_stencil_2d = DepthStencilState {
2019
            format: CORE_2D_DEPTH_FORMAT,
2020
            // Use depth buffer with alpha-masked particles, not with transparent ones
2021
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2022
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2023
            depth_compare: CompareFunction::GreaterEqual,
2024
            stencil: StencilState::default(),
2✔
2025
            bias: DepthBiasState::default(),
2✔
2026
        };
2027

2028
        #[cfg(feature = "3d")]
2029
        let depth_stencil_3d = DepthStencilState {
2030
            format: CORE_3D_DEPTH_FORMAT,
2031
            // Use depth buffer with alpha-masked or opaque particles, not
2032
            // with transparent ones
2033
            depth_write_enabled: matches!(
2✔
2034
                key.alpha_mask,
2035
                ParticleRenderAlphaMaskPipelineKey::AlphaMask
2036
                    | ParticleRenderAlphaMaskPipelineKey::Opaque
2037
            ),
2038
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2039
            depth_compare: CompareFunction::GreaterEqual,
2040
            stencil: StencilState::default(),
2✔
2041
            bias: DepthBiasState::default(),
2✔
2042
        };
2043

2044
        #[cfg(all(feature = "2d", feature = "3d"))]
2045
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2✔
2046
        #[cfg(all(feature = "2d", feature = "3d"))]
2047
        let depth_stencil = match key.pipeline_mode {
4✔
2048
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2049
            PipelineMode::Camera3d => Some(depth_stencil_3d),
2✔
2050
        };
2051

2052
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2053
        let depth_stencil = Some(depth_stencil_2d);
2054

2055
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2056
        let depth_stencil = Some(depth_stencil_3d);
2057

2058
        let format = if key.hdr {
4✔
2059
            ViewTarget::TEXTURE_FORMAT_HDR
×
2060
        } else {
2061
            TextureFormat::bevy_default()
2✔
2062
        };
2063

2064
        let hash = calc_func_id(&key);
6✔
2065
        let label = format!("hanabi:pipeline:render_{hash:016X}");
6✔
2066
        trace!(
2✔
2067
            "-> creating pipeline '{}' with shader defs:{}",
2✔
2068
            label,
2069
            shader_defs
2✔
2070
                .iter()
2✔
2071
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
2072
        );
2073

2074
        RenderPipelineDescriptor {
2075
            label: Some(label.into()),
4✔
2076
            vertex: VertexState {
4✔
2077
                shader: key.shader.clone(),
2078
                entry_point: "vertex".into(),
2079
                shader_defs: shader_defs.clone(),
2080
                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
2081
            },
2082
            fragment: Some(FragmentState {
4✔
2083
                shader: key.shader,
2084
                shader_defs,
2085
                entry_point: "fragment".into(),
2086
                targets: vec![Some(ColorTargetState {
2087
                    format,
2088
                    blend: Some(key.alpha_mode.into()),
2089
                    write_mask: ColorWrites::ALL,
2090
                })],
2091
            }),
2092
            layout,
2093
            primitive: PrimitiveState {
4✔
2094
                front_face: FrontFace::Ccw,
2095
                cull_mode: None,
2096
                unclipped_depth: false,
2097
                polygon_mode: PolygonMode::Fill,
2098
                conservative: false,
2099
                topology: PrimitiveTopology::TriangleList,
2100
                strip_index_format: None,
2101
            },
2102
            depth_stencil,
2103
            multisample: MultisampleState {
2✔
2104
                count: key.msaa_samples,
2105
                mask: !0,
2106
                alpha_to_coverage_enabled: false,
2107
            },
2108
            push_constant_ranges: Vec::new(),
2✔
2109
            zero_initialize_workgroup_memory: false,
2110
        }
2111
    }
2112
}
2113

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

2166
pub struct AddedEffectParent {
2167
    pub entity: MainEntity,
2168
    pub layout: ParticleLayout,
2169
    /// GPU spawn event count to allocate for this effect.
2170
    pub event_count: u32,
2171
}
2172

2173
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
2174
/// GPU allocation.
2175
///
2176
/// [`ParticleEffect`]: crate::ParticleEffect
2177
pub struct AddedEffect {
2178
    /// Entity with a newly-added [`ParticleEffect`] component.
2179
    ///
2180
    /// [`ParticleEffect`]: crate::ParticleEffect
2181
    pub entity: MainEntity,
2182
    #[allow(dead_code)]
2183
    pub render_entity: RenderEntity,
2184
    /// Capacity, in number of particles, of the effect.
2185
    pub capacity: u32,
2186
    /// Resolved particle mesh, either the one provided by the user or the
2187
    /// default one. This should always be valid.
2188
    pub mesh: Handle<Mesh>,
2189
    /// Parent effect, if any.
2190
    pub parent: Option<AddedEffectParent>,
2191
    /// Layout of particle attributes.
2192
    pub particle_layout: ParticleLayout,
2193
    /// Layout of properties for the effect, if properties are used at all, or
2194
    /// an empty layout.
2195
    pub property_layout: PropertyLayout,
2196
    /// Effect flags.
2197
    pub layout_flags: LayoutFlags,
2198
    /// Handle of the effect asset.
2199
    pub handle: Handle<EffectAsset>,
2200
}
2201

2202
/// Collection of all extracted effects for this frame, inserted into the
2203
/// render world as a render resource.
2204
#[derive(Default, Resource)]
2205
pub(crate) struct ExtractedEffects {
2206
    /// Extracted effects this frame.
2207
    pub effects: Vec<ExtractedEffect>,
2208
    /// Newly added effects without a GPU allocation yet.
2209
    pub added_effects: Vec<AddedEffect>,
2210
}
2211

2212
#[derive(Default, Resource)]
2213
pub(crate) struct EffectAssetEvents {
2214
    pub images: Vec<AssetEvent<Image>>,
2215
}
2216

2217
/// System extracting all the asset events for the [`Image`] assets to enable
2218
/// dynamic update of images bound to any effect.
2219
///
2220
/// This system runs in parallel of [`extract_effects`].
2221
pub(crate) fn extract_effect_events(
1,030✔
2222
    mut events: ResMut<EffectAssetEvents>,
2223
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2224
) {
2225
    #[cfg(feature = "trace")]
2226
    let _span = bevy::log::info_span!("extract_effect_events").entered();
3,090✔
2227
    trace!("extract_effect_events()");
2,050✔
2228

2229
    let EffectAssetEvents { ref mut images } = *events;
2,060✔
2230
    *images = image_events.read().copied().collect();
4,120✔
2231
}
2232

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

2267
    /// Enable automatically starting a GPU debugger capture when one or more
2268
    /// effects are spawned.
2269
    ///
2270
    /// Enable this feature to automatically capture one or more GPU frames when
2271
    /// a new effect is spawned (as detected by ECS change detection). This
2272
    /// instructs any attached GPU debugger to start a capture; this has no
2273
    /// effect if no debugger is attached.
2274
    pub start_capture_on_new_effect: bool,
2275

2276
    /// Number of frames to capture with a GPU debugger.
2277
    ///
2278
    /// By default this value is zero, and a GPU debugger capture runs for a
2279
    /// single frame. If a non-zero frame count is specified here, the capture
2280
    /// will instead stop once the specified number of frames has been recorded.
2281
    ///
2282
    /// You should avoid setting this to a value too large, to prevent the
2283
    /// capture size from getting out of control. A typical value is 1 to 3
2284
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2285
    /// debuggers or graphics APIs might further limit this value on their own,
2286
    /// so there's no guarantee the graphics API will honor this value.
2287
    pub capture_frame_count: u32,
2288
}
2289

2290
#[derive(Debug, Default, Clone, Copy, Resource)]
2291
pub(crate) struct RenderDebugSettings {
2292
    /// Is a GPU debugger capture on-going?
2293
    is_capturing: bool,
2294
    /// Start time of any on-going GPU debugger capture.
2295
    capture_start: Duration,
2296
    /// Number of frames captured so far for on-going GPU debugger capture.
2297
    captured_frames: u32,
2298
}
2299

2300
/// System extracting data for rendering of all active [`ParticleEffect`]
2301
/// components.
2302
///
2303
/// Extract rendering data for all [`ParticleEffect`] components in the world
2304
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
2305
/// the data into a new [`ExtractedEffect`] instance added to the
2306
/// [`ExtractedEffects`] resource.
2307
///
2308
/// This system runs in parallel of [`extract_effect_events`].
2309
///
2310
/// If any GPU debug capture is configured to start or stop in
2311
/// [`DebugSettings`], they do so at the beginning of this system. This ensures
2312
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2313
/// from Bevy itself, if another Bevy system runs before this one).
2314
///
2315
/// [`ParticleEffect`]: crate::ParticleEffect
2316
pub(crate) fn extract_effects(
1,030✔
2317
    real_time: Extract<Res<Time<Real>>>,
2318
    virtual_time: Extract<Res<Time<Virtual>>>,
2319
    time: Extract<Res<Time<EffectSimulation>>>,
2320
    effects: Extract<Res<Assets<EffectAsset>>>,
2321
    q_added_effects: Extract<
2322
        Query<
2323
            (Entity, &RenderEntity, &CompiledParticleEffect),
2324
            (Added<CompiledParticleEffect>, With<GlobalTransform>),
2325
        >,
2326
    >,
2327
    q_effects: Extract<
2328
        Query<(
2329
            Entity,
2330
            &RenderEntity,
2331
            Option<&InheritedVisibility>,
2332
            Option<&ViewVisibility>,
2333
            &EffectSpawner,
2334
            &CompiledParticleEffect,
2335
            Option<Ref<EffectProperties>>,
2336
            &GlobalTransform,
2337
        )>,
2338
    >,
2339
    q_all_effects: Extract<Query<(&RenderEntity, &CompiledParticleEffect), With<GlobalTransform>>>,
2340
    mut pending_effects: Local<Vec<MainEntity>>,
2341
    render_device: Res<RenderDevice>,
2342
    debug_settings: Extract<Res<DebugSettings>>,
2343
    default_mesh: Extract<Res<DefaultMesh>>,
2344
    mut sim_params: ResMut<SimParams>,
2345
    mut extracted_effects: ResMut<ExtractedEffects>,
2346
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2347
) {
2348
    #[cfg(feature = "trace")]
2349
    let _span = bevy::log::info_span!("extract_effects").entered();
3,090✔
2350
    trace!("extract_effects()");
2,050✔
2351

2352
    // Manage GPU debug capture
2353
    if render_debug_settings.is_capturing {
1,030✔
2354
        render_debug_settings.captured_frames += 1;
×
2355

2356
        // Stop any pending capture if needed
2357
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2358
            render_device.wgpu_device().stop_capture();
×
2359
            render_debug_settings.is_capturing = false;
×
2360
            warn!(
×
2361
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2362
                render_debug_settings.captured_frames,
×
2363
                real_time.elapsed().as_secs_f64()
×
2364
            );
2365
        }
2366
    }
2367
    if !render_debug_settings.is_capturing {
1,030✔
2368
        // If no pending capture, consider starting a new one
2369
        if debug_settings.start_capture_this_frame
1,030✔
2370
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty())
1,030✔
2371
        {
2372
            render_device.wgpu_device().start_capture();
×
2373
            render_debug_settings.is_capturing = true;
2374
            render_debug_settings.capture_start = real_time.elapsed();
2375
            render_debug_settings.captured_frames = 0;
2376
            warn!(
2377
                "Started GPU debug capture at t={}s.",
×
2378
                render_debug_settings.capture_start.as_secs_f64()
×
2379
            );
2380
        }
2381
    }
2382

2383
    // Save simulation params into render world
2384
    sim_params.time = time.elapsed_secs_f64();
2,060✔
2385
    sim_params.delta_time = time.delta_secs();
2,060✔
2386
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
2,060✔
2387
    sim_params.virtual_delta_time = virtual_time.delta_secs();
2,060✔
2388
    sim_params.real_time = real_time.elapsed_secs_f64();
2,060✔
2389
    sim_params.real_delta_time = real_time.delta_secs();
2,060✔
2390

2391
    // Collect added effects for later GPU data allocation
2392
    extracted_effects.added_effects = q_added_effects
2,060✔
2393
        .iter()
1,030✔
2394
        .chain(mem::take(&mut *pending_effects).into_iter().filter_map(|main_entity| {
5,159✔
2395
            q_all_effects.get(main_entity.id()).ok().map(|(render_entity, compiled_particle_effect)| {
54✔
2396
                (main_entity.id(), render_entity, compiled_particle_effect)
27✔
2397
            })
2398
        }))
2399
        .filter_map(|(entity, render_entity, compiled_effect)| {
1,042✔
2400
            let handle = compiled_effect.asset.clone_weak();
36✔
2401
            let asset = match effects.get(&compiled_effect.asset) {
26✔
2402
                None => {
2403
                    // The effect wasn't ready yet. Retry on subsequent frames.
2404
                    trace!("Failed to find asset for {:?}/{:?}, deferring to next frame", entity, render_entity);
10✔
2405
                    pending_effects.push(entity.into());
2406
                    return None;
2407
                }
2408
                Some(asset) => asset,
4✔
2409
            };
2410
            let particle_layout = asset.particle_layout();
6✔
2411
            assert!(
2✔
2412
                particle_layout.size() > 0,
2✔
2413
                "Invalid empty particle layout for effect '{}' on entity {:?} (render entity {:?}). Did you forget to add some modifier to the asset?",
×
2414
                asset.name,
2415
                entity,
2416
                render_entity.id(),
2417
            );
2418
            let property_layout = asset.property_layout();
6✔
2419
            let mesh = compiled_effect
4✔
2420
                .mesh
2✔
2421
                .clone()
2✔
2422
                .unwrap_or(default_mesh.0.clone());
6✔
2423

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

2435
            // 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
2436
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2437
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
6✔
2438
                entity: entity.into(),
×
2439
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2440
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
2441
            });
2442

2443
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
8✔
2444
            Some(AddedEffect {
2✔
2445
                entity: MainEntity::from(entity),
6✔
2446
                render_entity: *render_entity,
4✔
2447
                capacity: asset.capacity(),
6✔
2448
                mesh,
4✔
2449
                parent,
4✔
2450
                particle_layout,
4✔
2451
                property_layout,
4✔
2452
                layout_flags: compiled_effect.layout_flags,
2✔
2453
                handle,
2✔
2454
            })
2455
        })
2456
        .collect();
1,030✔
2457

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

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

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

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

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

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

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

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

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

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

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

2581
        let effect_metadata_aligned_size = NonZeroU32::new(
2582
            GpuEffectMetadata::min_size()
8✔
2583
                .get()
8✔
2584
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2585
        )
2586
        .unwrap();
2587

2588
        trace!(
4✔
2589
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2✔
2590
            storage_buffer_align,
2591
            GpuEffectMetadata::min_size().get(),
4✔
2592
            effect_metadata_aligned_size.get(),
4✔
2593
        );
2594

2595
        Self {
2596
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
12✔
2597
            effect_metadata_aligned_size,
2598
        }
2599
    }
2600

2601
    /// Byte alignment for any storage buffer binding.
2602
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2603
        self.storage_buffer_align
3✔
2604
    }
2605

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

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

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

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

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

2679
        Self {
2680
            view_bind_group: None,
2681
            indirect_sim_params_bind_group: None,
2682
            indirect_metadata_bind_group: None,
2683
            indirect_spawner_bind_group: None,
2684
            sim_params_uniforms: UniformBuffer::default(),
6✔
2685
            spawner_buffer: AlignedBufferVec::new(
6✔
2686
                BufferUsages::STORAGE,
2687
                NonZeroU64::new(item_align),
2688
                Some("hanabi:buffer:spawner".to_string()),
2689
            ),
2690
            update_dispatch_indirect_buffer: GpuBuffer::new(
6✔
2691
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2692
                Some("hanabi:buffer:update_dispatch_indirect".to_string()),
2693
            ),
2694
            effect_metadata_buffer: BufferTable::new(
6✔
2695
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2696
                NonZeroU64::new(item_align),
2697
                Some("hanabi:buffer:effect_metadata".to_string()),
2698
            ),
2699
            gpu_limits,
2700
            indirect_shader_noevent,
2701
            indirect_shader_events,
2702
            indirect_pipeline_ids: [
3✔
2703
                CachedComputePipelineId::INVALID,
2704
                CachedComputePipelineId::INVALID,
2705
            ],
2706
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2707
        }
2708
    }
2709

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

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

2736
            // Allocate an indirect dispatch arguments struct for this instance
2737
            let update_dispatch_indirect_buffer_row_index =
2738
                self.update_dispatch_indirect_buffer.allocate();
2739

2740
            // Allocate per-effect metadata.
2741
            let gpu_effect_metadata = GpuEffectMetadata {
2742
                alive_count: 0,
2743
                max_update: 0,
2744
                dead_count: added_effect.capacity,
2745
                max_spawn: added_effect.capacity,
2746
                ..default()
2747
            };
2748
            trace!("+ Effect: {:?}", gpu_effect_metadata);
2✔
2749
            let effect_metadata_buffer_table_id =
2750
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2751
            let dispatch_buffer_indices = DispatchBufferIndices {
2752
                update_dispatch_indirect_buffer_row_index,
2753
                effect_metadata_buffer_table_id,
2754
            };
2755

2756
            // Insert the effect into the cache. This will allocate all the necessary
2757
            // mandatory GPU resources as needed.
2758
            let cached_effect = effect_cache.insert(
2759
                added_effect.handle,
2760
                added_effect.capacity,
2761
                &added_effect.particle_layout,
2762
                added_effect.layout_flags,
2763
            );
2764
            let mut cmd = commands.entity(added_effect.render_entity.id());
2765
            cmd.insert((
2766
                added_effect.entity,
2767
                cached_effect,
2768
                dispatch_buffer_indices,
2769
                CachedMesh {
2770
                    mesh: added_effect.mesh.id(),
2771
                },
2772
            ));
2773

2774
            // Allocate storage for properties if needed
2775
            if !added_effect.property_layout.is_empty() {
1✔
2776
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
1✔
2777
                cmd.insert(cached_effect_properties);
1✔
2778
            } else {
2779
                cmd.remove::<CachedEffectProperties>();
1✔
2780
            }
2781

2782
            // Allocate storage for the reference to the parent effect if needed. Note that
2783
            // we cannot yet allocate the complete parent info (CachedChildInfo) because it
2784
            // depends on the list of children, which we can't resolve until all
2785
            // effects have been added/removed this frame. This will be done later in
2786
            // resolve_parents().
2787
            if let Some(parent) = added_effect.parent.as_ref() {
×
2788
                let cached_parent: CachedParentRef = CachedParentRef {
2789
                    entity: parent.entity,
2790
                };
2791
                cmd.insert(cached_parent);
2792
                trace!("+ new effect declares parent entity {:?}", parent.entity);
×
2793
            } else {
2794
                cmd.remove::<CachedParentRef>();
4✔
2795
                trace!("+ new effect declares no parent");
4✔
2796
            }
2797

2798
            // Allocate storage for GPU spawn events if needed
2799
            if let Some(parent) = added_effect.parent.as_ref() {
×
2800
                let cached_events = event_cache.allocate(parent.event_count);
2801
                cmd.insert(cached_events);
2802
            } else {
2803
                cmd.remove::<CachedEffectEvents>();
2✔
2804
            }
2805

2806
            // Ensure the particle@1 bind group layout exists for the given configuration of
2807
            // particle layout and (optionally) parent particle layout.
2808
            {
2809
                let parent_min_binding_size = added_effect
2810
                    .parent
2811
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2812
                effect_cache.ensure_particle_bind_group_layout(
2813
                    added_effect.particle_layout.min_binding_size32(),
2814
                    parent_min_binding_size,
2815
                );
2816
            }
2817

2818
            // Ensure the metadata@3 bind group layout exists for init pass.
2819
            {
2820
                let consume_gpu_spawn_events = added_effect
2821
                    .layout_flags
2822
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2823
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2824
            }
2825

2826
            // We cannot yet determine the layout of the metadata@3 bind group for the
2827
            // update pass, because it depends on the number of children, and
2828
            // this is encoded indirectly via the number of child effects
2829
            // pointing to this parent, and only calculated later in
2830
            // resolve_parents().
2831

2832
            trace!(
2833
                "+ added effect entity {:?}: main_entity={:?} \
2✔
2834
                first_update_group_dispatch_buffer_index={} \
2✔
2835
                render_effect_dispatch_buffer_id={}",
2✔
2836
                added_effect.render_entity,
2837
                added_effect.entity,
2838
                update_dispatch_indirect_buffer_row_index,
2839
                effect_metadata_buffer_table_id.0
2840
            );
2841
        }
2842
    }
2843

2844
    pub fn allocate_spawner(
1,014✔
2845
        &mut self,
2846
        global_transform: &GlobalTransform,
2847
        spawn_count: u32,
2848
        prng_seed: u32,
2849
        effect_metadata_buffer_table_id: BufferTableId,
2850
    ) -> u32 {
2851
        let spawner_base = self.spawner_buffer.len() as u32;
2,028✔
2852
        let transform = global_transform.compute_matrix().into();
4,056✔
2853
        let inverse_transform = Mat4::from(
2854
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2855
            // efficient than inversing the Mat4.
2856
            global_transform.affine().inverse(),
2,028✔
2857
        )
2858
        .into();
2859
        let spawner_params = GpuSpawnerParams {
2860
            transform,
2861
            inverse_transform,
2862
            spawn: spawn_count as i32,
2,028✔
2863
            seed: prng_seed,
2864
            effect_metadata_index: effect_metadata_buffer_table_id.0,
1,014✔
2865
            ..default()
2866
        };
2867
        trace!("spawner params = {:?}", spawner_params);
2,028✔
2868
        self.spawner_buffer.push(spawner_params);
3,042✔
2869
        spawner_base
1,014✔
2870
    }
2871
}
2872

2873
bitflags! {
2874
    /// Effect flags.
2875
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2876
    pub struct LayoutFlags: u32 {
2877
        /// No flags.
2878
        const NONE = 0;
2879
        // DEPRECATED - The effect uses an image texture.
2880
        //const PARTICLE_TEXTURE = (1 << 0);
2881
        /// The effect is simulated in local space.
2882
        const LOCAL_SPACE_SIMULATION = (1 << 2);
2883
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2884
        const USE_ALPHA_MASK = (1 << 3);
2885
        /// The effect is rendered with flipbook texture animation based on the
2886
        /// [`Attribute::SPRITE_INDEX`] of each particle.
2887
        const FLIPBOOK = (1 << 4);
2888
        /// The effect needs UVs.
2889
        const NEEDS_UV = (1 << 5);
2890
        /// The effect has ribbons.
2891
        const RIBBONS = (1 << 6);
2892
        /// The effects needs normals.
2893
        const NEEDS_NORMAL = (1 << 7);
2894
        /// The effect is fully-opaque.
2895
        const OPAQUE = (1 << 8);
2896
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
2897
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2898
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
2899
        /// a single CPU spawn count.
2900
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2901
        /// The (init or update) shader needs access to its parent particle. This allows
2902
        /// a particle init or update pass to read the data of a parent particle, for
2903
        /// example to inherit some of the attributes.
2904
        const READ_PARENT_PARTICLE = (1 << 11);
2905
        /// The effect access to the particle data in the fragment shader.
2906
        const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
2907
    }
2908
}
2909

2910
impl Default for LayoutFlags {
2911
    fn default() -> Self {
1✔
2912
        Self::NONE
1✔
2913
    }
2914
}
2915

2916
/// Observer raised when the [`CachedEffect`] component is removed, which
2917
/// indicates that the effect instance was despawned.
2918
pub(crate) fn on_remove_cached_effect(
1✔
2919
    trigger: Trigger<OnRemove, CachedEffect>,
2920
    query: Query<(
2921
        Entity,
2922
        MainEntity,
2923
        &CachedEffect,
2924
        &DispatchBufferIndices,
2925
        Option<&CachedEffectProperties>,
2926
        Option<&CachedParentInfo>,
2927
        Option<&CachedEffectEvents>,
2928
    )>,
2929
    mut effect_cache: ResMut<EffectCache>,
2930
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2931
    mut effects_meta: ResMut<EffectsMeta>,
2932
    mut event_cache: ResMut<EventCache>,
2933
) {
2934
    #[cfg(feature = "trace")]
2935
    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
3✔
2936

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

2940
    // Fecth the components of the effect being destroyed. Note that the despawn
2941
    // command above is not yet applied, so this query should always succeed.
2942
    let Ok((
2943
        render_entity,
1✔
2944
        main_entity,
2945
        cached_effect,
2946
        dispatch_buffer_indices,
2947
        _opt_props,
2948
        _opt_parent,
2949
        opt_cached_effect_events,
2950
    )) = query.get(trigger.target())
3✔
2951
    else {
2952
        return;
×
2953
    };
2954

2955
    // Dealllocate the effect slice in the event buffer, if any.
2956
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
2957
        match event_cache.free(cached_effect_events) {
2958
            Err(err) => {
×
2959
                error!("Error while freeing effect event slice: {err:?}");
×
2960
            }
2961
            Ok(buffer_state) => {
×
2962
                if buffer_state != BufferState::Used {
×
2963
                    // Clear bind groups associated with the old buffer
2964
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
2965
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
2966
                }
2967
            }
2968
        }
2969
    }
2970

2971
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
2972
    // last slice, also deallocate the GPU buffer itself.
2973
    trace!(
2974
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
1✔
2975
        render_entity,
2976
        main_entity,
2977
    );
2978
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
2979
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
2980
        // do.
2981
        return;
×
2982
    };
2983

2984
    // Clear bind groups associated with the removed buffer
2985
    trace!(
1✔
2986
        "=> GPU buffer #{} gone, destroying its bind groups...",
1✔
2987
        cached_effect.buffer_index
2988
    );
2989
    effect_bind_groups
2990
        .particle_buffers
2991
        .remove(&cached_effect.buffer_index);
2992
    effects_meta
2993
        .update_dispatch_indirect_buffer
2994
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
2995
    effects_meta
2996
        .effect_metadata_buffer
2997
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
2998
}
2999

3000
/// Update the [`CachedEffect`] component for any newly allocated effect.
3001
///
3002
/// After this system ran, and its commands are applied, all valid extracted
3003
/// effects have a corresponding entity in the render world, with a
3004
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3005
pub(crate) fn add_effects(
1,030✔
3006
    commands: Commands,
3007
    mut effects_meta: ResMut<EffectsMeta>,
3008
    mut effect_cache: ResMut<EffectCache>,
3009
    mut property_cache: ResMut<PropertyCache>,
3010
    mut event_cache: ResMut<EventCache>,
3011
    mut extracted_effects: ResMut<ExtractedEffects>,
3012
    mut sort_bind_groups: ResMut<SortBindGroups>,
3013
) {
3014
    #[cfg(feature = "trace")]
3015
    let _span = bevy::log::info_span!("add_effects").entered();
3,090✔
3016
    trace!("add_effects");
2,050✔
3017

3018
    // Clear last frame's buffer resizes which may have occured during last frame,
3019
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3020
    // the first point at which we can do that where we're not blocking the main
3021
    // world (so, excluding the extract system).
3022
    effects_meta
1,030✔
3023
        .update_dispatch_indirect_buffer
1,030✔
3024
        .clear_previous_frame_resizes();
3025
    effects_meta
1,030✔
3026
        .effect_metadata_buffer
1,030✔
3027
        .clear_previous_frame_resizes();
3028
    sort_bind_groups.clear_previous_frame_resizes();
1,030✔
3029
    event_cache.clear_previous_frame_resizes();
1,030✔
3030

3031
    // Allocate new effects
3032
    effects_meta.add_effects(
3,090✔
3033
        commands,
2,060✔
3034
        std::mem::take(&mut extracted_effects.added_effects),
3,090✔
3035
        &mut effect_cache,
2,060✔
3036
        &mut property_cache,
1,030✔
3037
        &mut event_cache,
1,030✔
3038
    );
3039

3040
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3041
    // because EffectBuffer already contains a reference to the
3042
    // RenderDevice, so has done so internally. This is not ideal
3043
    // design-wise, but works.
3044
}
3045

3046
/// Check if two lists of entities are equal.
3047
fn is_child_list_changed(
×
3048
    parent_entity: Entity,
3049
    old: impl ExactSizeIterator<Item = Entity>,
3050
    new: impl ExactSizeIterator<Item = Entity>,
3051
) -> bool {
3052
    if old.len() != new.len() {
×
3053
        trace!(
×
3054
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3055
            parent_entity,
×
3056
            old.len(),
×
3057
            new.len()
×
3058
        );
3059
        return true;
×
3060
    }
3061

3062
    // TODO - this value is arbitrary
3063
    if old.len() >= 16 {
×
3064
        // For large-ish lists, use a hash set.
3065
        let old = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(old);
×
3066
        let new = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(new);
×
3067
        if old != new {
×
3068
            trace!(
×
3069
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3070
            );
3071
            true
×
3072
        } else {
3073
            false
×
3074
        }
3075
    } else {
3076
        // For small lists, just use a linear array and sort it
3077
        let mut old = old.collect::<Vec<_>>();
×
3078
        let mut new = new.collect::<Vec<_>>();
×
3079
        old.sort_unstable();
×
3080
        new.sort_unstable();
×
3081
        if old != new {
×
3082
            trace!(
×
3083
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3084
            );
3085
            true
×
3086
        } else {
3087
            false
×
3088
        }
3089
    }
3090
}
3091

3092
/// Resolve parents and children, updating their [`CachedParent`] and
3093
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3094
/// slice for all children of each parent.
3095
pub(crate) fn resolve_parents(
1,030✔
3096
    mut commands: Commands,
3097
    q_child_effects: Query<
3098
        (
3099
            Entity,
3100
            &CachedParentRef,
3101
            &CachedEffectEvents,
3102
            Option<&CachedChildInfo>,
3103
        ),
3104
        With<CachedEffect>,
3105
    >,
3106
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3107
    effect_cache: Res<EffectCache>,
3108
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3109
    mut event_cache: ResMut<EventCache>,
3110
    mut children_from_parent: Local<
3111
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3112
    >,
3113
) {
3114
    #[cfg(feature = "trace")]
3115
    let _span = bevy::log::info_span!("resolve_parents").entered();
3,090✔
3116
    let num_parent_effects = q_parent_effects.iter().len();
3,090✔
3117
    trace!("resolve_parents: num_parents={num_parent_effects}");
2,050✔
3118

3119
    // Build map of render entity from main entity for all cached effects.
3120
    let render_from_main_entity = q_cached_effects
2,060✔
3121
        .iter()
3122
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
3,058✔
3123
        .collect::<HashMap<_, _>>();
3124

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

3130
    // Group child effects by parent, building a list of children for each parent,
3131
    // solely based on the declaration each child makes of its parent. This doesn't
3132
    // mean yet that the parent exists.
3133
    if children_from_parent.capacity() < num_parent_effects {
1,030✔
3134
        let extra = num_parent_effects - children_from_parent.capacity();
×
3135
        children_from_parent.reserve(extra);
×
3136
    }
3137
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3138
        q_child_effects.iter()
2,060✔
3139
    {
3140
        // Resolve the parent reference into the render world
3141
        let parent_main_entity = cached_parent_ref.entity;
3142
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3143
            warn!(
×
3144
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3145
                parent_main_entity, child_entity
3146
            );
3147
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3148
            continue;
×
3149
        };
3150
        let parent_entity = *parent_entity;
3151

3152
        // Resolve the parent
3153
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3154
            // Since we failed to resolve, remove this component so the next systems ignore
3155
            // this effect.
3156
            warn!(
×
3157
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3158
                parent_entity, child_entity
3159
            );
3160
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3161
            continue;
×
3162
        };
3163
        let Some(parent_buffer_binding_source) = effect_cache
×
3164
            .get_buffer(parent_cached_effect.buffer_index)
3165
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3166
        else {
3167
            // Since we failed to resolve, remove this component so the next systems ignore
3168
            // this effect.
3169
            warn!(
×
3170
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3171
                parent_cached_effect.buffer_index, child_entity
3172
            );
3173
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3174
            continue;
×
3175
        };
3176

3177
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3178
        else {
3179
            // Since we failed to resolve, remove this component so the next systems ignore
3180
            // this effect.
3181
            warn!(
×
3182
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3183
                cached_effect_events.buffer_index, child_entity
3184
            );
3185
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3186
            continue;
×
3187
        };
3188
        let child_buffer_binding_source = BufferBindingSource {
3189
            buffer: child_event_buffer.clone(),
3190
            offset: cached_effect_events.range.start,
3191
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3192
        };
3193

3194
        // Push the child entity into the children list
3195
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3196
        let local_child_index = child_vec.len() as u32;
3197
        child_vec.push((child_entity, child_buffer_binding_source));
3198
        child_infos.push(GpuChildInfo {
3199
            event_count: 0,
3200
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3201
        });
3202

3203
        // Check if child info changed. Avoid overwriting if no change.
3204
        if let Some(old_cached_child_info) = cached_child_info {
×
3205
            if parent_entity == old_cached_child_info.parent
3206
                && parent_cached_effect.slice.particle_layout
×
3207
                    == old_cached_child_info.parent_particle_layout
×
3208
                && parent_buffer_binding_source
×
3209
                    == old_cached_child_info.parent_buffer_binding_source
×
3210
                // 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.
3211
                && local_child_index == old_cached_child_info.local_child_index
×
3212
                && cached_effect_events.init_indirect_dispatch_index
×
3213
                    == old_cached_child_info.init_indirect_dispatch_index
×
3214
            {
3215
                trace!(
×
3216
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3217
                    child_entity
3218
                );
3219
                continue;
×
3220
            }
3221
        }
3222

3223
        // Allocate (or overwrite, if already existing) the child info, now that the
3224
        // parent is resolved.
3225
        let cached_child_info = CachedChildInfo {
3226
            parent: parent_entity,
3227
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
3228
            parent_buffer_binding_source,
3229
            local_child_index,
3230
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3231
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3232
        };
3233
        commands.entity(child_entity).insert(cached_child_info);
3234
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3235

3236
        // Make a note of the parent entity so that we remember to mark its
3237
        // `CachedParentInfo` as changed below.
3238
        parents_with_dirty_children.insert(parent_entity);
3239
    }
3240

3241
    // Once all parents are resolved, diff all children of already-cached parents,
3242
    // and re-allocate their GpuChildInfo if needed.
3243
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
2,060✔
3244
        // Fetch the newly extracted list of children
3245
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3246
        else {
3247
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3248
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3249
            continue;
×
3250
        };
3251

3252
        // If we updated `CachedChildInfo` for any of this entity's children,
3253
        // then even if the check below passes, we must still set the change
3254
        // flag on this entity's `CachedParentInfo`. That's because the
3255
        // `fixup_parents` system looks at the change flag for the parent in
3256
        // order to determine which `CachedChildInfo` it needs to update, and
3257
        // that system must process all newly-added `CachedChildInfo`s.
3258
        if parents_with_dirty_children.contains(&parent_entity) {
×
3259
            cached_parent_info.set_changed();
×
3260
        }
3261

3262
        // Check if any child changed compared to the existing CachedChildren component
3263
        if !is_child_list_changed(
3264
            parent_entity,
3265
            cached_parent_info
3266
                .children
3267
                .iter()
3268
                .map(|(entity, _)| *entity),
3269
            children.iter().map(|(entity, _)| *entity),
3270
        ) {
3271
            continue;
×
3272
        }
3273

3274
        event_cache.reallocate_child_infos(
3275
            parent_entity,
3276
            children,
3277
            &child_infos[..],
3278
            cached_parent_info.deref_mut(),
3279
        );
3280
    }
3281

3282
    // Once this is done, the children hash map contains all entries which don't
3283
    // already have a CachedParentInfo component. That is, all entities which are
3284
    // new parents.
3285
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
2,060✔
3286
        let cached_parent_info =
3287
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3288
        commands.entity(parent_entity).insert(cached_parent_info);
3289
    }
3290

3291
    // // Once all changes are applied, immediately schedule any GPU buffer
3292
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3293
    // content // will be written later.
3294
    // if event_cache
3295
    //     .child_infos()
3296
    //     .allocate_gpu(render_device, render_queue)
3297
    // {
3298
    //     // All those bind groups use the buffer so need to be re-created
3299
    //     effect_bind_groups.particle_buffers.clear();
3300
    // }
3301
}
3302

3303
pub fn fixup_parents(
1,030✔
3304
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3305
    mut q_children: Query<&mut CachedChildInfo>,
3306
) {
3307
    #[cfg(feature = "trace")]
3308
    let _span = bevy::log::info_span!("fixup_parents").entered();
3,090✔
3309
    trace!("fixup_parents");
2,050✔
3310

3311
    // Once all parents are (re-)allocated, fix up the global index of all
3312
    // children if the parent base index changed.
3313
    trace!(
1,030✔
3314
        "Updating the global index of children of parent effects whose child list just changed..."
1,020✔
3315
    );
3316
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
2,060✔
3317
        let base_index =
3318
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3319
        trace!(
3320
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3321
            cached_parent_info.children.len(),
×
3322
            parent_entity,
3323
            base_index
3324
        );
3325
        for (child_entity, _) in &cached_parent_info.children {
×
3326
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3327
                continue;
×
3328
            };
3329
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3330
            trace!(
×
3331
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3332
                child_entity,
3333
                parent_entity,
3334
                cached_child_info.local_child_index,
×
3335
                cached_child_info.global_child_index
×
3336
            );
3337
        }
3338
    }
3339
}
3340

3341
/// Update any cached mesh info based on any relocation done by Bevy itself.
3342
pub fn update_mesh_locations(
1,030✔
3343
    mut commands: Commands,
3344
    mesh_allocator: Res<MeshAllocator>,
3345
    render_meshes: Res<RenderAssets<RenderMesh>>,
3346
    mut q_cached_effects: Query<
3347
        (Entity, &CachedMesh, Option<&mut CachedMeshLocation>),
3348
        With<CachedEffect>,
3349
    >,
3350
) {
3351
    for (entity, cached_mesh, maybe_cached_mesh_location) in &mut q_cached_effects {
3,058✔
3352
        // Resolve the render mesh
3353
        let Some(render_mesh) = render_meshes.get(cached_mesh.mesh) else {
1,014✔
3354
            warn!(
×
3355
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3356
                entity, cached_mesh.mesh
3357
            );
3358
            continue;
×
3359
        };
3360

3361
        // Find the location where the render mesh was allocated. This is handled by
3362
        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3363
        // re-batch the vertex and optional index data of meshes together at any point,
3364
        // so we need to confirm that the location data we may have cached is still
3365
        // valid.
3366
        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&cached_mesh.mesh)
1,014✔
3367
        else {
3368
            trace!(
×
3369
                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
×
3370
                entity,
3371
                cached_mesh.mesh
3372
            );
3373
            continue;
×
3374
        };
3375
        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&cached_mesh.mesh);
3376
        let indexed =
1,014✔
3377
            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
1,014✔
3378
                if let Some(ref slice) = mesh_index_buffer_slice {
1,014✔
3379
                    Some(MeshIndexSlice {
3380
                        format: index_format,
3381
                        buffer: slice.buffer.clone(),
3382
                        range: slice.range.clone(),
3383
                    })
3384
                } else {
3385
                    trace!(
×
3386
                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
×
3387
                        entity,
3388
                        cached_mesh.mesh
3389
                    );
3390
                    continue;
×
3391
                }
3392
            } else {
3393
                None
×
3394
            };
3395

3396
        // Calculate the new mesh location as it should be based on Bevy's info
3397
        let new_mesh_location = match &mesh_index_buffer_slice {
3398
            // Indexed mesh rendering
3399
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3400
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
3,042✔
3401
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
2,028✔
3402
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
2,028✔
3403
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
1,014✔
3404
                indexed,
3405
            },
3406
            // Non-indexed mesh rendering
3407
            None => CachedMeshLocation {
3408
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3409
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
3410
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3411
                vertex_offset_or_base_instance: 0,
3412
                indexed: None,
3413
            },
3414
        };
3415

3416
        // Compare to any cached data and update if necessary, or insert if missing.
3417
        // This will trigger change detection in the ECS, which will in turn trigger
3418
        // GpuEffectMetadata re-upload.
3419
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
1,012✔
3420
            #[cfg(debug_assertions)]
3421
            if *old_mesh_location.deref() != new_mesh_location {
3422
                debug!(
×
3423
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3424
                    entity, old_mesh_location, new_mesh_location
3425
                );
3426
            }
3427

3428
            old_mesh_location.set_if_neq(new_mesh_location);
3429
        } else {
3430
            commands.entity(entity).insert(new_mesh_location);
6✔
3431
        }
3432
    }
3433
}
3434

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

3449
/// Render world cached mesh infos for a single effect instance.
3450
#[derive(Debug, Clone, Copy, Component)]
3451
pub(crate) struct CachedMesh {
3452
    /// Asset of the effect mesh to draw.
3453
    pub mesh: AssetId<Mesh>,
3454
}
3455

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

3468
impl PartialEq for MeshIndexSlice {
3469
    fn eq(&self, other: &Self) -> bool {
2,024✔
3470
        self.format == other.format
2,024✔
3471
            && self.buffer.id() == other.buffer.id()
4,048✔
3472
            && self.range == other.range
2,024✔
3473
    }
3474
}
3475

3476
impl Eq for MeshIndexSlice {}
3477

3478
/// Cached info about a mesh location in a Bevy buffer. This information is
3479
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
3480
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
3481
/// so we can invalidate that GPU data.
3482
#[derive(Debug, Clone, PartialEq, Eq, Component)]
3483
pub(crate) struct CachedMeshLocation {
3484
    /// Vertex buffer.
3485
    pub vertex_buffer: BufferId,
3486
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
3487
    pub vertex_or_index_count: u32,
3488
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
3489
    pub first_index_or_vertex_offset: u32,
3490
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
3491
    pub vertex_offset_or_base_instance: i32,
3492
    /// Indexed rendering metadata.
3493
    pub indexed: Option<MeshIndexSlice>,
3494
}
3495

3496
/// Render world cached properties info for a single effect instance.
3497
#[allow(unused)]
3498
#[derive(Debug, Component)]
3499
pub(crate) struct CachedProperties {
3500
    /// Layout of the effect properties.
3501
    pub layout: PropertyLayout,
3502
    /// Index of the buffer in the [`EffectCache`].
3503
    pub buffer_index: u32,
3504
    /// Offset in bytes inside the buffer.
3505
    pub offset: u32,
3506
    /// Binding size in bytes of the property struct.
3507
    pub binding_size: u32,
3508
}
3509

3510
#[derive(SystemParam)]
3511
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3512
    sim_params: Res<'w, SimParams>,
3513
    render_device: Res<'w, RenderDevice>,
3514
    render_queue: Res<'w, RenderQueue>,
3515
    marker: PhantomData<&'s usize>,
3516
}
3517

3518
#[derive(SystemParam)]
3519
pub struct PipelineSystemParams<'w, 's> {
3520
    pipeline_cache: Res<'w, PipelineCache>,
3521
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3522
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3523
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3524
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3525
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3526
    specialized_indirect_pipelines:
3527
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3528
    marker: PhantomData<&'s usize>,
3529
}
3530

3531
pub(crate) fn prepare_effects(
1,030✔
3532
    mut commands: Commands,
3533
    read_only_params: PrepareEffectsReadOnlyParams,
3534
    mut pipelines: PipelineSystemParams,
3535
    mut property_cache: ResMut<PropertyCache>,
3536
    event_cache: Res<EventCache>,
3537
    mut effect_cache: ResMut<EffectCache>,
3538
    mut effects_meta: ResMut<EffectsMeta>,
3539
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3540
    mut extracted_effects: ResMut<ExtractedEffects>,
3541
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3542
    q_cached_effects: Query<(
3543
        MainEntity,
3544
        &CachedEffect,
3545
        Ref<CachedMesh>,
3546
        Ref<CachedMeshLocation>,
3547
        &DispatchBufferIndices,
3548
        Option<&CachedEffectProperties>,
3549
        Option<&CachedParentInfo>,
3550
        Option<&CachedChildInfo>,
3551
        Option<&CachedEffectEvents>,
3552
    )>,
3553
    q_debug_all_entities: Query<MainEntity>,
3554
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
3555
    mut sort_bind_groups: ResMut<SortBindGroups>,
3556
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3557
) {
3558
    #[cfg(feature = "trace")]
3559
    let _span = bevy::log::info_span!("prepare_effects").entered();
3,090✔
3560
    trace!("prepare_effects");
2,050✔
3561

3562
    init_fill_dispatch_queue.clear();
1,030✔
3563

3564
    // Workaround for too many params in system (TODO: refactor to split work?)
3565
    let sim_params = read_only_params.sim_params.into_inner();
3,090✔
3566
    let render_device = read_only_params.render_device.into_inner();
3,090✔
3567
    let render_queue = read_only_params.render_queue.into_inner();
3,090✔
3568
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
3,090✔
3569
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
3,090✔
3570
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
3,090✔
3571
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
3,090✔
3572

3573
    // // sort first by z and then by handle. this ensures that, when possible,
3574
    // batches span multiple z layers // batches won't span z-layers if there is
3575
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3576
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3577
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3578
    //         other => other,
3579
    //     }
3580
    // });
3581

3582
    // Ensure the indirect pipelines are created
3583
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
1,033✔
3584
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
12✔
3585
            pipeline_cache,
6✔
3586
            &pipelines.indirect_pipeline,
3✔
3587
            DispatchIndirectPipelineKey { has_events: false },
3✔
3588
        );
3589
    }
3590
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
1,033✔
3591
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
12✔
3592
            pipeline_cache,
6✔
3593
            &pipelines.indirect_pipeline,
3✔
3594
            DispatchIndirectPipelineKey { has_events: true },
3✔
3595
        );
3596
    }
3597
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
1,033✔
3598
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
6✔
3599
    } else {
3600
        // If this is the first time we insert an event buffer, we need to switch the
3601
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3602
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3603
        // more effect using GPU spawn events, we can deallocate.
3604
        let was_empty =
1,027✔
3605
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
3606
        let is_empty = event_cache.child_infos().is_empty();
3607
        if was_empty && !is_empty {
1,027✔
3608
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3609
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3610
        } else if is_empty && !was_empty {
2,054✔
3611
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3612
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3613
        }
3614
    }
3615

3616
    gpu_buffer_operations.begin_frame();
1,030✔
3617

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

3621
    // Build batcher inputs from extracted effects, updating all cached components
3622
    // for each effect on the fly.
3623
    let effects = std::mem::take(&mut extracted_effects.effects);
3,090✔
3624
    let extracted_effect_count = effects.len();
3,090✔
3625
    let mut prepared_effect_count = 0;
2,060✔
3626
    for extracted_effect in effects.into_iter() {
3,074✔
3627
        // Skip effects not cached. Since we're iterating over the extracted effects
3628
        // instead of the cached ones, it might happen we didn't cache some effect on
3629
        // purpose because they failed earlier validations.
3630
        // FIXME - extract into ECS directly so we don't have to do that?
3631
        let Ok((
3632
            main_entity,
1,014✔
3633
            cached_effect,
3634
            cached_mesh,
3635
            cached_mesh_location,
3636
            dispatch_buffer_indices,
3637
            cached_effect_properties,
3638
            cached_parent_info,
3639
            cached_child_info,
3640
            cached_effect_events,
3641
        )) = q_cached_effects.get(extracted_effect.render_entity.id())
3,042✔
3642
        else {
3643
            warn!(
×
3644
                "Unknown render entity {:?} for extracted effect.",
×
3645
                extracted_effect.render_entity.id()
×
3646
            );
3647
            if let Ok(main_entity) = q_debug_all_entities.get(extracted_effect.render_entity.id()) {
×
3648
                info!(
3649
                    "Render entity {:?} exists with main entity {:?}, some component missing!",
×
3650
                    extracted_effect.render_entity.id(),
×
3651
                    main_entity
3652
                );
3653
            } else {
3654
                info!(
×
3655
                    "Render entity {:?} does not exists with a MainEntity.",
×
3656
                    extracted_effect.render_entity.id()
×
3657
                );
3658
            }
3659
            continue;
×
3660
        };
3661

3662
        let effect_slice = EffectSlice {
3663
            slice: cached_effect.slice.range(),
3664
            buffer_index: cached_effect.buffer_index,
3665
            particle_layout: cached_effect.slice.particle_layout.clone(),
3666
        };
3667

3668
        let has_event_buffer = cached_child_info.is_some();
3669
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3670
        // should be Option<T>, not T)
3671
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3672
            None
1,005✔
3673
        } else {
3674
            Some(extracted_effect.property_layout.min_binding_size())
9✔
3675
        };
3676

3677
        // Schedule some GPU buffer operation to update the number of workgroups to
3678
        // dispatch during the indirect init pass of this effect based on the number of
3679
        // GPU spawn events written in its buffer.
3680
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3681
            (cached_effect_events, cached_child_info)
3682
        {
3683
            debug_assert_eq!(
3684
                GpuChildInfo::min_size().get() % 4,
3685
                0,
3686
                "Invalid GpuChildInfo alignment."
×
3687
            );
3688

3689
            // Resolve parent entry
3690
            let Ok((_, _, _, _, _, _, cached_parent_info, _, _)) =
×
3691
                q_cached_effects.get(cached_child_info.parent)
×
3692
            else {
3693
                continue;
×
3694
            };
3695
            let Some(cached_parent_info) = cached_parent_info else {
×
3696
                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);
×
3697
                continue;
×
3698
            };
3699

3700
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3701
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3702
            let global_child_index = cached_child_info.global_child_index;
×
3703

3704
            // Schedule a fill dispatch
3705
            trace!(
×
3706
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3707
                global_child_index,
3708
                init_indirect_dispatch_index,
3709
            );
3710
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3711
        }
3712

3713
        // Create init pipeline key flags.
3714
        let init_pipeline_key_flags = {
1,014✔
3715
            let mut flags = ParticleInitPipelineKeyFlags::empty();
3716
            flags.set(
3717
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3718
                effect_slice.particle_layout.contains(Attribute::PREV),
3719
            );
3720
            flags.set(
3721
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3722
                effect_slice.particle_layout.contains(Attribute::NEXT),
3723
            );
3724
            flags.set(
3725
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3726
                has_event_buffer,
3727
            );
3728
            flags
3729
        };
3730

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

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

3791
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3792
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3793

3794
        // Specialize the init pipeline based on the effect.
3795
        let init_pipeline_id = {
3796
            let consume_gpu_spawn_events = init_pipeline_key_flags
3797
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3798

3799
            // Fetch the metadata@3 bind group layout from the cache
3800
            let metadata_bind_group_layout = effect_cache
3801
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3802
                .unwrap()
3803
                .clone();
3804

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

3834
            init_pipeline_id
3835
        };
3836

3837
        let update_pipeline_id = {
3838
            let num_event_buffers = cached_parent_info
3839
                .map(|p| p.children.len() as u32)
×
3840
                .unwrap_or_default();
3841

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

3849
            // Fetch the bind group layouts from the cache
3850
            let metadata_bind_group_layout = effect_cache
3851
                .metadata_update_bind_group_layout(num_event_buffers)
3852
                .unwrap()
3853
                .clone();
3854

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

3884
            update_pipeline_id
3885
        };
3886

3887
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3888
            init: init_pipeline_id,
3889
            update: update_pipeline_id,
3890
        };
3891

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

3907
        // Output some debug info
3908
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
2,028✔
3909
        trace!(
3910
            "update_shader = {:?}",
1,014✔
3911
            extracted_effect.effect_shaders.update
3912
        );
3913
        trace!(
3914
            "render_shader = {:?}",
1,014✔
3915
            extracted_effect.effect_shaders.render
3916
        );
3917
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
1,014✔
3918
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
1,014✔
3919

3920
        let spawner_index = effects_meta.allocate_spawner(
3921
            &extracted_effect.transform,
3922
            extracted_effect.spawn_count,
3923
            extracted_effect.prng_seed,
3924
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
3925
        );
3926

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

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

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

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

4020
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4021
        // update its GpuEffectMetadata with all those infos.
4022
        // FIXME - should do this only when the below changes (not only the mesh), via
4023
        // some invalidation mechanism and ECS change detection.
4024
        if !cached_mesh.is_changed() && !cached_mesh_location.is_changed() {
1,012✔
4025
            prepared_effect_count += 1;
1,012✔
4026
            continue;
4027
        }
4028

4029
        let capacity = cached_effect.slice.len();
4030

4031
        // Global and local indices of this effect as a child of another (parent) effect
4032
        let (global_child_index, local_child_index) = cached_child_info
UNCOV
4033
            .map(|cci| (cci.global_child_index, cci.local_child_index))
×
4034
            .unwrap_or_default();
4035

4036
        // Base index of all children of this (parent) effect
4037
        let base_child_index = cached_parent_info
4038
            .map(|cpi| {
×
4039
                debug_assert_eq!(
×
4040
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4041
                    0
4042
                );
4043
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4044
            })
4045
            .unwrap_or_default();
4046

4047
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
4048
        let sort_key_offset = extracted_effect
4049
            .particle_layout
4050
            .offset(Attribute::RIBBON_ID)
4051
            .unwrap_or_default()
4052
            / 4;
4053
        let sort_key2_offset = extracted_effect
4054
            .particle_layout
4055
            .offset(Attribute::AGE)
4056
            .unwrap_or_default()
4057
            / 4;
4058

4059
        let gpu_effect_metadata = GpuEffectMetadata {
4060
            vertex_or_index_count: cached_mesh_location.vertex_or_index_count,
4061
            instance_count: 0,
4062
            first_index_or_vertex_offset: cached_mesh_location.first_index_or_vertex_offset,
4063
            vertex_offset_or_base_instance: cached_mesh_location.vertex_offset_or_base_instance,
4064
            base_instance: 0,
4065
            alive_count: 0,
4066
            max_update: 0,
4067
            dead_count: capacity,
4068
            max_spawn: capacity,
4069
            ping: 0,
4070
            indirect_dispatch_index: dispatch_buffer_indices
4071
                .update_dispatch_indirect_buffer_row_index,
4072
            // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4073
            indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
4074
            init_indirect_dispatch_index: cached_effect_events
4075
                .map(|cee| cee.init_indirect_dispatch_index)
4076
                .unwrap_or_default(),
4077
            local_child_index,
4078
            global_child_index,
4079
            base_child_index,
4080
            particle_stride,
4081
            sort_key_offset,
4082
            sort_key2_offset,
4083
            ..default()
4084
        };
4085

4086
        assert!(dispatch_buffer_indices
4087
            .effect_metadata_buffer_table_id
4088
            .is_valid());
4089
        effects_meta.effect_metadata_buffer.update(
2✔
4090
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
4091
            gpu_effect_metadata,
4092
        );
4093

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

4102
        prepared_effect_count += 1;
4103
    }
4104
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
2,050✔
4105

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

4118
    // Write the entire spawner buffer for this frame, for all effects combined
4119
    assert_eq!(
4120
        prepared_effect_count,
4121
        effects_meta.spawner_buffer.len() as u32
4122
    );
4123
    if effects_meta
1,030✔
4124
        .spawner_buffer
1,030✔
4125
        .write_buffer(render_device, render_queue)
3,090✔
4126
    {
4127
        // All property bind groups use the spawner buffer, which was reallocate
4128
        effect_bind_groups.particle_buffers.clear();
6✔
4129
        property_bind_groups.clear(true);
4✔
4130
        effects_meta.indirect_spawner_bind_group = None;
2✔
4131
    }
4132

4133
    // Update simulation parameters
4134
    effects_meta.sim_params_uniforms.set(sim_params.into());
4,120✔
4135
    {
4136
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
3,090✔
4137
        gpu_sim_params.num_effects = prepared_effect_count;
1,030✔
4138

4139
        trace!(
1,030✔
4140
            "Simulation parameters: time={} delta_time={} virtual_time={} \
1,020✔
4141
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
1,020✔
4142
            gpu_sim_params.time,
4143
            gpu_sim_params.delta_time,
4144
            gpu_sim_params.virtual_time,
4145
            gpu_sim_params.virtual_delta_time,
4146
            gpu_sim_params.real_time,
4147
            gpu_sim_params.real_delta_time,
4148
            gpu_sim_params.num_effects,
4149
        );
4150
    }
4151
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
6,174✔
4152
    effects_meta
1,030✔
4153
        .sim_params_uniforms
1,030✔
4154
        .write_buffer(render_device, render_queue);
3,090✔
4155
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
6,183✔
4156
        // Buffer changed, invalidate bind groups
4157
        effects_meta.indirect_sim_params_bind_group = None;
3✔
4158
    }
4159
}
4160

4161
pub(crate) fn batch_effects(
1,030✔
4162
    mut commands: Commands,
4163
    effects_meta: Res<EffectsMeta>,
4164
    mut sort_bind_groups: ResMut<SortBindGroups>,
4165
    mut q_cached_effects: Query<(
4166
        Entity,
4167
        &MainEntity,
4168
        &CachedMesh,
4169
        Option<&CachedEffectEvents>,
4170
        Option<&CachedChildInfo>,
4171
        Option<&CachedProperties>,
4172
        &mut DispatchBufferIndices,
4173
        &mut BatchInput,
4174
    )>,
4175
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4176
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4177
) {
4178
    trace!("batch_effects");
2,050✔
4179

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

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

4192
    let mut sort_queue = GpuBufferOperationQueue::new();
2,060✔
4193

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

4226
        let translation = input.position;
2,028✔
4227

4228
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4229
        // most of the data needed to drive rendering. However this doesn't drive
4230
        // rendering; this is just storage.
4231
        let mut effect_batch = EffectBatch::from_input(
4232
            cached_mesh,
1,014✔
4233
            cached_effect_events,
1,014✔
4234
            cached_child_info,
1,014✔
4235
            &mut input,
1,014✔
4236
            *dispatch_buffer_indices.as_ref(),
1,014✔
4237
            cached_properties.map(|cp| PropertyBindGroupKey {
2,028✔
4238
                buffer_index: cp.buffer_index,
9✔
4239
                binding_size: cp.binding_size,
9✔
4240
            }),
4241
            cached_properties.map(|cp| cp.offset),
2,028✔
4242
        );
4243

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

4255
            // Allocate a GpuDispatchIndirect entry
4256
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4257
            effect_batch.sort_fill_indirect_dispatch_index =
4258
                Some(sort_fill_indirect_dispatch_index);
4259

4260
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4261
            // compute a number of workgroups to dispatch based on that particle count, and
4262
            // store the result into a GpuDispatchIndirect struct which will be used to
4263
            // dispatch the fill-sort pass.
4264
            {
4265
                let src_buffer = effect_metadata_buffer.clone();
4266
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4267
                    effect_batch
4268
                        .dispatch_buffer_indices
4269
                        .effect_metadata_buffer_table_id,
4270
                );
4271
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4272
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4273
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4274
                    continue;
×
4275
                };
4276
                let dst_buffer = dst_buffer.clone();
4277
                let dst_binding_offset = 0; // see dst_offset below
4278
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4279
                trace!(
4280
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4281
                    src_buffer.id(),
×
4282
                    src_binding_offset,
4283
                    src_binding_size.get(),
×
4284
                    dst_buffer.id(),
×
4285
                    dst_binding_offset,
4286
                    -1, //dst_binding_size.get(),
4287
                );
4288
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4289
                debug_assert_eq!(
4290
                    src_offset, 5,
4291
                    "GpuEffectMetadata changed, update this assert."
×
4292
                );
4293
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4294
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4295
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4296
                // binding offset of 0 and binds the entire destination buffer,
4297
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4298
                // index the proper offset in the buffer. This requires of
4299
                // course binding the entire buffer, or at least enough to index all operations
4300
                // (hence the None below). This is not really a general solution, so should be
4301
                // reviewed.
4302
                let dst_offset = sort_bind_groups
×
4303
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
4304
                    / 4;
4305
                sort_queue.enqueue(
4306
                    GpuBufferOperationType::FillDispatchArgs,
4307
                    GpuBufferOperationArgs {
4308
                        src_offset,
4309
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
4310
                        dst_offset,
4311
                        dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
4312
                        count: 1,
4313
                    },
4314
                    src_buffer,
4315
                    src_binding_offset,
4316
                    Some(src_binding_size),
4317
                    dst_buffer,
4318
                    dst_binding_offset,
4319
                    None, //Some(dst_binding_size),
4320
                );
4321
            }
4322
        }
4323

4324
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
1,014✔
4325
        trace!(
4326
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
1,014✔
4327
            effect_batch_index,
4328
            entity,
4329
        );
4330

4331
        // Spawn an EffectDrawBatch, to actually drive rendering.
4332
        commands
4333
            .spawn(EffectDrawBatch {
4334
                effect_batch_index,
4335
                translation,
4336
                main_entity: *main_entity,
4337
            })
4338
            .insert(TemporaryRenderEntity);
4339
    }
4340

4341
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
1,030✔
4342
    if !sort_queue.operation_queue.is_empty() {
1,030✔
4343
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4344
    }
4345

4346
    sorted_effect_batches.sort();
1,030✔
4347
}
4348

4349
/// Per-buffer bind groups for a GPU effect buffer.
4350
///
4351
/// This contains all bind groups specific to a single [`EffectBuffer`].
4352
///
4353
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4354
pub(crate) struct BufferBindGroups {
4355
    /// Bind group for the render shader.
4356
    ///
4357
    /// ```wgsl
4358
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4359
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4360
    /// @binding(2) var<storage, read> spawner : Spawner;
4361
    /// ```
4362
    render: BindGroup,
4363
    // /// Bind group for filling the indirect dispatch arguments of any child init
4364
    // /// pass.
4365
    // ///
4366
    // /// This bind group is optional; it's only created if the current effect has
4367
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4368
    // /// (although normally the event buffer is not created if there's no
4369
    // /// children).
4370
    // ///
4371
    // /// The source buffer is always the current effect's event buffer. The
4372
    // /// destination buffer is the global shared buffer for indirect fill args
4373
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4374
    // /// args contains the data to index the relevant part of the global shared
4375
    // /// buffer for this effect buffer; it may contain multiple entries in case
4376
    // /// multiple effects are batched inside the current effect buffer.
4377
    // ///
4378
    // /// ```wgsl
4379
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4380
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4381
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4382
    // /// ```
4383
    // init_fill_dispatch: Option<BindGroup>,
4384
}
4385

4386
/// Combination of a texture layout and the bound textures.
4387
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4388
struct Material {
4389
    layout: TextureLayout,
4390
    textures: Vec<AssetId<Image>>,
4391
}
4392

4393
impl Material {
4394
    /// Get the bind group entries to create a bind group.
4395
    pub fn make_entries<'a>(
×
4396
        &self,
4397
        gpu_images: &'a RenderAssets<GpuImage>,
4398
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4399
        if self.textures.is_empty() {
×
4400
            return Ok(vec![]);
×
4401
        }
4402

4403
        let entries: Vec<BindGroupEntry<'a>> = self
×
4404
            .textures
×
4405
            .iter()
4406
            .enumerate()
4407
            .flat_map(|(index, id)| {
×
4408
                let base_binding = index as u32 * 2;
×
4409
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4410
                    vec![
×
4411
                        BindGroupEntry {
×
4412
                            binding: base_binding,
×
4413
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4414
                        },
4415
                        BindGroupEntry {
×
4416
                            binding: base_binding + 1,
×
4417
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4418
                        },
4419
                    ]
4420
                } else {
4421
                    vec![]
×
4422
                }
4423
            })
4424
            .collect();
4425
        if entries.len() == self.textures.len() * 2 {
×
4426
            return Ok(entries);
×
4427
        }
4428
        Err(())
×
4429
    }
4430
}
4431

4432
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4433
struct BindingKey {
4434
    pub buffer_id: BufferId,
4435
    pub offset: u32,
4436
    pub size: NonZeroU32,
4437
}
4438

4439
impl<'a> From<BufferSlice<'a>> for BindingKey {
4440
    fn from(value: BufferSlice<'a>) -> Self {
×
4441
        Self {
4442
            buffer_id: value.buffer.id(),
×
4443
            offset: value.offset,
×
4444
            size: value.size,
×
4445
        }
4446
    }
4447
}
4448

4449
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4450
    fn from(value: &BufferSlice<'a>) -> Self {
×
4451
        Self {
4452
            buffer_id: value.buffer.id(),
×
4453
            offset: value.offset,
×
4454
            size: value.size,
×
4455
        }
4456
    }
4457
}
4458

4459
impl From<&BufferBindingSource> for BindingKey {
4460
    fn from(value: &BufferBindingSource) -> Self {
×
4461
        Self {
4462
            buffer_id: value.buffer.id(),
×
4463
            offset: value.offset,
×
4464
            size: value.size,
×
4465
        }
4466
    }
4467
}
4468

4469
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4470
struct ConsumeEventKey {
4471
    child_infos_buffer_id: BufferId,
4472
    events: BindingKey,
4473
}
4474

4475
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4476
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4477
        Self {
4478
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4479
            events: value.events.into(),
×
4480
        }
4481
    }
4482
}
4483

4484
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4485
struct InitMetadataBindGroupKey {
4486
    pub buffer_index: u32,
4487
    pub effect_metadata_buffer: BufferId,
4488
    pub effect_metadata_offset: u32,
4489
    pub consume_event_key: Option<ConsumeEventKey>,
4490
}
4491

4492
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4493
struct UpdateMetadataBindGroupKey {
4494
    pub buffer_index: u32,
4495
    pub effect_metadata_buffer: BufferId,
4496
    pub effect_metadata_offset: u32,
4497
    pub child_info_buffer_id: Option<BufferId>,
4498
    pub event_buffers_keys: Vec<BindingKey>,
4499
}
4500

4501
struct CachedBindGroup<K: Eq> {
4502
    /// Key the bind group was created from. Each time the key changes, the bind
4503
    /// group should be re-created.
4504
    key: K,
4505
    /// Bind group created from the key.
4506
    bind_group: BindGroup,
4507
}
4508

4509
#[derive(Debug, Clone, Copy)]
4510
struct BufferSlice<'a> {
4511
    pub buffer: &'a Buffer,
4512
    pub offset: u32,
4513
    pub size: NonZeroU32,
4514
}
4515

4516
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4517
    fn from(value: BufferSlice<'a>) -> Self {
×
4518
        Self {
4519
            buffer: value.buffer,
×
4520
            offset: value.offset.into(),
×
4521
            size: Some(value.size.into()),
×
4522
        }
4523
    }
4524
}
4525

4526
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4527
    fn from(value: &BufferSlice<'a>) -> Self {
×
4528
        Self {
4529
            buffer: value.buffer,
×
4530
            offset: value.offset.into(),
×
4531
            size: Some(value.size.into()),
×
4532
        }
4533
    }
4534
}
4535

4536
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4537
    fn from(value: &'a BufferBindingSource) -> Self {
×
4538
        Self {
4539
            buffer: &value.buffer,
×
4540
            offset: value.offset,
×
4541
            size: value.size,
×
4542
        }
4543
    }
4544
}
4545

4546
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4547
/// the init pass consumes GPU events as a mechanism to spawn particles.
4548
struct ConsumeEventBuffers<'a> {
4549
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4550
    /// This is dynamically indexed inside the shader.
4551
    child_infos_buffer: &'a Buffer,
4552
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4553
    events: BufferSlice<'a>,
4554
}
4555

4556
#[derive(Default, Resource)]
4557
pub struct EffectBindGroups {
4558
    /// Map from buffer index to the bind groups shared among all effects that
4559
    /// use that buffer.
4560
    particle_buffers: HashMap<u32, BufferBindGroups>,
4561
    /// Map of bind groups for image assets used as particle textures.
4562
    images: HashMap<AssetId<Image>, BindGroup>,
4563
    /// Map from buffer index to its metadata bind group (group 3) for the init
4564
    /// pass.
4565
    // FIXME - doesn't work with batching; this should be the instance ID
4566
    init_metadata_bind_groups: HashMap<u32, CachedBindGroup<InitMetadataBindGroupKey>>,
4567
    /// Map from buffer index to its metadata bind group (group 3) for the
4568
    /// update pass.
4569
    // FIXME - doesn't work with batching; this should be the instance ID
4570
    update_metadata_bind_groups: HashMap<u32, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4571
    /// Map from an effect material to its bind group.
4572
    material_bind_groups: HashMap<Material, BindGroup>,
4573
}
4574

4575
impl EffectBindGroups {
4576
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
1,013✔
4577
        self.particle_buffers
1,013✔
4578
            .get(&buffer_index)
2,026✔
4579
            .map(|bg| &bg.render)
1,013✔
4580
    }
4581

4582
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4583
    /// needed.
4584
    pub(self) fn get_or_create_init_metadata(
1,014✔
4585
        &mut self,
4586
        effect_batch: &EffectBatch,
4587
        gpu_limits: &GpuLimits,
4588
        render_device: &RenderDevice,
4589
        layout: &BindGroupLayout,
4590
        effect_metadata_buffer: &Buffer,
4591
        consume_event_buffers: Option<ConsumeEventBuffers>,
4592
    ) -> Result<&BindGroup, ()> {
4593
        let DispatchBufferIndices {
4594
            effect_metadata_buffer_table_id,
1,014✔
4595
            ..
4596
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4597

4598
        let effect_metadata_offset =
1,014✔
4599
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
2,028✔
4600
        let key = InitMetadataBindGroupKey {
4601
            buffer_index: effect_batch.buffer_index,
2,028✔
4602
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,042✔
4603
            effect_metadata_offset,
4604
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
2,028✔
4605
        };
4606

4607
        let make_entry = || {
1,016✔
4608
            let mut entries = Vec::with_capacity(3);
4✔
4609
            entries.push(
4✔
4610
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4611
                BindGroupEntry {
2✔
4612
                    binding: 0,
2✔
4613
                    resource: BindingResource::Buffer(BufferBinding {
2✔
4614
                        buffer: effect_metadata_buffer,
4✔
4615
                        offset: key.effect_metadata_offset as u64,
4✔
4616
                        size: Some(gpu_limits.effect_metadata_size()),
2✔
4617
                    }),
4618
                },
4619
            );
4620
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
4621
                entries.push(
4622
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4623
                    // ChildInfoBuffer;
4624
                    BindGroupEntry {
4625
                        binding: 1,
4626
                        resource: BindingResource::Buffer(BufferBinding {
4627
                            buffer: consume_event_buffers.child_infos_buffer,
4628
                            offset: 0,
4629
                            size: None,
4630
                        }),
4631
                    },
4632
                );
4633
                entries.push(
4634
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4635
                    BindGroupEntry {
4636
                        binding: 2,
4637
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4638
                    },
4639
                );
4640
            }
4641

4642
            let bind_group = render_device.create_bind_group(
6✔
4643
                "hanabi:bind_group:init:metadata@3",
4644
                layout,
2✔
4645
                &entries[..],
2✔
4646
            );
4647

4648
            trace!(
2✔
4649
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
4650
                    effect_batch.buffer_index,
4651
                    effect_metadata_buffer_table_id.0,
4652
                );
4653

4654
            bind_group
2✔
4655
        };
4656

4657
        Ok(&self
1,014✔
4658
            .init_metadata_bind_groups
1,014✔
4659
            .entry(effect_batch.buffer_index)
2,028✔
4660
            .and_modify(|cbg| {
2,026✔
4661
                if cbg.key != key {
1,012✔
4662
                    trace!(
×
4663
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4664
                        cbg.key,
4665
                        key
4666
                    );
4667
                    cbg.key = key;
×
4668
                    cbg.bind_group = make_entry();
×
4669
                }
4670
            })
4671
            .or_insert_with(|| {
1,016✔
4672
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
4673
                CachedBindGroup {
2✔
4674
                    key,
2✔
4675
                    bind_group: make_entry(),
2✔
4676
                }
4677
            })
4678
            .bind_group)
4679
    }
4680

4681
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4682
    /// needed.
4683
    pub(self) fn get_or_create_update_metadata(
1,014✔
4684
        &mut self,
4685
        effect_batch: &EffectBatch,
4686
        gpu_limits: &GpuLimits,
4687
        render_device: &RenderDevice,
4688
        layout: &BindGroupLayout,
4689
        effect_metadata_buffer: &Buffer,
4690
        child_info_buffer: Option<&Buffer>,
4691
        event_buffers: &[(Entity, BufferBindingSource)],
4692
    ) -> Result<&BindGroup, ()> {
4693
        let DispatchBufferIndices {
4694
            effect_metadata_buffer_table_id,
1,014✔
4695
            ..
4696
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4697

4698
        // Check arguments consistency
4699
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
5,070✔
4700
        let emits_gpu_spawn_events = !event_buffers.is_empty();
2,028✔
4701
        let child_info_buffer_id = if emits_gpu_spawn_events {
2,028✔
4702
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4703
        } else {
4704
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4705
            // if relevant, that is if the effect emits GPU spawn events.
4706
            None
1,014✔
4707
        };
4708
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
3,042✔
4709

4710
        let event_buffers_keys = event_buffers
2,028✔
4711
            .iter()
4712
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
1,014✔
4713
            .collect::<Vec<_>>();
4714

4715
        let key = UpdateMetadataBindGroupKey {
4716
            buffer_index: effect_batch.buffer_index,
2,028✔
4717
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,042✔
4718
            effect_metadata_offset: gpu_limits
3,042✔
4719
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4720
                as u32,
4721
            child_info_buffer_id,
4722
            event_buffers_keys,
4723
        };
4724

4725
        let make_entry = || {
1,016✔
4726
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
6✔
4727
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4728
            // EffectMetadata;
4729
            entries.push(BindGroupEntry {
6✔
4730
                binding: 0,
2✔
4731
                resource: BindingResource::Buffer(BufferBinding {
2✔
4732
                    buffer: effect_metadata_buffer,
4✔
4733
                    offset: key.effect_metadata_offset as u64,
4✔
4734
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
2✔
4735
                }),
4736
            });
4737
            if emits_gpu_spawn_events {
2✔
4738
                let child_info_buffer = child_info_buffer.unwrap();
×
4739

4740
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4741
                // ChildInfoBuffer;
4742
                entries.push(BindGroupEntry {
×
4743
                    binding: 1,
×
4744
                    resource: BindingResource::Buffer(BufferBinding {
×
4745
                        buffer: child_info_buffer,
×
4746
                        offset: 0,
×
4747
                        size: None,
×
4748
                    }),
4749
                });
4750

4751
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4752
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4753
                    // EventBuffer;
4754
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4755
                    // then moved to counting in bytes, so now need some conversion. Need to review
4756
                    // all of this...
4757
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4758
                    buffer_binding.offset *= 4;
4759
                    buffer_binding.size = buffer_binding
4760
                        .size
4761
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4762
                    entries.push(BindGroupEntry {
4763
                        binding: 2 + index as u32,
4764
                        resource: BindingResource::Buffer(buffer_binding),
4765
                    });
4766
                }
4767
            }
4768

4769
            let bind_group = render_device.create_bind_group(
6✔
4770
                "hanabi:bind_group:update:metadata@3",
4771
                layout,
2✔
4772
                &entries[..],
2✔
4773
            );
4774

4775
            trace!(
2✔
4776
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
2✔
4777
                effect_batch.buffer_index,
4778
                effect_metadata_buffer_table_id.0,
4779
            );
4780

4781
            bind_group
2✔
4782
        };
4783

4784
        Ok(&self
1,014✔
4785
            .update_metadata_bind_groups
1,014✔
4786
            .entry(effect_batch.buffer_index)
2,028✔
4787
            .and_modify(|cbg| {
2,026✔
4788
                if cbg.key != key {
1,012✔
4789
                    trace!(
×
4790
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4791
                        cbg.key,
4792
                        key
4793
                    );
4794
                    cbg.key = key.clone();
×
4795
                    cbg.bind_group = make_entry();
×
4796
                }
4797
            })
4798
            .or_insert_with(|| {
1,016✔
4799
                trace!(
2✔
4800
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
4801
                    key
4802
                );
4803
                CachedBindGroup {
2✔
4804
                    key: key.clone(),
4✔
4805
                    bind_group: make_entry(),
2✔
4806
                }
4807
            })
4808
            .bind_group)
4809
    }
4810
}
4811

4812
#[derive(SystemParam)]
4813
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4814
    #[cfg(feature = "2d")]
4815
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4816
    #[cfg(feature = "3d")]
4817
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4818
    #[cfg(feature = "3d")]
4819
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4820
    #[cfg(feature = "3d")]
4821
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4822
    marker: PhantomData<&'s usize>,
4823
}
4824

4825
fn emit_sorted_draw<T, F>(
2,028✔
4826
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
4827
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
4828
    view_entities: &mut FixedBitSet,
4829
    sorted_effect_batches: &SortedEffectBatches,
4830
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
4831
    render_pipeline: &mut ParticlesRenderPipeline,
4832
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
4833
    render_meshes: &RenderAssets<RenderMesh>,
4834
    pipeline_cache: &PipelineCache,
4835
    make_phase_item: F,
4836
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
4837
) where
4838
    T: SortedPhaseItem,
4839
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
4840
{
4841
    trace!("emit_sorted_draw() {} views", views.iter().len());
8,112✔
4842

4843
    for (visible_entities, view, msaa) in views.iter() {
6,084✔
4844
        trace!(
×
4845
            "Process new sorted view with {} visible particle effect entities",
2,028✔
4846
            visible_entities.len::<CompiledParticleEffect>()
4,056✔
4847
        );
4848

4849
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
1,014✔
4850
            continue;
1,014✔
4851
        };
4852

4853
        {
4854
            #[cfg(feature = "trace")]
4855
            let _span = bevy::log::info_span!("collect_view_entities").entered();
3,042✔
4856

4857
            view_entities.clear();
2,028✔
4858
            view_entities.extend(
2,028✔
4859
                visible_entities
1,014✔
4860
                    .iter::<EffectVisibilityClass>()
1,014✔
4861
                    .map(|e| e.1.index() as usize),
2,028✔
4862
            );
4863
        }
4864

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

4872
            trace!(
×
4873
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
1,014✔
4874
                draw_entity,
×
4875
                draw_batch.effect_batch_index,
×
4876
            );
4877

4878
            // Get the EffectBatches this EffectDrawBatch is part of.
4879
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
1,014✔
4880
            else {
×
4881
                continue;
×
4882
            };
4883

4884
            trace!(
×
4885
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
1,014✔
4886
                effect_batch.buffer_index,
×
4887
                effect_batch.spawner_base,
×
4888
                effect_batch.layout_flags,
×
4889
            );
4890

4891
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
4892
            if effect_batch
×
4893
                .layout_flags
×
4894
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
4895
            {
4896
                trace!("Non-transparent batch. Skipped.");
×
4897
                continue;
×
4898
            }
4899

4900
            // Check if batch contains any entity visible in the current view. Otherwise we
4901
            // can skip the entire batch. Note: This is O(n^2) but (unlike
4902
            // the Sprite renderer this is inspired from) we don't expect more than
4903
            // a handful of particle effect instances, so would rather not pay the memory
4904
            // cost of a FixedBitSet for the sake of an arguable speed-up.
4905
            // TODO - Profile to confirm.
4906
            #[cfg(feature = "trace")]
4907
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
4908
            let has_visible_entity = effect_batch
×
4909
                .entities
×
4910
                .iter()
4911
                .any(|index| view_entities.contains(*index as usize));
3,042✔
4912
            if !has_visible_entity {
×
4913
                trace!("No visible entity for view, not emitting any draw call.");
×
4914
                continue;
×
4915
            }
4916
            #[cfg(feature = "trace")]
4917
            _span_check_vis.exit();
2,028✔
4918

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

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

4925
            let local_space_simulation = effect_batch
2,028✔
4926
                .layout_flags
1,014✔
4927
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
1,014✔
4928
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
3,042✔
4929
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
3,042✔
4930
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
3,042✔
4931
            let needs_normal = effect_batch
2,028✔
4932
                .layout_flags
1,014✔
4933
                .contains(LayoutFlags::NEEDS_NORMAL);
1,014✔
4934
            let needs_particle_fragment = effect_batch
2,028✔
4935
                .layout_flags
1,014✔
4936
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
1,014✔
4937
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
3,042✔
4938
            let image_count = effect_batch.texture_layout.layout.len() as u8;
2,028✔
4939

4940
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
4941
            // re-querying here...?
4942
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
3,042✔
4943
                trace!("Batch has no render mesh, skipped.");
×
4944
                continue;
×
4945
            };
4946
            let mesh_layout = render_mesh.layout.clone();
×
4947

4948
            // Specialize the render pipeline based on the effect batch
4949
            trace!(
×
4950
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
1,014✔
4951
                effect_batch.render_shader,
×
4952
                image_count,
×
4953
                alpha_mask,
×
4954
                flipbook,
×
4955
                view.hdr
×
4956
            );
4957

4958
            // Add a draw pass for the effect batch
4959
            trace!("Emitting individual draw for batch");
1,014✔
4960

4961
            let alpha_mode = effect_batch.alpha_mode;
×
4962

4963
            #[cfg(feature = "trace")]
4964
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
4965
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
4966
                pipeline_cache,
×
4967
                render_pipeline,
×
4968
                ParticleRenderPipelineKey {
×
4969
                    shader: effect_batch.render_shader.clone(),
×
4970
                    mesh_layout: Some(mesh_layout),
×
4971
                    particle_layout: effect_batch.particle_layout.clone(),
×
4972
                    texture_layout: effect_batch.texture_layout.clone(),
×
4973
                    local_space_simulation,
×
4974
                    alpha_mask,
×
4975
                    alpha_mode,
×
4976
                    flipbook,
×
4977
                    needs_uv,
×
4978
                    needs_normal,
×
4979
                    needs_particle_fragment,
×
4980
                    ribbons,
×
4981
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
4982
                    pipeline_mode,
×
4983
                    msaa_samples: msaa.samples(),
×
4984
                    hdr: view.hdr,
×
4985
                },
4986
            );
4987
            #[cfg(feature = "trace")]
4988
            _span_specialize.exit();
×
4989

4990
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
1,014✔
4991
            trace!(
×
4992
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
1,014✔
4993
                spawner_base={} handle={:?}",
1,014✔
4994
                draw_entity,
×
4995
                effect_batch.buffer_index,
×
4996
                effect_batch.spawner_base,
×
4997
                effect_batch.handle
×
4998
            );
4999
            render_phase.add(make_phase_item(
×
5000
                render_pipeline_id,
×
5001
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5002
                draw_batch,
×
5003
                view,
×
5004
            ));
5005
        }
5006
    }
5007
}
5008

5009
#[cfg(feature = "3d")]
5010
fn emit_binned_draw<T, F, G>(
2,028✔
5011
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5012
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5013
    view_entities: &mut FixedBitSet,
5014
    sorted_effect_batches: &SortedEffectBatches,
5015
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5016
    render_pipeline: &mut ParticlesRenderPipeline,
5017
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5018
    pipeline_cache: &PipelineCache,
5019
    render_meshes: &RenderAssets<RenderMesh>,
5020
    make_batch_set_key: F,
5021
    make_bin_key: G,
5022
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5023
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5024
    change_tick: &mut Tick,
5025
) where
5026
    T: BinnedPhaseItem,
5027
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5028
    G: Fn() -> T::BinKey,
5029
{
5030
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5031

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

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

5037
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
2,028✔
5038
            continue;
×
5039
        };
5040

5041
        {
5042
            #[cfg(feature = "trace")]
5043
            let _span = bevy::log::info_span!("collect_view_entities").entered();
6,084✔
5044

5045
            view_entities.clear();
4,056✔
5046
            view_entities.extend(
4,056✔
5047
                visible_entities
2,028✔
5048
                    .iter::<EffectVisibilityClass>()
2,028✔
5049
                    .map(|e| e.1.index() as usize),
4,056✔
5050
            );
5051
        }
5052

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

5060
            trace!(
×
5061
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
2,028✔
5062
                draw_entity,
×
5063
                draw_batch.effect_batch_index,
×
5064
            );
5065

5066
            // Get the EffectBatches this EffectDrawBatch is part of.
5067
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
2,028✔
5068
            else {
×
5069
                continue;
×
5070
            };
5071

5072
            trace!(
×
5073
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
2,028✔
5074
                effect_batch.buffer_index,
×
5075
                effect_batch.spawner_base,
×
5076
                effect_batch.layout_flags,
×
5077
            );
5078

5079
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5080
                trace!(
2,028✔
5081
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
2,028✔
5082
                    effect_batch.layout_flags,
×
5083
                    alpha_mask
×
5084
                );
5085
                continue;
2,028✔
5086
            }
5087

5088
            // Check if batch contains any entity visible in the current view. Otherwise we
5089
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5090
            // the Sprite renderer this is inspired from) we don't expect more than
5091
            // a handful of particle effect instances, so would rather not pay the memory
5092
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5093
            // TODO - Profile to confirm.
5094
            #[cfg(feature = "trace")]
5095
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5096
            let has_visible_entity = effect_batch
×
5097
                .entities
×
5098
                .iter()
5099
                .any(|index| view_entities.contains(*index as usize));
×
5100
            if !has_visible_entity {
×
5101
                trace!("No visible entity for view, not emitting any draw call.");
×
5102
                continue;
×
5103
            }
5104
            #[cfg(feature = "trace")]
5105
            _span_check_vis.exit();
×
5106

5107
            // Create and cache the bind group layout for this texture layout
5108
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5109

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

5113
            let local_space_simulation = effect_batch
×
5114
                .layout_flags
×
5115
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5116
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5117
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5118
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5119
            let needs_normal = effect_batch
×
5120
                .layout_flags
×
5121
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5122
            let needs_particle_fragment = effect_batch
×
5123
                .layout_flags
×
5124
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5125
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5126
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5127
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5128

5129
            // Specialize the render pipeline based on the effect batch
5130
            trace!(
×
5131
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5132
                effect_batch.render_shader,
×
5133
                image_count,
×
5134
                alpha_mask,
×
5135
                flipbook,
×
5136
                view.hdr
×
5137
            );
5138

5139
            // Add a draw pass for the effect batch
5140
            trace!("Emitting individual draw for batch");
×
5141

5142
            let alpha_mode = effect_batch.alpha_mode;
×
5143

5144
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5145
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5146
                continue;
×
5147
            };
5148

5149
            #[cfg(feature = "trace")]
5150
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5151
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5152
                pipeline_cache,
×
5153
                render_pipeline,
×
5154
                ParticleRenderPipelineKey {
×
5155
                    shader: effect_batch.render_shader.clone(),
×
5156
                    mesh_layout: Some(mesh_layout),
×
5157
                    particle_layout: effect_batch.particle_layout.clone(),
×
5158
                    texture_layout: effect_batch.texture_layout.clone(),
×
5159
                    local_space_simulation,
×
5160
                    alpha_mask,
×
5161
                    alpha_mode,
×
5162
                    flipbook,
×
5163
                    needs_uv,
×
5164
                    needs_normal,
×
5165
                    needs_particle_fragment,
×
5166
                    ribbons,
×
5167
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5168
                    pipeline_mode,
×
5169
                    msaa_samples: msaa.samples(),
×
5170
                    hdr: view.hdr,
×
5171
                },
5172
            );
5173
            #[cfg(feature = "trace")]
5174
            _span_specialize.exit();
×
5175

5176
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5177
            trace!(
×
5178
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5179
                spawner_base={} handle={:?}",
×
5180
                draw_entity,
×
5181
                effect_batch.buffer_index,
×
5182
                effect_batch.spawner_base,
×
5183
                effect_batch.handle
×
5184
            );
5185
            render_phase.add(
×
5186
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
5187
                make_bin_key(),
×
5188
                (draw_entity, draw_batch.main_entity),
×
5189
                InputUniformIndex::default(),
×
5190
                BinnedRenderPhaseType::NonMesh,
×
5191
                *change_tick,
×
5192
            );
5193
        }
5194
    }
5195
}
5196

5197
#[allow(clippy::too_many_arguments)]
5198
pub(crate) fn queue_effects(
1,030✔
5199
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5200
    effects_meta: Res<EffectsMeta>,
5201
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5202
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5203
    pipeline_cache: Res<PipelineCache>,
5204
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5205
    sorted_effect_batches: Res<SortedEffectBatches>,
5206
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5207
    events: Res<EffectAssetEvents>,
5208
    render_meshes: Res<RenderAssets<RenderMesh>>,
5209
    read_params: QueueEffectsReadOnlyParams,
5210
    mut view_entities: Local<FixedBitSet>,
5211
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5212
        ViewSortedRenderPhases<Transparent2d>,
5213
    >,
5214
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5215
        ViewSortedRenderPhases<Transparent3d>,
5216
    >,
5217
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5218
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5219
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5220
    ),
5221
    mut change_tick: Local<Tick>,
5222
) {
5223
    #[cfg(feature = "trace")]
5224
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
3,090✔
5225

5226
    trace!("queue_effects");
2,050✔
5227

5228
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5229
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5230
    // reuse stale data.
5231
    let next_change_tick = change_tick.get() + 1;
2,060✔
5232
    change_tick.set(next_change_tick);
2,060✔
5233

5234
    // If an image has changed, the GpuImage has (probably) changed
5235
    for event in &events.images {
1,057✔
5236
        match event {
5237
            AssetEvent::Added { .. } => None,
24✔
5238
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5239
            AssetEvent::Unused { .. } => None,
×
5240
            AssetEvent::Modified { id } => {
×
5241
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5242
                effect_bind_groups.images.remove(id)
×
5243
            }
5244
            AssetEvent::Removed { id } => {
3✔
5245
                trace!("Destroy bind group of removed image asset {:?}", id);
5✔
5246
                effect_bind_groups.images.remove(id)
9✔
5247
            }
5248
        };
5249
    }
5250

5251
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
3,080✔
5252
        // No spawners are active
5253
        return;
16✔
5254
    }
5255

5256
    // Loop over all 2D cameras/views that need to render effects
5257
    #[cfg(feature = "2d")]
5258
    {
5259
        #[cfg(feature = "trace")]
5260
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5261

5262
        let draw_effects_function_2d = read_params
5263
            .draw_functions_2d
5264
            .read()
5265
            .get_id::<DrawEffects>()
5266
            .unwrap();
5267

5268
        // Effects with full alpha blending
5269
        if !views.is_empty() {
5270
            trace!("Emit effect draw calls for alpha blended 2D views...");
2,028✔
5271
            emit_sorted_draw(
5272
                &views,
5273
                &mut transparent_2d_render_phases,
5274
                &mut view_entities,
5275
                &sorted_effect_batches,
5276
                &effect_draw_batches,
5277
                &mut render_pipeline,
5278
                specialized_render_pipelines.reborrow(),
5279
                &render_meshes,
5280
                &pipeline_cache,
5281
                |id, entity, draw_batch, _view| Transparent2d {
5282
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5283
                    entity,
×
5284
                    pipeline: id,
×
5285
                    draw_function: draw_effects_function_2d,
×
5286
                    batch_range: 0..1,
×
5287
                    extracted_index: 0, // ???
5288
                    extra_index: PhaseItemExtraIndex::None,
×
5289
                    indexed: true, // ???
5290
                },
5291
                #[cfg(feature = "3d")]
5292
                PipelineMode::Camera2d,
5293
            );
5294
        }
5295
    }
5296

5297
    // Loop over all 3D cameras/views that need to render effects
5298
    #[cfg(feature = "3d")]
5299
    {
5300
        #[cfg(feature = "trace")]
5301
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5302

5303
        // Effects with full alpha blending
5304
        if !views.is_empty() {
5305
            trace!("Emit effect draw calls for alpha blended 3D views...");
2,028✔
5306

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

5313
            emit_sorted_draw(
5314
                &views,
5315
                &mut transparent_3d_render_phases,
5316
                &mut view_entities,
5317
                &sorted_effect_batches,
5318
                &effect_draw_batches,
5319
                &mut render_pipeline,
5320
                specialized_render_pipelines.reborrow(),
5321
                &render_meshes,
5322
                &pipeline_cache,
5323
                |id, entity, batch, view| Transparent3d {
5324
                    distance: view
1,014✔
5325
                        .rangefinder3d()
1,014✔
5326
                        .distance_translation(&batch.translation),
2,028✔
5327
                    pipeline: id,
1,014✔
5328
                    entity,
1,014✔
5329
                    draw_function: draw_effects_function_3d,
1,014✔
5330
                    batch_range: 0..1,
1,014✔
5331
                    extra_index: PhaseItemExtraIndex::None,
1,014✔
5332
                    indexed: true, // ???
5333
                },
5334
                #[cfg(feature = "2d")]
5335
                PipelineMode::Camera3d,
5336
            );
5337
        }
5338

5339
        // Effects with alpha mask
5340
        if !views.is_empty() {
5341
            #[cfg(feature = "trace")]
5342
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
1,014✔
5343

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

5346
            let draw_effects_function_alpha_mask = read_params
5347
                .draw_functions_alpha_mask
5348
                .read()
5349
                .get_id::<DrawEffects>()
5350
                .unwrap();
5351

5352
            emit_binned_draw(
5353
                &views,
5354
                &mut alpha_mask_3d_render_phases,
5355
                &mut view_entities,
5356
                &sorted_effect_batches,
5357
                &effect_draw_batches,
5358
                &mut render_pipeline,
5359
                specialized_render_pipelines.reborrow(),
5360
                &pipeline_cache,
5361
                &render_meshes,
5362
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
5363
                    pipeline: id,
×
5364
                    draw_function: draw_effects_function_alpha_mask,
×
5365
                    material_bind_group_index: None,
×
5366
                    vertex_slab: default(),
×
5367
                    index_slab: None,
×
5368
                },
5369
                // Unused for now
5370
                || OpaqueNoLightmap3dBinKey {
5371
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5372
                },
5373
                #[cfg(feature = "2d")]
5374
                PipelineMode::Camera3d,
5375
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5376
                &mut change_tick,
5377
            );
5378
        }
5379

5380
        // Opaque particles
5381
        if !views.is_empty() {
5382
            #[cfg(feature = "trace")]
5383
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
1,014✔
5384

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

5387
            let draw_effects_function_opaque = read_params
5388
                .draw_functions_opaque
5389
                .read()
5390
                .get_id::<DrawEffects>()
5391
                .unwrap();
5392

5393
            emit_binned_draw(
5394
                &views,
5395
                &mut opaque_3d_render_phases,
5396
                &mut view_entities,
5397
                &sorted_effect_batches,
5398
                &effect_draw_batches,
5399
                &mut render_pipeline,
5400
                specialized_render_pipelines.reborrow(),
5401
                &pipeline_cache,
5402
                &render_meshes,
5403
                |id, _batch, _view| Opaque3dBatchSetKey {
5404
                    pipeline: id,
×
5405
                    draw_function: draw_effects_function_opaque,
×
5406
                    material_bind_group_index: None,
×
5407
                    vertex_slab: default(),
×
5408
                    index_slab: None,
×
5409
                    lightmap_slab: None,
×
5410
                },
5411
                // Unused for now
5412
                || Opaque3dBinKey {
5413
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5414
                },
5415
                #[cfg(feature = "2d")]
5416
                PipelineMode::Camera3d,
5417
                ParticleRenderAlphaMaskPipelineKey::Opaque,
5418
                &mut change_tick,
5419
            );
5420
        }
5421
    }
5422
}
5423

5424
/// Prepare GPU resources for effect rendering.
5425
///
5426
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5427
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5428
/// access to the current camera view.
5429
pub(crate) fn prepare_gpu_resources(
1,030✔
5430
    mut effects_meta: ResMut<EffectsMeta>,
5431
    //mut effect_cache: ResMut<EffectCache>,
5432
    mut event_cache: ResMut<EventCache>,
5433
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5434
    mut sort_bind_groups: ResMut<SortBindGroups>,
5435
    render_device: Res<RenderDevice>,
5436
    render_queue: Res<RenderQueue>,
5437
    view_uniforms: Res<ViewUniforms>,
5438
    render_pipeline: Res<ParticlesRenderPipeline>,
5439
) {
5440
    // Get the binding for the ViewUniform, the uniform data structure containing
5441
    // the Camera data for the current view. If not available, we cannot render
5442
    // anything.
5443
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
2,060✔
5444
        return;
×
5445
    };
5446

5447
    // Create the bind group for the camera/view parameters
5448
    // FIXME - Not here!
5449
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5450
        "hanabi:bind_group_camera_view",
5451
        &render_pipeline.view_layout,
5452
        &[
5453
            BindGroupEntry {
5454
                binding: 0,
5455
                resource: view_binding,
5456
            },
5457
            BindGroupEntry {
5458
                binding: 1,
5459
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5460
            },
5461
        ],
5462
    ));
5463

5464
    // Re-/allocate any GPU buffer if needed
5465
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5466
    // effect_bind_groups);
5467
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5468
    sort_bind_groups.prepare_buffers(&render_device);
5469
    if effects_meta
5470
        .update_dispatch_indirect_buffer
5471
        .prepare_buffers(&render_device)
5472
    {
5473
        // All those bind groups use the buffer so need to be re-created
5474
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
4✔
5475
        effect_bind_groups.particle_buffers.clear();
4✔
5476
    }
5477
}
5478

5479
/// Read the queued init fill dispatch operations, batch them together by
5480
/// contiguous source and destination entries in the buffers, and enqueue
5481
/// corresponding GPU buffer fill dispatch operations for all batches.
5482
///
5483
/// This system runs after the GPU buffers have been (re-)allocated in
5484
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5485
/// reference them from the generic [`GpuBufferOperationQueue`].
5486
pub(crate) fn queue_init_fill_dispatch_ops(
1,030✔
5487
    event_cache: Res<EventCache>,
5488
    render_device: Res<RenderDevice>,
5489
    render_queue: Res<RenderQueue>,
5490
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5491
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5492
) {
5493
    // Submit all queued init fill dispatch operations with the proper buffers
5494
    if !init_fill_dispatch_queue.is_empty() {
1,030✔
5495
        let src_buffer = event_cache.child_infos().buffer();
×
5496
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
5497
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
5498
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
5499
        } else {
5500
            if src_buffer.is_none() {
×
5501
                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());
×
5502
            }
5503
            if dst_buffer.is_none() {
×
5504
                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());
×
5505
            }
5506
        }
5507
    }
5508

5509
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5510
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
3,090✔
5511
}
5512

5513
pub(crate) fn prepare_bind_groups(
1,030✔
5514
    mut effects_meta: ResMut<EffectsMeta>,
5515
    mut effect_cache: ResMut<EffectCache>,
5516
    mut event_cache: ResMut<EventCache>,
5517
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5518
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5519
    mut sort_bind_groups: ResMut<SortBindGroups>,
5520
    property_cache: Res<PropertyCache>,
5521
    sorted_effect_batched: Res<SortedEffectBatches>,
5522
    render_device: Res<RenderDevice>,
5523
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5524
    utils_pipeline: Res<UtilsPipeline>,
5525
    update_pipeline: Res<ParticlesUpdatePipeline>,
5526
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5527
    gpu_images: Res<RenderAssets<GpuImage>>,
5528
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5529
) {
5530
    // We can't simulate nor render anything without at least the spawner buffer
5531
    if effects_meta.spawner_buffer.is_empty() {
2,060✔
5532
        return;
16✔
5533
    }
5534
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
5535
        return;
×
5536
    };
5537

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

5543
    {
5544
        #[cfg(feature = "trace")]
5545
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
5546

5547
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5548
        // loop below. Also allows earlying out before doing any work in case some
5549
        // buffer is missing.
5550
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
5551
            return;
×
5552
        };
5553

5554
        // Create the sim_params@0 bind group for the global simulation parameters,
5555
        // which is shared by the init and update passes.
5556
        if effects_meta.indirect_sim_params_bind_group.is_none() {
2✔
5557
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
8✔
5558
                "hanabi:bind_group:vfx_indirect:sim_params@0",
2✔
5559
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
4✔
5560
                &[BindGroupEntry {
2✔
5561
                    binding: 0,
2✔
5562
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
4✔
5563
                }],
5564
            ));
5565
        }
5566

5567
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5568
        // effects at once
5569
        effects_meta.indirect_metadata_bind_group = match (
5570
            effects_meta.effect_metadata_buffer.buffer(),
5571
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5572
        ) {
5573
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
1,014✔
5574
                // Base bind group for indirect pass
5575
                Some(render_device.create_bind_group(
5576
                    "hanabi:bind_group:vfx_indirect:metadata@1",
5577
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
5578
                    &[
5579
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5580
                        BindGroupEntry {
5581
                            binding: 0,
5582
                            resource: BindingResource::Buffer(BufferBinding {
5583
                                buffer: effect_metadata_buffer,
5584
                                offset: 0,
5585
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
5586
                            }),
5587
                        },
5588
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5589
                        BindGroupEntry {
5590
                            binding: 1,
5591
                            resource: BindingResource::Buffer(BufferBinding {
5592
                                buffer: dispatch_indirect_buffer,
5593
                                offset: 0,
5594
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
5595
                            }),
5596
                        },
5597
                    ],
5598
                ))
5599
            }
5600

5601
            // Some buffer is not yet available, can't create the bind group
5602
            _ => None,
×
5603
        };
5604

5605
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5606
        // effects at once
5607
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
5608
            let bind_group = render_device.create_bind_group(
10✔
5609
                "hanabi:bind_group:vfx_indirect:spawner@2",
5610
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
6✔
5611
                &[
4✔
5612
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5613
                    BindGroupEntry {
4✔
5614
                        binding: 0,
4✔
5615
                        resource: BindingResource::Buffer(BufferBinding {
4✔
5616
                            buffer: &spawner_buffer,
4✔
5617
                            offset: 0,
4✔
5618
                            size: None,
4✔
5619
                        }),
5620
                    },
5621
                ],
5622
            );
5623

5624
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
5625
        }
5626
    }
5627

5628
    // Create the per-buffer bind groups
5629
    trace!("Create per-buffer bind groups...");
1,014✔
5630
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
1,014✔
5631
        #[cfg(feature = "trace")]
5632
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
5633

5634
        let Some(effect_buffer) = effect_buffer else {
1,014✔
5635
            trace!(
×
5636
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5637
                buffer_index
5638
            );
5639
            continue;
×
5640
        };
5641

5642
        // Ensure all effects in this batch have a bind group for the entire buffer of
5643
        // the group, since the update phase runs on an entire group/buffer at once,
5644
        // with all the effect instances in it batched together.
5645
        trace!("effect particle buffer_index=#{}", buffer_index);
1,014✔
5646
        effect_bind_groups
5647
            .particle_buffers
5648
            .entry(buffer_index as u32)
5649
            .or_insert_with(|| {
2✔
5650
                // Bind group particle@1 for render pass
5651
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
4✔
5652
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
4✔
5653
                    render_device.limits().min_storage_buffer_offset_alignment,
2✔
5654
                );
5655
                let entries = [
4✔
5656
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5657
                    BindGroupEntry {
4✔
5658
                        binding: 0,
4✔
5659
                        resource: effect_buffer.max_binding(),
4✔
5660
                    },
5661
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5662
                    BindGroupEntry {
4✔
5663
                        binding: 1,
4✔
5664
                        resource: effect_buffer.indirect_index_max_binding(),
4✔
5665
                    },
5666
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5667
                    BindGroupEntry {
2✔
5668
                        binding: 2,
2✔
5669
                        resource: BindingResource::Buffer(BufferBinding {
2✔
5670
                            buffer: &spawner_buffer,
2✔
5671
                            offset: 0,
2✔
5672
                            size: Some(spawner_min_binding_size),
2✔
5673
                        }),
5674
                    },
5675
                ];
5676
                let render = render_device.create_bind_group(
8✔
5677
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
6✔
5678
                    effect_buffer.render_particles_buffer_layout(),
4✔
5679
                    &entries[..],
2✔
5680
                );
5681

5682
                BufferBindGroups { render }
2✔
5683
            });
5684
    }
5685

5686
    // Create bind groups for queued GPU buffer operations
5687
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5688

5689
    // Create the per-effect bind groups
5690
    let spawner_buffer_binding_size =
5691
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5692
    for effect_batch in sorted_effect_batched.iter() {
1,014✔
5693
        #[cfg(feature = "trace")]
5694
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
3,042✔
5695

5696
        // Create the property bind group @2 if needed
5697
        if let Some(property_key) = &effect_batch.property_key {
1,023✔
5698
            if let Err(err) = property_bind_groups.ensure_exists(
×
5699
                property_key,
5700
                &property_cache,
5701
                &spawner_buffer,
5702
                spawner_buffer_binding_size,
5703
                &render_device,
5704
            ) {
5705
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5706
                continue;
5707
            }
5708
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
3,015✔
5709
            &property_cache,
2,010✔
5710
            &spawner_buffer,
2,010✔
5711
            spawner_buffer_binding_size,
1,005✔
5712
            &render_device,
1,005✔
5713
        ) {
5714
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5715
            continue;
5716
        }
5717

5718
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5719
        // simulate particles.
5720
        if effect_cache
1,014✔
5721
            .create_particle_sim_bind_group(
5722
                effect_batch.buffer_index,
5723
                &render_device,
5724
                effect_batch.particle_layout.min_binding_size32(),
5725
                effect_batch.parent_min_binding_size,
5726
                effect_batch.parent_binding_source.as_ref(),
5727
            )
5728
            .is_err()
5729
        {
5730
            error!("No particle buffer allocated for effect batch.");
×
5731
            continue;
×
5732
        }
5733

5734
        // Bind group @3 of init pass
5735
        // FIXME - this is instance-dependent, not buffer-dependent
5736
        {
5737
            let consume_gpu_spawn_events = effect_batch
5738
                .layout_flags
5739
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
5740
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
1,014✔
5741
                effect_batch.spawn_info
5742
            {
5743
                assert!(consume_gpu_spawn_events);
×
5744
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
5745
                Some(ConsumeEventBuffers {
×
5746
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
5747
                    events: BufferSlice {
×
5748
                        buffer: event_cache
×
5749
                            .get_buffer(cached_effect_events.buffer_index)
×
5750
                            .unwrap(),
×
5751
                        // Note: event range is in u32 count, not bytes
5752
                        offset: cached_effect_events.range.start * 4,
×
5753
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5754
                    },
5755
                })
5756
            } else {
5757
                assert!(!consume_gpu_spawn_events);
2,028✔
5758
                None
1,014✔
5759
            };
5760
            let Some(init_metadata_layout) =
1,014✔
5761
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
5762
            else {
5763
                continue;
×
5764
            };
5765
            if effect_bind_groups
5766
                .get_or_create_init_metadata(
5767
                    effect_batch,
5768
                    &effects_meta.gpu_limits,
5769
                    &render_device,
5770
                    init_metadata_layout,
5771
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5772
                    consume_event_buffers,
5773
                )
5774
                .is_err()
5775
            {
5776
                continue;
×
5777
            }
5778
        }
5779

5780
        // Bind group @3 of update pass
5781
        // FIXME - this is instance-dependent, not buffer-dependent#
5782
        {
5783
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
5784

5785
            let Some(update_metadata_layout) =
1,014✔
5786
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5787
            else {
5788
                continue;
×
5789
            };
5790
            if effect_bind_groups
5791
                .get_or_create_update_metadata(
5792
                    effect_batch,
5793
                    &effects_meta.gpu_limits,
5794
                    &render_device,
5795
                    update_metadata_layout,
5796
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5797
                    event_cache.child_infos_buffer(),
5798
                    &effect_batch.child_event_buffers[..],
5799
                )
5800
                .is_err()
5801
            {
5802
                continue;
×
5803
            }
5804
        }
5805

5806
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
5807
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
5808

5809
            // Bind group @0 of sort-fill pass
5810
            let particle_buffer = effect_buffer.particle_buffer();
×
5811
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5812
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
5813
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
5814
                &effect_batch.particle_layout,
×
5815
                particle_buffer,
×
5816
                indirect_index_buffer,
×
5817
                effect_metadata_buffer,
×
5818
            ) {
5819
                error!(
5820
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
5821
                    err
5822
                );
5823
                continue;
5824
            }
5825

5826
            // Bind group @0 of sort-copy pass
5827
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
5828
            if let Err(err) = sort_bind_groups
×
5829
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
5830
            {
5831
                error!(
5832
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
5833
                    err
5834
                );
5835
                continue;
5836
            }
5837
        }
5838

5839
        // Ensure the particle texture(s) are available as GPU resources and that a bind
5840
        // group for them exists
5841
        // FIXME fix this insert+get below
5842
        if !effect_batch.texture_layout.layout.is_empty() {
1,014✔
5843
            // This should always be available, as this is cached into the render pipeline
5844
            // just before we start specializing it.
5845
            let Some(material_bind_group_layout) =
×
5846
                render_pipeline.get_material(&effect_batch.texture_layout)
×
5847
            else {
5848
                error!(
×
5849
                    "Failed to find material bind group layout for buffer #{}",
×
5850
                    effect_batch.buffer_index
5851
                );
5852
                continue;
×
5853
            };
5854

5855
            // TODO = move
5856
            let material = Material {
5857
                layout: effect_batch.texture_layout.clone(),
5858
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
5859
            };
5860
            assert_eq!(material.layout.layout.len(), material.textures.len());
5861

5862
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
5863
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
5864
                trace!(
×
5865
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
5866
                    material
5867
                );
5868
                continue;
×
5869
            };
5870

5871
            effect_bind_groups
5872
                .material_bind_groups
5873
                .entry(material.clone())
5874
                .or_insert_with(|| {
×
5875
                    debug!("Creating material bind group for material {:?}", material);
×
5876
                    render_device.create_bind_group(
×
5877
                        &format!(
×
5878
                            "hanabi:material_bind_group_{}",
×
5879
                            material.layout.layout.len()
×
5880
                        )[..],
×
5881
                        material_bind_group_layout,
×
5882
                        &bind_group_entries[..],
×
5883
                    )
5884
                });
5885
        }
5886
    }
5887
}
5888

5889
type DrawEffectsSystemState = SystemState<(
5890
    SRes<EffectsMeta>,
5891
    SRes<EffectBindGroups>,
5892
    SRes<PipelineCache>,
5893
    SRes<RenderAssets<RenderMesh>>,
5894
    SRes<MeshAllocator>,
5895
    SQuery<Read<ViewUniformOffset>>,
5896
    SRes<SortedEffectBatches>,
5897
    SQuery<Read<EffectDrawBatch>>,
5898
)>;
5899

5900
/// Draw function for rendering all active effects for the current frame.
5901
///
5902
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
5903
/// and the [`Transparent3d`] phase of the main 3D pass.
5904
pub(crate) struct DrawEffects {
5905
    params: DrawEffectsSystemState,
5906
}
5907

5908
impl DrawEffects {
5909
    pub fn new(world: &mut World) -> Self {
12✔
5910
        Self {
5911
            params: SystemState::new(world),
12✔
5912
        }
5913
    }
5914
}
5915

5916
/// Draw all particles of a single effect in view, in 2D or 3D.
5917
///
5918
/// FIXME: use pipeline ID to look up which group index it is.
5919
fn draw<'w>(
1,013✔
5920
    world: &'w World,
5921
    pass: &mut TrackedRenderPass<'w>,
5922
    view: Entity,
5923
    entity: (Entity, MainEntity),
5924
    pipeline_id: CachedRenderPipelineId,
5925
    params: &mut DrawEffectsSystemState,
5926
) {
UNCOV
5927
    let (
×
5928
        effects_meta,
1,013✔
5929
        effect_bind_groups,
1,013✔
5930
        pipeline_cache,
1,013✔
5931
        meshes,
1,013✔
5932
        mesh_allocator,
1,013✔
5933
        views,
1,013✔
5934
        sorted_effect_batches,
1,013✔
5935
        effect_draw_batches,
1,013✔
5936
    ) = params.get(world);
2,026✔
5937
    let view_uniform = views.get(view).unwrap();
5,065✔
5938
    let effects_meta = effects_meta.into_inner();
3,039✔
5939
    let effect_bind_groups = effect_bind_groups.into_inner();
3,039✔
5940
    let meshes = meshes.into_inner();
3,039✔
5941
    let mesh_allocator = mesh_allocator.into_inner();
3,039✔
5942
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
5,065✔
5943
    let effect_batch = sorted_effect_batches
3,039✔
5944
        .get(effect_draw_batch.effect_batch_index)
1,013✔
5945
        .unwrap();
5946

5947
    let gpu_limits = &effects_meta.gpu_limits;
2,026✔
5948

5949
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
3,039✔
5950
        return;
×
5951
    };
5952

5953
    trace!("render pass");
1,013✔
5954

5955
    pass.set_render_pipeline(pipeline);
×
5956

5957
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
1,013✔
5958
        return;
×
5959
    };
5960
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
1,013✔
5961
        return;
×
5962
    };
5963

5964
    // Vertex buffer containing the particle model to draw. Generally a quad.
5965
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
5966
    // "base_vertex" in the indirect struct...
5967
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
5968

5969
    // View properties (camera matrix, etc.)
5970
    pass.set_bind_group(
×
5971
        0,
5972
        effects_meta.view_bind_group.as_ref().unwrap(),
×
5973
        &[view_uniform.offset],
×
5974
    );
5975

5976
    // Particles buffer
5977
    let spawner_base = effect_batch.spawner_base;
×
5978
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
5979
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
5980
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
2,026✔
5981
    pass.set_bind_group(
2,026✔
5982
        1,
5983
        effect_bind_groups
2,026✔
5984
            .particle_render(effect_batch.buffer_index)
2,026✔
5985
            .unwrap(),
1,013✔
5986
        &[spawner_offset],
1,013✔
5987
    );
5988

5989
    // Particle texture
5990
    // TODO = move
5991
    let material = Material {
5992
        layout: effect_batch.texture_layout.clone(),
2,026✔
5993
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
3,039✔
5994
    };
5995
    if !effect_batch.texture_layout.layout.is_empty() {
1,013✔
5996
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
5997
            pass.set_bind_group(2, bind_group, &[]);
×
5998
        } else {
5999
            // Texture(s) not ready; skip this drawing for now
6000
            trace!(
×
6001
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
6002
                effect_batch.buffer_index,
×
6003
            );
6004
            return;
×
6005
        }
6006
    }
6007

6008
    let effect_metadata_index = effect_batch
1,013✔
UNCOV
6009
        .dispatch_buffer_indices
×
UNCOV
6010
        .effect_metadata_buffer_table_id
×
UNCOV
6011
        .0;
×
UNCOV
6012
    let effect_metadata_offset =
×
UNCOV
6013
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
×
UNCOV
6014
    trace!(
×
6015
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
1,013✔
6016
            (effect_metadata_index={}, offset={}B).",
1,013✔
6017
        effect_batch.slice.len(),
2,026✔
6018
        render_mesh.vertex_count,
×
6019
        effect_batch.buffer_index,
×
6020
        effect_metadata_index,
×
6021
        effect_metadata_offset,
×
6022
    );
6023

6024
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
6025
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
1,013✔
6026
        trace!(
×
6027
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6028
            effect_batch.buffer_index,
×
6029
        );
6030
        return;
×
6031
    };
6032

6033
    match render_mesh.buffer_info {
×
6034
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
1,013✔
6035
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
1,013✔
6036
            else {
×
6037
                return;
×
6038
            };
6039

6040
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6041
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6042
        }
6043
        RenderMeshBufferInfo::NonIndexed => {
×
6044
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6045
        }
6046
    }
6047
}
6048

6049
#[cfg(feature = "2d")]
6050
impl Draw<Transparent2d> for DrawEffects {
6051
    fn draw<'w>(
×
6052
        &mut self,
6053
        world: &'w World,
6054
        pass: &mut TrackedRenderPass<'w>,
6055
        view: Entity,
6056
        item: &Transparent2d,
6057
    ) -> Result<(), DrawError> {
6058
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6059
        draw(
6060
            world,
×
6061
            pass,
×
6062
            view,
×
6063
            item.entity,
×
6064
            item.pipeline,
×
6065
            &mut self.params,
×
6066
        );
6067
        Ok(())
×
6068
    }
6069
}
6070

6071
#[cfg(feature = "3d")]
6072
impl Draw<Transparent3d> for DrawEffects {
6073
    fn draw<'w>(
1,013✔
6074
        &mut self,
6075
        world: &'w World,
6076
        pass: &mut TrackedRenderPass<'w>,
6077
        view: Entity,
6078
        item: &Transparent3d,
6079
    ) -> Result<(), DrawError> {
6080
        trace!("Draw<Transparent3d>: view={:?}", view);
2,026✔
6081
        draw(
6082
            world,
1,013✔
6083
            pass,
1,013✔
6084
            view,
1,013✔
6085
            item.entity,
1,013✔
6086
            item.pipeline,
1,013✔
6087
            &mut self.params,
1,013✔
6088
        );
6089
        Ok(())
1,013✔
6090
    }
6091
}
6092

6093
#[cfg(feature = "3d")]
6094
impl Draw<AlphaMask3d> for DrawEffects {
6095
    fn draw<'w>(
×
6096
        &mut self,
6097
        world: &'w World,
6098
        pass: &mut TrackedRenderPass<'w>,
6099
        view: Entity,
6100
        item: &AlphaMask3d,
6101
    ) -> Result<(), DrawError> {
6102
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6103
        draw(
6104
            world,
×
6105
            pass,
×
6106
            view,
×
6107
            item.representative_entity,
×
6108
            item.batch_set_key.pipeline,
×
6109
            &mut self.params,
×
6110
        );
6111
        Ok(())
×
6112
    }
6113
}
6114

6115
#[cfg(feature = "3d")]
6116
impl Draw<Opaque3d> for DrawEffects {
6117
    fn draw<'w>(
×
6118
        &mut self,
6119
        world: &'w World,
6120
        pass: &mut TrackedRenderPass<'w>,
6121
        view: Entity,
6122
        item: &Opaque3d,
6123
    ) -> Result<(), DrawError> {
6124
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6125
        draw(
6126
            world,
×
6127
            pass,
×
6128
            view,
×
6129
            item.representative_entity,
×
6130
            item.batch_set_key.pipeline,
×
6131
            &mut self.params,
×
6132
        );
6133
        Ok(())
×
6134
    }
6135
}
6136

6137
/// Render node to run the simulation sub-graph once per frame.
6138
///
6139
/// This node doesn't simulate anything by itself, but instead schedules the
6140
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6141
/// actual simulation.
6142
///
6143
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6144
/// renders all the views, such that rendered views have access to the
6145
/// just-simulated particles to render them.
6146
///
6147
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6148
pub(crate) struct VfxSimulateDriverNode;
6149

6150
impl Node for VfxSimulateDriverNode {
6151
    fn run(
1,030✔
6152
        &self,
6153
        graph: &mut RenderGraphContext,
6154
        _render_context: &mut RenderContext,
6155
        _world: &World,
6156
    ) -> Result<(), NodeRunError> {
6157
        graph.run_sub_graph(
2,060✔
6158
            crate::plugin::simulate_graph::HanabiSimulateGraph,
1,030✔
6159
            vec![],
1,030✔
6160
            None,
1,030✔
6161
        )?;
6162
        Ok(())
1,030✔
6163
    }
6164
}
6165

6166
#[derive(Debug, Clone, PartialEq, Eq)]
6167
enum HanabiPipelineId {
6168
    Invalid,
6169
    Cached(CachedComputePipelineId),
6170
}
6171

6172
pub(crate) enum ComputePipelineError {
6173
    Queued,
6174
    Creating,
6175
    Error,
6176
}
6177

6178
impl From<&CachedPipelineState> for ComputePipelineError {
6179
    fn from(value: &CachedPipelineState) -> Self {
×
6180
        match value {
×
6181
            CachedPipelineState::Queued => Self::Queued,
×
6182
            CachedPipelineState::Creating(_) => Self::Creating,
×
6183
            CachedPipelineState::Err(_) => Self::Error,
×
6184
            _ => panic!("Trying to convert Ok state to error."),
×
6185
        }
6186
    }
6187
}
6188

6189
pub(crate) struct HanabiComputePass<'a> {
6190
    /// Pipeline cache to fetch cached compute pipelines by ID.
6191
    pipeline_cache: &'a PipelineCache,
6192
    /// WGPU compute pass.
6193
    compute_pass: ComputePass<'a>,
6194
    /// Current pipeline (cached).
6195
    pipeline_id: HanabiPipelineId,
6196
}
6197

6198
impl<'a> Deref for HanabiComputePass<'a> {
6199
    type Target = ComputePass<'a>;
6200

6201
    fn deref(&self) -> &Self::Target {
×
6202
        &self.compute_pass
×
6203
    }
6204
}
6205

6206
impl DerefMut for HanabiComputePass<'_> {
6207
    fn deref_mut(&mut self) -> &mut Self::Target {
14,140✔
6208
        &mut self.compute_pass
14,140✔
6209
    }
6210
}
6211

6212
impl<'a> HanabiComputePass<'a> {
6213
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
4,056✔
6214
        Self {
6215
            pipeline_cache,
6216
            compute_pass,
6217
            pipeline_id: HanabiPipelineId::Invalid,
6218
        }
6219
    }
6220

6221
    pub fn set_cached_compute_pipeline(
3,028✔
6222
        &mut self,
6223
        pipeline_id: CachedComputePipelineId,
6224
    ) -> Result<(), ComputePipelineError> {
6225
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
6,056✔
6226
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
3,028✔
6227
            trace!("-> already set; skipped");
×
6228
            return Ok(());
×
6229
        }
6230
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
3,028✔
6231
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6232
            if let CachedPipelineState::Err(err) = state {
×
6233
                error!(
×
6234
                    "Failed to find compute pipeline #{}: {:?}",
×
6235
                    pipeline_id.id(),
×
6236
                    err
×
6237
                );
6238
            } else {
6239
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6240
            }
6241
            return Err(state.into());
×
6242
        };
6243
        self.compute_pass.set_pipeline(pipeline);
×
6244
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6245
        Ok(())
×
6246
    }
6247
}
6248

6249
/// Render node to run the simulation of all effects once per frame.
6250
///
6251
/// Runs inside the simulation sub-graph, looping over all extracted effect
6252
/// batches to simulate them.
6253
pub(crate) struct VfxSimulateNode {}
6254

6255
impl VfxSimulateNode {
6256
    /// Create a new node for simulating the effects of the given world.
6257
    pub fn new(_world: &mut World) -> Self {
3✔
6258
        Self {}
6259
    }
6260

6261
    /// Begin a new compute pass and return a wrapper with extra
6262
    /// functionalities.
6263
    pub fn begin_compute_pass<'encoder>(
4,056✔
6264
        &self,
6265
        label: &str,
6266
        pipeline_cache: &'encoder PipelineCache,
6267
        render_context: &'encoder mut RenderContext,
6268
    ) -> HanabiComputePass<'encoder> {
6269
        let compute_pass =
4,056✔
6270
            render_context
4,056✔
6271
                .command_encoder()
6272
                .begin_compute_pass(&ComputePassDescriptor {
8,112✔
6273
                    label: Some(label),
4,056✔
6274
                    timestamp_writes: None,
4,056✔
6275
                });
6276
        HanabiComputePass::new(pipeline_cache, compute_pass)
12,168✔
6277
    }
6278
}
6279

6280
impl Node for VfxSimulateNode {
6281
    fn input(&self) -> Vec<SlotInfo> {
3✔
6282
        vec![]
3✔
6283
    }
6284

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

6287
    fn run(
1,030✔
6288
        &self,
6289
        _graph: &mut RenderGraphContext,
6290
        render_context: &mut RenderContext,
6291
        world: &World,
6292
    ) -> Result<(), NodeRunError> {
6293
        trace!("VfxSimulateNode::run()");
2,050✔
6294

6295
        let pipeline_cache = world.resource::<PipelineCache>();
3,090✔
6296
        let effects_meta = world.resource::<EffectsMeta>();
3,090✔
6297
        let effect_bind_groups = world.resource::<EffectBindGroups>();
3,090✔
6298
        let property_bind_groups = world.resource::<PropertyBindGroups>();
3,090✔
6299
        let sort_bind_groups = world.resource::<SortBindGroups>();
3,090✔
6300
        let utils_pipeline = world.resource::<UtilsPipeline>();
3,090✔
6301
        let effect_cache = world.resource::<EffectCache>();
3,090✔
6302
        let event_cache = world.resource::<EventCache>();
3,090✔
6303
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
3,090✔
6304
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
3,090✔
6305
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
3,090✔
6306

6307
        // Make sure to schedule any buffer copy before accessing their content later in
6308
        // the GPU commands below.
6309
        {
6310
            let command_encoder = render_context.command_encoder();
4,120✔
6311
            effects_meta
2,060✔
6312
                .update_dispatch_indirect_buffer
2,060✔
6313
                .write_buffers(command_encoder);
3,090✔
6314
            effects_meta
2,060✔
6315
                .effect_metadata_buffer
2,060✔
6316
                .write_buffer(command_encoder);
3,090✔
6317
            event_cache.write_buffers(command_encoder);
4,120✔
6318
            sort_bind_groups.write_buffers(command_encoder);
2,060✔
6319
        }
6320

6321
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6322
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6323
        // the update pass of their parent effect during the previous frame.
6324
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
1,030✔
6325
            gpu_buffer_operations.dispatch(
6326
                *queue_index,
6327
                render_context,
6328
                utils_pipeline,
6329
                Some("hanabi:init_indirect_fill_dispatch"),
6330
            );
6331
        }
6332

6333
        // If there's no batch, there's nothing more to do. Avoid continuing because
6334
        // some GPU resources are missing, which is expected when there's no effect but
6335
        // is an error (and will log warnings/errors) otherwise.
6336
        if sorted_effect_batches.is_empty() {
2,060✔
6337
            return Ok(());
16✔
6338
        }
6339

6340
        // Compute init pass
6341
        {
6342
            trace!("init: loop over effect batches...");
1,014✔
6343

6344
            let mut compute_pass =
6345
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6346

6347
            // Bind group simparams@0 is common to everything, only set once per init pass
6348
            compute_pass.set_bind_group(
6349
                0,
6350
                effects_meta
6351
                    .indirect_sim_params_bind_group
6352
                    .as_ref()
6353
                    .unwrap(),
6354
                &[],
6355
            );
6356

6357
            // Dispatch init compute jobs for all batches
6358
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6359
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6360
                // batch. Note that this hopefully should have been skipped earlier.
6361
                {
6362
                    let use_indirect_dispatch = effect_batch
2,028✔
6363
                        .layout_flags
1,014✔
6364
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
1,014✔
6365
                    match effect_batch.spawn_info {
1,014✔
6366
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
1,014✔
6367
                            assert!(!use_indirect_dispatch);
6368
                            if total_spawn_count == 0 {
1,014✔
6369
                                continue;
14✔
6370
                            }
6371
                        }
6372
                        BatchSpawnInfo::GpuSpawner { .. } => {
6373
                            assert!(use_indirect_dispatch);
×
6374
                        }
6375
                    }
6376
                }
6377

6378
                // Fetch bind group particle@1
6379
                let Some(particle_bind_group) =
1,000✔
6380
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
1,000✔
6381
                else {
6382
                    error!(
×
6383
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6384
                        effect_batch.buffer_index
6385
                    );
6386
                    continue;
×
6387
                };
6388

6389
                // Fetch bind group metadata@3
6390
                let Some(metadata_bind_group) = effect_bind_groups
1,000✔
6391
                    .init_metadata_bind_groups
6392
                    .get(&effect_batch.buffer_index)
6393
                else {
6394
                    error!(
×
6395
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6396
                        effect_batch.buffer_index
6397
                    );
6398
                    continue;
×
6399
                };
6400

6401
                if compute_pass
6402
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6403
                    .is_err()
6404
                {
6405
                    continue;
×
6406
                }
6407

6408
                // Compute dynamic offsets
6409
                let spawner_base = effect_batch.spawner_base;
6410
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6411
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6412
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
2,000✔
6413
                let property_offset = effect_batch.property_offset;
2,000✔
6414

6415
                // Setup init pass
6416
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
3,000✔
6417
                let offsets = if let Some(property_offset) = property_offset {
2,000✔
6418
                    vec![spawner_offset, property_offset]
6419
                } else {
6420
                    vec![spawner_offset]
2,000✔
6421
                };
6422
                compute_pass.set_bind_group(
3,000✔
6423
                    2,
6424
                    property_bind_groups
2,000✔
6425
                        .get(effect_batch.property_key.as_ref())
4,000✔
6426
                        .unwrap(),
2,000✔
6427
                    &offsets[..],
1,000✔
6428
                );
6429
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
3,000✔
6430

6431
                // Dispatch init job
6432
                match effect_batch.spawn_info {
1,000✔
6433
                    // Indirect dispatch via GPU spawn events
6434
                    BatchSpawnInfo::GpuSpawner {
6435
                        init_indirect_dispatch_index,
×
6436
                        ..
6437
                    } => {
6438
                        assert!(effect_batch
×
6439
                            .layout_flags
×
6440
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6441

6442
                        // Note: the indirect offset of a dispatch workgroup only needs
6443
                        // 4-byte alignment
6444
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6445
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6446

6447
                        trace!(
×
6448
                            "record commands for indirect init pipeline of effect {:?} \
×
6449
                                init_indirect_dispatch_index={} \
×
6450
                                indirect_offset={} \
×
6451
                                spawner_base={} \
×
6452
                                spawner_offset={} \
×
6453
                                property_key={:?}...",
×
6454
                            effect_batch.handle,
6455
                            init_indirect_dispatch_index,
6456
                            indirect_offset,
6457
                            spawner_base,
6458
                            spawner_offset,
6459
                            effect_batch.property_key,
6460
                        );
6461

6462
                        compute_pass.dispatch_workgroups_indirect(
×
6463
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6464
                            indirect_offset,
×
6465
                        );
6466
                    }
6467

6468
                    // Direct dispatch via CPU spawn count
6469
                    BatchSpawnInfo::CpuSpawner {
6470
                        total_spawn_count: spawn_count,
1,000✔
6471
                    } => {
6472
                        assert!(!effect_batch
6473
                            .layout_flags
6474
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
6475

6476
                        const WORKGROUP_SIZE: u32 = 64;
6477
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
1,000✔
6478

6479
                        trace!(
6480
                            "record commands for init pipeline of effect {:?} \
1,000✔
6481
                                (spawn {} particles => {} workgroups) spawner_base={} \
1,000✔
6482
                                spawner_offset={} \
1,000✔
6483
                                property_key={:?}...",
1,000✔
6484
                            effect_batch.handle,
6485
                            spawn_count,
6486
                            workgroup_count,
6487
                            spawner_base,
6488
                            spawner_offset,
6489
                            effect_batch.property_key,
6490
                        );
6491

6492
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6493
                    }
6494
                }
6495

6496
                trace!("init compute dispatched");
2,000✔
6497
            }
6498
        }
6499

6500
        // Compute indirect dispatch pass
6501
        if effects_meta.spawner_buffer.buffer().is_some()
1,014✔
6502
            && !effects_meta.spawner_buffer.is_empty()
1,014✔
6503
            && effects_meta.indirect_metadata_bind_group.is_some()
1,014✔
6504
            && effects_meta.indirect_sim_params_bind_group.is_some()
2,028✔
6505
        {
6506
            // Only start a compute pass if there's an effect; makes things clearer in
6507
            // debugger.
6508
            let mut compute_pass =
1,014✔
6509
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
5,070✔
6510

6511
            // Dispatch indirect dispatch compute job
6512
            trace!("record commands for indirect dispatch pipeline...");
2,028✔
6513

6514
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
2,028✔
6515
            if has_gpu_spawn_events {
1,014✔
6516
                if let Some(indirect_child_info_buffer_bind_group) =
×
6517
                    event_cache.indirect_child_info_buffer_bind_group()
×
6518
                {
6519
                    assert!(has_gpu_spawn_events);
6520
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6521
                } else {
6522
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6523
                    // render_context
6524
                    //     .command_encoder()
6525
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6526
                    // FIXME - Bevy doesn't allow returning custom errors here...
6527
                    return Ok(());
×
6528
                }
6529
            }
6530

6531
            if compute_pass
1,014✔
6532
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
6533
                .is_err()
6534
            {
6535
                // FIXME - Bevy doesn't allow returning custom errors here...
6536
                return Ok(());
×
6537
            }
6538

6539
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6540
            // the size exluding gaps!");
6541
            const WORKGROUP_SIZE: u32 = 64;
6542
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6543
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6544
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6545

6546
            // Setup vfx_indirect pass
6547
            compute_pass.set_bind_group(
6548
                0,
6549
                effects_meta
6550
                    .indirect_sim_params_bind_group
6551
                    .as_ref()
6552
                    .unwrap(),
6553
                &[],
6554
            );
6555
            compute_pass.set_bind_group(
6556
                1,
6557
                // FIXME - got some unwrap() panic here, investigate... possibly race
6558
                // condition!
6559
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6560
                &[],
6561
            );
6562
            compute_pass.set_bind_group(
6563
                2,
6564
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6565
                &[],
6566
            );
6567
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6568
            trace!(
6569
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
1,014✔
6570
                total_effect_count,
6571
                workgroup_count
6572
            );
6573
        }
6574

6575
        // Compute update pass
6576
        {
6577
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
2,028✔
6578
            else {
6579
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6580
                render_context
×
6581
                    .command_encoder()
6582
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6583
                // FIXME - Bevy doesn't allow returning custom errors here...
6584
                return Ok(());
×
6585
            };
6586

6587
            let mut compute_pass =
6588
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6589

6590
            // Bind group simparams@0 is common to everything, only set once per update pass
6591
            compute_pass.set_bind_group(
6592
                0,
6593
                effects_meta
6594
                    .indirect_sim_params_bind_group
6595
                    .as_ref()
6596
                    .unwrap(),
6597
                &[],
6598
            );
6599

6600
            // Dispatch update compute jobs
6601
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6602
                // Fetch bind group particle@1
6603
                let Some(particle_bind_group) =
1,014✔
6604
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
2,028✔
6605
                else {
6606
                    error!(
×
6607
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6608
                        effect_batch.buffer_index
6609
                    );
6610
                    continue;
×
6611
                };
6612

6613
                // Fetch bind group metadata@3
6614
                let Some(metadata_bind_group) = effect_bind_groups
1,014✔
6615
                    .update_metadata_bind_groups
6616
                    .get(&effect_batch.buffer_index)
6617
                else {
6618
                    error!(
×
6619
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6620
                        effect_batch.buffer_index
6621
                    );
6622
                    continue;
×
6623
                };
6624

6625
                // Fetch compute pipeline
6626
                if compute_pass
6627
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6628
                    .is_err()
6629
                {
6630
                    continue;
×
6631
                }
6632

6633
                // Compute dynamic offsets
6634
                let spawner_index = effect_batch.spawner_base;
6635
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
6636
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
6637
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
1,014✔
6638
                let property_offset = effect_batch.property_offset;
6639

6640
                trace!(
6641
                    "record commands for update pipeline of effect {:?} spawner_base={}",
1,014✔
6642
                    effect_batch.handle,
6643
                    spawner_index,
6644
                );
6645

6646
                // Setup update pass
6647
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6648
                let offsets = if let Some(property_offset) = property_offset {
9✔
6649
                    vec![spawner_offset, property_offset]
6650
                } else {
6651
                    vec![spawner_offset]
2,010✔
6652
                };
6653
                compute_pass.set_bind_group(
6654
                    2,
6655
                    property_bind_groups
6656
                        .get(effect_batch.property_key.as_ref())
6657
                        .unwrap(),
6658
                    &offsets[..],
6659
                );
6660
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6661

6662
                // Dispatch update job
6663
                let dispatch_indirect_offset = effect_batch
6664
                    .dispatch_buffer_indices
6665
                    .update_dispatch_indirect_buffer_row_index
6666
                    * 12;
6667
                trace!(
6668
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
1,014✔
6669
                    indirect_buffer,
6670
                    dispatch_indirect_offset,
6671
                );
6672
                compute_pass
6673
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6674

6675
                trace!("update compute dispatched");
1,014✔
6676
            }
6677
        }
6678

6679
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6680
        // batch of particles which needs sorting, based on the actual number of alive
6681
        // particles in the batch after their update in the compute update pass. Since
6682
        // particles may die during update, this may be different from the number of
6683
        // particles updated.
6684
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
1,014✔
6685
            gpu_buffer_operations.dispatch(
6686
                *queue_index,
6687
                render_context,
6688
                utils_pipeline,
6689
                Some("hanabi:sort_fill_dispatch"),
6690
            );
6691
        }
6692

6693
        // Compute sort pass
6694
        {
6695
            let mut compute_pass =
6696
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6697

6698
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6699
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6700

6701
            // Loop on batches and find those which need sorting
6702
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6703
                trace!("Processing effect batch for sorting...");
2,028✔
6704
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
1,014✔
6705
                    continue;
1,014✔
6706
                }
6707
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6708
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6709

6710
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6711
                    warn!("Missing sort-fill effect buffer.");
×
6712
                    continue;
×
6713
                };
6714

6715
                let indirect_dispatch_index = *effect_batch
6716
                    .sort_fill_indirect_dispatch_index
6717
                    .as_ref()
6718
                    .unwrap();
6719
                let indirect_offset =
6720
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6721

6722
                // Fill the sort buffer with the key-value pairs to sort
6723
                {
6724
                    compute_pass.push_debug_group("hanabi:sort_fill");
6725

6726
                    // Fetch compute pipeline
6727
                    let Some(pipeline_id) =
×
6728
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6729
                    else {
6730
                        warn!("Missing sort-fill pipeline.");
×
6731
                        continue;
×
6732
                    };
6733
                    if compute_pass
6734
                        .set_cached_compute_pipeline(pipeline_id)
6735
                        .is_err()
6736
                    {
6737
                        compute_pass.pop_debug_group();
×
6738
                        // FIXME - Bevy doesn't allow returning custom errors here...
6739
                        return Ok(());
×
6740
                    }
6741

6742
                    // Bind group sort_fill@0
6743
                    let particle_buffer = effect_buffer.particle_buffer();
6744
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
6745
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6746
                        particle_buffer.id(),
6747
                        indirect_index_buffer.id(),
6748
                        effect_metadata_buffer.id(),
6749
                    ) else {
6750
                        warn!("Missing sort-fill bind group.");
×
6751
                        continue;
×
6752
                    };
6753
                    let particle_offset = effect_buffer.particle_offset(effect_batch.slice.start);
6754
                    let indirect_index_offset =
6755
                        effect_buffer.indirect_index_offset(effect_batch.slice.start);
6756
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6757
                        effect_batch
6758
                            .dispatch_buffer_indices
6759
                            .effect_metadata_buffer_table_id
6760
                            .0,
6761
                    ) as u32;
6762
                    compute_pass.set_bind_group(
6763
                        0,
6764
                        bind_group,
6765
                        &[
6766
                            particle_offset,
6767
                            indirect_index_offset,
6768
                            effect_metadata_offset,
6769
                        ],
6770
                    );
6771

6772
                    compute_pass
6773
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6774
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6775

6776
                    compute_pass.pop_debug_group();
6777
                }
6778

6779
                // Do the actual sort
6780
                {
6781
                    compute_pass.push_debug_group("hanabi:sort");
6782

6783
                    if compute_pass
6784
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
6785
                        .is_err()
6786
                    {
6787
                        compute_pass.pop_debug_group();
×
6788
                        // FIXME - Bevy doesn't allow returning custom errors here...
6789
                        return Ok(());
×
6790
                    }
6791

6792
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
6793
                    compute_pass
6794
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6795
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
6796

6797
                    compute_pass.pop_debug_group();
6798
                }
6799

6800
                // Copy the sorted particle indices back into the indirect index buffer, where
6801
                // the render pass will read them.
6802
                {
6803
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
6804

6805
                    // Fetch compute pipeline
6806
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
6807
                    if compute_pass
6808
                        .set_cached_compute_pipeline(pipeline_id)
6809
                        .is_err()
6810
                    {
6811
                        compute_pass.pop_debug_group();
×
6812
                        // FIXME - Bevy doesn't allow returning custom errors here...
6813
                        return Ok(());
6814
                    }
6815

6816
                    // Bind group sort_copy@0
6817
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
6818
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
6819
                        indirect_index_buffer.id(),
6820
                        effect_metadata_buffer.id(),
6821
                    ) else {
6822
                        warn!("Missing sort-copy bind group.");
×
6823
                        continue;
×
6824
                    };
6825
                    let indirect_index_offset = effect_batch.slice.start;
6826
                    let effect_metadata_offset =
6827
                        effects_meta.effect_metadata_buffer.dynamic_offset(
6828
                            effect_batch
6829
                                .dispatch_buffer_indices
6830
                                .effect_metadata_buffer_table_id,
6831
                        );
6832
                    compute_pass.set_bind_group(
6833
                        0,
6834
                        bind_group,
6835
                        &[indirect_index_offset, effect_metadata_offset],
6836
                    );
6837

6838
                    compute_pass
6839
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6840
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
6841

6842
                    compute_pass.pop_debug_group();
6843
                }
6844
            }
6845
        }
6846

6847
        Ok(())
1,014✔
6848
    }
6849
}
6850

6851
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
6852
    fn from(layout_flags: LayoutFlags) -> Self {
3,042✔
6853
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
6,084✔
6854
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
6855
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
3,042✔
6856
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
6857
        } else {
6858
            ParticleRenderAlphaMaskPipelineKey::Blend
3,042✔
6859
        }
6860
    }
6861
}
6862

6863
#[cfg(test)]
6864
mod tests {
6865
    use super::*;
6866

6867
    #[test]
6868
    fn layout_flags() {
6869
        let flags = LayoutFlags::default();
6870
        assert_eq!(flags, LayoutFlags::NONE);
6871
    }
6872

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

6878
        let renderer = MockRenderer::new();
6879
        let device = renderer.device();
6880
        let limits = GpuLimits::from_device(&device);
6881

6882
        // assert!(limits.storage_buffer_align().get() >= 1);
6883
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
6884
    }
6885

6886
    #[cfg(feature = "gpu_tests")]
6887
    #[test]
6888
    fn gpu_ops_ifda() {
6889
        use crate::test_utils::MockRenderer;
6890

6891
        let renderer = MockRenderer::new();
6892
        let device = renderer.device();
6893
        let render_queue = renderer.queue();
6894

6895
        let mut world = World::new();
6896
        world.insert_resource(device.clone());
6897
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
6898

6899
        let src_buffer = device.create_buffer(&BufferDescriptor {
6900
            label: None,
6901
            size: 256,
6902
            usage: BufferUsages::STORAGE,
6903
            mapped_at_creation: false,
6904
        });
6905
        let dst_buffer = device.create_buffer(&BufferDescriptor {
6906
            label: None,
6907
            size: 256,
6908
            usage: BufferUsages::STORAGE,
6909
            mapped_at_creation: false,
6910
        });
6911

6912
        // Two consecutive ops can be merged. This includes having contiguous slices
6913
        // both in source and destination.
6914
        buffer_ops.begin_frame();
6915
        {
6916
            let mut q = InitFillDispatchQueue::default();
6917
            q.enqueue(0, 0);
6918
            assert_eq!(q.queue.len(), 1);
6919
            q.enqueue(1, 1);
6920
            // Ops are not batched yet
6921
            assert_eq!(q.queue.len(), 2);
6922
            // On submit, the ops get batched together
6923
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6924
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6925
        }
6926
        buffer_ops.end_frame(&device, &render_queue);
6927

6928
        // Even if out of order, the init fill dispatch ops are batchable. Here the
6929
        // offsets are enqueued inverted.
6930
        buffer_ops.begin_frame();
6931
        {
6932
            let mut q = InitFillDispatchQueue::default();
6933
            q.enqueue(1, 1);
6934
            assert_eq!(q.queue.len(), 1);
6935
            q.enqueue(0, 0);
6936
            // Ops are not batched yet
6937
            assert_eq!(q.queue.len(), 2);
6938
            // On submit, the ops get batched together
6939
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6940
            assert_eq!(buffer_ops.args_buffer.len(), 1);
6941
        }
6942
        buffer_ops.end_frame(&device, &render_queue);
6943

6944
        // However, both the source and destination need to be contiguous at the same
6945
        // time. Here they are mixed so we can't batch.
6946
        buffer_ops.begin_frame();
6947
        {
6948
            let mut q = InitFillDispatchQueue::default();
6949
            q.enqueue(0, 1);
6950
            assert_eq!(q.queue.len(), 1);
6951
            q.enqueue(1, 0);
6952
            // Ops are not batched yet
6953
            assert_eq!(q.queue.len(), 2);
6954
            // On submit, the ops cannot get batched together
6955
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
6956
            assert_eq!(buffer_ops.args_buffer.len(), 2);
6957
        }
6958
        buffer_ops.end_frame(&device, &render_queue);
6959
    }
6960
}
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