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

djeedai / bevy_hanabi / 18243240671

04 Oct 2025 10:36AM UTC coverage: 66.58% (+0.1%) from 66.455%
18243240671

push

github

web-flow
Split extraction and render into unit systems (#499)

Reorganize most of the extraction and render systems into smaller,
unit-like systems with limited (ideally, a single) responsibility. Split
most of the data into separate, smaller components too. This not only
enable better multithreading, but also greatly simplify maintenance by
clarifying the logic and responsibility of each system and component.

As part of this change, add a "ready state" to the effect, which is read
back from the render world and informs the main world about whether an
effect is ready for simulation and rendering. This includes:

- All GPU resources being allocated, and in particular the PSOs
  (pipelines) which in Bevy are compiled asynchronously and can be very
  slow (many frames of delay).
- The ready state of all descendant effects, recursively. This ensures a
  child is ready to _e.g._ receive GPU spawn events before its parent,
  which emits those events, starts simulating.

This new ready state is accessed via
`CompiledParticleEffect::is_ready()`. Note that the state is updated
during the extract phase with the information collected from the
previous render frame, so by the time `is_ready()` returns `true`,
already one frame of simulation and rendering generally occurred.

Remove the outdated `copyless` dependency.

594 of 896 new or added lines in 12 files covered. (66.29%)

21 existing lines in 3 files now uncovered.

5116 of 7684 relevant lines covered (66.58%)

416.91 hits per line

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

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

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

64
use crate::{
65
    asset::{DefaultMesh, EffectAsset},
66
    calc_func_id,
67
    render::{
68
        batch::{BatchInput, EffectDrawBatch, EffectSorter, InitAndUpdatePipelineIds},
69
        effect_cache::{
70
            AnyDrawIndirectArgs, CachedDrawIndirectArgs, DispatchBufferIndices, SlabId,
71
        },
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::{allocate_events, on_remove_cached_effect_events, EventCache};
94
pub(crate) use property::{
95
    allocate_properties, on_remove_cached_properties, prepare_property_buffers, PropertyBindGroups,
96
    PropertyCache,
97
};
98
use property::{CachedEffectProperties, PropertyBindGroupKey};
99
pub use shader_cache::ShaderCache;
100
pub(crate) use sort::SortBindGroups;
101

102
use self::batch::EffectBatch;
103

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1081
        // Upload to GPU buffer
1082
        self.args_buffer.write_buffer(device, render_queue);
4,132✔
1083
    }
1084

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1376
            let shader_defs = default();
6✔
1377

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

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

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

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

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

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

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

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

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

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

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

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

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

1568
impl SpecializedComputePipeline for ParticlesInitPipeline {
1569
    type Key = ParticleInitPipelineKey;
1570

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

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

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

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

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

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

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

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

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

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

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

1729
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1730
    type Key = ParticleUpdatePipelineKey;
1731

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

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

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

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

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

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

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

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

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

1856
        self.material_layouts
1857
            .insert(layout.clone(), material_bind_group_layout);
1858
    }
1859

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

1867
        self.material_layouts.get(layout)
1868
    }
1869
}
1870

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

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

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

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

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

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

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

1999
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
2000
    type Key = ParticleRenderPipelineKey;
2001

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2354
    type IntoIter = std::slice::Iter<'a, Entity>;
2355

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

2362
impl Deref for ChildrenEffects {
2363
    type Target = [Entity];
2364

NEW
2365
    fn deref(&self) -> &Self::Target {
×
NEW
2366
        &self.0
×
2367
    }
2368
}
2369

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

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

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

2407
    let EffectAssetEvents { ref mut images } = *events;
2,060✔
2408
    *images = image_events.read().copied().collect();
4,120✔
2409
}
2410

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

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

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

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

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

2498
    // Stop any pending capture if needed
2499
    if render_debug_settings.is_capturing {
1,030✔
2500
        render_debug_settings.captured_frames += 1;
×
2501

UNCOV
2502
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2503
            render_device.wgpu_device().stop_capture();
×
2504
            render_debug_settings.is_capturing = false;
×
2505
            warn!(
×
2506
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2507
                render_debug_settings.captured_frames,
×
2508
                real_time.elapsed().as_secs_f64()
×
2509
            );
2510
        }
2511
    }
2512

2513
    // If no pending capture, consider starting a new one
2514
    if !render_debug_settings.is_capturing
1,030✔
2515
        && (debug_settings.start_capture_this_frame
1,030✔
2516
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty()))
1,030✔
2517
    {
NEW
2518
        render_device.wgpu_device().start_capture();
×
2519
        render_debug_settings.is_capturing = true;
2520
        render_debug_settings.capture_start = real_time.elapsed();
2521
        render_debug_settings.captured_frames = 0;
2522
        warn!(
NEW
2523
            "Started GPU debug capture of {} frames at t={}s.",
×
NEW
2524
            debug_settings.capture_frame_count,
×
NEW
2525
            render_debug_settings.capture_start.as_secs_f64()
×
2526
        );
2527
    }
2528
}
2529

2530
/// Write the ready state of all render world effects back into their source
2531
/// effect in the main world.
2532
pub(crate) fn report_ready_state(
1,030✔
2533
    mut main_world: ResMut<MainWorld>,
2534
    q_ready_state: Query<&CachedReadyState>,
2535
) {
2536
    let mut q_effects = main_world.query::<(RenderEntity, &mut CompiledParticleEffect)>();
2,060✔
2537
    for (render_entity, mut compiled_particle_effect) in q_effects.iter_mut(&mut main_world) {
4,114✔
2538
        if let Ok(cached_ready_state) = q_ready_state.get(render_entity) {
1,012✔
2539
            compiled_particle_effect.is_ready = cached_ready_state.is_ready();
2540
        }
2541
    }
2542
}
2543

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

2578
    // Loop over all existing effects to extract them
2579
    trace!("Extracting {} effects...", q_effects.iter().len());
4,090✔
2580
    for (
2581
        main_entity,
1,014✔
2582
        render_entity,
2583
        maybe_inherited_visibility,
2584
        maybe_view_visibility,
2585
        effect_spawner,
2586
        compiled_effect,
2587
        maybe_properties,
2588
        transform,
2589
    ) in q_effects.iter()
2,060✔
2590
    {
2591
        // Check if shaders are configured
2592
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
1,014✔
NEW
2593
            trace!("Effect {:?}: no configured shader, skipped.", main_entity);
×
UNCOV
2594
            continue;
×
2595
        };
2596

2597
        // Check if asset is available, otherwise silently ignore
2598
        let Some(asset) = effects.get(&compiled_effect.asset) else {
1,014✔
2599
            trace!(
×
NEW
2600
                "Effect {:?}: EffectAsset not ready, skipped. asset:{:?}",
×
2601
                main_entity,
2602
                compiled_effect.asset
2603
            );
2604
            continue;
×
2605
        };
2606

2607
        let is_visible = maybe_inherited_visibility
2608
            .map(|cv| cv.get())
2,028✔
2609
            .unwrap_or(true)
2610
            && maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true);
5,070✔
2611

2612
        let mut cmd = commands.entity(render_entity);
2613

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

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

2665
        // Extract the spawner data
2666
        let new_spawner = ExtractedSpawner {
2667
            spawn_count: effect_spawner.spawn_count,
2668
            prng_seed: compiled_effect.prng_seed,
2669
            transform: *transform,
2670
            is_visible,
2671
        };
2672
        trace!(
2673
            "[Effect {}] spawn_count={} prng_seed={}",
1,014✔
2674
            render_entity,
2675
            new_spawner.spawn_count,
2676
            new_spawner.prng_seed
2677
        );
2678
        if let Some(mut extracted_spawner) = maybe_extracted_spawner {
1,012✔
2679
            extracted_spawner.set_if_neq(new_spawner);
2680
        } else {
2681
            trace!(
2✔
2682
                "Inserting new ExtractedSpawner component on {}",
2✔
2683
                render_entity
2684
            );
2685
            cmd.insert(new_spawner);
6✔
2686
        }
2687

2688
        // Extract the effect mesh
2689
        let mesh = compiled_effect
2690
            .mesh
2691
            .clone()
2692
            .unwrap_or(default_mesh.0.clone());
2693
        let new_mesh = ExtractedEffectMesh { mesh: mesh.id() };
2694
        if let Some(mut extracted_mesh) = maybe_extracted_mesh {
1,012✔
2695
            extracted_mesh.set_if_neq(new_mesh);
2696
        } else {
2697
            trace!(
2✔
2698
                "Inserting new ExtractedEffectMesh component on {:?}",
2✔
2699
                render_entity
2700
            );
2701
            cmd.insert(new_mesh);
6✔
2702
        }
2703

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

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

2761
            let new_properties = ExtractedProperties {
2762
                property_layout,
2763
                property_data,
2764
            };
2765
            trace!("new_properties = {new_properties:?}");
9✔
2766

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

2790
pub(crate) fn extract_sim_params(
1,030✔
2791
    real_time: Extract<Res<Time<Real>>>,
2792
    virtual_time: Extract<Res<Time<Virtual>>>,
2793
    time: Extract<Res<Time<EffectSimulation>>>,
2794
    mut sim_params: ResMut<SimParams>,
2795
) {
2796
    #[cfg(feature = "trace")]
2797
    let _span = bevy::log::info_span!("extract_sim_params").entered();
3,090✔
2798
    trace!("extract_sim_params()");
2,050✔
2799

2800
    // Save simulation params into render world
2801
    sim_params.time = time.elapsed_secs_f64();
2,060✔
2802
    sim_params.delta_time = time.delta_secs();
2,060✔
2803
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
2,060✔
2804
    sim_params.virtual_delta_time = virtual_time.delta_secs();
2,060✔
2805
    sim_params.real_time = real_time.elapsed_secs_f64();
2,060✔
2806
    sim_params.real_delta_time = real_time.delta_secs();
2,060✔
2807
    trace!(
1,030✔
2808
        "SimParams: time={} delta_time={} vtime={} delta_vtime={} rtime={} delta_rtime={}",
1,020✔
2809
        sim_params.time,
1,020✔
2810
        sim_params.delta_time,
1,020✔
2811
        sim_params.virtual_time,
1,020✔
2812
        sim_params.virtual_delta_time,
1,020✔
2813
        sim_params.real_time,
1,020✔
2814
        sim_params.real_delta_time,
1,020✔
2815
    );
2816
}
2817

2818
/// Various GPU limits and aligned sizes computed once and cached.
2819
struct GpuLimits {
2820
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2821
    ///
2822
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2823
    storage_buffer_align: NonZeroU32,
2824

2825
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2826
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2827
    ///
2828
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2829
    effect_metadata_aligned_size: NonZeroU32,
2830
}
2831

2832
impl GpuLimits {
2833
    pub fn from_device(render_device: &RenderDevice) -> Self {
4✔
2834
        let storage_buffer_align =
4✔
2835
            render_device.limits().min_storage_buffer_offset_alignment as u64;
4✔
2836

2837
        let effect_metadata_aligned_size = NonZeroU32::new(
2838
            GpuEffectMetadata::min_size()
8✔
2839
                .get()
8✔
2840
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2841
        )
2842
        .unwrap();
2843

2844
        trace!(
4✔
2845
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2✔
2846
            storage_buffer_align,
2847
            GpuEffectMetadata::min_size().get(),
4✔
2848
            effect_metadata_aligned_size.get(),
4✔
2849
        );
2850

2851
        Self {
2852
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
12✔
2853
            effect_metadata_aligned_size,
2854
        }
2855
    }
2856

2857
    /// Byte alignment for any storage buffer binding.
2858
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2859
        self.storage_buffer_align
3✔
2860
    }
2861

2862
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2863
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
2,025✔
2864
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
2,025✔
2865
    }
2866

2867
    /// Byte alignment for [`GpuEffectMetadata`].
2868
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
2✔
2869
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
6✔
2870
    }
2871
}
2872

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

2929
impl EffectsMeta {
2930
    pub fn new(
3✔
2931
        device: RenderDevice,
2932
        indirect_shader_noevent: Handle<Shader>,
2933
        indirect_shader_events: Handle<Shader>,
2934
    ) -> Self {
2935
        let gpu_limits = GpuLimits::from_device(&device);
9✔
2936

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

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

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

3014
    pub fn allocate_draw_indirect(
2✔
3015
        &mut self,
3016
        draw_args: &AnyDrawIndirectArgs,
3017
    ) -> CachedDrawIndirectArgs {
3018
        let row = self
4✔
3019
            .draw_indirect_buffer
2✔
3020
            .insert(draw_args.bitcast_to_row_entry());
6✔
3021
        CachedDrawIndirectArgs {
3022
            row,
3023
            args: *draw_args,
2✔
3024
        }
3025
    }
3026

NEW
3027
    pub fn update_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
×
NEW
3028
        self.draw_indirect_buffer
×
NEW
3029
            .update(row_index.get_row(), row_index.args.bitcast_to_row_entry());
×
3030
    }
3031

3032
    pub fn free_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
1✔
3033
        self.draw_indirect_buffer.remove(row_index.get_row());
4✔
3034
    }
3035
}
3036

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

3074
impl Default for LayoutFlags {
3075
    fn default() -> Self {
1✔
3076
        Self::NONE
1✔
3077
    }
3078
}
3079

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

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

3104
    // Fecth the components of the effect being destroyed. Note that the despawn
3105
    // command above is not yet applied, so this query should always succeed.
3106
    let Ok((
3107
        render_entity,
1✔
3108
        main_entity,
3109
        cached_effect,
3110
        dispatch_buffer_indices,
3111
        _opt_props,
3112
        _opt_parent,
3113
        opt_cached_effect_events,
3114
    )) = query.get(trigger.target())
3✔
3115
    else {
3116
        return;
×
3117
    };
3118

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

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

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

3161
/// Observer raised when the [`CachedEffectMetadata`] component is removed, to
3162
/// deallocate the GPU resources associated with the indirect draw args.
3163
pub(crate) fn on_remove_cached_metadata(
1✔
3164
    trigger: Trigger<OnRemove, CachedEffectMetadata>,
3165
    query: Query<&CachedEffectMetadata>,
3166
    mut effects_meta: ResMut<EffectsMeta>,
3167
) {
3168
    #[cfg(feature = "trace")]
3169
    let _span = bevy::log::info_span!("on_remove_cached_metadata").entered();
3✔
3170

3171
    if let Ok(cached_metadata) = query.get(trigger.target()) {
4✔
3172
        if cached_metadata.table_id.is_valid() {
1✔
3173
            effects_meta
2✔
3174
                .effect_metadata_buffer
2✔
3175
                .remove(cached_metadata.table_id);
1✔
3176
        }
3177
    };
3178
}
3179

3180
/// Observer raised when the [`CachedDrawIndirectArgs`] component is removed, to
3181
/// deallocate the GPU resources associated with the indirect draw args.
3182
pub(crate) fn on_remove_cached_draw_indirect_args(
1✔
3183
    trigger: Trigger<OnRemove, CachedDrawIndirectArgs>,
3184
    query: Query<&CachedDrawIndirectArgs>,
3185
    mut effects_meta: ResMut<EffectsMeta>,
3186
) {
3187
    #[cfg(feature = "trace")]
3188
    let _span = bevy::log::info_span!("on_remove_cached_draw_indirect_args").entered();
3✔
3189

3190
    if let Ok(cached_draw_args) = query.get(trigger.target()) {
4✔
3191
        effects_meta.free_draw_indirect(cached_draw_args);
3192
    };
3193
}
3194

3195
/// Clear pending GPU resources left from previous frame.
3196
///
3197
/// Those generally are source buffers for buffer-to-buffer copies on capacity
3198
/// growth, which need the source buffer to be alive until the copy is done,
3199
/// then can be discarded here.
3200
pub(crate) fn clear_previous_frame_resizes(
1,030✔
3201
    mut effects_meta: ResMut<EffectsMeta>,
3202
    mut sort_bind_groups: ResMut<SortBindGroups>,
3203
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3204
) {
3205
    #[cfg(feature = "trace")]
3206
    let _span = bevy::log::info_span!("clear_previous_frame_resizes").entered();
3,090✔
3207
    trace!("clear_previous_frame_resizes");
2,050✔
3208

3209
    init_fill_dispatch_queue.clear();
1,030✔
3210

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

3227
// Fixup the [`CachedChildInfo::global_child_index`] once all child infos have
3228
// been allocated.
3229
pub fn fixup_parents(
1,030✔
3230
    q_changed_parents: Query<(Entity, Ref<CachedParentInfo>)>,
3231
    mut q_children: Query<&mut CachedChildInfo>,
3232
) {
3233
    #[cfg(feature = "trace")]
3234
    let _span = bevy::log::info_span!("fixup_parents").entered();
3,090✔
3235
    trace!("fixup_parents");
2,050✔
3236

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

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

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

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

3332
        // Ensure the metadata@3 bind group layout exists for the init pass.
3333
        {
3334
            let consume_gpu_spawn_events = extracted_effect
3335
                .layout_flags
3336
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
3337
            effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
3338
        }
3339

3340
        // Allocate DispatchBufferIndices if not present yet
3341
        if !has_dispatch_buffer_indices {
2✔
3342
            let update_dispatch_indirect_buffer_row_index =
2✔
3343
                effects_meta.dispatch_indirect_buffer.allocate();
2✔
3344
            commands.entity(entity).insert(DispatchBufferIndices {
2✔
3345
                update_dispatch_indirect_buffer_row_index,
2✔
3346
            });
3347
        }
3348
    }
3349
}
3350

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

3375
    for (entity, extracted_mesh, maybe_cached_mesh_location, maybe_cached_draw_indirect_args) in
4,056✔
3376
        &mut q_cached_effects
2,044✔
3377
    {
3378
        let mut cmds = commands.entity(entity);
4,056✔
3379

3380
        // Resolve the render mesh
3381
        let Some(render_mesh) = render_meshes.get(extracted_mesh.mesh) else {
3,042✔
3382
            warn!(
×
3383
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3384
                entity, extracted_mesh.mesh
3385
            );
NEW
3386
            cmds.remove::<CachedMeshLocation>();
×
UNCOV
3387
            continue;
×
3388
        };
3389

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

3427
        // Calculate the new draw args and mesh location based on Bevy's info
3428
        let new_draw_args = AnyDrawIndirectArgs::from_slices(
3429
            &mesh_vertex_buffer_slice,
3430
            mesh_index_buffer_slice.as_ref(),
3431
        );
3432
        let new_mesh_location = match &mesh_index_buffer_slice {
3433
            // Indexed mesh rendering
3434
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3435
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
3,042✔
3436
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
2,028✔
3437
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
2,028✔
3438
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
1,014✔
3439
                indexed,
3440
            },
3441
            // Non-indexed mesh rendering
3442
            None => CachedMeshLocation {
3443
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3444
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
3445
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3446
                vertex_offset_or_base_instance: 0,
3447
                indexed: None,
3448
            },
3449
        };
3450

3451
        // We don't allocate the draw indirect args ahead of time because we need to
3452
        // select the indexed vs. non-indexed buffer. Now that we know whether the mesh
3453
        // is indexed, we can allocate it (or reallocate it if indexing mode changed).
3454
        if let Some(mut cached_draw_indirect) = maybe_cached_draw_indirect_args {
1,012✔
3455
            assert!(cached_draw_indirect.row.is_valid());
3456

3457
            // If the GPU draw args changed, re-upload to GPU.
3458
            if new_draw_args != cached_draw_indirect.args {
1,012✔
NEW
3459
                debug!(
×
NEW
3460
                    "Indirect draw args changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
NEW
3461
                    entity, cached_draw_indirect.args, new_draw_args
×
3462
                );
NEW
3463
                cached_draw_indirect.args = new_draw_args;
×
NEW
3464
                effects_meta.update_draw_indirect(cached_draw_indirect.as_ref());
×
3465
            }
3466
        } else {
3467
            cmds.insert(effects_meta.allocate_draw_indirect(&new_draw_args));
8✔
3468
        }
3469

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

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

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

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

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

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

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

3608
    // Loop on all parent effects and ensure their CachedParentInfo is up-to-date.
NEW
3609
    for (parent_entity, parent_extracted_effect, _, children_effects, maybe_cached_parent_info) in
×
3610
        &mut q_parent_effects
1,030✔
3611
    {
NEW
3612
        let parent_min_binding_size = parent_extracted_effect.particle_layout.min_binding_size32();
×
3613

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

3629
            // Fetch the GPU event buffer of the child
NEW
3630
            let Some(event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3631
            else {
NEW
3632
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} doesn't have an allocated GPU event buffer. The parent effect cannot be processed.");
×
3633
                break;
3634
            };
3635

3636
            let buffer_binding_source = BufferBindingSource {
3637
                buffer: event_buffer.clone(),
3638
                offset: cached_effect_events.range.start,
3639
                size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3640
            };
3641
            new_children.push((*child_entity, buffer_binding_source));
3642

3643
            new_child_infos.push(GpuChildInfo {
3644
                event_count: 0,
3645
                init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3646
            });
3647

3648
            // Ensure the particle@1 bind group layout exists for the given configuration of
3649
            // particle layout. We do this here only for effects with a parent; for those
3650
            // without a parent, we already did this in allocate_effects().
3651
            effect_cache.ensure_particle_bind_group_layout(
3652
                child_extracted_effect.particle_layout.min_binding_size32(),
3653
                Some(parent_min_binding_size),
3654
            );
3655
        }
3656

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

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

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

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

3726
    for (
3727
        entity,
1,014✔
3728
        extracted_effect,
3729
        cached_effect,
3730
        maybe_cached_child_info,
3731
        maybe_cached_parent_info,
3732
        maybe_cached_properties,
3733
        mut cached_pipelines,
3734
    ) in &mut q_effects
2,044✔
3735
    {
3736
        trace!(
3737
            "Preparing pipelines for effect {:?}... (flags: {:?})",
1,014✔
3738
            entity,
3739
            cached_pipelines.flags
1,014✔
3740
        );
3741

3742
        let particle_layout = &cached_effect.slice.particle_layout;
3743
        let particle_layout_min_binding_size = particle_layout.min_binding_size32();
3744
        let has_event_buffer = maybe_cached_child_info.is_some();
3745
        let parent_particle_layout_min_binding_size = maybe_cached_child_info
3746
            .as_ref()
NEW
3747
            .map(|cci| cci.parent_particle_layout.min_binding_size32());
×
3748

3749
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
1,014✔
3750
            particle_layout_min_binding_size,
3751
            parent_particle_layout_min_binding_size,
3752
        ) else {
NEW
3753
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}",
×
3754
                particle_layout_min_binding_size, parent_particle_layout_min_binding_size);
UNCOV
3755
            continue;
×
3756
        };
3757
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3758

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

3778
        // Resolve the init pipeline
3779
        let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
1,012✔
3780
            *init_pipeline_id
3781
        } else {
3782
            // Clear flag just in case, to ensure consistency.
3783
            cached_pipelines
2✔
3784
                .flags
2✔
3785
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3786

3787
            // Fetch the metadata@3 bind group layout from the cache
3788
            let metadata_bind_group_layout = effect_cache
6✔
3789
                .metadata_init_bind_group_layout(has_event_buffer)
2✔
3790
                .unwrap()
3791
                .clone();
3792

3793
            let init_pipeline_key_flags = {
2✔
3794
                let mut flags = ParticleInitPipelineKeyFlags::empty();
4✔
3795
                flags.set(
4✔
3796
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3797
                    particle_layout.contains(Attribute::PREV),
4✔
3798
                );
3799
                flags.set(
4✔
3800
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3801
                    particle_layout.contains(Attribute::NEXT),
4✔
3802
                );
3803
                flags.set(
4✔
3804
                    ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3805
                    has_event_buffer,
2✔
3806
                );
3807
                flags
2✔
3808
            };
3809

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

3837
            cached_pipelines.init = Some(init_pipeline_id);
2✔
3838
            init_pipeline_id
2✔
3839
        };
3840

3841
        // Resolve the update pipeline
3842
        let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
1,012✔
3843
        {
3844
            *update_pipeline_id
3845
        } else {
3846
            // Clear flag just in case, to ensure consistency.
3847
            cached_pipelines
2✔
3848
                .flags
2✔
3849
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
2✔
3850

3851
            let num_event_buffers = maybe_cached_parent_info
4✔
3852
                .as_ref()
3853
                .map(|p| p.children.len() as u32)
2✔
3854
                .unwrap_or_default();
3855

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

3863
            // Fetch the bind group layouts from the cache
3864
            let metadata_bind_group_layout = effect_cache
6✔
3865
                .metadata_update_bind_group_layout(num_event_buffers)
2✔
3866
                .unwrap()
3867
                .clone();
3868

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

3896
            cached_pipelines.update = Some(update_pipeline_id);
2✔
3897
            update_pipeline_id
2✔
3898
        };
3899

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

3918
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3919
        // ready
3920
        cached_pipelines
3921
            .flags
3922
            .insert(CachedPipelineFlags::INIT_PIPELINE_READY);
3923
        trace!("[Effect {:?}] Init pipeline ready.", entity);
1,012✔
3924

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

3943
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3944
        // ready
3945
        cached_pipelines
3946
            .flags
3947
            .insert(CachedPipelineFlags::UPDATE_PIPELINE_READY);
3948
        trace!("[Effect {:?}] Update pipeline ready.", entity);
1,012✔
3949
    }
3950
}
3951

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

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

4003
// TEMP - Mark all cached effects as invalid for this frame until another system
4004
// explicitly marks them as valid. Otherwise we early out in some parts, and
4005
// reuse by mistake the previous frame's extraction.
4006
pub fn clear_transient_batch_inputs(
1,030✔
4007
    mut commands: Commands,
4008
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
4009
) {
4010
    for entity in &mut q_cached_effects {
3,050✔
4011
        if let Ok(mut cmd) = commands.get_entity(entity) {
1,010✔
4012
            cmd.remove::<BatchInput>();
4013
        }
4014
    }
4015
}
4016

4017
/// Effect mesh extracted from the main world.
4018
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
4019
pub(crate) struct ExtractedEffectMesh {
4020
    /// Asset of the effect mesh to draw.
4021
    pub mesh: AssetId<Mesh>,
4022
}
4023

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

4036
impl PartialEq for MeshIndexSlice {
4037
    fn eq(&self, other: &Self) -> bool {
1,012✔
4038
        self.format == other.format
1,012✔
4039
            && self.buffer.id() == other.buffer.id()
2,024✔
4040
            && self.range == other.range
1,012✔
4041
    }
4042
}
4043

4044
impl Eq for MeshIndexSlice {}
4045

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

4064
/// Cached info about the [`GpuEffectMetadata`] allocation for this effect.
4065
///
4066
/// The component is present when the [`GpuEffectMetadata`] is allocated.
4067
#[derive(Debug, Clone, PartialEq, Eq, Component)]
4068
pub(crate) struct CachedMetadata {
4069
    pub buffer_table_id: BufferTableId,
4070
}
4071

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

4083
impl Default for CachedPipelineFlags {
4084
    fn default() -> Self {
2✔
4085
        Self::NONE
2✔
4086
    }
4087
}
4088

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

4113
impl CachedPipelines {
4114
    /// Check if all pipelines for this effect are ready.
4115
    #[inline]
4116
    pub fn is_ready(&self) -> bool {
2,028✔
4117
        self.flags.contains(
4,056✔
4118
            CachedPipelineFlags::INIT_PIPELINE_READY | CachedPipelineFlags::UPDATE_PIPELINE_READY,
2,028✔
4119
        )
4120
    }
4121
}
4122

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

4137
impl CachedReadyState {
4138
    #[inline(always)]
4139
    pub fn new(is_ready: bool) -> Self {
1,014✔
4140
        Self { is_ready }
4141
    }
4142

4143
    #[inline(always)]
NEW
4144
    pub fn and(mut self, ancestors_ready: bool) -> Self {
×
NEW
4145
        self.and_with(ancestors_ready);
×
NEW
4146
        self
×
4147
    }
4148

4149
    #[inline(always)]
NEW
4150
    pub fn and_with(&mut self, ancestors_ready: bool) {
×
NEW
4151
        self.is_ready = self.is_ready && ancestors_ready;
×
4152
    }
4153

4154
    #[inline(always)]
4155
    pub fn is_ready(&self) -> bool {
2,028✔
4156
        self.is_ready
2,028✔
4157
    }
4158
}
4159

4160
#[derive(SystemParam)]
4161
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
4162
    sim_params: Res<'w, SimParams>,
4163
    render_device: Res<'w, RenderDevice>,
4164
    render_queue: Res<'w, RenderQueue>,
4165
    marker: PhantomData<&'s usize>,
4166
}
4167

4168
#[derive(SystemParam)]
4169
pub struct PipelineSystemParams<'w, 's> {
4170
    pipeline_cache: Res<'w, PipelineCache>,
4171
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
4172
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
4173
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
4174
    marker: PhantomData<&'s usize>,
4175
}
4176

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

4205
    // Update orphaned list for this frame, and sort it so we can efficiently binary
4206
    // search it
4207
    orphaned_entities.clear();
1,030✔
4208
    orphaned_entities.extend(orphaned.read());
3,090✔
4209
    orphaned_entities.sort_unstable();
1,030✔
4210

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

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

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

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

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

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

4371
    // Workaround for too many params in system (TODO: refactor to split work?)
4372
    let sim_params = read_only_params.sim_params.into_inner();
3,090✔
4373
    let render_device = read_only_params.render_device.into_inner();
3,090✔
4374
    let render_queue = read_only_params.render_queue.into_inner();
3,090✔
4375
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
3,090✔
4376

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

4380
    // Build batcher inputs from extracted effects, updating all cached components
4381
    // for each effect on the fly.
4382
    let mut extracted_effect_count = 0;
2,060✔
4383
    let mut prepared_effect_count = 0;
2,060✔
4384
    for (
4385
        main_entity,
1,014✔
4386
        render_entity,
1,014✔
4387
        extracted_effect,
1,014✔
4388
        extracted_spawner,
1,014✔
4389
        cached_effect,
1,014✔
4390
        cached_effect_metadata,
1,014✔
4391
        cached_ready_state,
1,014✔
4392
        cached_pipelines,
1,014✔
4393
        maybe_cached_draw_indirect_args,
1,014✔
4394
        maybe_cached_parent_info,
1,014✔
4395
        maybe_child_effect_of,
1,014✔
4396
        maybe_cached_child_info,
1,014✔
4397
        maybe_cached_effect_events,
1,014✔
4398
    ) in &q_cached_effects
2,044✔
4399
    {
4400
        extracted_effect_count += 1;
1,014✔
4401

4402
        // Skip this effect if not ready
4403
        if !cached_ready_state.is_ready() {
1,014✔
4404
            trace!("Pipelines not ready for effect {}, skipped.", render_entity);
4✔
4405
            continue;
4406
        }
4407

4408
        // Skip this effect if not visible and not simulating when hidden
4409
        if !extracted_spawner.is_visible
1,012✔
NEW
4410
            && (extracted_effect.simulation_condition == SimulationCondition::WhenVisible)
×
4411
        {
NEW
4412
            trace!(
×
NEW
4413
                "Effect {} not visible, and simulation condition is WhenVisible, so skipped.",
×
4414
                render_entity
4415
            );
NEW
4416
            continue;
×
4417
        }
4418

4419
        // Fetch the init and update pipelines.
4420
        // SAFETY: If is_ready() returns true, this means the pipelines are cached and
4421
        // ready, so the IDs must be valid.
4422
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
4423
            init: cached_pipelines.init.unwrap(),
4424
            update: cached_pipelines.update.unwrap(),
4425
        };
4426

4427
        let effect_slice = EffectSlice {
4428
            slice: cached_effect.slice.range(),
4429
            slab_id: cached_effect.slab_id,
4430
            particle_layout: cached_effect.slice.particle_layout.clone(),
4431
        };
4432

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

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

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

4481
        // Output some debug info
4482
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
2,024✔
4483
        trace!(
4484
            "update_shader = {:?}",
1,012✔
4485
            extracted_effect.effect_shaders.update
4486
        );
4487
        trace!(
4488
            "render_shader = {:?}",
1,012✔
4489
            extracted_effect.effect_shaders.render
4490
        );
4491
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
1,012✔
4492
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
1,012✔
4493

4494
        assert!(cached_effect_metadata.table_id.is_valid());
4495
        let spawner_index = effects_meta.allocate_spawner(
1,012✔
4496
            &extracted_spawner.transform,
4497
            extracted_spawner.spawn_count,
4498
            extracted_spawner.prng_seed,
4499
            cached_effect_metadata.table_id,
4500
            maybe_cached_draw_indirect_args,
4501
        );
4502

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

4521
        prepared_effect_count += 1;
4522
    }
4523
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
2,050✔
4524

4525
    // Update simulation parameters, including the total effect count for this frame
4526
    {
4527
        let mut gpu_sim_params: GpuSimParams = sim_params.into();
4528
        gpu_sim_params.num_effects = prepared_effect_count;
4529
        trace!(
4530
            "Simulation parameters: time={} delta_time={} virtual_time={} \
1,020✔
4531
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
1,020✔
4532
            gpu_sim_params.time,
4533
            gpu_sim_params.delta_time,
4534
            gpu_sim_params.virtual_time,
4535
            gpu_sim_params.virtual_delta_time,
4536
            gpu_sim_params.real_time,
4537
            gpu_sim_params.real_delta_time,
4538
            gpu_sim_params.num_effects,
4539
        );
4540
        effects_meta.sim_params_uniforms.set(gpu_sim_params);
4541
    }
4542

4543
    // Write the entire spawner buffer for this frame, for all effects combined
4544
    assert_eq!(
4545
        prepared_effect_count,
4546
        effects_meta.spawner_buffer.len() as u32
4547
    );
4548
    if effects_meta
1,030✔
4549
        .spawner_buffer
1,030✔
4550
        .write_buffer(render_device, render_queue)
3,090✔
4551
    {
4552
        // All property bind groups use the spawner buffer, which was reallocate
4553
        effect_bind_groups.particle_slabs.clear();
6✔
4554
        property_bind_groups.clear(true);
4✔
4555
        effects_meta.indirect_spawner_bind_group = None;
2✔
4556
    }
4557
}
4558

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

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

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

4614
    let mut sort_queue = GpuBufferOperationQueue::new();
2,060✔
4615

4616
    // Loop on all extracted effects in sorted order, and try to batch them together
4617
    // to reduce draw calls. -- currently does nothing, batching was broken and
4618
    // never fixed, but at least we minimize the GPU state changes with the sorting!
4619
    trace!("Batching {} effects...", q_cached_effects.iter().len());
4,090✔
4620
    sorted_effect_batches.clear();
1,030✔
4621
    for entity in effect_sorter.effects.iter().map(|e| e.entity) {
3,072✔
4622
        let Ok((
4623
            entity,
1,012✔
4624
            main_entity,
4625
            extracted_effect,
4626
            extracted_spawner,
4627
            extracted_effect_mesh,
4628
            cached_draw_indirect_args,
4629
            cached_effect_metadata,
4630
            cached_effect_events,
4631
            _,
4632
            cached_child_info,
4633
            cached_properties,
4634
            dispatch_buffer_indices,
4635
            mut input,
4636
        )) = q_cached_effects.get_mut(entity)
2,024✔
4637
        else {
NEW
4638
            continue;
×
4639
        };
4640

4641
        let translation = extracted_spawner.transform.translation();
4642

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

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

4675
            // Allocate a GpuDispatchIndirect entry
4676
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4677
            effect_batch.sort_fill_indirect_dispatch_index =
4678
                Some(sort_fill_indirect_dispatch_index);
4679

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

4742
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
1,012✔
4743
        trace!(
4744
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
1,012✔
4745
            effect_batch_index,
4746
            entity,
4747
        );
4748

4749
        // Spawn an EffectDrawBatch, to actually drive rendering.
4750
        commands
4751
            .spawn(EffectDrawBatch {
4752
                effect_batch_index,
4753
                translation,
4754
                main_entity: *main_entity,
4755
            })
4756
            .insert(TemporaryRenderEntity);
4757
    }
4758

4759
    gpu_buffer_operations.begin_frame();
1,030✔
4760
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
4761
    if !sort_queue.operation_queue.is_empty() {
1,030✔
4762
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4763
    }
4764
}
4765

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

4803
/// Combination of a texture layout and the bound textures.
4804
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4805
struct Material {
4806
    layout: TextureLayout,
4807
    textures: Vec<AssetId<Image>>,
4808
}
4809

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

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

4849
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4850
struct BindingKey {
4851
    pub buffer_id: BufferId,
4852
    pub offset: u32,
4853
    pub size: NonZeroU32,
4854
}
4855

4856
impl<'a> From<BufferSlice<'a>> for BindingKey {
4857
    fn from(value: BufferSlice<'a>) -> Self {
×
4858
        Self {
4859
            buffer_id: value.buffer.id(),
×
4860
            offset: value.offset,
×
4861
            size: value.size,
×
4862
        }
4863
    }
4864
}
4865

4866
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4867
    fn from(value: &BufferSlice<'a>) -> Self {
×
4868
        Self {
4869
            buffer_id: value.buffer.id(),
×
4870
            offset: value.offset,
×
4871
            size: value.size,
×
4872
        }
4873
    }
4874
}
4875

4876
impl From<&BufferBindingSource> for BindingKey {
4877
    fn from(value: &BufferBindingSource) -> Self {
×
4878
        Self {
4879
            buffer_id: value.buffer.id(),
×
4880
            offset: value.offset,
×
4881
            size: value.size,
×
4882
        }
4883
    }
4884
}
4885

4886
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4887
struct ConsumeEventKey {
4888
    child_infos_buffer_id: BufferId,
4889
    events: BindingKey,
4890
}
4891

4892
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4893
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4894
        Self {
4895
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4896
            events: value.events.into(),
×
4897
        }
4898
    }
4899
}
4900

4901
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4902
struct InitMetadataBindGroupKey {
4903
    pub slab_id: SlabId,
4904
    pub effect_metadata_buffer: BufferId,
4905
    pub effect_metadata_offset: u32,
4906
    pub consume_event_key: Option<ConsumeEventKey>,
4907
}
4908

4909
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4910
struct UpdateMetadataBindGroupKey {
4911
    pub slab_id: SlabId,
4912
    pub effect_metadata_buffer: BufferId,
4913
    pub effect_metadata_offset: u32,
4914
    pub child_info_buffer_id: Option<BufferId>,
4915
    pub event_buffers_keys: Vec<BindingKey>,
4916
}
4917

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

4933
#[derive(Debug, Clone, Copy)]
4934
struct BufferSlice<'a> {
4935
    pub buffer: &'a Buffer,
4936
    pub offset: u32,
4937
    pub size: NonZeroU32,
4938
}
4939

4940
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4941
    fn from(value: BufferSlice<'a>) -> Self {
×
4942
        Self {
4943
            buffer: value.buffer,
×
4944
            offset: value.offset.into(),
×
4945
            size: Some(value.size.into()),
×
4946
        }
4947
    }
4948
}
4949

4950
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4951
    fn from(value: &BufferSlice<'a>) -> Self {
×
4952
        Self {
4953
            buffer: value.buffer,
×
4954
            offset: value.offset.into(),
×
4955
            size: Some(value.size.into()),
×
4956
        }
4957
    }
4958
}
4959

4960
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4961
    fn from(value: &'a BufferBindingSource) -> Self {
×
4962
        Self {
4963
            buffer: &value.buffer,
×
4964
            offset: value.offset,
×
4965
            size: value.size,
×
4966
        }
4967
    }
4968
}
4969

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

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

4999
impl EffectBindGroups {
5000
    pub fn particle_render(&self, slab_id: &SlabId) -> Option<&BindGroup> {
1,011✔
5001
        self.particle_slabs.get(slab_id).map(|bg| &bg.render)
4,044✔
5002
    }
5003

5004
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
5005
    /// needed.
5006
    pub(self) fn get_or_create_init_metadata(
1,012✔
5007
        &mut self,
5008
        effect_batch: &EffectBatch,
5009
        gpu_limits: &GpuLimits,
5010
        render_device: &RenderDevice,
5011
        layout: &BindGroupLayout,
5012
        effect_metadata_buffer: &Buffer,
5013
        consume_event_buffers: Option<ConsumeEventBuffers>,
5014
    ) -> Result<&BindGroup, ()> {
5015
        assert!(effect_batch.metadata_table_id.is_valid());
3,036✔
5016

5017
        let effect_metadata_offset =
1,012✔
5018
            gpu_limits.effect_metadata_offset(effect_batch.metadata_table_id.0) as u32;
2,024✔
5019
        let key = InitMetadataBindGroupKey {
5020
            slab_id: effect_batch.slab_id,
2,024✔
5021
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,036✔
5022
            effect_metadata_offset,
5023
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
2,024✔
5024
        };
5025

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

5061
            let bind_group = render_device.create_bind_group(
6✔
5062
                "hanabi:bind_group:init:metadata@3",
5063
                layout,
2✔
5064
                &entries[..],
2✔
5065
            );
5066

5067
            trace!(
2✔
5068
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
5069
                    effect_batch.slab_id.index(),
4✔
5070
                    effect_batch.metadata_table_id.0,
5071
                );
5072

5073
            bind_group
2✔
5074
        };
5075

5076
        Ok(&self
1,012✔
5077
            .init_metadata_bind_groups
1,012✔
5078
            .entry(effect_batch.slab_id)
2,024✔
5079
            .and_modify(|cbg| {
2,022✔
5080
                if cbg.key != key {
1,010✔
5081
                    trace!(
×
5082
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
5083
                        cbg.key,
5084
                        key
5085
                    );
5086
                    cbg.key = key;
×
5087
                    cbg.bind_group = make_entry();
×
5088
                }
5089
            })
5090
            .or_insert_with(|| {
1,014✔
5091
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
5092
                CachedBindGroup {
2✔
5093
                    key,
2✔
5094
                    bind_group: make_entry(),
2✔
5095
                }
5096
            })
5097
            .bind_group)
5098
    }
5099

5100
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
5101
    /// needed.
5102
    pub(self) fn get_or_create_update_metadata(
1,012✔
5103
        &mut self,
5104
        effect_batch: &EffectBatch,
5105
        gpu_limits: &GpuLimits,
5106
        render_device: &RenderDevice,
5107
        layout: &BindGroupLayout,
5108
        effect_metadata_buffer: &Buffer,
5109
        child_info_buffer: Option<&Buffer>,
5110
        event_buffers: &[(Entity, BufferBindingSource)],
5111
    ) -> Result<&BindGroup, ()> {
5112
        assert!(effect_batch.metadata_table_id.is_valid());
3,036✔
5113

5114
        // Check arguments consistency
5115
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
5,060✔
5116
        let emits_gpu_spawn_events = !event_buffers.is_empty();
2,024✔
5117
        let child_info_buffer_id = if emits_gpu_spawn_events {
2,024✔
5118
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
5119
        } else {
5120
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
5121
            // if relevant, that is if the effect emits GPU spawn events.
5122
            None
1,012✔
5123
        };
5124
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
3,036✔
5125

5126
        let event_buffers_keys = event_buffers
2,024✔
5127
            .iter()
5128
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
1,012✔
5129
            .collect::<Vec<_>>();
5130

5131
        let key = UpdateMetadataBindGroupKey {
5132
            slab_id: effect_batch.slab_id,
2,024✔
5133
            effect_metadata_buffer: effect_metadata_buffer.id(),
3,036✔
5134
            effect_metadata_offset: gpu_limits
3,036✔
5135
                .effect_metadata_offset(effect_batch.metadata_table_id.0)
5136
                as u32,
5137
            child_info_buffer_id,
5138
            event_buffers_keys,
5139
        };
5140

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

5156
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
5157
                // ChildInfoBuffer;
5158
                entries.push(BindGroupEntry {
×
5159
                    binding: 1,
×
5160
                    resource: BindingResource::Buffer(BufferBinding {
×
5161
                        buffer: child_info_buffer,
×
5162
                        offset: 0,
×
5163
                        size: None,
×
5164
                    }),
5165
                });
5166

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

5185
            let bind_group = render_device.create_bind_group(
6✔
5186
                "hanabi:bind_group:update:metadata@3",
5187
                layout,
2✔
5188
                &entries[..],
2✔
5189
            );
5190

5191
            trace!(
2✔
5192
                "Created new metadata@3 bind group for update pass and slab ID {}: effect_metadata={}",
2✔
5193
                effect_batch.slab_id.index(),
4✔
5194
                effect_batch.metadata_table_id.0,
5195
            );
5196

5197
            bind_group
2✔
5198
        };
5199

5200
        Ok(&self
1,012✔
5201
            .update_metadata_bind_groups
1,012✔
5202
            .entry(effect_batch.slab_id)
2,024✔
5203
            .and_modify(|cbg| {
2,022✔
5204
                if cbg.key != key {
1,010✔
5205
                    trace!(
×
5206
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
5207
                        cbg.key,
5208
                        key
5209
                    );
5210
                    cbg.key = key.clone();
×
5211
                    cbg.bind_group = make_entry();
×
5212
                }
5213
            })
5214
            .or_insert_with(|| {
1,014✔
5215
                trace!(
2✔
5216
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
5217
                    key
5218
                );
5219
                CachedBindGroup {
2✔
5220
                    key: key.clone(),
4✔
5221
                    bind_group: make_entry(),
2✔
5222
                }
5223
            })
5224
            .bind_group)
5225
    }
5226
}
5227

5228
#[derive(SystemParam)]
5229
pub struct QueueEffectsReadOnlyParams<'w, 's> {
5230
    #[cfg(feature = "2d")]
5231
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5232
    #[cfg(feature = "3d")]
5233
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5234
    #[cfg(feature = "3d")]
5235
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5236
    #[cfg(feature = "3d")]
5237
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5238
    marker: PhantomData<&'s usize>,
5239
}
5240

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

5259
    for (visible_entities, view, msaa) in views.iter() {
6,072✔
5260
        trace!(
×
5261
            "Process new sorted view with {} visible particle effect entities",
2,024✔
5262
            visible_entities.len::<CompiledParticleEffect>()
4,048✔
5263
        );
5264

5265
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
1,012✔
5266
            continue;
1,012✔
5267
        };
5268

5269
        {
5270
            #[cfg(feature = "trace")]
5271
            let _span = bevy::log::info_span!("collect_view_entities").entered();
3,036✔
5272

5273
            view_entities.clear();
2,024✔
5274
            view_entities.extend(
2,024✔
5275
                visible_entities
1,012✔
5276
                    .iter::<EffectVisibilityClass>()
1,012✔
5277
                    .map(|e| e.1.index() as usize),
2,024✔
5278
            );
5279
        }
5280

5281
        // For each view, loop over all the effect batches to determine if the effect
5282
        // needs to be rendered for that view, and enqueue a view-dependent
5283
        // batch if so.
5284
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
3,036✔
5285
            #[cfg(feature = "trace")]
5286
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5287

5288
            trace!(
×
5289
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
1,012✔
5290
                draw_entity,
×
5291
                draw_batch.effect_batch_index,
×
5292
            );
5293

5294
            // Get the EffectBatches this EffectDrawBatch is part of.
5295
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
1,012✔
5296
            else {
×
5297
                continue;
×
5298
            };
5299

5300
            trace!(
×
5301
                "-> EffectBach: slab_id={} spawner_base={} layout_flags={:?}",
1,012✔
5302
                effect_batch.slab_id.index(),
2,024✔
5303
                effect_batch.spawner_base,
×
5304
                effect_batch.layout_flags,
×
5305
            );
5306

5307
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5308
            if effect_batch
×
5309
                .layout_flags
×
5310
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5311
            {
5312
                trace!("Non-transparent batch. Skipped.");
×
5313
                continue;
×
5314
            }
5315

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

5335
            // Create and cache the bind group layout for this texture layout
5336
            render_pipeline.cache_material(&effect_batch.texture_layout);
3,036✔
5337

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

5341
            let local_space_simulation = effect_batch
2,024✔
5342
                .layout_flags
1,012✔
5343
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
1,012✔
5344
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
3,036✔
5345
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
3,036✔
5346
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
3,036✔
5347
            let needs_normal = effect_batch
2,024✔
5348
                .layout_flags
1,012✔
5349
                .contains(LayoutFlags::NEEDS_NORMAL);
1,012✔
5350
            let needs_particle_fragment = effect_batch
2,024✔
5351
                .layout_flags
1,012✔
5352
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
1,012✔
5353
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
3,036✔
5354
            let image_count = effect_batch.texture_layout.layout.len() as u8;
2,024✔
5355

5356
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5357
            // re-querying here...?
5358
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
3,036✔
5359
                trace!("Batch has no render mesh, skipped.");
×
5360
                continue;
×
5361
            };
5362
            let mesh_layout = render_mesh.layout.clone();
×
5363

5364
            // Specialize the render pipeline based on the effect batch
5365
            trace!(
×
5366
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
1,012✔
5367
                effect_batch.render_shader,
×
5368
                image_count,
×
5369
                alpha_mask,
×
5370
                flipbook,
×
5371
                view.hdr
×
5372
            );
5373

5374
            // Add a draw pass for the effect batch
5375
            trace!("Emitting individual draw for batch");
1,012✔
5376

5377
            let alpha_mode = effect_batch.alpha_mode;
×
5378

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

5406
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
1,012✔
5407
            trace!(
×
5408
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
1,012✔
5409
                spawner_base={} handle={:?}",
1,012✔
5410
                draw_entity,
×
5411
                effect_batch.slab_id.index(),
2,024✔
5412
                effect_batch.spawner_base,
×
5413
                effect_batch.handle
×
5414
            );
5415
            render_phase.add(make_phase_item(
×
5416
                render_pipeline_id,
×
5417
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5418
                draw_batch,
×
5419
                view,
×
5420
            ));
5421
        }
5422
    }
5423
}
5424

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

5448
    trace!("emit_binned_draw() {} views", views.iter().len());
8,096✔
5449

5450
    for (visible_entities, view, msaa) in views.iter() {
6,072✔
5451
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
2,024✔
5452

5453
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
2,024✔
5454
            continue;
×
5455
        };
5456

5457
        {
5458
            #[cfg(feature = "trace")]
5459
            let _span = bevy::log::info_span!("collect_view_entities").entered();
6,072✔
5460

5461
            view_entities.clear();
4,048✔
5462
            view_entities.extend(
4,048✔
5463
                visible_entities
2,024✔
5464
                    .iter::<EffectVisibilityClass>()
2,024✔
5465
                    .map(|e| e.1.index() as usize),
4,048✔
5466
            );
5467
        }
5468

5469
        // For each view, loop over all the effect batches to determine if the effect
5470
        // needs to be rendered for that view, and enqueue a view-dependent
5471
        // batch if so.
5472
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
6,072✔
5473
            #[cfg(feature = "trace")]
5474
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5475

5476
            trace!(
×
5477
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
2,024✔
5478
                draw_entity,
×
5479
                draw_batch.effect_batch_index,
×
5480
            );
5481

5482
            // Get the EffectBatches this EffectDrawBatch is part of.
5483
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
2,024✔
5484
            else {
×
5485
                continue;
×
5486
            };
5487

5488
            trace!(
×
5489
                "-> EffectBaches: slab_id={} spawner_base={} layout_flags={:?}",
2,024✔
5490
                effect_batch.slab_id.index(),
4,048✔
5491
                effect_batch.spawner_base,
×
5492
                effect_batch.layout_flags,
×
5493
            );
5494

5495
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5496
                trace!(
2,024✔
5497
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
2,024✔
5498
                    effect_batch.layout_flags,
×
5499
                    alpha_mask
×
5500
                );
5501
                continue;
2,024✔
5502
            }
5503

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

5523
            // Create and cache the bind group layout for this texture layout
5524
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5525

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

5529
            let local_space_simulation = effect_batch
×
5530
                .layout_flags
×
5531
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5532
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5533
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5534
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5535
            let needs_normal = effect_batch
×
5536
                .layout_flags
×
5537
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5538
            let needs_particle_fragment = effect_batch
×
5539
                .layout_flags
×
5540
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5541
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5542
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5543
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5544

5545
            // Specialize the render pipeline based on the effect batch
5546
            trace!(
×
5547
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5548
                effect_batch.render_shader,
×
5549
                image_count,
×
5550
                alpha_mask,
×
5551
                flipbook,
×
5552
                view.hdr
×
5553
            );
5554

5555
            // Add a draw pass for the effect batch
5556
            trace!("Emitting individual draw for batch");
×
5557

5558
            let alpha_mode = effect_batch.alpha_mode;
×
5559

5560
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5561
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5562
                continue;
×
5563
            };
5564

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

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

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

5642
    trace!("queue_effects");
2,050✔
5643

5644
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5645
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5646
    // reuse stale data.
5647
    let next_change_tick = change_tick.get() + 1;
2,060✔
5648
    change_tick.set(next_change_tick);
2,060✔
5649

5650
    // If an image has changed, the GpuImage has (probably) changed
5651
    for event in &events.images {
1,057✔
5652
        match event {
5653
            AssetEvent::Added { .. } => (),
24✔
NEW
5654
            AssetEvent::LoadedWithDependencies { .. } => (),
×
NEW
5655
            AssetEvent::Unused { .. } => (),
×
5656
            AssetEvent::Modified { id } => {
×
NEW
5657
                if effect_bind_groups.images.remove(id).is_some() {
×
NEW
5658
                    trace!("Destroyed bind group of modified image asset {:?}", id);
×
5659
                }
5660
            }
5661
            AssetEvent::Removed { id } => {
3✔
5662
                if effect_bind_groups.images.remove(id).is_some() {
9✔
NEW
5663
                    trace!("Destroyes bind group of removed image asset {:?}", id);
×
5664
                }
5665
            }
5666
        };
5667
    }
5668

5669
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
3,078✔
5670
        // No spawners are active
5671
        return;
18✔
5672
    }
5673

5674
    // Loop over all 2D cameras/views that need to render effects
5675
    #[cfg(feature = "2d")]
5676
    {
5677
        #[cfg(feature = "trace")]
5678
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5679

5680
        let draw_effects_function_2d = read_params
5681
            .draw_functions_2d
5682
            .read()
5683
            .get_id::<DrawEffects>()
5684
            .unwrap();
5685

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

5715
    // Loop over all 3D cameras/views that need to render effects
5716
    #[cfg(feature = "3d")]
5717
    {
5718
        #[cfg(feature = "trace")]
5719
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
5720

5721
        // Effects with full alpha blending
5722
        if !views.is_empty() {
5723
            trace!("Emit effect draw calls for alpha blended 3D views...");
2,024✔
5724

5725
            let draw_effects_function_3d = read_params
5726
                .draw_functions_3d
5727
                .read()
5728
                .get_id::<DrawEffects>()
5729
                .unwrap();
5730

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

5757
        // Effects with alpha mask
5758
        if !views.is_empty() {
5759
            #[cfg(feature = "trace")]
5760
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
1,012✔
5761

5762
            trace!("Emit effect draw calls for alpha masked 3D views...");
1,012✔
5763

5764
            let draw_effects_function_alpha_mask = read_params
5765
                .draw_functions_alpha_mask
5766
                .read()
5767
                .get_id::<DrawEffects>()
5768
                .unwrap();
5769

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

5798
        // Opaque particles
5799
        if !views.is_empty() {
5800
            #[cfg(feature = "trace")]
5801
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
1,012✔
5802

5803
            trace!("Emit effect draw calls for opaque 3D views...");
1,012✔
5804

5805
            let draw_effects_function_opaque = read_params
5806
                .draw_functions_opaque
5807
                .read()
5808
                .get_id::<DrawEffects>()
5809
                .unwrap();
5810

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

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

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

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

5896
    // Upload simulation parameters for this frame
5897
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
2,054✔
5898
    effects_meta
5899
        .sim_params_uniforms
5900
        .write_buffer(&render_device, &render_queue);
5901
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
2,063✔
5902
        // Buffer changed, invalidate bind groups
5903
        effects_meta.update_sim_params_bind_group = None;
9✔
5904
        effects_meta.indirect_sim_params_bind_group = None;
3✔
5905
    }
5906

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

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

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

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

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

6033
        let capacity = cached_effect.slice.len();
9✔
6034

6035
        // Global and local indices of this effect as a child of another (parent) effect
6036
        let (global_child_index, local_child_index) = maybe_cached_child_info
9✔
6037
            .map(|cci| (cci.global_child_index, cci.local_child_index))
3✔
6038
            .unwrap_or((u32::MAX, u32::MAX));
6✔
6039

6040
        // Base index of all children of this (parent) effect
6041
        let base_child_index = maybe_cached_parent_info
6✔
6042
            .map(|cpi| {
3✔
NEW
6043
                debug_assert_eq!(
×
NEW
6044
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
6045
                    0
6046
                );
NEW
6047
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
6048
            })
6049
            .unwrap_or(u32::MAX);
3✔
6050

6051
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
9✔
6052
        let sort_key_offset = extracted_effect
6✔
6053
            .particle_layout
3✔
6054
            .byte_offset(Attribute::RIBBON_ID)
3✔
6055
            .map(|byte_offset| byte_offset / 4)
3✔
6056
            .unwrap_or(u32::MAX);
3✔
6057
        let sort_key2_offset = extracted_effect
6✔
6058
            .particle_layout
3✔
6059
            .byte_offset(Attribute::AGE)
3✔
6060
            .map(|byte_offset| byte_offset / 4)
3✔
6061
            .unwrap_or(u32::MAX);
3✔
6062

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

6086
        // Insert of update entry in GPU buffer table
6087
        assert!(cached_effect_metadata.table_id.is_valid());
9✔
6088
        if gpu_effect_metadata != cached_effect_metadata.metadata {
3✔
6089
            effects_meta
2✔
6090
                .effect_metadata_buffer
2✔
6091
                .update(cached_effect_metadata.table_id, gpu_effect_metadata);
6✔
6092

6093
            cached_effect_metadata.metadata = gpu_effect_metadata;
2✔
6094

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

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

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

6148
    // Once all GPU operations for this frame are enqueued, upload them to GPU
6149
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
3,090✔
6150
}
6151

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

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

6183
    {
6184
        #[cfg(feature = "trace")]
6185
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
6186

6187
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
6188
        // loop below. Also allows earlying out before doing any work in case some
6189
        // buffer is missing.
6190
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,012✔
6191
            return;
×
6192
        };
6193

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

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

6272
            // Some buffer is not yet available, can't create the bind group
6273
            _ => None,
×
6274
        };
6275

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

6295
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
6296
        }
6297
    }
6298

6299
    // Create the per-slab bind groups
6300
    trace!("Create per-slab bind groups...");
1,012✔
6301
    for (slab_index, particle_slab) in effect_cache.slabs().iter().enumerate() {
1,012✔
6302
        #[cfg(feature = "trace")]
6303
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
6304

6305
        let Some(particle_slab) = particle_slab else {
1,012✔
6306
            trace!(
×
NEW
6307
                "Particle slab index #{} has no allocated EffectBuffer, skipped.",
×
6308
                slab_index
6309
            );
6310
            continue;
×
6311
        };
6312

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

6353
                BufferBindGroups { render }
2✔
6354
            });
6355
    }
6356

6357
    // Create bind groups for queued GPU buffer operations
6358
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
6359

6360
    // Create the per-effect bind groups
6361
    let spawner_buffer_binding_size =
6362
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
6363
    for effect_batch in sorted_effect_batched.iter() {
1,012✔
6364
        #[cfg(feature = "trace")]
6365
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
3,036✔
6366

6367
        // Create the property bind group @2 if needed
6368
        if let Some(property_key) = &effect_batch.property_key {
1,025✔
6369
            if let Err(err) = property_bind_groups.ensure_exists(
×
6370
                property_key,
6371
                &property_cache,
6372
                &spawner_buffer,
6373
                spawner_buffer_binding_size,
6374
                &render_device,
6375
            ) {
6376
                error!("Failed to create property bind group for effect batch: {err:?}");
×
6377
                continue;
6378
            }
6379
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
2,997✔
6380
            &property_cache,
1,998✔
6381
            &spawner_buffer,
1,998✔
6382
            spawner_buffer_binding_size,
999✔
6383
            &render_device,
999✔
6384
        ) {
6385
            error!("Failed to create property bind group for effect batch: {err:?}");
×
6386
            continue;
6387
        }
6388

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

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

6451
        // Bind group @3 of update pass
6452
        // FIXME - this is instance-dependent, not buffer-dependent#
6453
        {
6454
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
6455

6456
            let Some(update_metadata_layout) =
1,012✔
6457
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
6458
            else {
6459
                continue;
×
6460
            };
6461
            if effect_bind_groups
6462
                .get_or_create_update_metadata(
6463
                    effect_batch,
6464
                    &effects_meta.gpu_limits,
6465
                    &render_device,
6466
                    update_metadata_layout,
6467
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
6468
                    event_cache.child_infos_buffer(),
6469
                    &effect_batch.child_event_buffers[..],
6470
                )
6471
                .is_err()
6472
            {
6473
                continue;
×
6474
            }
6475
        }
6476

6477
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
NEW
6478
            let effect_buffer = effect_cache.get_slab(&effect_batch.slab_id).unwrap();
×
6479

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

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

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

6526
            // TODO = move
6527
            let material = Material {
6528
                layout: effect_batch.texture_layout.clone(),
6529
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6530
            };
6531
            assert_eq!(material.layout.layout.len(), material.textures.len());
6532

6533
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6534
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6535
                trace!(
×
6536
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
6537
                    material
6538
                );
6539
                continue;
×
6540
            };
6541

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

6560
type DrawEffectsSystemState = SystemState<(
6561
    SRes<EffectsMeta>,
6562
    SRes<EffectBindGroups>,
6563
    SRes<PipelineCache>,
6564
    SRes<RenderAssets<RenderMesh>>,
6565
    SRes<MeshAllocator>,
6566
    SQuery<Read<ViewUniformOffset>>,
6567
    SRes<SortedEffectBatches>,
6568
    SQuery<Read<EffectDrawBatch>>,
6569
)>;
6570

6571
/// Draw function for rendering all active effects for the current frame.
6572
///
6573
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6574
/// and the [`Transparent3d`] phase of the main 3D pass.
6575
pub(crate) struct DrawEffects {
6576
    params: DrawEffectsSystemState,
6577
}
6578

6579
impl DrawEffects {
6580
    pub fn new(world: &mut World) -> Self {
12✔
6581
        Self {
6582
            params: SystemState::new(world),
12✔
6583
        }
6584
    }
6585
}
6586

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

6618
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
3,033✔
6619
        return;
×
6620
    };
6621

6622
    trace!("render pass");
1,011✔
6623

6624
    pass.set_render_pipeline(pipeline);
×
6625

6626
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
1,011✔
6627
        return;
×
6628
    };
6629
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
1,011✔
6630
        return;
×
6631
    };
6632

6633
    // Vertex buffer containing the particle model to draw. Generally a quad.
6634
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6635
    // "base_vertex" in the indirect struct...
6636
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6637

6638
    // View properties (camera matrix, etc.)
6639
    pass.set_bind_group(
×
6640
        0,
6641
        effects_meta.view_bind_group.as_ref().unwrap(),
×
6642
        &[view_uniform.offset],
×
6643
    );
6644

6645
    // Particles buffer
6646
    let spawner_base = effect_batch.spawner_base;
×
6647
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
6648
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6649
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
2,022✔
6650
    pass.set_bind_group(
2,022✔
6651
        1,
6652
        effect_bind_groups
2,022✔
6653
            .particle_render(&effect_batch.slab_id)
2,022✔
6654
            .unwrap(),
1,011✔
6655
        &[spawner_offset],
1,011✔
6656
    );
6657

6658
    // Particle texture
6659
    // TODO = move
6660
    let material = Material {
6661
        layout: effect_batch.texture_layout.clone(),
2,022✔
6662
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
3,033✔
6663
    };
6664
    if !effect_batch.texture_layout.layout.is_empty() {
1,011✔
6665
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6666
            pass.set_bind_group(2, bind_group, &[]);
×
6667
        } else {
6668
            // Texture(s) not ready; skip this drawing for now
6669
            trace!(
×
NEW
6670
                "Particle material bind group not available for batch slab_id={}. Skipping draw call.",
×
NEW
6671
                effect_batch.slab_id.index(),
×
6672
            );
6673
            return;
×
6674
        }
6675
    }
6676

6677
    let draw_indirect_index = effect_batch.draw_indirect_buffer_row_index.0;
1,011✔
6678
    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
×
6679
    let draw_indirect_offset =
1,011✔
6680
        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
1,011✔
6681
    trace!(
1,011✔
6682
        "Draw up to {} particles with {} vertices per particle for batch from particle slab #{} \
1,011✔
6683
            (effect_metadata_index={}, draw_indirect_offset={}B).",
1,011✔
6684
        effect_batch.slice.len(),
2,022✔
6685
        render_mesh.vertex_count,
×
6686
        effect_batch.slab_id.index(),
2,022✔
6687
        draw_indirect_index,
×
6688
        draw_indirect_offset,
×
6689
    );
6690

6691
    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
2,022✔
6692
        trace!(
×
NEW
6693
            "The draw indirect buffer containing the indirect draw args is not ready for batch slab_id=#{}. Skipping draw call.",
×
NEW
6694
            effect_batch.slab_id.index(),
×
6695
        );
6696
        return;
×
6697
    };
6698

6699
    match render_mesh.buffer_info {
×
6700
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
1,011✔
6701
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
1,011✔
6702
            else {
×
6703
                trace!(
×
NEW
6704
                    "The index buffer for indexed rendering is not ready for batch slab_id=#{}. Skipping draw call.",
×
NEW
6705
                    effect_batch.slab_id.index(),
×
6706
                );
6707
                return;
×
6708
            };
6709

6710
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6711
            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
×
6712
        }
6713
        RenderMeshBufferInfo::NonIndexed => {
×
6714
            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
×
6715
        }
6716
    }
6717
}
6718

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

6741
#[cfg(feature = "3d")]
6742
impl Draw<Transparent3d> for DrawEffects {
6743
    fn draw<'w>(
1,011✔
6744
        &mut self,
6745
        world: &'w World,
6746
        pass: &mut TrackedRenderPass<'w>,
6747
        view: Entity,
6748
        item: &Transparent3d,
6749
    ) -> Result<(), DrawError> {
6750
        trace!("Draw<Transparent3d>: view={:?}", view);
2,022✔
6751
        draw(
6752
            world,
1,011✔
6753
            pass,
1,011✔
6754
            view,
1,011✔
6755
            item.entity,
1,011✔
6756
            item.pipeline,
1,011✔
6757
            &mut self.params,
1,011✔
6758
        );
6759
        Ok(())
1,011✔
6760
    }
6761
}
6762

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

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

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

6820
impl Node for VfxSimulateDriverNode {
6821
    fn run(
1,030✔
6822
        &self,
6823
        graph: &mut RenderGraphContext,
6824
        _render_context: &mut RenderContext,
6825
        _world: &World,
6826
    ) -> Result<(), NodeRunError> {
6827
        graph.run_sub_graph(
2,060✔
6828
            crate::plugin::simulate_graph::HanabiSimulateGraph,
1,030✔
6829
            vec![],
1,030✔
6830
            None,
1,030✔
6831
        )?;
6832
        Ok(())
1,030✔
6833
    }
6834
}
6835

6836
#[derive(Debug, Clone, PartialEq, Eq)]
6837
enum HanabiPipelineId {
6838
    Invalid,
6839
    Cached(CachedComputePipelineId),
6840
}
6841

6842
#[derive(Debug)]
6843
pub(crate) enum ComputePipelineError {
6844
    Queued,
6845
    Creating,
6846
    Error,
6847
}
6848

6849
impl From<&CachedPipelineState> for ComputePipelineError {
6850
    fn from(value: &CachedPipelineState) -> Self {
×
6851
        match value {
×
6852
            CachedPipelineState::Queued => Self::Queued,
×
6853
            CachedPipelineState::Creating(_) => Self::Creating,
×
6854
            CachedPipelineState::Err(_) => Self::Error,
×
6855
            _ => panic!("Trying to convert Ok state to error."),
×
6856
        }
6857
    }
6858
}
6859

6860
pub(crate) struct HanabiComputePass<'a> {
6861
    /// Pipeline cache to fetch cached compute pipelines by ID.
6862
    pipeline_cache: &'a PipelineCache,
6863
    /// WGPU compute pass.
6864
    compute_pass: ComputePass<'a>,
6865
    /// Current pipeline (cached).
6866
    pipeline_id: HanabiPipelineId,
6867
}
6868

6869
impl<'a> Deref for HanabiComputePass<'a> {
6870
    type Target = ComputePass<'a>;
6871

6872
    fn deref(&self) -> &Self::Target {
×
6873
        &self.compute_pass
×
6874
    }
6875
}
6876

6877
impl DerefMut for HanabiComputePass<'_> {
6878
    fn deref_mut(&mut self) -> &mut Self::Target {
14,108✔
6879
        &mut self.compute_pass
14,108✔
6880
    }
6881
}
6882

6883
impl<'a> HanabiComputePass<'a> {
6884
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
4,048✔
6885
        Self {
6886
            pipeline_cache,
6887
            compute_pass,
6888
            pipeline_id: HanabiPipelineId::Invalid,
6889
        }
6890
    }
6891

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

6920
/// Render node to run the simulation of all effects once per frame.
6921
///
6922
/// Runs inside the simulation sub-graph, looping over all extracted effect
6923
/// batches to simulate them.
6924
pub(crate) struct VfxSimulateNode {}
6925

6926
impl VfxSimulateNode {
6927
    /// Create a new node for simulating the effects of the given world.
6928
    pub fn new(_world: &mut World) -> Self {
3✔
6929
        Self {}
6930
    }
6931

6932
    /// Begin a new compute pass and return a wrapper with extra
6933
    /// functionalities.
6934
    pub fn begin_compute_pass<'encoder>(
4,048✔
6935
        &self,
6936
        label: &str,
6937
        pipeline_cache: &'encoder PipelineCache,
6938
        render_context: &'encoder mut RenderContext,
6939
    ) -> HanabiComputePass<'encoder> {
6940
        let compute_pass =
4,048✔
6941
            render_context
4,048✔
6942
                .command_encoder()
6943
                .begin_compute_pass(&ComputePassDescriptor {
8,096✔
6944
                    label: Some(label),
4,048✔
6945
                    timestamp_writes: None,
4,048✔
6946
                });
6947
        HanabiComputePass::new(pipeline_cache, compute_pass)
12,144✔
6948
    }
6949
}
6950

6951
impl Node for VfxSimulateNode {
6952
    fn input(&self) -> Vec<SlotInfo> {
3✔
6953
        vec![]
3✔
6954
    }
6955

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

6958
    fn run(
1,030✔
6959
        &self,
6960
        _graph: &mut RenderGraphContext,
6961
        render_context: &mut RenderContext,
6962
        world: &World,
6963
    ) -> Result<(), NodeRunError> {
6964
        trace!("VfxSimulateNode::run()");
2,050✔
6965

6966
        let pipeline_cache = world.resource::<PipelineCache>();
3,090✔
6967
        let effects_meta = world.resource::<EffectsMeta>();
3,090✔
6968
        let effect_bind_groups = world.resource::<EffectBindGroups>();
3,090✔
6969
        let property_bind_groups = world.resource::<PropertyBindGroups>();
3,090✔
6970
        let sort_bind_groups = world.resource::<SortBindGroups>();
3,090✔
6971
        let utils_pipeline = world.resource::<UtilsPipeline>();
3,090✔
6972
        let effect_cache = world.resource::<EffectCache>();
3,090✔
6973
        let event_cache = world.resource::<EventCache>();
3,090✔
6974
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
3,090✔
6975
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
3,090✔
6976
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
3,090✔
6977

6978
        // Make sure to schedule any buffer copy before accessing their content later in
6979
        // the GPU commands below.
6980
        {
6981
            let command_encoder = render_context.command_encoder();
4,120✔
6982
            effects_meta
2,060✔
6983
                .dispatch_indirect_buffer
2,060✔
6984
                .write_buffers(command_encoder);
3,090✔
6985
            effects_meta
2,060✔
6986
                .draw_indirect_buffer
2,060✔
6987
                .write_buffer(command_encoder);
3,090✔
6988
            effects_meta
2,060✔
6989
                .effect_metadata_buffer
2,060✔
6990
                .write_buffer(command_encoder);
3,090✔
6991
            event_cache.write_buffers(command_encoder);
4,120✔
6992
            sort_bind_groups.write_buffers(command_encoder);
2,060✔
6993
        }
6994

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

7007
        // If there's no batch, there's nothing more to do. Avoid continuing because
7008
        // some GPU resources are missing, which is expected when there's no effect but
7009
        // is an error (and will log warnings/errors) otherwise.
7010
        if sorted_effect_batches.is_empty() {
2,060✔
7011
            return Ok(());
18✔
7012
        }
7013

7014
        // Compute init pass
7015
        {
7016
            trace!("init: loop over effect batches...");
1,012✔
7017

7018
            let mut compute_pass =
7019
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
7020

7021
            // Bind group simparams@0 is common to everything, only set once per init pass
7022
            compute_pass.set_bind_group(
7023
                0,
7024
                effects_meta
7025
                    .indirect_sim_params_bind_group
7026
                    .as_ref()
7027
                    .unwrap(),
7028
                &[],
7029
            );
7030

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

7052
                // Fetch bind group particle@1
7053
                let Some(particle_bind_group) =
997✔
7054
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
997✔
7055
                else {
7056
                    error!(
×
NEW
7057
                        "Failed to find init particle@1 bind group for slab #{}",
×
NEW
7058
                        effect_batch.slab_id.index()
×
7059
                    );
7060
                    continue;
×
7061
                };
7062

7063
                // Fetch bind group metadata@3
7064
                let Some(metadata_bind_group) = effect_bind_groups
997✔
7065
                    .init_metadata_bind_groups
7066
                    .get(&effect_batch.slab_id)
7067
                else {
7068
                    error!(
×
NEW
7069
                        "Failed to find init metadata@3 bind group for slab #{}",
×
NEW
7070
                        effect_batch.slab_id.index()
×
7071
                    );
7072
                    continue;
×
7073
                };
7074

7075
                if compute_pass
7076
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
7077
                    .is_err()
7078
                {
7079
                    continue;
×
7080
                }
7081

7082
                // Compute dynamic offsets
7083
                let spawner_base = effect_batch.spawner_base;
7084
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
7085
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
7086
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
1,994✔
7087
                let property_offset = effect_batch.property_offset;
1,994✔
7088

7089
                // Setup init pass
7090
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
2,991✔
7091
                let offsets = if let Some(property_offset) = property_offset {
1,994✔
7092
                    vec![spawner_offset, property_offset]
7093
                } else {
7094
                    vec![spawner_offset]
1,994✔
7095
                };
7096
                compute_pass.set_bind_group(
2,991✔
7097
                    2,
7098
                    property_bind_groups
1,994✔
7099
                        .get(effect_batch.property_key.as_ref())
3,988✔
7100
                        .unwrap(),
1,994✔
7101
                    &offsets[..],
997✔
7102
                );
7103
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
2,991✔
7104

7105
                // Dispatch init job
7106
                match effect_batch.spawn_info {
997✔
7107
                    // Indirect dispatch via GPU spawn events
7108
                    BatchSpawnInfo::GpuSpawner {
7109
                        init_indirect_dispatch_index,
×
7110
                        ..
7111
                    } => {
7112
                        assert!(effect_batch
×
7113
                            .layout_flags
×
7114
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
7115

7116
                        // Note: the indirect offset of a dispatch workgroup only needs
7117
                        // 4-byte alignment
7118
                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
×
7119
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
7120

7121
                        trace!(
×
7122
                            "record commands for indirect init pipeline of effect {:?} \
×
7123
                                init_indirect_dispatch_index={} \
×
7124
                                indirect_offset={} \
×
7125
                                spawner_base={} \
×
7126
                                spawner_offset={} \
×
7127
                                property_key={:?}...",
×
7128
                            effect_batch.handle,
7129
                            init_indirect_dispatch_index,
7130
                            indirect_offset,
7131
                            spawner_base,
7132
                            spawner_offset,
7133
                            effect_batch.property_key,
7134
                        );
7135

7136
                        compute_pass.dispatch_workgroups_indirect(
×
7137
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
7138
                            indirect_offset,
×
7139
                        );
7140
                    }
7141

7142
                    // Direct dispatch via CPU spawn count
7143
                    BatchSpawnInfo::CpuSpawner {
7144
                        total_spawn_count: spawn_count,
997✔
7145
                    } => {
7146
                        assert!(!effect_batch
7147
                            .layout_flags
7148
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
7149

7150
                        const WORKGROUP_SIZE: u32 = 64;
7151
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
997✔
7152

7153
                        trace!(
7154
                            "record commands for init pipeline of effect {:?} \
997✔
7155
                                (spawn {} particles => {} workgroups) spawner_base={} \
997✔
7156
                                spawner_offset={} \
997✔
7157
                                property_key={:?}...",
997✔
7158
                            effect_batch.handle,
7159
                            spawn_count,
7160
                            workgroup_count,
7161
                            spawner_base,
7162
                            spawner_offset,
7163
                            effect_batch.property_key,
7164
                        );
7165

7166
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
7167
                    }
7168
                }
7169

7170
                trace!("init compute dispatched");
1,994✔
7171
            }
7172
        }
7173

7174
        // Compute indirect dispatch pass
7175
        if effects_meta.spawner_buffer.buffer().is_some()
1,012✔
7176
            && !effects_meta.spawner_buffer.is_empty()
1,012✔
7177
            && effects_meta.indirect_metadata_bind_group.is_some()
1,012✔
7178
            && effects_meta.indirect_sim_params_bind_group.is_some()
2,024✔
7179
        {
7180
            // Only start a compute pass if there's an effect; makes things clearer in
7181
            // debugger.
7182
            let mut compute_pass =
1,012✔
7183
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
5,060✔
7184

7185
            // Dispatch indirect dispatch compute job
7186
            trace!("record commands for indirect dispatch pipeline...");
2,024✔
7187

7188
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
2,024✔
7189
            if has_gpu_spawn_events {
1,012✔
7190
                if let Some(indirect_child_info_buffer_bind_group) =
×
7191
                    event_cache.indirect_child_info_buffer_bind_group()
×
7192
                {
7193
                    assert!(has_gpu_spawn_events);
7194
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
7195
                } else {
7196
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
7197
                    // render_context
7198
                    //     .command_encoder()
7199
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
7200
                    // FIXME - Bevy doesn't allow returning custom errors here...
7201
                    return Ok(());
×
7202
                }
7203
            }
7204

7205
            if compute_pass
1,012✔
7206
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
7207
                .is_err()
7208
            {
7209
                // FIXME - Bevy doesn't allow returning custom errors here...
7210
                return Ok(());
×
7211
            }
7212

7213
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
7214
            // the size exluding gaps!");
7215
            const WORKGROUP_SIZE: u32 = 64;
7216
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
7217
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
7218
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
7219

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

7249
        // Compute update pass
7250
        {
7251
            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
2,024✔
7252
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
7253
                render_context
×
7254
                    .command_encoder()
7255
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
7256
                // FIXME - Bevy doesn't allow returning custom errors here...
7257
                return Ok(());
×
7258
            };
7259

7260
            let mut compute_pass =
7261
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
7262

7263
            // Bind group simparams@0 is common to everything, only set once per update pass
7264
            compute_pass.set_bind_group(
7265
                0,
7266
                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
7267
                &[],
7268
            );
7269

7270
            // Dispatch update compute jobs
7271
            for effect_batch in sorted_effect_batches.iter() {
1,012✔
7272
                // Fetch bind group particle@1
7273
                let Some(particle_bind_group) =
1,012✔
7274
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
2,024✔
7275
                else {
7276
                    error!(
×
NEW
7277
                        "Failed to find update particle@1 bind group for slab #{}",
×
NEW
7278
                        effect_batch.slab_id.index()
×
7279
                    );
NEW
7280
                    compute_pass.insert_debug_marker("ERROR:MissingParticleSimBindGroup");
×
UNCOV
7281
                    continue;
×
7282
                };
7283

7284
                // Fetch bind group metadata@3
7285
                let Some(metadata_bind_group) = effect_bind_groups
1,012✔
7286
                    .update_metadata_bind_groups
7287
                    .get(&effect_batch.slab_id)
7288
                else {
7289
                    error!(
×
NEW
7290
                        "Failed to find update metadata@3 bind group for slab #{}",
×
NEW
7291
                        effect_batch.slab_id.index()
×
7292
                    );
NEW
7293
                    compute_pass.insert_debug_marker("ERROR:MissingMetadataBindGroup");
×
UNCOV
7294
                    continue;
×
7295
                };
7296

7297
                // Fetch compute pipeline
NEW
7298
                if let Err(err) = compute_pass
×
7299
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
7300
                {
7301
                    compute_pass.insert_debug_marker(&format!(
7302
                        "ERROR:FailedToSetCachedUpdatePipeline:{:?}",
7303
                        err
7304
                    ));
7305
                    continue;
7306
                }
7307

7308
                // Compute dynamic offsets
7309
                let spawner_index = effect_batch.spawner_base;
2,024✔
7310
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
3,036✔
7311
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
3,036✔
7312
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
1,012✔
7313
                let property_offset = effect_batch.property_offset;
7314

7315
                trace!(
7316
                    "record commands for update pipeline of effect {:?} spawner_base={}",
1,012✔
7317
                    effect_batch.handle,
7318
                    spawner_index,
7319
                );
7320

7321
                // Setup update pass
7322
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
7323
                let offsets = if let Some(property_offset) = property_offset {
13✔
7324
                    vec![spawner_offset, property_offset]
7325
                } else {
7326
                    vec![spawner_offset]
1,998✔
7327
                };
7328
                compute_pass.set_bind_group(
7329
                    2,
7330
                    property_bind_groups
7331
                        .get(effect_batch.property_key.as_ref())
7332
                        .unwrap(),
7333
                    &offsets[..],
7334
                );
7335
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
7336

7337
                // Dispatch update job
7338
                let dispatch_indirect_offset = effect_batch
7339
                    .dispatch_buffer_indices
7340
                    .update_dispatch_indirect_buffer_row_index
7341
                    * 12;
7342
                trace!(
7343
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
1,012✔
7344
                    indirect_buffer,
7345
                    dispatch_indirect_offset,
7346
                );
7347
                compute_pass
7348
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
7349

7350
                trace!("update compute dispatched");
1,012✔
7351
            }
7352
        }
7353

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

7368
        // Compute sort pass
7369
        {
7370
            let mut compute_pass =
7371
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
7372

7373
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
7374
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
7375

7376
            // Loop on batches and find those which need sorting
7377
            for effect_batch in sorted_effect_batches.iter() {
1,012✔
7378
                trace!("Processing effect batch for sorting...");
2,024✔
7379
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
1,012✔
7380
                    continue;
1,012✔
7381
                }
7382
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
7383
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
7384

NEW
7385
                let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
×
7386
                    warn!("Missing sort-fill effect buffer.");
×
7387
                    // render_context
7388
                    //     .command_encoder()
7389
                    //     .insert_debug_marker("ERROR:MissingEffectBatchBuffer");
UNCOV
7390
                    continue;
×
7391
                };
7392

7393
                let indirect_dispatch_index = *effect_batch
7394
                    .sort_fill_indirect_dispatch_index
7395
                    .as_ref()
7396
                    .unwrap();
7397
                let indirect_offset =
7398
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
7399

7400
                // Fill the sort buffer with the key-value pairs to sort
7401
                {
7402
                    compute_pass.push_debug_group("hanabi:sort_fill");
7403

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

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

7451
                    compute_pass
7452
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7453
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
7454

7455
                    compute_pass.pop_debug_group();
7456
                }
7457

7458
                // Do the actual sort
7459
                {
7460
                    compute_pass.push_debug_group("hanabi:sort");
7461

7462
                    if compute_pass
7463
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
7464
                        .is_err()
7465
                    {
NEW
7466
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortPipeline");
×
UNCOV
7467
                        compute_pass.pop_debug_group();
×
7468
                        // FIXME - Bevy doesn't allow returning custom errors here...
7469
                        return Ok(());
×
7470
                    }
7471

7472
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
7473
                    compute_pass
7474
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7475
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7476

7477
                    compute_pass.pop_debug_group();
7478
                }
7479

7480
                // Copy the sorted particle indices back into the indirect index buffer, where
7481
                // the render pass will read them.
7482
                {
7483
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7484

7485
                    // Fetch compute pipeline
7486
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7487
                    if compute_pass
7488
                        .set_cached_compute_pipeline(pipeline_id)
7489
                        .is_err()
7490
                    {
NEW
7491
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortCopyPipeline");
×
7492
                        compute_pass.pop_debug_group();
7493
                        // FIXME - Bevy doesn't allow returning custom errors here...
7494
                        return Ok(());
7495
                    }
7496

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

7517
                    compute_pass
7518
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7519
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7520

7521
                    compute_pass.pop_debug_group();
7522
                }
7523
            }
7524
        }
7525

7526
        Ok(())
1,012✔
7527
    }
7528
}
7529

7530
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7531
    fn from(layout_flags: LayoutFlags) -> Self {
3,036✔
7532
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
6,072✔
7533
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7534
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
3,036✔
7535
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7536
        } else {
7537
            ParticleRenderAlphaMaskPipelineKey::Blend
3,036✔
7538
        }
7539
    }
7540
}
7541

7542
#[cfg(test)]
7543
mod tests {
7544
    use super::*;
7545

7546
    #[test]
7547
    fn layout_flags() {
7548
        let flags = LayoutFlags::default();
7549
        assert_eq!(flags, LayoutFlags::NONE);
7550
    }
7551

7552
    #[cfg(feature = "gpu_tests")]
7553
    #[test]
7554
    fn gpu_limits() {
7555
        use crate::test_utils::MockRenderer;
7556

7557
        let renderer = MockRenderer::new();
7558
        let device = renderer.device();
7559
        let limits = GpuLimits::from_device(&device);
7560

7561
        // assert!(limits.storage_buffer_align().get() >= 1);
7562
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7563
    }
7564

7565
    #[cfg(feature = "gpu_tests")]
7566
    #[test]
7567
    fn gpu_ops_ifda() {
7568
        use crate::test_utils::MockRenderer;
7569

7570
        let renderer = MockRenderer::new();
7571
        let device = renderer.device();
7572
        let render_queue = renderer.queue();
7573

7574
        let mut world = World::new();
7575
        world.insert_resource(device.clone());
7576
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7577

7578
        let src_buffer = device.create_buffer(&BufferDescriptor {
7579
            label: None,
7580
            size: 256,
7581
            usage: BufferUsages::STORAGE,
7582
            mapped_at_creation: false,
7583
        });
7584
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7585
            label: None,
7586
            size: 256,
7587
            usage: BufferUsages::STORAGE,
7588
            mapped_at_creation: false,
7589
        });
7590

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

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

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