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

djeedai / bevy_hanabi / 21562713316

01 Feb 2026 12:16PM UTC coverage: 58.351% (-8.1%) from 66.442%
21562713316

Pull #521

github

web-flow
Merge 104a2fa15 into 305172905
Pull Request #521: Update to Bevy v0.18

93 of 170 new or added lines in 6 files covered. (54.71%)

968 existing lines in 17 files now uncovered.

4954 of 8490 relevant lines covered (58.35%)

190.52 hits per line

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

47.47
/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
        change_detection::Tick,
29
        prelude::*,
30
        system::{lifetimeless::*, SystemParam, SystemState},
31
    },
32
    log::trace,
33
    mesh::MeshVertexBufferLayoutRef,
34
    platform::collections::HashMap,
35
    prelude::*,
36
    render::{
37
        mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo},
38
        render_asset::RenderAssets,
39
        render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo},
40
        render_phase::{
41
            Draw, DrawError, DrawFunctions, PhaseItemExtraIndex, SortedPhaseItem,
42
            TrackedRenderPass, ViewSortedRenderPhases,
43
        },
44
        render_resource::*,
45
        renderer::{RenderContext, RenderDevice, RenderQueue},
46
        sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity},
47
        texture::GpuImage,
48
        view::{
49
            ExtractedView, RenderVisibleEntities, ViewTarget, ViewUniform, ViewUniformOffset,
50
            ViewUniforms,
51
        },
52
        Extract, MainWorld,
53
    },
54
};
55
use bitflags::bitflags;
56
use bytemuck::{Pod, Zeroable};
57
use effect_cache::{CachedEffect, EffectSlice, SlabState};
58
use event::{CachedChildInfo, CachedEffectEvents, CachedParentInfo, GpuChildInfo};
59
use fixedbitset::FixedBitSet;
60
use gpu_buffer::GpuBuffer;
61
use naga_oil::compose::{Composer, NagaModuleDescriptor};
62

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

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

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

101
use self::batch::EffectBatch;
102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

390
impl Default for GpuDispatchIndirectArgs {
391
    fn default() -> Self {
×
392
        Self { x: 0, y: 1, z: 1 }
393
    }
394
}
395

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

414
impl Default for GpuDrawIndirectArgs {
415
    fn default() -> Self {
×
416
        Self {
417
            vertex_count: 0,
418
            instance_count: 1,
419
            first_vertex: 0,
420
            first_instance: 0,
421
        }
422
    }
423
}
424

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

444
impl Default for GpuDrawIndexedIndirectArgs {
445
    fn default() -> Self {
×
446
        Self {
447
            index_count: 0,
448
            instance_count: 1,
449
            first_index: 0,
450
            base_vertex: 0,
451
            first_instance: 0,
452
        }
453
    }
454
}
455

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

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

505
    /// Particle stride, in number of u32.
506
    pub particle_stride: u32,
507
    /// Offset from the particle start to the first sort key, in number of u32.
508
    pub sort_key_offset: u32,
509
    /// Offset from the particle start to the second sort key, in number of u32.
510
    pub sort_key2_offset: u32,
511

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

521
/// Single init fill dispatch item in an [`InitFillDispatchQueue`].
522
#[derive(Debug)]
523
pub(super) struct InitFillDispatchItem {
524
    /// Index of the source [`GpuChildInfo`] entry to read the event count from.
525
    pub global_child_index: u32,
526
    /// Index of the [`GpuDispatchIndirect`] entry to write the workgroup count
527
    /// to.
528
    pub dispatch_indirect_index: u32,
529
}
530

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

544
impl InitFillDispatchQueue {
545
    /// Clear the queue.
546
    #[inline]
547
    pub fn clear(&mut self) {
330✔
548
        self.queue.clear();
660✔
549
        self.submitted_queue_index = None;
330✔
550
    }
551

552
    /// Check if the queue is empty.
553
    #[inline]
554
    pub fn is_empty(&self) -> bool {
330✔
555
        self.queue.is_empty()
660✔
556
    }
557

558
    /// Enqueue a new operation.
559
    #[inline]
560
    pub fn enqueue(&mut self, global_child_index: u32, dispatch_indirect_index: u32) {
6✔
561
        assert!(global_child_index != u32::MAX);
12✔
562
        self.queue.push(InitFillDispatchItem {
18✔
563
            global_child_index,
6✔
564
            dispatch_indirect_index,
6✔
565
        });
566
    }
567

568
    /// Submit pending operations for this frame.
569
    pub fn submit(
3✔
570
        &mut self,
571
        src_buffer: &Buffer,
572
        dst_buffer: &Buffer,
573
        gpu_buffer_operations: &mut GpuBufferOperations,
574
    ) {
575
        if self.queue.is_empty() {
6✔
576
            return;
×
577
        }
578

579
        // Sort by source. We can only batch if the destination is also contiguous, so
580
        // we can check with a linear walk if the source is already sorted.
581
        self.queue
582
            .sort_unstable_by_key(|item| item.global_child_index);
583

584
        let mut fill_queue = GpuBufferOperationQueue::new();
585

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

666
        debug_assert!(self.submitted_queue_index.is_none());
3✔
667
        if !fill_queue.operation_queue.is_empty() {
6✔
668
            self.submitted_queue_index = Some(gpu_buffer_operations.submit(fill_queue));
3✔
669
        }
670
    }
671
}
672

673
/// Compute pipeline to run the `vfx_indirect` dispatch workgroup calculation
674
/// shader.
675
#[derive(Resource)]
676
pub(crate) struct DispatchIndirectPipeline {
677
    /// Layout of bind group sim_params@0.
678
    sim_params_bind_group_layout_desc: BindGroupLayoutDescriptor,
679
    /// Layout of bind group effect_metadata@1.
680
    effect_metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
681
    /// Layout of bind group spawner@2.
682
    spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
683
    /// Layout of bind group child_infos@3.
684
    child_infos_bind_group_layout_desc: BindGroupLayoutDescriptor,
685
    /// Shader when no GPU events are used (no bind group @3).
686
    indirect_shader_noevent: Handle<Shader>,
687
    /// Shader when GPU events are used (bind group @3 present).
688
    indirect_shader_events: Handle<Shader>,
689
}
690

691
impl FromWorld for DispatchIndirectPipeline {
692
    fn from_world(world: &mut World) -> Self {
3✔
693
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
694

695
        // Copy the indirect pipeline shaders to self, because we can't access anything
696
        // else during pipeline specialization.
697
        let (indirect_shader_noevent, indirect_shader_events) = {
9✔
698
            let effects_meta = world.get_resource::<EffectsMeta>().unwrap();
15✔
699
            (
700
                effects_meta.indirect_shader_noevent.clone(),
9✔
701
                effects_meta.indirect_shader_events.clone(),
3✔
702
            )
703
        };
704

705
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
6✔
706
        let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
9✔
707
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
9✔
708

709
        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
710
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
5✔
711
        let sim_params_bind_group_layout = BindGroupLayoutDescriptor::new(
712
            "hanabi:bind_group_layout:dispatch_indirect:sim_params",
713
            &[BindGroupLayoutEntry {
3✔
714
                binding: 0,
3✔
715
                visibility: ShaderStages::COMPUTE,
3✔
716
                ty: BindingType::Buffer {
3✔
717
                    ty: BufferBindingType::Uniform,
3✔
718
                    has_dynamic_offset: false,
3✔
719
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
720
                },
721
                count: None,
3✔
722
            }],
723
        );
724

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

774
        // @group(2) @binding(0) var<storage, read_write> spawner_buffer :
775
        // array<Spawner>;
776
        let spawner_bind_group_layout = BindGroupLayoutDescriptor::new(
777
            "hanabi:bind_group_layout:dispatch_indirect:spawner@2",
778
            &[BindGroupLayoutEntry {
3✔
779
                binding: 0,
3✔
780
                visibility: ShaderStages::COMPUTE,
3✔
781
                ty: BindingType::Buffer {
3✔
782
                    ty: BufferBindingType::Storage { read_only: false },
3✔
783
                    has_dynamic_offset: false,
3✔
784
                    min_binding_size: Some(spawner_min_binding_size),
3✔
785
                },
786
                count: None,
3✔
787
            }],
788
        );
789

790
        // @group(3) @binding(0) var<storage, read_write> child_info_buffer :
791
        // ChildInfoBuffer;
792
        let child_infos_bind_group_layout = BindGroupLayoutDescriptor::new(
793
            "hanabi:bind_group_layout:dispatch_indirect:child_infos",
794
            &[BindGroupLayoutEntry {
3✔
795
                binding: 0,
3✔
796
                visibility: ShaderStages::COMPUTE,
3✔
797
                ty: BindingType::Buffer {
3✔
798
                    ty: BufferBindingType::Storage { read_only: false },
3✔
799
                    has_dynamic_offset: false,
3✔
800
                    min_binding_size: Some(GpuChildInfo::min_size()),
3✔
801
                },
802
                count: None,
3✔
803
            }],
804
        );
805

806
        Self {
807
            sim_params_bind_group_layout_desc: sim_params_bind_group_layout,
808
            effect_metadata_bind_group_layout_desc: effect_metadata_bind_group_layout,
809
            spawner_bind_group_layout_desc: spawner_bind_group_layout,
810
            child_infos_bind_group_layout_desc: child_infos_bind_group_layout,
811
            indirect_shader_noevent,
812
            indirect_shader_events,
813
        }
814
    }
815
}
816

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

827
impl SpecializedComputePipeline for DispatchIndirectPipeline {
828
    type Key = DispatchIndirectPipelineKey;
829

830
    fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
6✔
831
        trace!(
6✔
832
            "Specializing indirect pipeline (has_events={})",
833
            key.has_events
834
        );
835

836
        let mut shader_defs = Vec::with_capacity(2);
12✔
837
        // Spawner struct needs to be defined with padding, because it's bound as an
838
        // array
839
        shader_defs.push("SPAWNER_PADDING".into());
24✔
840
        if key.has_events {
9✔
841
            shader_defs.push("HAS_GPU_SPAWN_EVENTS".into());
9✔
842
        }
843

844
        let mut layout = Vec::with_capacity(4);
12✔
845
        layout.push(self.sim_params_bind_group_layout_desc.clone());
24✔
846
        layout.push(self.effect_metadata_bind_group_layout_desc.clone());
24✔
847
        layout.push(self.spawner_bind_group_layout_desc.clone());
24✔
848
        if key.has_events {
9✔
849
            layout.push(self.child_infos_bind_group_layout_desc.clone());
9✔
850
        }
851

852
        let label = format!(
12✔
853
            "hanabi:compute_pipeline:dispatch_indirect{}",
854
            if key.has_events {
6✔
855
                "_events"
3✔
856
            } else {
857
                "_noevent"
3✔
858
            }
859
        );
860

861
        ComputePipelineDescriptor {
862
            label: Some(label.into()),
6✔
863
            layout,
864
            shader: if key.has_events {
6✔
865
                self.indirect_shader_events.clone()
866
            } else {
867
                self.indirect_shader_noevent.clone()
868
            },
869
            shader_defs,
870
            entry_point: Some("main".into()),
6✔
871
            push_constant_ranges: vec![],
6✔
872
            zero_initialize_workgroup_memory: false,
873
        }
874
    }
875
}
876

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

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

926
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
927
struct QueuedOperationBindGroupKey {
928
    src_buffer: BufferId,
929
    src_binding_size: Option<NonZeroU32>,
930
    dst_buffer: BufferId,
931
    dst_binding_size: Option<NonZeroU32>,
932
}
933

934
#[derive(Debug, Clone)]
935
struct QueuedOperation {
936
    op: GpuBufferOperationType,
937
    args_index: u32,
938
    src_buffer: Buffer,
939
    src_binding_offset: u32,
940
    src_binding_size: Option<NonZeroU32>,
941
    dst_buffer: Buffer,
942
    dst_binding_offset: u32,
943
    dst_binding_size: Option<NonZeroU32>,
944
}
945

946
impl From<&QueuedOperation> for QueuedOperationBindGroupKey {
947
    fn from(value: &QueuedOperation) -> Self {
×
948
        Self {
949
            src_buffer: value.src_buffer.id(),
×
950
            src_binding_size: value.src_binding_size,
×
951
            dst_buffer: value.dst_buffer.id(),
×
952
            dst_binding_size: value.dst_binding_size,
×
953
        }
954
    }
955
}
956

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

970
impl GpuBufferOperationQueue {
971
    /// Create a new empty queue.
972
    pub fn new() -> Self {
333✔
973
        Self {
974
            args: vec![],
333✔
975
            operation_queue: vec![],
333✔
976
        }
977
    }
978

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

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

1028
    /// Bind groups for the submitted operations.
1029
    bind_groups: HashMap<QueuedOperationBindGroupKey, BindGroup>,
1030

1031
    /// Submitted queues for this frame.
1032
    queues: Vec<Vec<QueuedOperation>>,
1033
}
1034

1035
impl FromWorld for GpuBufferOperations {
1036
    fn from_world(world: &mut World) -> Self {
4✔
1037
        let render_device = world.get_resource::<RenderDevice>().unwrap();
16✔
1038
        let align = render_device.limits().min_uniform_buffer_offset_alignment;
8✔
1039
        Self::new(align)
8✔
1040
    }
1041
}
1042

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

1057
    /// Clear the queue and begin recording operations for a new frame.
1058
    pub fn begin_frame(&mut self) {
333✔
1059
        self.args_buffer.clear();
666✔
1060
        self.bind_groups.clear(); // for now; might consider caching frame-to-frame
666✔
1061
        self.queues.clear();
666✔
1062
    }
1063

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

1079
    /// Finish recording operations for this frame, and schedule buffer writes
1080
    /// to GPU.
1081
    pub fn end_frame(&mut self, device: &RenderDevice, render_queue: &RenderQueue) {
333✔
1082
        assert_eq!(
333✔
1083
            self.args_buffer.len(),
666✔
1084
            self.queues.iter().fold(0, |len, q| len + q.len())
675✔
1085
        );
1086

1087
        // Upload to GPU buffer
1088
        self.args_buffer.write_buffer(device, render_queue);
1,332✔
1089
    }
1090

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

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

1188
        if queue.is_empty() {
×
1189
            return;
×
1190
        }
1191

UNCOV
1192
        let mut compute_pass =
×
UNCOV
1193
            render_context
×
1194
                .command_encoder()
UNCOV
1195
                .begin_compute_pass(&ComputePassDescriptor {
×
UNCOV
1196
                    label: compute_pass_label,
×
UNCOV
1197
                    timestamp_writes: None,
×
1198
                });
1199

UNCOV
1200
        let mut prev_op = None;
×
1201
        for qop in queue {
×
1202
            trace!("qop={:?}", qop);
×
1203

1204
            if Some(qop.op) != prev_op {
×
1205
                compute_pass.set_pipeline(utils_pipeline.get_pipeline(qop.op));
×
1206
                prev_op = Some(qop.op);
×
1207
            }
1208

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

1230
            // Dispatch the operations for this buffer
1231
            const WORKGROUP_SIZE: u32 = 64;
UNCOV
1232
            let num_ops = 1u32; // TODO - batching!
×
UNCOV
1233
            let workgroup_count = num_ops.div_ceil(WORKGROUP_SIZE);
×
UNCOV
1234
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
UNCOV
1235
            trace!(
×
1236
                "-> fill dispatch compute dispatched: num_ops={} workgroup_count={}",
1237
                num_ops,
1238
                workgroup_count
1239
            );
1240
        }
1241
    }
1242
}
1243

1244
/// Compute pipeline to run the `vfx_utils` shader.
1245
#[derive(Resource)]
1246
pub(crate) struct UtilsPipeline {
1247
    #[allow(dead_code)]
1248
    bind_group_layout: BindGroupLayout,
1249
    bind_group_layout_dyn: BindGroupLayout,
1250
    bind_group_layout_no_src: BindGroupLayout,
1251
    pipelines: [ComputePipeline; 4],
1252
}
1253

1254
impl FromWorld for UtilsPipeline {
1255
    fn from_world(world: &mut World) -> Self {
3✔
1256
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1257

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

1294
        let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1295
            label: Some("hanabi:pipeline_layout:utils"),
6✔
1296
            bind_group_layouts: &[&bind_group_layout],
3✔
1297
            push_constant_ranges: &[],
3✔
1298
        });
1299

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

1336
        let pipeline_layout_dyn = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
12✔
1337
            label: Some("hanabi:pipeline_layout:utils_dyn"),
6✔
1338
            bind_group_layouts: &[&bind_group_layout_dyn],
3✔
1339
            push_constant_ranges: &[],
3✔
1340
        });
1341

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

1368
        let pipeline_layout_no_src =
3✔
1369
            render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
9✔
1370
                label: Some("hanabi:pipeline_layout:utils_no_src"),
6✔
1371
                bind_group_layouts: &[&bind_group_layout_no_src],
3✔
1372
                push_constant_ranges: &[],
3✔
1373
            });
1374

1375
        let shader_code = include_str!("vfx_utils.wgsl");
6✔
1376

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

1382
            let shader_defs = default();
6✔
1383

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

1398
        debug!("Create utils shader module:\n{}", shader_code);
3✔
1399
        #[allow(unsafe_code)]
1400
        let shader_module = unsafe {
1401
            render_device.create_shader_module(ShaderModuleDescriptor {
9✔
1402
                label: Some("hanabi:shader:utils"),
3✔
1403
                source: shader_source,
3✔
1404
            })
1405
        };
1406

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

1455
        Self {
1456
            bind_group_layout,
1457
            bind_group_layout_dyn,
1458
            bind_group_layout_no_src,
1459
            pipelines: [
3✔
1460
                zero_pipeline,
1461
                copy_pipeline,
1462
                fill_dispatch_args_pipeline,
1463
                fill_dispatch_args_self_pipeline,
1464
            ],
1465
        }
1466
    }
1467
}
1468

1469
impl UtilsPipeline {
1470
    fn get_pipeline(&self, op: GpuBufferOperationType) -> &ComputePipeline {
×
1471
        match op {
×
1472
            GpuBufferOperationType::Zero => &self.pipelines[0],
×
1473
            GpuBufferOperationType::Copy => &self.pipelines[1],
×
1474
            GpuBufferOperationType::FillDispatchArgs => &self.pipelines[2],
×
1475
            GpuBufferOperationType::FillDispatchArgsSelf => &self.pipelines[3],
×
1476
        }
1477
    }
1478

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

1498
#[derive(Resource)]
1499
pub(crate) struct ParticlesInitPipeline {
1500
    sim_params_layout_desc: BindGroupLayoutDescriptor,
1501
}
1502

1503
impl Default for ParticlesInitPipeline {
1504
    fn default() -> Self {
3✔
1505
        let sim_params_layout_desc = BindGroupLayoutDescriptor::new(
1506
            "hanabi:bind_group_layout:vfx_init:sim_params@0",
1507
            // @group(0) @binding(0) var<uniform> sim_params: SimParams;
1508
            &[BindGroupLayoutEntry {
3✔
1509
                binding: 0,
3✔
1510
                visibility: ShaderStages::COMPUTE,
3✔
1511
                ty: BindingType::Buffer {
3✔
1512
                    ty: BufferBindingType::Uniform,
3✔
1513
                    has_dynamic_offset: false,
3✔
1514
                    min_binding_size: Some(GpuSimParams::min_size()),
3✔
1515
                },
1516
                count: None,
3✔
1517
            }],
1518
        );
1519

1520
        Self {
1521
            sim_params_layout_desc,
1522
        }
1523
    }
1524
}
1525

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

1536
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1537
pub(crate) struct ParticleInitPipelineKey {
1538
    /// Compute shader, with snippets applied, but not preprocessed yet.
1539
    shader: Handle<Shader>,
1540
    /// Minimum binding size in bytes for the particle layout buffer.
1541
    particle_layout_min_binding_size: NonZeroU32,
1542
    /// Minimum binding size in bytes for the particle layout buffer of the
1543
    /// parent effect, if any.
1544
    /// Key: READ_PARENT_PARTICLE
1545
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1546
    /// Pipeline flags.
1547
    flags: ParticleInitPipelineKeyFlags,
1548
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1549
    particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
1550
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1551
    spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
1552
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1553
    metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
1554
}
1555

1556
impl SpecializedComputePipeline for ParticlesInitPipeline {
1557
    type Key = ParticleInitPipelineKey;
1558

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

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

1591
        let label = format!("hanabi:pipeline:init_{hash:016X}");
2✔
UNCOV
1592
        trace!(
×
1593
            "-> creating pipeline '{}' with shader defs:{}",
1594
            label,
1595
            shader_defs
2✔
1596
                .iter()
2✔
1597
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
1598
        );
1599

1600
        ComputePipelineDescriptor {
UNCOV
1601
            label: Some(label.into()),
×
UNCOV
1602
            layout: vec![
×
1603
                self.sim_params_layout_desc.clone(),
1604
                key.particle_bind_group_layout_desc.clone(),
1605
                key.spawner_bind_group_layout_desc.clone(),
1606
                key.metadata_bind_group_layout_desc.clone(),
1607
            ],
UNCOV
1608
            shader: key.shader,
×
1609
            shader_defs,
UNCOV
1610
            entry_point: Some("main".into()),
×
UNCOV
1611
            push_constant_ranges: vec![],
×
1612
            zero_initialize_workgroup_memory: false,
1613
        }
1614
    }
1615
}
1616

1617
#[derive(Resource)]
1618
pub(crate) struct ParticlesUpdatePipeline {
1619
    sim_params_layout_desc: BindGroupLayoutDescriptor,
1620
}
1621

1622
impl Default for ParticlesUpdatePipeline {
1623
    fn default() -> Self {
3✔
1624
        trace!("GpuSimParams: min_size={}", GpuSimParams::min_size());
5✔
1625
        let sim_params_layout_desc = BindGroupLayoutDescriptor::new(
1626
            "hanabi:bind_group_layout:vfx_update:sim_params@0",
1627
            &[
3✔
1628
                // @group(0) @binding(0) var<uniform> sim_params : SimParams;
1629
                BindGroupLayoutEntry {
6✔
1630
                    binding: 0,
6✔
1631
                    visibility: ShaderStages::COMPUTE,
6✔
1632
                    ty: BindingType::Buffer {
6✔
1633
                        ty: BufferBindingType::Uniform,
6✔
1634
                        has_dynamic_offset: false,
6✔
1635
                        min_binding_size: Some(GpuSimParams::min_size()),
6✔
1636
                    },
1637
                    count: None,
6✔
1638
                },
1639
                // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
1640
                // array<DrawIndexedIndirectArgs>;
1641
                BindGroupLayoutEntry {
3✔
1642
                    binding: 1,
3✔
1643
                    visibility: ShaderStages::COMPUTE,
3✔
1644
                    ty: BindingType::Buffer {
3✔
1645
                        ty: BufferBindingType::Storage { read_only: false },
3✔
1646
                        has_dynamic_offset: false,
3✔
1647
                        min_binding_size: Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
3✔
1648
                    },
1649
                    count: None,
3✔
1650
                },
1651
            ],
1652
        );
1653

1654
        Self {
1655
            sim_params_layout_desc,
1656
        }
1657
    }
1658
}
1659

1660
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1661
pub(crate) struct ParticleUpdatePipelineKey {
1662
    /// Compute shader, with snippets applied, but not preprocessed yet.
1663
    shader: Handle<Shader>,
1664
    /// Particle layout.
1665
    particle_layout: ParticleLayout,
1666
    /// Minimum binding size in bytes for the particle layout buffer of the
1667
    /// parent effect, if any.
1668
    /// Key: READ_PARENT_PARTICLE
1669
    parent_particle_layout_min_binding_size: Option<NonZeroU32>,
1670
    /// Key: EMITS_GPU_SPAWN_EVENTS
1671
    num_event_buffers: u32,
1672
    /// Layout of the particle@1 bind group this pipeline was specialized with.
1673
    particle_bind_group_layout_desc: BindGroupLayoutDescriptor,
1674
    /// Layout of the spawner@2 bind group this pipeline was specialized with.
1675
    spawner_bind_group_layout_desc: BindGroupLayoutDescriptor,
1676
    /// Layout of the metadata@3 bind group this pipeline was specialized with.
1677
    metadata_bind_group_layout_desc: BindGroupLayoutDescriptor,
1678
}
1679

1680
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1681
    type Key = ParticleUpdatePipelineKey;
1682

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

1688
        let mut shader_defs = Vec::with_capacity(6);
4✔
1689
        shader_defs.push("EM_MAX_SPAWN_ATOMIC".into());
8✔
1690
        // ChildInfo needs atomic event_count because all threads append to the event
1691
        // buffer(s) in parallel.
1692
        shader_defs.push("CHILD_INFO_EVENT_COUNT_IS_ATOMIC".into());
8✔
1693
        if key.particle_layout.contains(Attribute::PREV) {
4✔
1694
            shader_defs.push("ATTRIBUTE_PREV".into());
×
1695
        }
1696
        if key.particle_layout.contains(Attribute::NEXT) {
4✔
1697
            shader_defs.push("ATTRIBUTE_NEXT".into());
×
1698
        }
1699
        if key.parent_particle_layout_min_binding_size.is_some() {
4✔
1700
            shader_defs.push("READ_PARENT_PARTICLE".into());
×
1701
        }
1702
        if key.num_event_buffers > 0 {
2✔
1703
            shader_defs.push("EMITS_GPU_SPAWN_EVENTS".into());
×
1704
        }
1705

1706
        let hash = calc_func_id(&key);
6✔
1707
        let label = format!("hanabi:pipeline:update_{hash:016X}");
4✔
1708
        trace!(
2✔
1709
            "-> creating pipeline '{}' with shader defs:{}",
1710
            label,
1711
            shader_defs
2✔
1712
                .iter()
2✔
1713
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
12✔
1714
        );
1715

1716
        ComputePipelineDescriptor {
1717
            label: Some(label.into()),
4✔
1718
            layout: vec![
4✔
1719
                self.sim_params_layout_desc.clone(),
1720
                key.particle_bind_group_layout_desc.clone(),
1721
                key.spawner_bind_group_layout_desc.clone(),
1722
                key.metadata_bind_group_layout_desc.clone(),
1723
            ],
1724
            shader: key.shader,
4✔
1725
            shader_defs,
1726
            entry_point: Some("main".into()),
2✔
1727
            push_constant_ranges: Vec::new(),
2✔
1728
            zero_initialize_workgroup_memory: false,
1729
        }
1730
    }
1731
}
1732

1733
#[derive(Resource)]
1734
pub(crate) struct ParticlesRenderPipeline {
1735
    render_device: RenderDevice,
1736
    view_layout_desc: BindGroupLayoutDescriptor,
1737
    material_layout_descs: HashMap<TextureLayout, BindGroupLayoutDescriptor>,
1738
}
1739

1740
impl ParticlesRenderPipeline {
1741
    /// Cache a material, creating its bind group layout based on the texture
1742
    /// layout.
1743
    pub fn cache_material(&mut self, layout: &TextureLayout) {
312✔
1744
        if layout.layout.is_empty() {
624✔
1745
            return;
312✔
1746
        }
1747

1748
        // FIXME - no current stable API to insert an entry into a HashMap only if it
1749
        // doesn't exist, and without having to build a key (as opposed to a reference).
1750
        // So do 2 lookups instead, to avoid having to clone the layout if it's already
1751
        // cached (which should be the common case).
NEW
1752
        if self.material_layout_descs.contains_key(layout) {
×
1753
            return;
×
1754
        }
1755

UNCOV
1756
        let mut entries = Vec::with_capacity(layout.layout.len() * 2);
×
UNCOV
1757
        let mut index = 0;
×
1758
        for _slot in &layout.layout {
×
UNCOV
1759
            entries.push(BindGroupLayoutEntry {
×
UNCOV
1760
                binding: index,
×
UNCOV
1761
                visibility: ShaderStages::FRAGMENT,
×
UNCOV
1762
                ty: BindingType::Texture {
×
UNCOV
1763
                    multisampled: false,
×
UNCOV
1764
                    sample_type: TextureSampleType::Float { filterable: true },
×
UNCOV
1765
                    view_dimension: TextureViewDimension::D2,
×
1766
                },
UNCOV
1767
                count: None,
×
1768
            });
UNCOV
1769
            entries.push(BindGroupLayoutEntry {
×
UNCOV
1770
                binding: index + 1,
×
UNCOV
1771
                visibility: ShaderStages::FRAGMENT,
×
UNCOV
1772
                ty: BindingType::Sampler(SamplerBindingType::Filtering),
×
UNCOV
1773
                count: None,
×
1774
            });
UNCOV
1775
            index += 2;
×
1776
        }
UNCOV
1777
        debug!(
×
1778
            "Creating material bind group with {} entries [{:?}] for layout {:?}",
1779
            entries.len(),
×
1780
            entries,
1781
            layout
1782
        );
NEW
1783
        let material_bind_group_layout_desc =
×
NEW
1784
            BindGroupLayoutDescriptor::new("hanabi:material_layout_render", &entries[..]);
×
NEW
1785
        self.material_layout_descs
×
NEW
1786
            .insert(layout.clone(), material_bind_group_layout_desc);
×
1787
    }
1788

1789
    /// Retrieve a bind group layout for a cached material.
1790
    pub fn get_material(&self, layout: &TextureLayout) -> Option<&BindGroupLayoutDescriptor> {
2✔
1791
        // Prevent a hash and lookup for the trivial case of an empty layout
1792
        if layout.layout.is_empty() {
4✔
1793
            return None;
2✔
1794
        }
1795

NEW
1796
        self.material_layout_descs.get(layout)
×
1797
    }
1798
}
1799

1800
impl FromWorld for ParticlesRenderPipeline {
1801
    fn from_world(world: &mut World) -> Self {
3✔
1802
        let render_device = world.get_resource::<RenderDevice>().unwrap();
12✔
1803

1804
        let view_layout_desc = BindGroupLayoutDescriptor::new(
1805
            "hanabi:bind_group_layout:render:view@0",
1806
            &[
3✔
1807
                // @group(0) @binding(0) var<uniform> view: View;
1808
                BindGroupLayoutEntry {
6✔
1809
                    binding: 0,
6✔
1810
                    visibility: ShaderStages::VERTEX_FRAGMENT,
6✔
1811
                    ty: BindingType::Buffer {
6✔
1812
                        ty: BufferBindingType::Uniform,
6✔
1813
                        has_dynamic_offset: true,
6✔
1814
                        min_binding_size: Some(ViewUniform::min_size()),
6✔
1815
                    },
1816
                    count: None,
6✔
1817
                },
1818
                // @group(0) @binding(1) var<uniform> sim_params : SimParams;
1819
                BindGroupLayoutEntry {
3✔
1820
                    binding: 1,
3✔
1821
                    visibility: ShaderStages::VERTEX_FRAGMENT,
3✔
1822
                    ty: BindingType::Buffer {
3✔
1823
                        ty: BufferBindingType::Uniform,
3✔
1824
                        has_dynamic_offset: false,
3✔
1825
                        min_binding_size: Some(GpuSimParams::min_size()),
3✔
1826
                    },
1827
                    count: None,
3✔
1828
                },
1829
            ],
1830
        );
1831

1832
        Self {
1833
            render_device: render_device.clone(),
9✔
1834
            view_layout_desc,
1835
            material_layout_descs: default(),
3✔
1836
        }
1837
    }
1838
}
1839

1840
#[cfg(all(feature = "2d", feature = "3d"))]
1841
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1842
enum PipelineMode {
1843
    Camera2d,
1844
    Camera3d,
1845
}
1846

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

1893
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, Debug)]
1894
pub(crate) enum ParticleRenderAlphaMaskPipelineKey {
1895
    #[default]
1896
    Blend,
1897
    /// Key: USE_ALPHA_MASK
1898
    /// The effect is rendered with alpha masking.
1899
    AlphaMask,
1900
    /// Key: OPAQUE
1901
    /// The effect is rendered fully-opaquely.
1902
    Opaque,
1903
}
1904

1905
impl Default for ParticleRenderPipelineKey {
1906
    fn default() -> Self {
×
1907
        Self {
1908
            shader: Handle::default(),
×
1909
            particle_layout: ParticleLayout::empty(),
×
1910
            mesh_layout: None,
1911
            texture_layout: default(),
×
1912
            local_space_simulation: false,
1913
            alpha_mask: default(),
×
1914
            alpha_mode: AlphaMode::Blend,
1915
            flipbook: false,
1916
            needs_uv: false,
1917
            needs_normal: false,
1918
            needs_particle_fragment: false,
1919
            ribbons: false,
1920
            #[cfg(all(feature = "2d", feature = "3d"))]
1921
            pipeline_mode: PipelineMode::Camera3d,
1922
            msaa_samples: Msaa::default().samples(),
×
1923
            hdr: false,
1924
        }
1925
    }
1926
}
1927

1928
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1929
    type Key = ParticleRenderPipelineKey;
1930

1931
    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
2✔
1932
        trace!("Specializing render pipeline for key: {key:?}");
2✔
1933

1934
        trace!("Creating layout for bind group particle@1 of render pass");
2✔
1935
        let alignment = self
4✔
1936
            .render_device
2✔
1937
            .limits()
2✔
1938
            .min_storage_buffer_offset_alignment;
2✔
1939
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(alignment);
6✔
1940
        let entries = [
4✔
1941
            // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
1942
            BindGroupLayoutEntry {
4✔
1943
                binding: 0,
4✔
1944
                visibility: ShaderStages::VERTEX_FRAGMENT,
4✔
1945
                ty: BindingType::Buffer {
4✔
1946
                    ty: BufferBindingType::Storage { read_only: true },
6✔
1947
                    has_dynamic_offset: false,
4✔
1948
                    min_binding_size: Some(key.particle_layout.min_binding_size()),
4✔
1949
                },
1950
                count: None,
4✔
1951
            },
1952
            // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
1953
            BindGroupLayoutEntry {
4✔
1954
                binding: 1,
4✔
1955
                visibility: ShaderStages::VERTEX,
4✔
1956
                ty: BindingType::Buffer {
4✔
1957
                    ty: BufferBindingType::Storage { read_only: true },
6✔
1958
                    has_dynamic_offset: false,
4✔
1959
                    min_binding_size: Some(NonZeroU64::new(INDIRECT_INDEX_SIZE as u64).unwrap()),
6✔
1960
                },
1961
                count: None,
4✔
1962
            },
1963
            // @group(1) @binding(2) var<storage, read> spawner : Spawner;
1964
            BindGroupLayoutEntry {
2✔
1965
                binding: 2,
2✔
1966
                visibility: ShaderStages::VERTEX,
2✔
1967
                ty: BindingType::Buffer {
2✔
1968
                    ty: BufferBindingType::Storage { read_only: true },
2✔
1969
                    has_dynamic_offset: true,
2✔
1970
                    min_binding_size: Some(spawner_min_binding_size),
2✔
1971
                },
1972
                count: None,
2✔
1973
            },
1974
        ];
1975
        let particle_bind_group_layout_desc = BindGroupLayoutDescriptor::new(
1976
            "hanabi:bind_group_layout:render:particle@1",
1977
            &entries[..],
2✔
1978
        );
1979

1980
        let mut layout = vec![
4✔
1981
            self.view_layout_desc.clone(),
4✔
1982
            particle_bind_group_layout_desc,
2✔
1983
        ];
1984
        let mut shader_defs = vec![];
4✔
1985

1986
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
10✔
1987
            mesh_layout
4✔
1988
                .0
4✔
1989
                .get_layout(&[
4✔
1990
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
6✔
1991
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
6✔
1992
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
2✔
1993
                ])
1994
                .ok()
2✔
1995
        });
1996

1997
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
4✔
UNCOV
1998
            layout.push(material_bind_group_layout.clone());
×
1999
        }
2000

2001
        // Key: LOCAL_SPACE_SIMULATION
2002
        if key.local_space_simulation {
2✔
2003
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2004
        }
2005

2006
        match key.alpha_mask {
2✔
2007
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
2✔
2008
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2009
                // Key: USE_ALPHA_MASK
2010
                shader_defs.push("USE_ALPHA_MASK".into())
×
2011
            }
2012
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2013
                // Key: OPAQUE
2014
                shader_defs.push("OPAQUE".into())
×
2015
            }
2016
        }
2017

2018
        // Key: FLIPBOOK
2019
        if key.flipbook {
2✔
2020
            shader_defs.push("FLIPBOOK".into());
×
2021
        }
2022

2023
        // Key: NEEDS_UV
2024
        if key.needs_uv {
2✔
2025
            shader_defs.push("NEEDS_UV".into());
×
2026
        }
2027

2028
        // Key: NEEDS_NORMAL
2029
        if key.needs_normal {
2✔
2030
            shader_defs.push("NEEDS_NORMAL".into());
×
2031
        }
2032

2033
        if key.needs_particle_fragment {
2✔
2034
            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
×
2035
        }
2036

2037
        // Key: RIBBONS
2038
        if key.ribbons {
2✔
2039
            shader_defs.push("RIBBONS".into());
×
2040
        }
2041

2042
        #[cfg(feature = "2d")]
2043
        let depth_stencil_2d = DepthStencilState {
2044
            format: CORE_2D_DEPTH_FORMAT,
2045
            // Use depth buffer with alpha-masked particles, not with transparent ones
2046
            depth_write_enabled: false, // TODO - opaque/alphamask 2d
2047
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2048
            depth_compare: CompareFunction::GreaterEqual,
2049
            stencil: StencilState::default(),
2✔
2050
            bias: DepthBiasState::default(),
2✔
2051
        };
2052

2053
        #[cfg(feature = "3d")]
2054
        let depth_stencil_3d = DepthStencilState {
2055
            format: CORE_3D_DEPTH_FORMAT,
2056
            // Use depth buffer with alpha-masked or opaque particles, not
2057
            // with transparent ones
2058
            depth_write_enabled: matches!(
2✔
2059
                key.alpha_mask,
2060
                ParticleRenderAlphaMaskPipelineKey::AlphaMask
2061
                    | ParticleRenderAlphaMaskPipelineKey::Opaque
2062
            ),
2063
            // Bevy uses reverse-Z, so GreaterEqual really means closer
2064
            depth_compare: CompareFunction::GreaterEqual,
2065
            stencil: StencilState::default(),
2✔
2066
            bias: DepthBiasState::default(),
2✔
2067
        };
2068

2069
        #[cfg(all(feature = "2d", feature = "3d"))]
2070
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2✔
2071
        #[cfg(all(feature = "2d", feature = "3d"))]
2072
        let depth_stencil = match key.pipeline_mode {
4✔
2073
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2074
            PipelineMode::Camera3d => Some(depth_stencil_3d),
2✔
2075
        };
2076

2077
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2078
        let depth_stencil = Some(depth_stencil_2d);
2079

2080
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2081
        let depth_stencil = Some(depth_stencil_3d);
2082

2083
        let format = if key.hdr {
4✔
2084
            ViewTarget::TEXTURE_FORMAT_HDR
×
2085
        } else {
2086
            TextureFormat::bevy_default()
2✔
2087
        };
2088

2089
        let hash = calc_func_id(&key);
6✔
2090
        let label = format!("hanabi:pipeline:render_{hash:016X}");
4✔
2091
        trace!(
2✔
2092
            "-> creating pipeline '{}' with shader defs:{}",
2093
            label,
2094
            shader_defs
2✔
2095
                .iter()
2✔
2096
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
2097
        );
2098

2099
        RenderPipelineDescriptor {
2100
            label: Some(label.into()),
4✔
2101
            vertex: VertexState {
4✔
2102
                shader: key.shader.clone(),
2103
                entry_point: Some("vertex".into()),
2104
                shader_defs: shader_defs.clone(),
2105
                buffers: vec![vertex_buffer_layout.expect("Vertex buffer layout not present")],
2106
            },
2107
            fragment: Some(FragmentState {
4✔
2108
                shader: key.shader,
2109
                shader_defs,
2110
                entry_point: Some("fragment".into()),
2111
                targets: vec![Some(ColorTargetState {
2112
                    format,
2113
                    blend: Some(key.alpha_mode.into()),
2114
                    write_mask: ColorWrites::ALL,
2115
                })],
2116
            }),
2117
            layout,
2118
            primitive: PrimitiveState {
4✔
2119
                front_face: FrontFace::Ccw,
2120
                cull_mode: None,
2121
                unclipped_depth: false,
2122
                polygon_mode: PolygonMode::Fill,
2123
                conservative: false,
2124
                topology: PrimitiveTopology::TriangleList,
2125
                strip_index_format: None,
2126
            },
2127
            depth_stencil,
2128
            multisample: MultisampleState {
2✔
2129
                count: key.msaa_samples,
2130
                mask: !0,
2131
                alpha_to_coverage_enabled: false,
2132
            },
2133
            push_constant_ranges: Vec::new(),
2✔
2134
            zero_initialize_workgroup_memory: false,
2135
        }
2136
    }
2137
}
2138

2139
/// A single effect instance extracted from a [`ParticleEffect`] as a
2140
/// render world item.
2141
///
2142
/// [`ParticleEffect`]: crate::ParticleEffect
2143
#[derive(Debug, Clone, PartialEq, Component)]
2144
#[require(CachedPipelines, CachedReadyState, CachedEffectMetadata)]
2145
pub(crate) struct ExtractedEffect {
2146
    /// Handle to the effect asset this instance is based on.
2147
    /// The handle is weak to prevent refcount cycles and gracefully handle
2148
    /// assets unloaded or destroyed after a draw call has been submitted.
2149
    pub handle: Handle<EffectAsset>,
2150
    /// Particle layout for the effect.
2151
    pub particle_layout: ParticleLayout,
2152
    /// Effect capacity, in number of particles.
2153
    pub capacity: u32,
2154
    /// Layout flags.
2155
    pub layout_flags: LayoutFlags,
2156
    /// Texture layout.
2157
    pub texture_layout: TextureLayout,
2158
    /// Textures.
2159
    pub textures: Vec<Handle<Image>>,
2160
    /// Alpha mode.
2161
    pub alpha_mode: AlphaMode,
2162
    /// Effect shaders.
2163
    pub effect_shaders: EffectShader,
2164
    /// Condition under which the effect is simulated.
2165
    pub simulation_condition: SimulationCondition,
2166
}
2167

2168
/// Extracted data for the [`GpuSpawnerParams`].
2169
///
2170
/// This contains all data which may change each frame during the regular usage
2171
/// of the effect, but doesn't require any particular GPU resource update
2172
/// (except re-uploading that new data to GPU, of course).
2173
#[derive(Debug, Clone, PartialEq, Component)]
2174
pub(crate) struct ExtractedSpawner {
2175
    /// Number of particles to spawn this frame.
2176
    ///
2177
    /// This is ignored if the effect is a child effect consuming GPU spawn
2178
    /// events.
2179
    pub spawn_count: u32,
2180
    /// PRNG seed.
2181
    pub prng_seed: u32,
2182
    /// Global transform of the effect origin.
2183
    pub transform: GlobalTransform,
2184
    /// Is the effect visible this frame?
2185
    pub is_visible: bool,
2186
}
2187

2188
/// Cache info for the metadata of the effect.
2189
///
2190
/// This manages the GPU allocation of the [`GpuEffectMetadata`] for this
2191
/// effect.
2192
#[derive(Debug, Default, Component)]
2193
pub(crate) struct CachedEffectMetadata {
2194
    /// Allocation ID.
2195
    pub table_id: BufferTableId,
2196
    /// Current metadata values, cached on CPU for change detection.
2197
    pub metadata: GpuEffectMetadata,
2198
}
2199

2200
/// Extracted parent information for a child effect.
2201
///
2202
/// This component is present on the [`RenderEntity`] of an extracted effect if
2203
/// the effect has a parent effect. Otherwise, it's removed.
2204
///
2205
/// This components forms an ECS relationship with [`ChildrenEffects`].
2206
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
2207
#[relationship(relationship_target = ChildrenEffects)]
2208
pub(crate) struct ChildEffectOf {
2209
    /// Render entity of the parent.
2210
    pub parent: Entity,
2211
}
2212

2213
/// Extracted children information for a parent effect.
2214
///
2215
/// This component is present on the [`RenderEntity`] of an extracted effect if
2216
/// the effect is a parent effect for one or more child effects. Otherwise, it's
2217
/// removed.
2218
///
2219
/// This components forms an ECS relationship with [`ChildEffectOf`]. Note that
2220
/// we don't use `linked_spawn` because:
2221
/// 1. This would fight with the `SyncToRenderWorld` as the main world
2222
///    parent-child hierarchy is by design not an ECS relationship (it's a lose
2223
///    declarative coupling).
2224
/// 2. The components on the render entity often store GPU resources or other
2225
///    data we need to clean-up manually, and not all of them currently use
2226
///    lifecycle hooks, so we want to manage despawning manually to prevent
2227
///    leaks.
2228
#[derive(Debug, Clone, PartialEq, Eq, Component)]
2229
#[relationship_target(relationship = ChildEffectOf)]
2230
pub(crate) struct ChildrenEffects(Vec<Entity>);
2231

2232
impl<'a> IntoIterator for &'a ChildrenEffects {
2233
    type Item = <Self::IntoIter as Iterator>::Item;
2234

2235
    type IntoIter = std::slice::Iter<'a, Entity>;
2236

2237
    #[inline(always)]
2238
    fn into_iter(self) -> Self::IntoIter {
×
2239
        self.0.iter()
×
2240
    }
2241
}
2242

2243
impl Deref for ChildrenEffects {
2244
    type Target = [Entity];
2245

2246
    fn deref(&self) -> &Self::Target {
×
2247
        &self.0
×
2248
    }
2249
}
2250

2251
/// Extracted data for an effect's properties, if any.
2252
///
2253
/// This component is present on the [`RenderEntity`] of an extracted effect if
2254
/// that effect has properties. It optionally contains new CPU data to
2255
/// (re-)upload this frame. If the effect has no property, this component is
2256
/// removed.
2257
#[derive(Debug, Component)]
2258
pub(crate) struct ExtractedProperties {
2259
    /// Property layout for the effect.
2260
    pub property_layout: PropertyLayout,
2261
    /// Values of properties written in a binary blob according to
2262
    /// [`property_layout`].
2263
    ///
2264
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2265
    /// `None` if nothing needs to be done for this frame.
2266
    ///
2267
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2268
    pub property_data: Option<Vec<u8>>,
2269
}
2270

2271
#[derive(Default, Resource)]
2272
pub(crate) struct EffectAssetEvents {
2273
    pub images: Vec<AssetEvent<Image>>,
2274
}
2275

2276
/// System extracting all the asset events for the [`Image`] assets to enable
2277
/// dynamic update of images bound to any effect.
2278
///
2279
/// This system runs in parallel of [`extract_effects`].
2280
pub(crate) fn extract_effect_events(
330✔
2281
    mut events: ResMut<EffectAssetEvents>,
2282
    mut image_events: Extract<MessageReader<AssetEvent<Image>>>,
2283
) {
2284
    #[cfg(feature = "trace")]
2285
    let _span = bevy::log::info_span!("extract_effect_events").entered();
990✔
2286
    trace!("extract_effect_events()");
330✔
2287

2288
    let EffectAssetEvents { ref mut images } = *events;
660✔
2289
    *images = image_events.read().copied().collect();
1,320✔
2290
}
2291

2292
/// Debugging settings.
2293
///
2294
/// Settings used to debug Hanabi. These have no effect on the actual behavior
2295
/// of Hanabi, but may affect its performance.
2296
///
2297
/// # Example
2298
///
2299
/// ```
2300
/// # use bevy::prelude::*;
2301
/// # use bevy_hanabi::*;
2302
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2303
///     // Each time a new effect is spawned, capture 2 frames
2304
///     debug_settings.start_capture_on_new_effect = true;
2305
///     debug_settings.capture_frame_count = 2;
2306
/// }
2307
/// ```
2308
#[derive(Debug, Default, Clone, Copy, Resource)]
2309
pub struct DebugSettings {
2310
    /// Enable automatically starting a GPU debugger capture as soon as this
2311
    /// frame starts rendering (extract phase).
2312
    ///
2313
    /// Enable this feature to automatically capture one or more GPU frames when
2314
    /// the `extract_effects()` system runs next. This instructs any attached
2315
    /// GPU debugger to start a capture; this has no effect if no debugger
2316
    /// is attached.
2317
    ///
2318
    /// If a capture is already on-going this has no effect; the on-going
2319
    /// capture needs to be terminated first. Note however that a capture can
2320
    /// stop and another start in the same frame.
2321
    ///
2322
    /// This value is not reset automatically. If you set this to `true`, you
2323
    /// should set it back to `false` on next frame to avoid capturing forever.
2324
    pub start_capture_this_frame: bool,
2325

2326
    /// Enable automatically starting a GPU debugger capture when one or more
2327
    /// effects are spawned.
2328
    ///
2329
    /// Enable this feature to automatically capture one or more GPU frames when
2330
    /// a new effect is spawned (as detected by ECS change detection). This
2331
    /// instructs any attached GPU debugger to start a capture; this has no
2332
    /// effect if no debugger is attached.
2333
    pub start_capture_on_new_effect: bool,
2334

2335
    /// Number of frames to capture with a GPU debugger.
2336
    ///
2337
    /// By default this value is zero, and a GPU debugger capture runs for a
2338
    /// single frame. If a non-zero frame count is specified here, the capture
2339
    /// will instead stop once the specified number of frames has been recorded.
2340
    ///
2341
    /// You should avoid setting this to a value too large, to prevent the
2342
    /// capture size from getting out of control. A typical value is 1 to 3
2343
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2344
    /// debuggers or graphics APIs might further limit this value on their own,
2345
    /// so there's no guarantee the graphics API will honor this value.
2346
    pub capture_frame_count: u32,
2347
}
2348

2349
#[derive(Debug, Default, Clone, Copy, Resource)]
2350
pub(crate) struct RenderDebugSettings {
2351
    /// Is a GPU debugger capture on-going?
2352
    is_capturing: bool,
2353
    /// Start time of any on-going GPU debugger capture.
2354
    capture_start: Duration,
2355
    /// Number of frames captured so far for on-going GPU debugger capture.
2356
    captured_frames: u32,
2357
}
2358

2359
/// Manage GPU debug capture start/stop.
2360
///
2361
/// If any GPU debug capture is configured to start or stop in
2362
/// [`DebugSettings`], they do so during this system's run. This ensures
2363
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2364
/// from Bevy itself, if another Bevy system runs before this one).
2365
///
2366
/// We do this during extract to try and capture as close as possible to an
2367
/// entire GPU frame.
2368
pub(crate) fn start_stop_gpu_debug_capture(
330✔
2369
    real_time: Extract<Res<Time<Real>>>,
2370
    render_device: Res<RenderDevice>,
2371
    debug_settings: Extract<Res<DebugSettings>>,
2372
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2373
    q_added_effects: Extract<Query<(), Added<CompiledParticleEffect>>>,
2374
) {
2375
    #[cfg(feature = "trace")]
2376
    let _span = bevy::log::info_span!("start_stop_debug_capture").entered();
990✔
2377
    trace!("start_stop_debug_capture()");
330✔
2378

2379
    // Stop any pending capture if needed
2380
    if render_debug_settings.is_capturing {
330✔
2381
        render_debug_settings.captured_frames += 1;
×
2382

2383
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2384
            #[expect(unsafe_code, reason = "Debugging only")]
2385
            unsafe {
2386
                render_device.wgpu_device().stop_graphics_debugger_capture();
×
2387
            }
2388
            render_debug_settings.is_capturing = false;
×
2389
            warn!(
×
2390
                "Stopped GPU debug capture after {} frames, at t={}s.",
2391
                render_debug_settings.captured_frames,
×
2392
                real_time.elapsed().as_secs_f64()
×
2393
            );
2394
        }
2395
    }
2396

2397
    // If no pending capture, consider starting a new one
2398
    if !render_debug_settings.is_capturing
330✔
2399
        && (debug_settings.start_capture_this_frame
330✔
2400
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty()))
330✔
2401
    {
2402
        #[expect(unsafe_code, reason = "Debugging only")]
2403
        unsafe {
2404
            render_device
×
2405
                .wgpu_device()
2406
                .start_graphics_debugger_capture();
2407
        }
UNCOV
2408
        render_debug_settings.is_capturing = true;
×
UNCOV
2409
        render_debug_settings.capture_start = real_time.elapsed();
×
UNCOV
2410
        render_debug_settings.captured_frames = 0;
×
UNCOV
2411
        warn!(
×
2412
            "Started GPU debug capture of {} frames at t={}s.",
2413
            debug_settings.capture_frame_count,
×
2414
            render_debug_settings.capture_start.as_secs_f64()
×
2415
        );
2416
    }
2417
}
2418

2419
/// Write the ready state of all render world effects back into their source
2420
/// effect in the main world.
2421
pub(crate) fn report_ready_state(
330✔
2422
    mut main_world: ResMut<MainWorld>,
2423
    q_ready_state: Query<&CachedReadyState>,
2424
) {
2425
    let mut q_effects = main_world.query::<(RenderEntity, &mut CompiledParticleEffect)>();
660✔
2426
    for (render_entity, mut compiled_particle_effect) in q_effects.iter_mut(&mut main_world) {
1,314✔
2427
        if let Ok(cached_ready_state) = q_ready_state.get(render_entity) {
312✔
UNCOV
2428
            compiled_particle_effect.is_ready = cached_ready_state.is_ready();
×
2429
        }
2430
    }
2431
}
2432

2433
/// System extracting data for rendering of all active [`ParticleEffect`]
2434
/// components.
2435
///
2436
/// [`ParticleEffect`]: crate::ParticleEffect
2437
pub(crate) fn extract_effects(
330✔
2438
    mut commands: Commands,
2439
    effects: Extract<Res<Assets<EffectAsset>>>,
2440
    default_mesh: Extract<Res<DefaultMesh>>,
2441
    // Main world effects to extract
2442
    q_effects: Extract<
2443
        Query<(
2444
            Entity,
2445
            RenderEntity,
2446
            Option<&InheritedVisibility>,
2447
            Option<&ViewVisibility>,
2448
            &EffectSpawner,
2449
            &CompiledParticleEffect,
2450
            Option<Ref<EffectProperties>>,
2451
            &GlobalTransform,
2452
        )>,
2453
    >,
2454
    // Render world effects extracted from a previous frame, if any
2455
    mut q_extracted_effects: Query<(
2456
        &mut ExtractedEffect,
2457
        Option<&mut ExtractedSpawner>,
2458
        Option<&ChildEffectOf>, // immutable, because of relationship
2459
        Option<&mut ExtractedEffectMesh>,
2460
        Option<&mut ExtractedProperties>,
2461
    )>,
2462
) {
2463
    #[cfg(feature = "trace")]
2464
    let _span = bevy::log::info_span!("extract_effects").entered();
990✔
2465
    trace!("extract_effects()");
330✔
2466

2467
    // Loop over all existing effects to extract them
2468
    trace!("Extracting {} effects...", q_effects.iter().len());
970✔
2469
    for (
2470
        main_entity,
314✔
UNCOV
2471
        render_entity,
×
UNCOV
2472
        maybe_inherited_visibility,
×
UNCOV
2473
        maybe_view_visibility,
×
UNCOV
2474
        effect_spawner,
×
UNCOV
2475
        compiled_effect,
×
UNCOV
2476
        maybe_properties,
×
UNCOV
2477
        transform,
×
2478
    ) in q_effects.iter()
330✔
2479
    {
2480
        // Check if shaders are configured
2481
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
314✔
2482
            trace!("Effect {:?}: no configured shader, skipped.", main_entity);
×
2483
            continue;
×
2484
        };
2485

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

UNCOV
2496
        let is_visible = maybe_inherited_visibility
×
2497
            .map(|cv| cv.get())
628✔
UNCOV
2498
            .unwrap_or(true)
×
2499
            && maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true);
1,570✔
2500

UNCOV
2501
        let mut cmd = commands.entity(render_entity);
×
2502

2503
        // Fetch the existing extraction compoennts, if any, which we need to update.
2504
        // Because we use SyncToRenderWorld, there's always a render entity, but it may
2505
        // miss all components. And because we can't query only optional components
2506
        // (that would match all entities in the entire world), we force querying
2507
        // ExtractedEffect, which means we get a miss if it's the first extraction and
2508
        // it's not spawned yet. That's OK, we'll spawn it below.
2509
        let (
UNCOV
2510
            maybe_extracted_effect,
×
UNCOV
2511
            maybe_extracted_spawner,
×
UNCOV
2512
            maybe_child_of,
×
UNCOV
2513
            maybe_extracted_mesh,
×
UNCOV
2514
            maybe_extracted_properties,
×
UNCOV
2515
        ) = q_extracted_effects
×
UNCOV
2516
            .get_mut(render_entity)
×
2517
            .map(|(extracted_effect, b, c, d, e)| (Some(extracted_effect), b, c, d, e))
1,560✔
UNCOV
2518
            .unwrap_or((None, None, None, None, None));
×
2519

2520
        // Extract general effect data
UNCOV
2521
        let texture_layout = asset.module().texture_layout();
×
UNCOV
2522
        let layout_flags = compiled_effect.layout_flags;
×
UNCOV
2523
        let alpha_mode = compiled_effect.alpha_mode;
×
UNCOV
2524
        trace!(
×
2525
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
2526
            asset.name,
2527
            main_entity,
2528
            render_entity,
2529
            texture_layout.layout.len(),
628✔
2530
            compiled_effect.textures.len(),
628✔
2531
            layout_flags,
2532
        );
2533
        let new_extracted_effect = ExtractedEffect {
UNCOV
2534
            handle: compiled_effect.asset.clone(),
×
UNCOV
2535
            particle_layout: asset.particle_layout().clone(),
×
UNCOV
2536
            capacity: asset.capacity(),
×
2537
            layout_flags,
2538
            texture_layout,
UNCOV
2539
            textures: compiled_effect.textures.clone(),
×
2540
            alpha_mode,
UNCOV
2541
            effect_shaders: effect_shaders.clone(),
×
UNCOV
2542
            simulation_condition: asset.simulation_condition,
×
2543
        };
2544
        if let Some(mut extracted_effect) = maybe_extracted_effect {
312✔
UNCOV
2545
            extracted_effect.set_if_neq(new_extracted_effect);
×
2546
        } else {
2547
            trace!(
2✔
2548
                "Inserting new ExtractedEffect component on {:?}",
2549
                render_entity
2550
            );
2551
            cmd.insert(new_extracted_effect);
6✔
2552
        }
2553

2554
        // Extract the spawner data
2555
        let new_spawner = ExtractedSpawner {
UNCOV
2556
            spawn_count: effect_spawner.spawn_count,
×
UNCOV
2557
            prng_seed: compiled_effect.prng_seed,
×
UNCOV
2558
            transform: *transform,
×
2559
            is_visible,
2560
        };
UNCOV
2561
        trace!(
×
2562
            "[Effect {}] spawn_count={} prng_seed={}",
2563
            render_entity,
2564
            new_spawner.spawn_count,
2565
            new_spawner.prng_seed
2566
        );
2567
        if let Some(mut extracted_spawner) = maybe_extracted_spawner {
312✔
UNCOV
2568
            extracted_spawner.set_if_neq(new_spawner);
×
2569
        } else {
2570
            trace!(
2✔
2571
                "Inserting new ExtractedSpawner component on {}",
2572
                render_entity
2573
            );
2574
            cmd.insert(new_spawner);
6✔
2575
        }
2576

2577
        // Extract the effect mesh
UNCOV
2578
        let mesh = compiled_effect
×
UNCOV
2579
            .mesh
×
2580
            .clone()
UNCOV
2581
            .unwrap_or(default_mesh.0.clone());
×
UNCOV
2582
        let new_mesh = ExtractedEffectMesh { mesh: mesh.id() };
×
2583
        if let Some(mut extracted_mesh) = maybe_extracted_mesh {
312✔
UNCOV
2584
            extracted_mesh.set_if_neq(new_mesh);
×
2585
        } else {
2586
            trace!(
2✔
2587
                "Inserting new ExtractedEffectMesh component on {:?}",
2588
                render_entity
2589
            );
2590
            cmd.insert(new_mesh);
6✔
2591
        }
2592

2593
        // Extract the parent, if any, and resolve its render entity
2594
        let parent_render_entity = if let Some(main_entity) = compiled_effect.parent {
314✔
2595
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2596
                error!(
×
2597
                    "Failed to resolve render entity of parent with main entity {:?}.",
2598
                    main_entity
2599
                );
2600
                cmd.remove::<ChildEffectOf>();
×
2601
                // TODO - prevent extraction altogether here, instead of just de-parenting?
2602
                continue;
×
2603
            };
UNCOV
2604
            Some(render_entity)
×
2605
        } else {
2606
            None
314✔
2607
        };
2608
        if let Some(render_entity) = parent_render_entity {
×
2609
            let new_child_of = ChildEffectOf {
2610
                parent: render_entity,
2611
            };
2612
            // If there's already an ExtractedParent component, ensure we overwrite only if
2613
            // different, to not trigger ECS change detection that we rely on.
2614
            if let Some(child_effect_of) = maybe_child_of {
×
2615
                // The relationship makes ChildEffectOf immutable, so re-insert to mutate
2616
                if *child_effect_of != new_child_of {
×
2617
                    cmd.insert(new_child_of);
×
2618
                }
2619
            } else {
2620
                trace!(
×
2621
                    "Inserting new ChildEffectOf component on {:?}",
2622
                    render_entity
2623
                );
2624
                cmd.insert(new_child_of);
×
2625
            }
2626
        } else {
2627
            cmd.remove::<ChildEffectOf>();
314✔
2628
        }
2629

2630
        // Extract property data
UNCOV
2631
        let property_layout = asset.property_layout();
×
2632
        if property_layout.is_empty() {
305✔
2633
            cmd.remove::<ExtractedProperties>();
305✔
2634
        } else {
2635
            // Re-extract CPU property data if any. Note that this data is not a "new value"
2636
            // but instead a "value that must be uploaded this frame", and therefore is
2637
            // empty when there's no change (as opposed to, having a constant value
2638
            // frame-to-frame).
2639
            let property_data = if let Some(properties) = maybe_properties {
9✔
UNCOV
2640
                if properties.is_changed() {
×
2641
                    trace!("Detected property change, re-serializing...");
×
2642
                    Some(properties.serialize(&property_layout))
×
2643
                } else {
2644
                    None
×
2645
                }
2646
            } else {
2647
                None
9✔
2648
            };
2649

2650
            let new_properties = ExtractedProperties {
2651
                property_layout,
2652
                property_data,
2653
            };
UNCOV
2654
            trace!("new_properties = {new_properties:?}");
×
2655

2656
            if let Some(mut extracted_properties) = maybe_extracted_properties {
8✔
2657
                // Always mutate if there's new CPU data to re-upload. Otherwise check for any
2658
                // other change.
UNCOV
2659
                if new_properties.property_data.is_some()
×
2660
                    || (extracted_properties.property_layout != new_properties.property_layout)
8✔
2661
                {
2662
                    trace!(
×
2663
                        "Updating existing ExtractedProperties (was: {:?})",
2664
                        extracted_properties.as_ref()
×
2665
                    );
UNCOV
2666
                    *extracted_properties = new_properties;
×
2667
                }
2668
            } else {
2669
                trace!(
1✔
2670
                    "Inserting new ExtractedProperties component on {:?}",
2671
                    render_entity
2672
                );
2673
                cmd.insert(new_properties);
3✔
2674
            }
2675
        }
2676
    }
2677
}
2678

2679
pub(crate) fn extract_sim_params(
330✔
2680
    real_time: Extract<Res<Time<Real>>>,
2681
    virtual_time: Extract<Res<Time<Virtual>>>,
2682
    time: Extract<Res<Time<EffectSimulation>>>,
2683
    mut sim_params: ResMut<SimParams>,
2684
) {
2685
    #[cfg(feature = "trace")]
2686
    let _span = bevy::log::info_span!("extract_sim_params").entered();
990✔
2687
    trace!("extract_sim_params()");
330✔
2688

2689
    // Save simulation params into render world
2690
    sim_params.time = time.elapsed_secs_f64();
660✔
2691
    sim_params.delta_time = time.delta_secs();
660✔
2692
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
660✔
2693
    sim_params.virtual_delta_time = virtual_time.delta_secs();
660✔
2694
    sim_params.real_time = real_time.elapsed_secs_f64();
660✔
2695
    sim_params.real_delta_time = real_time.delta_secs();
660✔
2696
    trace!(
330✔
2697
        "SimParams: time={} delta_time={} vtime={} delta_vtime={} rtime={} delta_rtime={}",
2698
        sim_params.time,
320✔
2699
        sim_params.delta_time,
320✔
2700
        sim_params.virtual_time,
320✔
2701
        sim_params.virtual_delta_time,
320✔
2702
        sim_params.real_time,
320✔
2703
        sim_params.real_delta_time,
320✔
2704
    );
2705
}
2706

2707
/// Various GPU limits and aligned sizes computed once and cached.
2708
struct GpuLimits {
2709
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2710
    ///
2711
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2712
    storage_buffer_align: NonZeroU32,
2713

2714
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2715
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2716
    ///
2717
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2718
    effect_metadata_aligned_size: NonZeroU32,
2719
}
2720

2721
impl GpuLimits {
2722
    pub fn from_device(render_device: &RenderDevice) -> Self {
4✔
2723
        let storage_buffer_align =
4✔
2724
            render_device.limits().min_storage_buffer_offset_alignment as u64;
4✔
2725

2726
        let effect_metadata_aligned_size = NonZeroU32::new(
2727
            GpuEffectMetadata::min_size()
8✔
2728
                .get()
8✔
2729
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2730
        )
2731
        .unwrap();
2732

2733
        trace!(
4✔
2734
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2735
            storage_buffer_align,
2736
            GpuEffectMetadata::min_size().get(),
4✔
2737
            effect_metadata_aligned_size.get(),
4✔
2738
        );
2739

2740
        Self {
2741
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
12✔
2742
            effect_metadata_aligned_size,
2743
        }
2744
    }
2745

2746
    /// Byte alignment for any storage buffer binding.
2747
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2748
        self.storage_buffer_align
3✔
2749
    }
2750

2751
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2752
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
1✔
2753
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
1✔
2754
    }
2755
}
2756

2757
/// Global render world resource containing the GPU data to draw all the
2758
/// particle effects in all views.
2759
///
2760
/// The resource is populated by [`prepare_effects()`] with all the effects to
2761
/// render for the current frame, for all views in the frame, and consumed by
2762
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2763
/// effects.
2764
#[derive(Resource)]
2765
pub struct EffectsMeta {
2766
    /// Bind group for the camera view, containing the camera projection and
2767
    /// other uniform values related to the camera.
2768
    view_bind_group: Option<BindGroup>,
2769
    /// Bind group #0 of the vfx_update shader, for the simulation parameters
2770
    /// like the current time and frame delta time.
2771
    update_sim_params_bind_group: Option<BindGroup>,
2772
    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2773
    /// like the current time and frame delta time. This is shared with the
2774
    /// vfx_init pass too.
2775
    indirect_sim_params_bind_group: Option<BindGroup>,
2776
    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2777
    /// compute dispatch and render buffers.
2778
    indirect_metadata_bind_group: Option<BindGroup>,
2779
    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2780
    indirect_spawner_bind_group: Option<BindGroup>,
2781
    /// Global shared GPU uniform buffer storing the simulation parameters,
2782
    /// uploaded each frame from CPU to GPU.
2783
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2784
    /// Global shared GPU buffer storing the various spawner parameter structs
2785
    /// for the active effect instances.
2786
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2787
    /// Global shared GPU buffer storing the various indirect dispatch structs
2788
    /// for the indirect dispatch of the Update pass.
2789
    dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirectArgs>,
2790
    /// Global shared GPU buffer storing the various indirect draw structs
2791
    /// for the indirect Render pass. Note that we use
2792
    /// GpuDrawIndexedIndirectArgs as the largest of the two variants (the
2793
    /// other being GpuDrawIndirectArgs). For non-indexed entries, we ignore
2794
    /// the last `u32` value.
2795
    draw_indirect_buffer: BufferTable<GpuDrawIndexedIndirectArgs>,
2796
    /// Global shared GPU buffer storing the various `EffectMetadata`
2797
    /// structs for the active effect instances.
2798
    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2799
    /// Various GPU limits and aligned sizes lazily allocated and cached for
2800
    /// convenience.
2801
    gpu_limits: GpuLimits,
2802
    indirect_shader_noevent: Handle<Shader>,
2803
    indirect_shader_events: Handle<Shader>,
2804
    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2805
    /// -noevent and -events variants).
2806
    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2807
    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2808
    /// is either the -noevent or -events variant depending on whether there's
2809
    /// any child effect with GPU events currently active.
2810
    active_indirect_pipeline_id: CachedComputePipelineId,
2811
}
2812

2813
impl EffectsMeta {
2814
    pub fn new(
3✔
2815
        device: RenderDevice,
2816
        indirect_shader_noevent: Handle<Shader>,
2817
        indirect_shader_events: Handle<Shader>,
2818
    ) -> Self {
2819
        let gpu_limits = GpuLimits::from_device(&device);
9✔
2820

2821
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2822
        // be addressed individually by the computer shaders.
2823
        let item_align = gpu_limits.storage_buffer_align();
9✔
2824
        trace!(
3✔
2825
            "Aligning storage buffers to {} bytes as device limits requires.",
2826
            item_align.get()
4✔
2827
        );
2828

2829
        Self {
2830
            view_bind_group: None,
2831
            update_sim_params_bind_group: None,
2832
            indirect_sim_params_bind_group: None,
2833
            indirect_metadata_bind_group: None,
2834
            indirect_spawner_bind_group: None,
2835
            sim_params_uniforms: UniformBuffer::default(),
6✔
2836
            spawner_buffer: AlignedBufferVec::new(
6✔
2837
                BufferUsages::STORAGE,
2838
                Some(item_align.into()),
2839
                Some("hanabi:buffer:spawner".to_string()),
2840
            ),
2841
            dispatch_indirect_buffer: GpuBuffer::new(
6✔
2842
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2843
                Some("hanabi:buffer:dispatch_indirect".to_string()),
2844
            ),
2845
            draw_indirect_buffer: BufferTable::new(
6✔
2846
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2847
                Some(GpuDrawIndexedIndirectArgs::SHADER_SIZE),
2848
                Some("hanabi:buffer:draw_indirect".to_string()),
2849
            ),
2850
            effect_metadata_buffer: BufferTable::new(
6✔
2851
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2852
                Some(item_align.into()),
2853
                Some("hanabi:buffer:effect_metadata".to_string()),
2854
            ),
2855
            gpu_limits,
2856
            indirect_shader_noevent,
2857
            indirect_shader_events,
2858
            indirect_pipeline_ids: [
3✔
2859
                CachedComputePipelineId::INVALID,
2860
                CachedComputePipelineId::INVALID,
2861
            ],
2862
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2863
        }
2864
    }
2865

2866
    pub fn allocate_spawner(
312✔
2867
        &mut self,
2868
        global_transform: &GlobalTransform,
2869
        spawn_count: u32,
2870
        prng_seed: u32,
2871
        slab_offset: u32,
2872
        parent_slab_offset: Option<u32>,
2873
        effect_metadata_buffer_table_id: BufferTableId,
2874
        maybe_cached_draw_indirect_args: Option<&CachedDrawIndirectArgs>,
2875
    ) -> u32 {
2876
        let spawner_base = self.spawner_buffer.len() as u32;
624✔
2877
        let transform = global_transform.to_matrix().into();
1,248✔
2878
        let inverse_transform = Mat4::from(
2879
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2880
            // efficient than inversing the Mat4.
2881
            global_transform.affine().inverse(),
624✔
2882
        )
2883
        .into();
2884
        let spawner_params = GpuSpawnerParams {
2885
            transform,
2886
            inverse_transform,
2887
            spawn: spawn_count as i32,
312✔
2888
            seed: prng_seed,
2889
            effect_metadata_index: effect_metadata_buffer_table_id.0,
312✔
2890
            draw_indirect_index: maybe_cached_draw_indirect_args
312✔
2891
                .map(|cdia| cdia.get_row().0)
2892
                .unwrap_or_default(),
2893
            slab_offset,
2894
            parent_slab_offset: parent_slab_offset.unwrap_or(u32::MAX),
624✔
2895
            ..default()
2896
        };
2897
        trace!("spawner params = {:?}", spawner_params);
312✔
2898
        self.spawner_buffer.push(spawner_params);
936✔
2899
        spawner_base
312✔
2900
    }
2901

2902
    pub fn allocate_draw_indirect(
2✔
2903
        &mut self,
2904
        draw_args: &AnyDrawIndirectArgs,
2905
    ) -> CachedDrawIndirectArgs {
2906
        let row = self
4✔
2907
            .draw_indirect_buffer
2✔
2908
            .insert(draw_args.bitcast_to_row_entry());
6✔
2909
        CachedDrawIndirectArgs {
2910
            row,
2911
            args: *draw_args,
2✔
2912
        }
2913
    }
2914

2915
    pub fn update_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
×
2916
        self.draw_indirect_buffer
×
2917
            .update(row_index.get_row(), row_index.args.bitcast_to_row_entry());
×
2918
    }
2919

2920
    pub fn free_draw_indirect(&mut self, row_index: &CachedDrawIndirectArgs) {
1✔
2921
        self.draw_indirect_buffer.remove(row_index.get_row());
4✔
2922
    }
2923
}
2924

2925
bitflags! {
2926
    /// Effect flags.
2927
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2928
    pub struct LayoutFlags: u32 {
2929
        /// No flags.
2930
        const NONE = 0;
2931
        // DEPRECATED - The effect uses an image texture.
2932
        //const PARTICLE_TEXTURE = (1 << 0);
2933
        /// The effect is simulated in local space.
2934
        const LOCAL_SPACE_SIMULATION = (1 << 2);
2935
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2936
        const USE_ALPHA_MASK = (1 << 3);
2937
        /// The effect is rendered with flipbook texture animation based on the
2938
        /// [`Attribute::SPRITE_INDEX`] of each particle.
2939
        const FLIPBOOK = (1 << 4);
2940
        /// The effect needs UVs.
2941
        const NEEDS_UV = (1 << 5);
2942
        /// The effect has ribbons.
2943
        const RIBBONS = (1 << 6);
2944
        /// The effects needs normals.
2945
        const NEEDS_NORMAL = (1 << 7);
2946
        /// The effect is fully-opaque.
2947
        const OPAQUE = (1 << 8);
2948
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
2949
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2950
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
2951
        /// a single CPU spawn count.
2952
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2953
        /// The (init or update) shader needs access to its parent particle. This allows
2954
        /// a particle init or update pass to read the data of a parent particle, for
2955
        /// example to inherit some of the attributes.
2956
        const READ_PARENT_PARTICLE = (1 << 11);
2957
        /// The effect access to the particle data in the fragment shader.
2958
        const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
2959
    }
2960
}
2961

2962
impl Default for LayoutFlags {
2963
    fn default() -> Self {
1✔
2964
        Self::NONE
1✔
2965
    }
2966
}
2967

2968
/// Observer raised when the [`CachedEffect`] component is removed, which
2969
/// indicates that the effect instance was despawned.
2970
pub(crate) fn on_remove_cached_effect(
1✔
2971
    trigger: On<Remove, CachedEffect>,
2972
    query: Query<(
2973
        Entity,
2974
        &MainEntity,
2975
        &CachedEffect,
2976
        &DispatchBufferIndices,
2977
        Option<&CachedEffectProperties>,
2978
        Option<&CachedParentInfo>,
2979
        Option<&CachedEffectEvents>,
2980
    )>,
2981
    mut effect_cache: ResMut<EffectCache>,
2982
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2983
    mut effects_meta: ResMut<EffectsMeta>,
2984
    mut event_cache: ResMut<EventCache>,
2985
) {
2986
    #[cfg(feature = "trace")]
2987
    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
3✔
2988

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

2992
    // Fecth the components of the effect being destroyed. Note that the despawn
2993
    // command above is not yet applied, so this query should always succeed.
2994
    let Ok((
2995
        render_entity,
1✔
UNCOV
2996
        main_entity,
×
UNCOV
2997
        cached_effect,
×
UNCOV
2998
        dispatch_buffer_indices,
×
UNCOV
2999
        _opt_props,
×
UNCOV
3000
        _opt_parent,
×
UNCOV
3001
        opt_cached_effect_events,
×
3002
    )) = query.get(trigger.event().entity)
3✔
3003
    else {
3004
        return;
×
3005
    };
3006

3007
    // Dealllocate the effect slice in the event buffer, if any.
3008
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
UNCOV
3009
        match event_cache.free(cached_effect_events) {
×
3010
            Err(err) => {
×
3011
                error!("Error while freeing effect event slice: {err:?}");
×
3012
            }
3013
            Ok(buffer_state) => {
×
3014
                if buffer_state != SlabState::Used {
×
3015
                    // Clear bind groups associated with the old buffer
3016
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
3017
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
3018
                }
3019
            }
3020
        }
3021
    }
3022

3023
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3024
    // last slice, also deallocate the GPU buffer itself.
UNCOV
3025
    trace!(
×
3026
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
3027
        render_entity,
3028
        main_entity,
3029
    );
UNCOV
3030
    let Ok(SlabState::Free) = effect_cache.remove(cached_effect) else {
×
3031
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3032
        // do.
3033
        return;
×
3034
    };
3035

3036
    // Clear bind groups associated with the removed buffer
3037
    trace!(
1✔
3038
        "=> GPU particle slab #{} gone, destroying its bind groups...",
3039
        cached_effect.slab_id.index()
2✔
3040
    );
UNCOV
3041
    effect_bind_groups
×
UNCOV
3042
        .particle_slabs
×
UNCOV
3043
        .remove(&cached_effect.slab_id);
×
UNCOV
3044
    effects_meta
×
UNCOV
3045
        .dispatch_indirect_buffer
×
UNCOV
3046
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
×
3047
}
3048

3049
/// Observer raised when the [`CachedEffectMetadata`] component is removed, to
3050
/// deallocate the GPU resources associated with the indirect draw args.
3051
pub(crate) fn on_remove_cached_metadata(
1✔
3052
    trigger: On<Remove, CachedEffectMetadata>,
3053
    query: Query<&CachedEffectMetadata>,
3054
    mut effects_meta: ResMut<EffectsMeta>,
3055
) {
3056
    #[cfg(feature = "trace")]
3057
    let _span = bevy::log::info_span!("on_remove_cached_metadata").entered();
3✔
3058

3059
    if let Ok(cached_metadata) = query.get(trigger.event().entity) {
4✔
3060
        if cached_metadata.table_id.is_valid() {
1✔
3061
            effects_meta
2✔
3062
                .effect_metadata_buffer
2✔
3063
                .remove(cached_metadata.table_id);
1✔
3064
        }
3065
    };
3066
}
3067

3068
/// Observer raised when the [`CachedDrawIndirectArgs`] component is removed, to
3069
/// deallocate the GPU resources associated with the indirect draw args.
3070
pub(crate) fn on_remove_cached_draw_indirect_args(
1✔
3071
    trigger: On<Remove, CachedDrawIndirectArgs>,
3072
    query: Query<&CachedDrawIndirectArgs>,
3073
    mut effects_meta: ResMut<EffectsMeta>,
3074
) {
3075
    #[cfg(feature = "trace")]
3076
    let _span = bevy::log::info_span!("on_remove_cached_draw_indirect_args").entered();
3✔
3077

3078
    if let Ok(cached_draw_args) = query.get(trigger.event().entity) {
4✔
UNCOV
3079
        effects_meta.free_draw_indirect(cached_draw_args);
×
3080
    };
3081
}
3082

3083
/// Clear pending GPU resources left from previous frame.
3084
///
3085
/// Those generally are source buffers for buffer-to-buffer copies on capacity
3086
/// growth, which need the source buffer to be alive until the copy is done,
3087
/// then can be discarded here.
3088
pub(crate) fn clear_previous_frame_resizes(
330✔
3089
    mut effects_meta: ResMut<EffectsMeta>,
3090
    mut sort_bind_groups: ResMut<SortBindGroups>,
3091
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3092
) {
3093
    #[cfg(feature = "trace")]
3094
    let _span = bevy::log::info_span!("clear_previous_frame_resizes").entered();
990✔
3095
    trace!("clear_previous_frame_resizes");
330✔
3096

3097
    init_fill_dispatch_queue.clear();
330✔
3098

3099
    // Clear last frame's buffer resizes which may have occured during last frame,
3100
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3101
    // the first point at which we can do that where we're not blocking the main
3102
    // world (so, excluding the extract system).
3103
    effects_meta
330✔
3104
        .dispatch_indirect_buffer
330✔
3105
        .clear_previous_frame_resizes();
3106
    effects_meta
330✔
3107
        .draw_indirect_buffer
330✔
3108
        .clear_previous_frame_resizes();
3109
    effects_meta
330✔
3110
        .effect_metadata_buffer
330✔
3111
        .clear_previous_frame_resizes();
3112
    sort_bind_groups.clear_previous_frame_resizes();
330✔
3113
}
3114

3115
// Fixup the [`CachedChildInfo::global_child_index`] once all child infos have
3116
// been allocated.
3117
pub fn fixup_parents(
330✔
3118
    q_changed_parents: Query<(Entity, Ref<CachedParentInfo>)>,
3119
    mut q_children: Query<&mut CachedChildInfo>,
3120
) {
3121
    #[cfg(feature = "trace")]
3122
    let _span = bevy::log::info_span!("fixup_parents").entered();
990✔
3123
    trace!("fixup_parents");
330✔
3124

3125
    // Once all parents are (re-)allocated, fix up the global index of all
3126
    // children if the parent base index changed.
3127
    trace!(
330✔
3128
        "Updating the global index of children of parent effects whose child list just changed..."
3129
    );
3130
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
660✔
UNCOV
3131
        let base_index =
×
UNCOV
3132
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
×
UNCOV
3133
        let parent_changed = cached_parent_info.is_changed();
×
UNCOV
3134
        trace!(
×
3135
            "Updating {} children of parent effect {:?} with base child index {} (parent_changed:{})...",
3136
            cached_parent_info.children.len(),
×
3137
            parent_entity,
3138
            base_index,
3139
            parent_changed
3140
        );
3141
        for (child_entity, _) in &cached_parent_info.children {
×
3142
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3143
                error!(
×
3144
                    "Cannot find child {:?} declared by parent {:?}",
3145
                    *child_entity, parent_entity
3146
                );
3147
                continue;
×
3148
            };
3149
            if !cached_child_info.is_changed() && !parent_changed {
×
3150
                continue;
×
3151
            }
3152
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3153
            trace!(
×
3154
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
3155
                child_entity,
3156
                parent_entity,
3157
                cached_child_info.local_child_index,
×
3158
                cached_child_info.global_child_index
×
3159
            );
3160
        }
3161
    }
3162
}
3163

3164
/// Allocate the GPU resources for all extracted effects.
3165
///
3166
/// This adds the [`CachedEffect`] component as needed, and update it with the
3167
/// allocation in the [`EffectCache`].
3168
pub fn allocate_effects(
330✔
3169
    mut commands: Commands,
3170
    mut q_extracted_effects: Query<
3171
        (
3172
            Entity,
3173
            &ExtractedEffect,
3174
            Has<ChildEffectOf>,
3175
            Option<&mut CachedEffect>,
3176
            Has<DispatchBufferIndices>,
3177
        ),
3178
        Changed<ExtractedEffect>,
3179
    >,
3180
    mut effect_cache: ResMut<EffectCache>,
3181
    mut effects_meta: ResMut<EffectsMeta>,
3182
) {
3183
    #[cfg(feature = "trace")]
3184
    let _span = bevy::log::info_span!("allocate_effects").entered();
990✔
3185
    trace!("allocate_effects");
330✔
3186

3187
    for (entity, extracted_effect, has_parent, maybe_cached_effect, has_dispatch_buffer_indices) in
3✔
3188
        &mut q_extracted_effects
330✔
3189
    {
3190
        // Insert or update the effect into the EffectCache
3191
        if let Some(mut cached_effect) = maybe_cached_effect {
1✔
UNCOV
3192
            trace!("Updating EffectCache entry for entity {entity:?}...");
×
UNCOV
3193
            let _ = effect_cache.remove(cached_effect.as_ref());
×
UNCOV
3194
            *cached_effect = effect_cache.insert(
×
UNCOV
3195
                extracted_effect.handle.clone(),
×
UNCOV
3196
                extracted_effect.capacity,
×
UNCOV
3197
                &extracted_effect.particle_layout,
×
3198
            );
3199
        } else {
3200
            trace!("Allocating new entry in EffectCache for entity {entity:?}...");
2✔
3201
            let cached_effect = effect_cache.insert(
8✔
3202
                extracted_effect.handle.clone(),
6✔
3203
                extracted_effect.capacity,
2✔
3204
                &extracted_effect.particle_layout,
2✔
3205
            );
3206
            commands.entity(entity).insert(cached_effect);
8✔
3207
        }
3208

3209
        // Ensure the particle@1 bind group layout exists for the given configuration of
3210
        // particle layout. We do this here only for effects without a parent; for those
3211
        // with a parent, we'll do it after we resolved that parent.
3212
        if !has_parent {
3✔
3213
            let parent_min_binding_size = None;
3✔
3214
            effect_cache.ensure_particle_bind_group_layout_desc(
3✔
3215
                extracted_effect.particle_layout.min_binding_size32(),
3✔
3216
                parent_min_binding_size,
3✔
3217
            );
3218
        }
3219

3220
        // Ensure the metadata@3 bind group layout exists for the init pass.
3221
        {
UNCOV
3222
            let consume_gpu_spawn_events = extracted_effect
×
UNCOV
3223
                .layout_flags
×
UNCOV
3224
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
NEW
3225
            effect_cache.ensure_metadata_init_bind_group_layout_desc(consume_gpu_spawn_events);
×
3226
        }
3227

3228
        // Allocate DispatchBufferIndices if not present yet
3229
        if !has_dispatch_buffer_indices {
2✔
3230
            let update_dispatch_indirect_buffer_row_index =
2✔
3231
                effects_meta.dispatch_indirect_buffer.allocate();
2✔
3232
            commands.entity(entity).insert(DispatchBufferIndices {
2✔
3233
                update_dispatch_indirect_buffer_row_index,
2✔
3234
            });
3235
        }
3236
    }
3237
}
3238

3239
/// Update any cached mesh info based on any relocation done by Bevy itself.
3240
///
3241
/// Bevy will merge small meshes into larger GPU buffers automatically. When
3242
/// this happens, the mesh location changes, and we need to update our
3243
/// references to it in order to know how to issue the draw commands.
3244
///
3245
/// This system updates both the [`CachedMeshLocation`] and the
3246
/// [`CachedIndirectDrawArgs`] components.
3247
pub fn update_mesh_locations(
330✔
3248
    mut commands: Commands,
3249
    mut effects_meta: ResMut<EffectsMeta>,
3250
    mesh_allocator: Res<MeshAllocator>,
3251
    render_meshes: Res<RenderAssets<RenderMesh>>,
3252
    mut q_cached_effects: Query<(
3253
        Entity,
3254
        &ExtractedEffectMesh,
3255
        Option<&mut CachedMeshLocation>,
3256
        Option<&mut CachedDrawIndirectArgs>,
3257
    )>,
3258
) {
3259
    #[cfg(feature = "trace")]
3260
    let _span = bevy::log::info_span!("update_mesh_locations").entered();
990✔
3261
    trace!("update_mesh_locations");
330✔
3262

3263
    for (entity, extracted_mesh, maybe_cached_mesh_location, maybe_cached_draw_indirect_args) in
1,256✔
3264
        &mut q_cached_effects
330✔
3265
    {
3266
        let mut cmds = commands.entity(entity);
1,256✔
3267

3268
        // Resolve the render mesh
3269
        let Some(render_mesh) = render_meshes.get(extracted_mesh.mesh) else {
942✔
3270
            warn!(
×
3271
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
3272
                entity, extracted_mesh.mesh
3273
            );
3274
            cmds.remove::<CachedMeshLocation>();
×
3275
            continue;
×
3276
        };
3277

3278
        // Find the location where the render mesh was allocated. This is handled by
3279
        // Bevy itself in the allocate_and_free_meshes() system. Bevy might
3280
        // re-batch the vertex and optional index data of meshes together at any point,
3281
        // so we need to confirm that the location data we may have cached is still
3282
        // valid.
3283
        let Some(mesh_vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&extracted_mesh.mesh)
314✔
3284
        else {
3285
            trace!(
×
3286
                "Effect main_entity {:?}: cannot find vertex slice of render mesh {:?}",
3287
                entity,
3288
                extracted_mesh.mesh
3289
            );
3290
            cmds.remove::<CachedMeshLocation>();
×
3291
            continue;
×
3292
        };
UNCOV
3293
        let mesh_index_buffer_slice = mesh_allocator.mesh_index_slice(&extracted_mesh.mesh);
×
3294
        let indexed =
314✔
3295
            if let RenderMeshBufferInfo::Indexed { index_format, .. } = render_mesh.buffer_info {
314✔
3296
                if let Some(ref slice) = mesh_index_buffer_slice {
314✔
UNCOV
3297
                    Some(MeshIndexSlice {
×
UNCOV
3298
                        format: index_format,
×
UNCOV
3299
                        buffer: slice.buffer.clone(),
×
UNCOV
3300
                        range: slice.range.clone(),
×
3301
                    })
3302
                } else {
3303
                    trace!(
×
3304
                        "Effect main_entity {:?}: cannot find index slice of render mesh {:?}",
3305
                        entity,
3306
                        extracted_mesh.mesh
3307
                    );
3308
                    cmds.remove::<CachedMeshLocation>();
×
3309
                    continue;
×
3310
                }
3311
            } else {
3312
                None
×
3313
            };
3314

3315
        // Calculate the new draw args and mesh location based on Bevy's info
3316
        let new_draw_args = AnyDrawIndirectArgs::from_slices(
UNCOV
3317
            &mesh_vertex_buffer_slice,
×
UNCOV
3318
            mesh_index_buffer_slice.as_ref(),
×
3319
        );
UNCOV
3320
        let new_mesh_location = match &mesh_index_buffer_slice {
×
3321
            // Indexed mesh rendering
3322
            Some(mesh_index_buffer_slice) => CachedMeshLocation {
3323
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
942✔
3324
                vertex_or_index_count: mesh_index_buffer_slice.range.len() as u32,
628✔
3325
                first_index_or_vertex_offset: mesh_index_buffer_slice.range.start,
628✔
3326
                vertex_offset_or_base_instance: mesh_vertex_buffer_slice.range.start as i32,
314✔
3327
                indexed,
3328
            },
3329
            // Non-indexed mesh rendering
3330
            None => CachedMeshLocation {
3331
                vertex_buffer: mesh_vertex_buffer_slice.buffer.id(),
×
3332
                vertex_or_index_count: mesh_vertex_buffer_slice.range.len() as u32,
×
3333
                first_index_or_vertex_offset: mesh_vertex_buffer_slice.range.start,
×
3334
                vertex_offset_or_base_instance: 0,
3335
                indexed: None,
3336
            },
3337
        };
3338

3339
        // We don't allocate the draw indirect args ahead of time because we need to
3340
        // select the indexed vs. non-indexed buffer. Now that we know whether the mesh
3341
        // is indexed, we can allocate it (or reallocate it if indexing mode changed).
3342
        if let Some(mut cached_draw_indirect) = maybe_cached_draw_indirect_args {
312✔
UNCOV
3343
            assert!(cached_draw_indirect.row.is_valid());
×
3344

3345
            // If the GPU draw args changed, re-upload to GPU.
3346
            if new_draw_args != cached_draw_indirect.args {
312✔
3347
                debug!(
×
3348
                    "Indirect draw args changed for asset {:?}\nold:{:?}\nnew:{:?}",
3349
                    entity, cached_draw_indirect.args, new_draw_args
×
3350
                );
3351
                cached_draw_indirect.args = new_draw_args;
×
3352
                effects_meta.update_draw_indirect(cached_draw_indirect.as_ref());
×
3353
            }
3354
        } else {
3355
            cmds.insert(effects_meta.allocate_draw_indirect(&new_draw_args));
8✔
3356
        }
3357

3358
        // Compare to any cached data and update if necessary, or insert if missing.
3359
        // This will trigger change detection in the ECS, which will in turn trigger
3360
        // GpuEffectMetadata re-upload.
3361
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
626✔
UNCOV
3362
            if *old_mesh_location != new_mesh_location {
×
3363
                debug!(
×
3364
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
3365
                    entity, old_mesh_location, new_mesh_location
3366
                );
3367
                *old_mesh_location = new_mesh_location;
×
3368
            }
3369
        } else {
3370
            cmds.insert(new_mesh_location);
4✔
3371
        }
3372
    }
3373
}
3374

3375
/// Allocate an entry in the GPU table for any [`CachedEffectMetadata`] missing
3376
/// one.
3377
///
3378
/// This system does NOT take care of (re-)uploading recent CPU data to GPU.
3379
/// This is done much later in the frame, after batching and once all data for
3380
/// it is ready. But it's necessary to ensure the allocation is determined
3381
/// already ahead of time, in order to do batching of contiguous metadata
3382
/// blocks (TODO; not currently used, also may end up using binary search in
3383
/// shader, in which case we won't need continguous-ness and can maybe remove
3384
/// this system).
3385
// TODO - consider using observer OnAdd instead?
3386
pub fn allocate_metadata(
330✔
3387
    mut effects_meta: ResMut<EffectsMeta>,
3388
    mut q_metadata: Query<&mut CachedEffectMetadata>,
3389
) {
3390
    for mut metadata in &mut q_metadata {
644✔
3391
        if !metadata.table_id.is_valid() {
2✔
3392
            metadata.table_id = effects_meta
2✔
3393
                .effect_metadata_buffer
2✔
3394
                .insert(metadata.metadata);
2✔
3395
        } else {
3396
            // Unless this is the first time we allocate the GPU entry (above),
3397
            // we should never reach the beginning of this frame
3398
            // with a changed metadata which has not
3399
            // been re-uploaded last frame.
3400
            // NO! We can only detect the change *since last run of THIS system*
3401
            // so wont' see that a latter system the data.
3402
            // assert!(!metadata.is_changed());
3403
        }
3404
    }
3405
}
3406

3407
/// Update the [`CachedParentInfo`] of parent effects and the
3408
/// [`CachedChildInfo`] of child effects.
3409
pub fn allocate_parent_child_infos(
330✔
3410
    mut commands: Commands,
3411
    mut effect_cache: ResMut<EffectCache>,
3412
    mut event_cache: ResMut<EventCache>,
3413
    // All extracted child effects. May or may not already have a CachedChildInfo. If not, this
3414
    // will be spawned below.
3415
    mut q_child_effects: Query<(
3416
        Entity,
3417
        &ExtractedEffect,
3418
        &ChildEffectOf,
3419
        &CachedEffectEvents,
3420
        Option<&mut CachedChildInfo>,
3421
    )>,
3422
    // All parent effects from a previous frame (already have CachedParentInfo), which can be
3423
    // updated in-place without spawning a new CachedParentInfo.
3424
    mut q_parent_effects: Query<(
3425
        Entity,
3426
        &ExtractedEffect,
3427
        &CachedEffect,
3428
        &ChildrenEffects,
3429
        Option<&mut CachedParentInfo>,
3430
    )>,
3431
) {
3432
    #[cfg(feature = "trace")]
3433
    let _span = bevy::log::info_span!("allocate_child_infos").entered();
990✔
3434
    trace!("allocate_child_infos");
330✔
3435

3436
    // Loop on all child effects and ensure their CachedChildInfo is up-to-date.
3437
    for (child_entity, _, child_effect_of, cached_effect_events, maybe_cached_child_info) in
×
3438
        &mut q_child_effects
330✔
3439
    {
3440
        // Fetch the parent effect
UNCOV
3441
        let parent_entity = child_effect_of.parent;
×
3442
        let Ok((_, _, parent_cached_effect, children_effects, _)) =
×
UNCOV
3443
            q_parent_effects.get(parent_entity)
×
3444
        else {
3445
            warn!("Unknown parent #{parent_entity:?} on child entity {child_entity:?}, removing CachedChildInfo.");
×
3446
            if maybe_cached_child_info.is_some() {
×
3447
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3448
            }
3449
            continue;
×
3450
        };
3451

3452
        // Find the index of this child entity in its parent's storage
3453
        let Some(local_child_index) = children_effects.0.iter().position(|e| *e == child_entity)
×
3454
        else {
3455
            warn!("Cannot find child entity {child_entity:?} in the children collection of parent entity {parent_entity:?}. Relationship desync?");
×
3456
            if maybe_cached_child_info.is_some() {
×
3457
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3458
            }
3459
            continue;
×
3460
        };
UNCOV
3461
        let local_child_index = local_child_index as u32;
×
3462

3463
        // Fetch the effect buffer of the parent effect
3464
        let Some(parent_buffer_binding_source) = effect_cache
×
UNCOV
3465
            .get_slab(&parent_cached_effect.slab_id)
×
3466
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3467
        else {
3468
            warn!(
×
3469
                "Unknown parent slab #{} on parent entity {:?}, removing CachedChildInfo.",
3470
                parent_cached_effect.slab_id.index(),
×
3471
                parent_entity
3472
            );
3473
            if maybe_cached_child_info.is_some() {
×
3474
                commands.entity(child_entity).remove::<CachedChildInfo>();
×
3475
            }
3476
            continue;
×
3477
        };
3478

3479
        let new_cached_child_info = CachedChildInfo {
UNCOV
3480
            parent_slab_id: parent_cached_effect.slab_id,
×
UNCOV
3481
            parent_slab_offset: parent_cached_effect.slice.range().start,
×
UNCOV
3482
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3483
            parent_buffer_binding_source,
3484
            local_child_index,
3485
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
UNCOV
3486
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3487
        };
3488
        if let Some(mut cached_child_info) = maybe_cached_child_info {
×
3489
            if !cached_child_info.is_locally_equal(&new_cached_child_info) {
×
3490
                *cached_child_info = new_cached_child_info;
×
3491
            }
3492
        } else {
3493
            commands.entity(child_entity).insert(new_cached_child_info);
×
3494
        }
3495
    }
3496

3497
    // Loop on all parent effects and ensure their CachedParentInfo is up-to-date.
3498
    for (parent_entity, parent_extracted_effect, _, children_effects, maybe_cached_parent_info) in
×
3499
        &mut q_parent_effects
330✔
3500
    {
3501
        let parent_min_binding_size = parent_extracted_effect.particle_layout.min_binding_size32();
×
3502

3503
        // Loop over children and gather GpuChildInfo
3504
        let mut new_children = Vec::with_capacity(children_effects.0.len());
×
3505
        let mut new_child_infos = Vec::with_capacity(children_effects.0.len());
×
3506
        for child_entity in children_effects.0.iter() {
×
3507
            // Fetch the child's event buffer allocation info
3508
            let Ok((_, child_extracted_effect, _, cached_effect_events, _)) =
×
3509
                q_child_effects.get(*child_entity)
×
3510
            else {
3511
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} didnt't resolve to a child instance. The parent effect cannot be processed.");
×
3512
                if maybe_cached_parent_info.is_some() {
×
3513
                    commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3514
                }
3515
                break;
×
3516
            };
3517

3518
            // Fetch the GPU event buffer of the child
3519
            let Some(event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3520
            else {
3521
                warn!("Child entity {child_entity:?} from parent entity {parent_entity:?} doesn't have an allocated GPU event buffer. The parent effect cannot be processed.");
×
UNCOV
3522
                break;
×
3523
            };
3524

3525
            let buffer_binding_source = BufferBindingSource {
UNCOV
3526
                buffer: event_buffer.clone(),
×
UNCOV
3527
                offset: cached_effect_events.range.start,
×
UNCOV
3528
                size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
×
3529
            };
UNCOV
3530
            new_children.push((*child_entity, buffer_binding_source));
×
3531

UNCOV
3532
            new_child_infos.push(GpuChildInfo {
×
UNCOV
3533
                event_count: 0,
×
UNCOV
3534
                init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3535
            });
3536

3537
            // Ensure the particle@1 bind group layout exists for the given configuration of
3538
            // particle layout. We do this here only for effects with a parent; for those
3539
            // without a parent, we already did this in allocate_effects().
NEW
3540
            effect_cache.ensure_particle_bind_group_layout_desc(
×
UNCOV
3541
                child_extracted_effect.particle_layout.min_binding_size32(),
×
UNCOV
3542
                Some(parent_min_binding_size),
×
3543
            );
3544
        }
3545

3546
        // If we don't have all children, just abort this effect. We don't try to have
3547
        // partial relationships, this is too complex for shader bindings.
3548
        debug_assert_eq!(new_children.len(), new_child_infos.len());
×
3549
        if (new_children.len() < children_effects.len()) && maybe_cached_parent_info.is_some() {
×
3550
            warn!("One or more child effect(s) on parent effect {parent_entity:?} failed to configure. The parent effect cannot be processed.");
×
3551
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3552
            continue;
×
3553
        }
3554

3555
        // Insert or update the CachedParentInfo component of the parent effect
3556
        if let Some(mut cached_parent_info) = maybe_cached_parent_info {
×
3557
            if cached_parent_info.children != new_children {
×
3558
                // FIXME - missing way to just update in-place without changing the allocation
3559
                // size!
3560
                // if cached_parent_info.children.len() == new_children.len() {
3561
                //} else {
3562
                event_cache.reallocate_child_infos(
×
3563
                    parent_entity,
×
3564
                    new_children,
×
3565
                    &new_child_infos[..],
×
3566
                    cached_parent_info.as_mut(),
×
3567
                );
3568
                //}
3569
            }
3570
        } else {
3571
            let cached_parent_info =
×
3572
                event_cache.allocate_child_infos(parent_entity, new_children, &new_child_infos[..]);
×
3573
            commands.entity(parent_entity).insert(cached_parent_info);
×
3574
        }
3575
    }
3576
}
3577

3578
/// Prepare the init and update compute pipelines for an effect.
3579
///
3580
/// This caches the pipeline IDs once resolved, and their compiling state when
3581
/// it changes, to determine when an effect is ready to be used.
3582
///
3583
/// Note that we do that proactively even if the effect will be skipped this
3584
/// frame (for example because it's not visible). This ensures we queue pipeline
3585
/// compilations ASAP, as they can take a long time (10+ frames). We also use
3586
/// the pipeline compiling state, which we query here, to inform whether the
3587
/// effect is ready for this frame. So in general if this is a new pipeline, it
3588
/// won't be ready this frame.
3589
pub fn prepare_init_update_pipelines(
330✔
3590
    mut q_effects: Query<(
3591
        Entity,
3592
        &ExtractedEffect,
3593
        &CachedEffect,
3594
        Option<&CachedChildInfo>,
3595
        Option<&CachedParentInfo>,
3596
        Option<&CachedEffectProperties>,
3597
        &mut CachedPipelines,
3598
    )>,
3599
    // FIXME - need mut for bind group layout creation; shouldn't be create there though
3600
    mut effect_cache: ResMut<EffectCache>,
3601
    pipeline_cache: Res<PipelineCache>,
3602
    property_cache: ResMut<PropertyCache>,
3603
    init_pipeline: Res<ParticlesInitPipeline>,
3604
    update_pipeline: Res<ParticlesUpdatePipeline>,
3605
    mut specialized_init_pipelines: ResMut<SpecializedComputePipelines<ParticlesInitPipeline>>,
3606
    mut specialized_update_pipelines: ResMut<SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3607
) {
3608
    #[cfg(feature = "trace")]
3609
    let _span = bevy::log::info_span!("prepare_init_update_pipelines").entered();
990✔
3610
    trace!("prepare_init_update_pipelines");
330✔
3611

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

3615
    for (
3616
        entity,
314✔
UNCOV
3617
        extracted_effect,
×
UNCOV
3618
        cached_effect,
×
UNCOV
3619
        maybe_cached_child_info,
×
UNCOV
3620
        maybe_cached_parent_info,
×
UNCOV
3621
        maybe_cached_properties,
×
UNCOV
3622
        mut cached_pipelines,
×
3623
    ) in &mut q_effects
330✔
3624
    {
UNCOV
3625
        trace!(
×
3626
            "Preparing pipelines for effect {:?}... (flags: {:?})",
3627
            entity,
3628
            cached_pipelines.flags
314✔
3629
        );
3630

UNCOV
3631
        let particle_layout = &cached_effect.slice.particle_layout;
×
UNCOV
3632
        let particle_layout_min_binding_size = particle_layout.min_binding_size32();
×
UNCOV
3633
        let has_event_buffer = maybe_cached_child_info.is_some();
×
UNCOV
3634
        let parent_particle_layout_min_binding_size = maybe_cached_child_info
×
3635
            .as_ref()
3636
            .map(|cci| cci.parent_particle_layout.min_binding_size32());
×
3637

3638
        let Some(particle_bind_group_layout_desc) = effect_cache.particle_bind_group_layout_desc(
314✔
UNCOV
3639
            particle_layout_min_binding_size,
×
UNCOV
3640
            parent_particle_layout_min_binding_size,
×
3641
        ) else {
3642
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}",
×
3643
                particle_layout_min_binding_size, parent_particle_layout_min_binding_size);
3644
            continue;
×
3645
        };
NEW
3646
        let particle_bind_group_layout_desc = particle_bind_group_layout_desc.clone();
×
3647

3648
        // This should always exist by the time we reach this point, because we should
3649
        // have inserted any property in the cache, which would have allocated the
3650
        // proper bind group layout (or the default no-property one).
UNCOV
3651
        let property_layout_min_binding_size =
×
3652
            maybe_cached_properties.map(|cp| cp.property_layout.min_binding_size());
28✔
NEW
3653
        let spawner_bind_group_layout_desc = property_cache
×
NEW
3654
            .bind_group_layout_desc(property_layout_min_binding_size)
×
3655
            .unwrap_or_else(|| {
×
3656
                panic!(
×
3657
                    "Failed to find spawner@2 bind group layout for property binding size {:?}",
3658
                    property_layout_min_binding_size,
3659
                )
3660
            });
UNCOV
3661
        trace!(
×
3662
            "Retrieved spawner@2 bind group layout desc for property binding size {}:  {:?}.",
3663
            property_layout_min_binding_size
314✔
3664
                .as_ref()
314✔
3665
                .map(|size| size.get())
342✔
3666
                .unwrap_or(0),
314✔
3667
            spawner_bind_group_layout_desc,
3668
        );
3669

3670
        // Resolve the init pipeline
3671
        let init_pipeline_id = if let Some(init_pipeline_id) = cached_pipelines.init.as_ref() {
312✔
UNCOV
3672
            *init_pipeline_id
×
3673
        } else {
3674
            // Clear flag just in case, to ensure consistency.
3675
            cached_pipelines
2✔
3676
                .flags
2✔
3677
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3678

3679
            // Fetch the metadata@3 bind group layout from the cache
3680
            let metadata_bind_group_layout_desc = effect_cache
6✔
3681
                .metadata_init_bind_group_layout_desc(has_event_buffer)
2✔
3682
                .unwrap()
3683
                .clone();
3684

3685
            let init_pipeline_key_flags = {
2✔
3686
                let mut flags = ParticleInitPipelineKeyFlags::empty();
4✔
3687
                flags.set(
4✔
3688
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
3689
                    particle_layout.contains(Attribute::PREV),
4✔
3690
                );
3691
                flags.set(
4✔
3692
                    ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
3693
                    particle_layout.contains(Attribute::NEXT),
4✔
3694
                );
3695
                flags.set(
4✔
3696
                    ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
3697
                    has_event_buffer,
2✔
3698
                );
3699
                flags
2✔
3700
            };
3701

3702
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
10✔
3703
                pipeline_cache.as_ref(),
4✔
3704
                &init_pipeline,
4✔
3705
                ParticleInitPipelineKey {
2✔
3706
                    shader: extracted_effect.effect_shaders.init.clone(),
6✔
3707
                    particle_layout_min_binding_size,
4✔
3708
                    parent_particle_layout_min_binding_size,
4✔
3709
                    flags: init_pipeline_key_flags,
4✔
3710
                    particle_bind_group_layout_desc: particle_bind_group_layout_desc.clone(),
6✔
3711
                    spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
4✔
3712
                    metadata_bind_group_layout_desc,
2✔
3713
                },
3714
            );
3715
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
2✔
3716

3717
            cached_pipelines.init = Some(init_pipeline_id);
2✔
3718
            init_pipeline_id
2✔
3719
        };
3720

3721
        // Resolve the update pipeline
3722
        let update_pipeline_id = if let Some(update_pipeline_id) = cached_pipelines.update.as_ref()
312✔
3723
        {
UNCOV
3724
            *update_pipeline_id
×
3725
        } else {
3726
            // Clear flag just in case, to ensure consistency.
3727
            cached_pipelines
2✔
3728
                .flags
2✔
3729
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
2✔
3730

3731
            let num_event_buffers = maybe_cached_parent_info
4✔
3732
                .as_ref()
3733
                .map(|p| p.children.len() as u32)
2✔
3734
                .unwrap_or_default();
3735

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

3743
            // Fetch the bind group layouts from the cache
3744
            let metadata_bind_group_layout_desc = effect_cache
6✔
3745
                .metadata_update_bind_group_layout_desc(num_event_buffers)
2✔
3746
                .unwrap()
3747
                .clone();
3748

3749
            let update_pipeline_id = specialized_update_pipelines.specialize(
8✔
3750
                pipeline_cache.as_ref(),
4✔
3751
                &update_pipeline,
4✔
3752
                ParticleUpdatePipelineKey {
2✔
3753
                    shader: extracted_effect.effect_shaders.update.clone(),
6✔
3754
                    particle_layout: particle_layout.clone(),
6✔
3755
                    parent_particle_layout_min_binding_size,
4✔
3756
                    num_event_buffers,
4✔
3757
                    particle_bind_group_layout_desc: particle_bind_group_layout_desc.clone(),
6✔
3758
                    spawner_bind_group_layout_desc: spawner_bind_group_layout_desc.clone(),
4✔
3759
                    metadata_bind_group_layout_desc,
2✔
3760
                },
3761
            );
3762
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
2✔
3763

3764
            cached_pipelines.update = Some(update_pipeline_id);
2✔
3765
            update_pipeline_id
2✔
3766
        };
3767

3768
        // Never batch an effect with a pipeline not available; this will prevent its
3769
        // init/update pass from running, but the vfx_indirect pass will run
3770
        // nonetheless, which causes desyncs and leads to bugs.
UNCOV
3771
        if pipeline_cache
×
UNCOV
3772
            .get_compute_pipeline(init_pipeline_id)
×
3773
            .is_none()
3774
        {
3775
            trace!(
2✔
3776
                "Skipping effect from render entity {:?} due to missing or not ready init pipeline (status: {:?})",
3777
                entity,
3778
                pipeline_cache.get_compute_pipeline_state(init_pipeline_id)
4✔
3779
            );
3780
            cached_pipelines
2✔
3781
                .flags
2✔
3782
                .remove(CachedPipelineFlags::INIT_PIPELINE_READY);
2✔
3783
            continue;
2✔
3784
        }
3785

3786
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3787
        // ready
UNCOV
3788
        cached_pipelines
×
UNCOV
3789
            .flags
×
UNCOV
3790
            .insert(CachedPipelineFlags::INIT_PIPELINE_READY);
×
UNCOV
3791
        trace!("[Effect {:?}] Init pipeline ready.", entity);
×
3792

3793
        // Never batch an effect with a pipeline not available; this will prevent its
3794
        // init/update pass from running, but the vfx_indirect pass will run
3795
        // nonetheless, which causes desyncs and leads to bugs.
UNCOV
3796
        if pipeline_cache
×
UNCOV
3797
            .get_compute_pipeline(update_pipeline_id)
×
3798
            .is_none()
3799
        {
3800
            trace!(
×
3801
                "Skipping effect from render entity {:?} due to missing or not ready update pipeline (status: {:?})",
3802
                entity,
3803
                pipeline_cache.get_compute_pipeline_state(update_pipeline_id)
×
3804
            );
3805
            cached_pipelines
×
3806
                .flags
×
3807
                .remove(CachedPipelineFlags::UPDATE_PIPELINE_READY);
×
3808
            continue;
×
3809
        }
3810

3811
        // PipelineCache::get_compute_pipeline() only returns a value if the pipeline is
3812
        // ready
UNCOV
3813
        cached_pipelines
×
UNCOV
3814
            .flags
×
UNCOV
3815
            .insert(CachedPipelineFlags::UPDATE_PIPELINE_READY);
×
UNCOV
3816
        trace!("[Effect {:?}] Update pipeline ready.", entity);
×
3817
    }
3818
}
3819

3820
pub fn prepare_indirect_pipeline(
330✔
3821
    event_cache: Res<EventCache>,
3822
    mut effects_meta: ResMut<EffectsMeta>,
3823
    pipeline_cache: Res<PipelineCache>,
3824
    indirect_pipeline: Res<DispatchIndirectPipeline>,
3825
    mut specialized_indirect_pipelines: ResMut<
3826
        SpecializedComputePipelines<DispatchIndirectPipeline>,
3827
    >,
3828
) {
3829
    // Ensure the 2 variants of the indirect pipelines are created.
3830
    // TODO - move that elsewhere in some one-time setup?
3831
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
333✔
3832
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
12✔
3833
            pipeline_cache.as_ref(),
6✔
3834
            &indirect_pipeline,
3✔
3835
            DispatchIndirectPipelineKey { has_events: false },
3✔
3836
        );
3837
    }
3838
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
333✔
3839
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
12✔
3840
            pipeline_cache.as_ref(),
6✔
3841
            &indirect_pipeline,
3✔
3842
            DispatchIndirectPipelineKey { has_events: true },
3✔
3843
        );
3844
    }
3845

3846
    // Select the active one depending on whether there's any child info to consume
3847
    let is_empty = event_cache.child_infos().is_empty();
990✔
3848
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
330✔
3849
        if is_empty {
6✔
3850
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
6✔
3851
        } else {
3852
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3853
        }
3854
    } else {
3855
        // If this is the first time we insert an event buffer, we need to switch the
3856
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3857
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3858
        // more effect using GPU spawn events, we can deallocate.
3859
        let was_empty =
327✔
UNCOV
3860
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
×
3861
        if was_empty && !is_empty {
327✔
3862
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3863
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3864
        } else if is_empty && !was_empty {
654✔
3865
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3866
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3867
        }
3868
    }
3869
}
3870

3871
// TEMP - Mark all cached effects as invalid for this frame until another system
3872
// explicitly marks them as valid. Otherwise we early out in some parts, and
3873
// reuse by mistake the previous frame's extraction.
3874
pub fn clear_transient_batch_inputs(
330✔
3875
    mut commands: Commands,
3876
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3877
) {
3878
    for entity in &mut q_cached_effects {
640✔
3879
        if let Ok(mut cmd) = commands.get_entity(entity) {
310✔
UNCOV
3880
            cmd.remove::<BatchInput>();
×
3881
        }
3882
    }
3883
}
3884

3885
/// Effect mesh extracted from the main world.
3886
#[derive(Debug, Clone, Copy, PartialEq, Eq, Component)]
3887
pub(crate) struct ExtractedEffectMesh {
3888
    /// Asset of the effect mesh to draw.
3889
    pub mesh: AssetId<Mesh>,
3890
}
3891

3892
/// Indexed mesh metadata for [`CachedMesh`].
3893
#[derive(Debug, Clone)]
3894
#[allow(dead_code)]
3895
pub(crate) struct MeshIndexSlice {
3896
    /// Index format.
3897
    pub format: IndexFormat,
3898
    /// GPU buffer containing the indices.
3899
    pub buffer: Buffer,
3900
    /// Range inside [`Self::buffer`] where the indices are.
3901
    pub range: Range<u32>,
3902
}
3903

3904
impl PartialEq for MeshIndexSlice {
3905
    fn eq(&self, other: &Self) -> bool {
312✔
3906
        self.format == other.format
312✔
3907
            && self.buffer.id() == other.buffer.id()
624✔
3908
            && self.range == other.range
312✔
3909
    }
3910
}
3911

3912
impl Eq for MeshIndexSlice {}
3913

3914
/// Cached info about a mesh location in a Bevy buffer. This information is
3915
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
3916
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
3917
/// so we can invalidate that GPU data.
3918
#[derive(Debug, Clone, PartialEq, Eq, Component)]
3919
pub(crate) struct CachedMeshLocation {
3920
    /// Vertex buffer.
3921
    pub vertex_buffer: BufferId,
3922
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
3923
    pub vertex_or_index_count: u32,
3924
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
3925
    pub first_index_or_vertex_offset: u32,
3926
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
3927
    pub vertex_offset_or_base_instance: i32,
3928
    /// Indexed rendering metadata.
3929
    pub indexed: Option<MeshIndexSlice>,
3930
}
3931

3932
bitflags! {
3933
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3934
    pub struct CachedPipelineFlags: u8 {
3935
        const NONE = 0;
3936
        /// The init pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
3937
        const INIT_PIPELINE_READY = (1u8 << 0);
3938
        /// The update pipeline for this effect is ready for use. This means the compute pipeline is compiled and cached.
3939
        const UPDATE_PIPELINE_READY = (1u8 << 1);
3940
    }
3941
}
3942

3943
impl Default for CachedPipelineFlags {
3944
    fn default() -> Self {
2✔
3945
        Self::NONE
2✔
3946
    }
3947
}
3948

3949
/// Render world cached shader pipelines for a [`CachedEffect`].
3950
///
3951
/// This is updated with the IDs of the pipelines when they are queued for
3952
/// compiling, and with the state of those pipelines to detect when the effect
3953
/// is ready to be used.
3954
///
3955
/// This component is always auto-inserted alongside [`ExtractedEffect`] as soon
3956
/// as a new effect instance is spawned, because it contains the readiness state
3957
/// of those pipelines, which we want to query each frame. The pipelines are
3958
/// also mandatory, so this component is always needed.
3959
#[derive(Debug, Default, Component)]
3960
pub(crate) struct CachedPipelines {
3961
    /// Caching flags indicating the pipelines readiness.
3962
    pub flags: CachedPipelineFlags,
3963
    /// ID of the cached init pipeline. This is valid once the pipeline is
3964
    /// queued for compilation, but this doesn't mean the pipeline is ready for
3965
    /// use. Readiness is encoded in [`Self::flags`].
3966
    pub init: Option<CachedComputePipelineId>,
3967
    /// ID of the cached update pipeline. This is valid once the pipeline is
3968
    /// queued for compilation, but this doesn't mean the pipeline is ready for
3969
    /// use. Readiness is encoded in [`Self::flags`].
3970
    pub update: Option<CachedComputePipelineId>,
3971
}
3972

3973
impl CachedPipelines {
3974
    /// Check if all pipelines for this effect are ready.
3975
    #[inline]
3976
    pub fn is_ready(&self) -> bool {
628✔
3977
        self.flags.contains(
1,256✔
3978
            CachedPipelineFlags::INIT_PIPELINE_READY | CachedPipelineFlags::UPDATE_PIPELINE_READY,
628✔
3979
        )
3980
    }
3981
}
3982

3983
/// Ready state for this effect.
3984
///
3985
/// An effect is ready if:
3986
/// - Its init and update pipelines are ready, as reported by
3987
///   [`CachedPipelines::is_ready()`].
3988
///
3989
/// This components holds the calculated ready state propagated from all
3990
/// ancestor effects, if any. That propagation is done by the
3991
/// [`propagate_ready_state()`] system.
3992
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Component)]
3993
pub(crate) struct CachedReadyState {
3994
    is_ready: bool,
3995
}
3996

3997
impl CachedReadyState {
3998
    #[inline(always)]
3999
    pub fn new(is_ready: bool) -> Self {
314✔
4000
        Self { is_ready }
4001
    }
4002

4003
    #[inline(always)]
4004
    pub fn and(mut self, ancestors_ready: bool) -> Self {
×
4005
        self.and_with(ancestors_ready);
×
4006
        self
×
4007
    }
4008

4009
    #[inline(always)]
4010
    pub fn and_with(&mut self, ancestors_ready: bool) {
×
4011
        self.is_ready = self.is_ready && ancestors_ready;
×
4012
    }
4013

4014
    #[inline(always)]
4015
    pub fn is_ready(&self) -> bool {
628✔
4016
        self.is_ready
628✔
4017
    }
4018
}
4019

4020
#[derive(SystemParam)]
4021
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
4022
    sim_params: Res<'w, SimParams>,
4023
    render_device: Res<'w, RenderDevice>,
4024
    render_queue: Res<'w, RenderQueue>,
4025
    marker: PhantomData<&'s usize>,
4026
}
4027

4028
/// Update the ready state of all effects, and propagate recursively to
4029
/// children.
4030
pub(crate) fn propagate_ready_state(
330✔
4031
    mut q_root_effects: Query<
4032
        (
4033
            Entity,
4034
            Option<&ChildrenEffects>,
4035
            Ref<CachedPipelines>,
4036
            &mut CachedReadyState,
4037
        ),
4038
        Without<ChildEffectOf>,
4039
    >,
4040
    mut orphaned: RemovedComponents<ChildEffectOf>,
4041
    q_ready_state: Query<
4042
        (
4043
            Ref<CachedPipelines>,
4044
            &mut CachedReadyState,
4045
            Option<&ChildrenEffects>,
4046
        ),
4047
        With<ChildEffectOf>,
4048
    >,
4049
    q_child_effects: Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4050
    mut orphaned_entities: Local<Vec<Entity>>,
4051
) {
4052
    #[cfg(feature = "trace")]
4053
    let _span = bevy::log::info_span!("propagate_ready_state").entered();
990✔
4054
    trace!("propagate_ready_state");
330✔
4055

4056
    // Update orphaned list for this frame, and sort it so we can efficiently binary
4057
    // search it
4058
    orphaned_entities.clear();
330✔
4059
    orphaned_entities.extend(orphaned.read());
990✔
4060
    orphaned_entities.sort_unstable();
330✔
4061

4062
    // Iterate in parallel over all root effects (those without any parent). This is
4063
    // the most common case, so should take care of the heavy lifting of propagating
4064
    // to most effects. For child effects, we then descend recursively.
4065
    q_root_effects.par_iter_mut().for_each(
990✔
4066
        |(entity, maybe_children, cached_pipelines, mut cached_ready_state)| {
314✔
4067
            // Update the ready state of this root effect
4068
            let changed = cached_pipelines.is_changed() || cached_ready_state.is_added() || orphaned_entities.binary_search(&entity).is_ok();
942✔
4069
            trace!("[Entity {}] changed={} cached_pipelines={} ready_state={}", entity, changed, cached_pipelines.is_ready(), cached_ready_state.is_ready);
942✔
4070
            if changed {
314✔
4071
                // Root effects by default are ready since they have no ancestors to check. After that we check the ready conditions for this effect alone.
4072
                let new_ready_state = CachedReadyState::new(cached_pipelines.is_ready());
942✔
4073
                if *cached_ready_state != new_ready_state {
314✔
4074
                    debug!(
2✔
4075
                        "[Entity {}] Changed ready to: {}",
4076
                        entity,
4077
                        new_ready_state.is_ready()
4✔
4078
                    );
4079
                    *cached_ready_state = new_ready_state;
2✔
4080
                }
4081
            }
4082

4083
            // Recursively update the ready state of its descendants
4084
            if let Some(children) = maybe_children {
314✔
4085
                for (child, child_of) in q_child_effects.iter_many(children) {
×
4086
                    assert_eq!(
×
4087
                        child_of.parent, entity,
×
4088
                        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
4089
                    );
4090
                    // SAFETY:
4091
                    // - `child` must have consistent parentage, or the above assertion would panic.
4092
                    //   Since `child` is parented to a root entity, the entire hierarchy leading to it
4093
                    //   is consistent.
4094
                    // - We may operate as if all descendants are consistent, since
4095
                    //   `propagate_ready_state_recursive` will panic before continuing to propagate if it
4096
                    //   encounters an entity with inconsistent parentage.
4097
                    // - Since each root entity is unique and the hierarchy is consistent and
4098
                    //   forest-like, other root entities' `propagate_ready_state_recursive` calls will not conflict
4099
                    //   with this one.
4100
                    // - Since this is the only place where `transform_query` gets used, there will be
4101
                    //   no conflicting fetches elsewhere.
4102
                    #[expect(unsafe_code, reason = "`propagate_ready_state_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
4103
                    unsafe {
4104
                        propagate_ready_state_recursive(
×
UNCOV
4105
                            &cached_ready_state,
×
UNCOV
4106
                            &q_ready_state,
×
UNCOV
4107
                            &q_child_effects,
×
UNCOV
4108
                            child,
×
4109
                            changed || child_of.is_changed(),
×
4110
                        );
4111
                    }
4112
                }
4113
            }
4114
        },
4115
    );
4116
}
4117

4118
#[expect(
4119
    unsafe_code,
4120
    reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
4121
)]
4122
unsafe fn propagate_ready_state_recursive(
×
4123
    parent_state: &CachedReadyState,
4124
    q_ready_state: &Query<
4125
        (
4126
            Ref<CachedPipelines>,
4127
            &mut CachedReadyState,
4128
            Option<&ChildrenEffects>,
4129
        ),
4130
        With<ChildEffectOf>,
4131
    >,
4132
    q_child_of: &Query<(Entity, Ref<ChildEffectOf>), With<CachedReadyState>>,
4133
    entity: Entity,
4134
    mut changed: bool,
4135
) {
4136
    // Update this effect in-place by checking its own state and the state of its
4137
    // parent (which has already been propagated from all the parent's ancestors, so
4138
    // is correct for this frame).
4139
    let (cached_ready_state, maybe_children) = {
×
UNCOV
4140
        let Ok((cached_pipelines, mut cached_ready_state, maybe_children)) =
×
4141
        // SAFETY: Copied from Bevy's transform propagation, same reasoning
4142
        (unsafe { q_ready_state.get_unchecked(entity) }) else {
×
4143
            return;
×
4144
        };
4145

4146
        changed |= cached_pipelines.is_changed() || cached_ready_state.is_added();
×
UNCOV
4147
        if changed {
×
4148
            let new_ready_state =
×
4149
                CachedReadyState::new(parent_state.is_ready()).and(cached_pipelines.is_ready());
×
4150
            // Ensure we don't trigger ECS change detection here if state didn't change, so
4151
            // we can avoid this effect branch on next iteration.
4152
            if *cached_ready_state != new_ready_state {
×
4153
                debug!(
×
4154
                    "[Entity {}] Changed ready to: {}",
4155
                    entity,
4156
                    new_ready_state.is_ready()
×
4157
                );
4158
                *cached_ready_state = new_ready_state;
×
4159
            }
4160
        }
UNCOV
4161
        (cached_ready_state, maybe_children)
×
4162
    };
4163

4164
    // Recurse into descendants
4165
    let Some(children) = maybe_children else {
×
4166
        return;
×
4167
    };
4168
    for (child, child_of) in q_child_of.iter_many(children) {
×
4169
        assert_eq!(
×
4170
        child_of.parent, entity,
×
4171
        "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
4172
    );
4173
        // SAFETY: The caller guarantees that `transform_query` will not be fetched for
4174
        // any descendants of `entity`, so it is safe to call
4175
        // `propagate_recursive` for each child.
4176
        //
4177
        // The above assertion ensures that each child has one and only one unique
4178
        // parent throughout the entire hierarchy.
4179
        unsafe {
4180
            propagate_ready_state_recursive(
UNCOV
4181
                cached_ready_state.as_ref(),
×
UNCOV
4182
                q_ready_state,
×
UNCOV
4183
                q_child_of,
×
UNCOV
4184
                child,
×
4185
                changed || child_of.is_changed(),
×
4186
            );
4187
        }
4188
    }
4189
}
4190

4191
/// Once all effects are extracted and all cached components are updated, it's
4192
/// time to prepare for sorting and batching. Collect all relevant data and
4193
/// insert/update the [`BatchInput`] for each effect.
4194
pub(crate) fn prepare_batch_inputs(
330✔
4195
    mut commands: Commands,
4196
    read_only_params: PrepareEffectsReadOnlyParams,
4197
    pipeline_cache: Res<PipelineCache>,
4198
    mut effects_meta: ResMut<EffectsMeta>,
4199
    mut effect_bind_groups: ResMut<EffectBindGroups>,
4200
    mut property_bind_groups: ResMut<PropertyBindGroups>,
4201
    q_cached_effects: Query<(
4202
        MainEntity,
4203
        Entity,
4204
        &ExtractedEffect,
4205
        &ExtractedSpawner,
4206
        &CachedEffect,
4207
        &CachedEffectMetadata,
4208
        &CachedReadyState,
4209
        &CachedPipelines,
4210
        Option<&CachedDrawIndirectArgs>,
4211
        Option<&CachedParentInfo>,
4212
        Option<&ChildEffectOf>,
4213
        Option<&CachedChildInfo>,
4214
        Option<&CachedEffectEvents>,
4215
    )>,
4216
    mut sort_bind_groups: ResMut<SortBindGroups>,
4217
) {
4218
    #[cfg(feature = "trace")]
4219
    let _span = bevy::log::info_span!("prepare_batch_inputs").entered();
990✔
4220
    trace!("prepare_batch_inputs");
330✔
4221

4222
    // Workaround for too many params in system (TODO: refactor to split work?)
4223
    let sim_params = read_only_params.sim_params.into_inner();
990✔
4224
    let render_device = read_only_params.render_device.into_inner();
990✔
4225
    let render_queue = read_only_params.render_queue.into_inner();
990✔
4226

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

4230
    // Build batcher inputs from extracted effects, updating all cached components
4231
    // for each effect on the fly.
4232
    let mut extracted_effect_count = 0;
660✔
4233
    let mut prepared_effect_count = 0;
660✔
4234
    for (
4235
        main_entity,
314✔
4236
        render_entity,
314✔
4237
        extracted_effect,
314✔
4238
        extracted_spawner,
314✔
4239
        cached_effect,
314✔
4240
        cached_effect_metadata,
314✔
4241
        cached_ready_state,
314✔
4242
        cached_pipelines,
314✔
4243
        maybe_cached_draw_indirect_args,
314✔
4244
        maybe_cached_parent_info,
314✔
4245
        maybe_child_effect_of,
314✔
4246
        maybe_cached_child_info,
314✔
4247
        maybe_cached_effect_events,
314✔
4248
    ) in &q_cached_effects
330✔
4249
    {
4250
        extracted_effect_count += 1;
314✔
4251

4252
        // Skip this effect if not ready
4253
        if !cached_ready_state.is_ready() {
314✔
4254
            trace!("Pipelines not ready for effect {}, skipped.", render_entity);
2✔
UNCOV
4255
            continue;
×
4256
        }
4257

4258
        // Skip this effect if not visible and not simulating when hidden
4259
        if !extracted_spawner.is_visible
312✔
4260
            && (extracted_effect.simulation_condition == SimulationCondition::WhenVisible)
×
4261
        {
4262
            trace!(
×
4263
                "Effect {} not visible, and simulation condition is WhenVisible, so skipped.",
4264
                render_entity
4265
            );
4266
            continue;
×
4267
        }
4268

4269
        // Fetch the init and update pipelines.
4270
        // SAFETY: If is_ready() returns true, this means the pipelines are cached and
4271
        // ready, so the IDs must be valid.
4272
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
UNCOV
4273
            init: cached_pipelines.init.unwrap(),
×
UNCOV
4274
            update: cached_pipelines.update.unwrap(),
×
4275
        };
4276

4277
        let effect_slice = EffectSlice {
UNCOV
4278
            slice: cached_effect.slice.range(),
×
UNCOV
4279
            slab_id: cached_effect.slab_id,
×
UNCOV
4280
            particle_layout: cached_effect.slice.particle_layout.clone(),
×
4281
        };
4282

4283
        // Fetch the bind group layouts from the cache
UNCOV
4284
        trace!("child_effect_of={:?}", maybe_child_effect_of);
×
4285
        let parent_slab_id = if let Some(child_effect_of) = maybe_child_effect_of {
312✔
4286
            let Ok((_, _, _, _, parent_cached_effect, _, _, _, _, _, _, _, _)) =
×
UNCOV
4287
                q_cached_effects.get(child_effect_of.parent)
×
4288
            else {
4289
                // At this point we should have discarded invalid effects with a missing parent,
4290
                // so if the parent is not found this is a bug.
4291
                error!(
×
4292
                    "Effect main_entity {:?}: parent render entity {:?} not found.",
4293
                    main_entity, child_effect_of.parent
4294
                );
4295
                continue;
×
4296
            };
UNCOV
4297
            Some(parent_cached_effect.slab_id)
×
4298
        } else {
4299
            None
312✔
4300
        };
4301

4302
        // For ribbons, we need the sorting pipeline to be ready to sort the ribbon's
4303
        // particles by age in order to build a contiguous mesh.
UNCOV
4304
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
×
4305
            // Ensure the bind group layout for sort-fill is ready. This will also ensure
4306
            // the pipeline is created and queued if needed.
NEW
4307
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout_desc(
×
4308
                &pipeline_cache,
×
4309
                &extracted_effect.particle_layout,
×
4310
            ) {
UNCOV
4311
                error!(
×
4312
                    "Failed to create bind group for ribbon effect sorting: {:?}",
4313
                    err
4314
                );
UNCOV
4315
                continue;
×
4316
            }
4317

4318
            // Check sort pipelines are ready, otherwise we might desync some buffers if
4319
            // running only some of them but not all.
4320
            if !sort_bind_groups
×
4321
                .is_pipeline_ready(&extracted_effect.particle_layout, &pipeline_cache)
×
4322
            {
4323
                trace!(
×
4324
                    "Sort pipeline not ready for effect on main entity {:?}; skipped.",
4325
                    main_entity
4326
                );
UNCOV
4327
                continue;
×
4328
            }
4329
        }
4330

4331
        // Output some debug info
4332
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
312✔
UNCOV
4333
        trace!(
×
4334
            "update_shader = {:?}",
4335
            extracted_effect.effect_shaders.update
4336
        );
UNCOV
4337
        trace!(
×
4338
            "render_shader = {:?}",
4339
            extracted_effect.effect_shaders.render
4340
        );
UNCOV
4341
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
×
UNCOV
4342
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
×
4343

UNCOV
4344
        let parent_slab_offset = maybe_cached_child_info.map(|cci| cci.parent_slab_offset);
×
4345

UNCOV
4346
        assert!(cached_effect_metadata.table_id.is_valid());
×
4347
        let spawner_index = effects_meta.allocate_spawner(
312✔
UNCOV
4348
            &extracted_spawner.transform,
×
UNCOV
4349
            extracted_spawner.spawn_count,
×
UNCOV
4350
            extracted_spawner.prng_seed,
×
UNCOV
4351
            cached_effect.slice.range().start,
×
UNCOV
4352
            parent_slab_offset,
×
UNCOV
4353
            cached_effect_metadata.table_id,
×
UNCOV
4354
            maybe_cached_draw_indirect_args,
×
4355
        );
4356

UNCOV
4357
        trace!("Updating cached effect at entity {render_entity:?}...");
×
UNCOV
4358
        let mut cmd = commands.entity(render_entity);
×
4359
        // Inserting the BatchInput component marks the effect as ready for this frame
UNCOV
4360
        cmd.insert(BatchInput {
×
UNCOV
4361
            effect_slice,
×
UNCOV
4362
            init_and_update_pipeline_ids,
×
UNCOV
4363
            parent_slab_id,
×
UNCOV
4364
            event_buffer_index: maybe_cached_effect_events.map(|cee| cee.buffer_index),
×
UNCOV
4365
            child_effects: maybe_cached_parent_info
×
UNCOV
4366
                .as_ref()
×
4367
                .map(|cp| cp.children.clone())
×
UNCOV
4368
                .unwrap_or_default(),
×
UNCOV
4369
            spawner_index,
×
UNCOV
4370
            init_indirect_dispatch_index: maybe_cached_child_info
×
UNCOV
4371
                .as_ref()
×
UNCOV
4372
                .map(|cc| cc.init_indirect_dispatch_index),
×
4373
        });
4374

UNCOV
4375
        prepared_effect_count += 1;
×
4376
    }
4377
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
330✔
4378

4379
    // Update simulation parameters, including the total effect count for this frame
4380
    {
UNCOV
4381
        let mut gpu_sim_params: GpuSimParams = sim_params.into();
×
UNCOV
4382
        gpu_sim_params.num_effects = prepared_effect_count;
×
UNCOV
4383
        trace!(
×
4384
            "Simulation parameters: time={} delta_time={} virtual_time={} \
4385
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
4386
            gpu_sim_params.time,
4387
            gpu_sim_params.delta_time,
4388
            gpu_sim_params.virtual_time,
4389
            gpu_sim_params.virtual_delta_time,
4390
            gpu_sim_params.real_time,
4391
            gpu_sim_params.real_delta_time,
4392
            gpu_sim_params.num_effects,
4393
        );
UNCOV
4394
        effects_meta.sim_params_uniforms.set(gpu_sim_params);
×
4395
    }
4396

4397
    // Write the entire spawner buffer for this frame, for all effects combined
UNCOV
4398
    assert_eq!(
×
4399
        prepared_effect_count,
UNCOV
4400
        effects_meta.spawner_buffer.len() as u32
×
4401
    );
4402
    if effects_meta
330✔
4403
        .spawner_buffer
330✔
4404
        .write_buffer(render_device, render_queue)
990✔
4405
    {
4406
        // All property bind groups use the spawner buffer, which was reallocate
4407
        effect_bind_groups.particle_slabs.clear();
6✔
4408
        property_bind_groups.clear(true);
4✔
4409
        effects_meta.indirect_spawner_bind_group = None;
2✔
4410
    }
4411
}
4412

4413
/// Batch compatible effects together into a single pass.
4414
///
4415
/// For all effects marked as ready for this frame (have a BatchInput
4416
/// component), sort the effects by grouping compatible effects together, then
4417
/// batch those groups together. Each batch can be updated and rendered with a
4418
/// single compute dispatch or draw call.
4419
pub(crate) fn batch_effects(
330✔
4420
    mut commands: Commands,
4421
    effects_meta: Res<EffectsMeta>,
4422
    mut sort_bind_groups: ResMut<SortBindGroups>,
4423
    mut q_cached_effects: Query<(
4424
        Entity,
4425
        &MainEntity,
4426
        &ExtractedEffect,
4427
        &ExtractedSpawner,
4428
        &ExtractedEffectMesh,
4429
        &CachedDrawIndirectArgs,
4430
        &CachedEffectMetadata,
4431
        Option<&CachedEffectEvents>,
4432
        Option<&ChildEffectOf>,
4433
        Option<&CachedChildInfo>,
4434
        Option<&CachedEffectProperties>,
4435
        &mut DispatchBufferIndices,
4436
        // The presence of BatchInput ensure the effect is ready
4437
        &mut BatchInput,
4438
    )>,
4439
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4440
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4441
) {
4442
    #[cfg(feature = "trace")]
4443
    let _span = bevy::log::info_span!("batch_effects").entered();
990✔
4444
    trace!("batch_effects");
330✔
4445

4446
    // Sort effects in batching order, so that we can batch by simply doing a linear
4447
    // scan of the effects in this order. Currently compatible effects mean:
4448
    // - same effect slab (so we can bind the buffers once for all batched effects)
4449
    // - in order of increasing sub-allocation inside those buffers (to make the
4450
    //   sort stable)
4451
    // - with parents before their children, to ensure ???? FIXME don't we need to
4452
    //   opposite?!!!
4453
    let mut effect_sorter = EffectSorter::new();
660✔
4454
    for (entity, _, _, _, _, _, _, _, child_of, _, _, _, input) in &q_cached_effects {
642✔
UNCOV
4455
        effect_sorter.insert(
×
UNCOV
4456
            entity,
×
UNCOV
4457
            input.effect_slice.slab_id,
×
UNCOV
4458
            input.effect_slice.slice.start,
×
UNCOV
4459
            child_of.map(|co| co.parent),
×
4460
        );
4461
    }
4462
    effect_sorter.sort();
660✔
4463

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

4468
    let mut sort_queue = GpuBufferOperationQueue::new();
660✔
4469

4470
    // Loop on all extracted effects in sorted order, and try to batch them together
4471
    // to reduce draw calls. -- currently does nothing, batching was broken and
4472
    // never fixed, but at least we minimize the GPU state changes with the sorting!
4473
    trace!("Batching {} effects...", q_cached_effects.iter().len());
970✔
4474
    sorted_effect_batches.clear();
330✔
4475
    for entity in effect_sorter.effects.iter().map(|e| e.entity) {
972✔
4476
        let Ok((
4477
            entity,
312✔
UNCOV
4478
            main_entity,
×
UNCOV
4479
            extracted_effect,
×
UNCOV
4480
            extracted_spawner,
×
UNCOV
4481
            extracted_effect_mesh,
×
UNCOV
4482
            cached_draw_indirect_args,
×
UNCOV
4483
            cached_effect_metadata,
×
UNCOV
4484
            cached_effect_events,
×
4485
            _,
UNCOV
4486
            cached_child_info,
×
UNCOV
4487
            cached_properties,
×
UNCOV
4488
            dispatch_buffer_indices,
×
UNCOV
4489
            mut input,
×
4490
        )) = q_cached_effects.get_mut(entity)
624✔
4491
        else {
4492
            continue;
×
4493
        };
4494

UNCOV
4495
        let translation = extracted_spawner.transform.translation();
×
4496

4497
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4498
        // most of the data needed to drive rendering. However this doesn't drive
4499
        // rendering; this is just storage.
4500
        let mut effect_batch = EffectBatch::from_input(
UNCOV
4501
            main_entity.id(),
×
UNCOV
4502
            extracted_effect,
×
UNCOV
4503
            extracted_spawner,
×
UNCOV
4504
            extracted_effect_mesh,
×
UNCOV
4505
            cached_effect_events,
×
UNCOV
4506
            cached_child_info,
×
UNCOV
4507
            &mut input,
×
UNCOV
4508
            *dispatch_buffer_indices,
×
UNCOV
4509
            cached_draw_indirect_args.row,
×
UNCOV
4510
            cached_effect_metadata.table_id,
×
UNCOV
4511
            cached_properties.map(|cp| PropertyBindGroupKey {
×
4512
                buffer_index: cp.buffer_index,
13✔
4513
                binding_size: cp.property_layout.min_binding_size().get() as u32,
26✔
4514
            }),
UNCOV
4515
            cached_properties.map(|cp| cp.range.start),
×
4516
        );
4517

4518
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4519
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4520
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4521
        // ribbon).
UNCOV
4522
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
×
4523
            // This buffer is allocated in prepare_effects(), so should always be available
4524
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4525
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4526
                continue;
×
4527
            };
4528

4529
            // Allocate a GpuDispatchIndirect entry
UNCOV
4530
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
×
UNCOV
4531
            effect_batch.sort_fill_indirect_dispatch_index =
×
UNCOV
4532
                Some(sort_fill_indirect_dispatch_index);
×
4533

4534
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4535
            // compute a number of workgroups to dispatch based on that particle count, and
4536
            // store the result into a GpuDispatchIndirect struct which will be used to
4537
            // dispatch the fill-sort pass.
4538
            {
UNCOV
4539
                let src_buffer = effect_metadata_buffer.clone();
×
UNCOV
4540
                let src_binding_offset = effects_meta
×
UNCOV
4541
                    .effect_metadata_buffer
×
UNCOV
4542
                    .dynamic_offset(effect_batch.metadata_table_id);
×
UNCOV
4543
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
×
4544
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4545
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4546
                    continue;
×
4547
                };
UNCOV
4548
                let dst_buffer = dst_buffer.clone();
×
UNCOV
4549
                let dst_binding_offset = 0; // see dst_offset below
×
4550
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
UNCOV
4551
                trace!(
×
4552
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
4553
                    src_buffer.id(),
×
4554
                    src_binding_offset,
4555
                    src_binding_size.get(),
×
4556
                    dst_buffer.id(),
×
4557
                    dst_binding_offset,
4558
                    -1, //dst_binding_size.get(),
4559
                );
UNCOV
4560
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
×
UNCOV
4561
                debug_assert_eq!(
×
4562
                    src_offset, 1,
4563
                    "GpuEffectMetadata changed, update this assert."
4564
                );
4565
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4566
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4567
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4568
                // binding offset of 0 and binds the entire destination buffer,
4569
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4570
                // index the proper offset in the buffer. This requires of
4571
                // course binding the entire buffer, or at least enough to index all operations
4572
                // (hence the None below). This is not really a general solution, so should be
4573
                // reviewed.
4574
                let dst_offset = sort_bind_groups
×
UNCOV
4575
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
×
UNCOV
4576
                    / 4;
×
UNCOV
4577
                sort_queue.enqueue(
×
UNCOV
4578
                    GpuBufferOperationType::FillDispatchArgs,
×
UNCOV
4579
                    GpuBufferOperationArgs {
×
UNCOV
4580
                        src_offset,
×
UNCOV
4581
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
×
UNCOV
4582
                        dst_offset,
×
UNCOV
4583
                        dst_stride: GpuDispatchIndirectArgs::SHADER_SIZE.get() as u32 / 4,
×
UNCOV
4584
                        count: 1,
×
4585
                    },
UNCOV
4586
                    src_buffer,
×
UNCOV
4587
                    src_binding_offset,
×
UNCOV
4588
                    Some(src_binding_size),
×
UNCOV
4589
                    dst_buffer,
×
UNCOV
4590
                    dst_binding_offset,
×
UNCOV
4591
                    None, //Some(dst_binding_size),
×
4592
                );
4593
            }
4594
        }
4595

4596
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
312✔
UNCOV
4597
        trace!(
×
4598
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
4599
            effect_batch_index,
4600
            entity,
4601
        );
4602

4603
        // Spawn an EffectDrawBatch, to actually drive rendering.
UNCOV
4604
        commands
×
UNCOV
4605
            .spawn(EffectDrawBatch {
×
UNCOV
4606
                effect_batch_index,
×
UNCOV
4607
                translation,
×
UNCOV
4608
                main_entity: *main_entity,
×
4609
            })
UNCOV
4610
            .insert(TemporaryRenderEntity);
×
4611
    }
4612

4613
    gpu_buffer_operations.begin_frame();
330✔
UNCOV
4614
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
×
4615
    if !sort_queue.operation_queue.is_empty() {
330✔
4616
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4617
    }
4618
}
4619

4620
/// Per-buffer bind groups for a GPU effect buffer.
4621
///
4622
/// This contains all bind groups specific to a single [`EffectBuffer`].
4623
///
4624
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4625
pub(crate) struct BufferBindGroups {
4626
    /// Bind group for the render shader.
4627
    ///
4628
    /// ```wgsl
4629
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4630
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4631
    /// @binding(2) var<storage, read> spawner : Spawner;
4632
    /// ```
4633
    render: BindGroup,
4634
    // /// Bind group for filling the indirect dispatch arguments of any child init
4635
    // /// pass.
4636
    // ///
4637
    // /// This bind group is optional; it's only created if the current effect has
4638
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4639
    // /// (although normally the event buffer is not created if there's no
4640
    // /// children).
4641
    // ///
4642
    // /// The source buffer is always the current effect's event buffer. The
4643
    // /// destination buffer is the global shared buffer for indirect fill args
4644
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4645
    // /// args contains the data to index the relevant part of the global shared
4646
    // /// buffer for this effect buffer; it may contain multiple entries in case
4647
    // /// multiple effects are batched inside the current effect buffer.
4648
    // ///
4649
    // /// ```wgsl
4650
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4651
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4652
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4653
    // /// ```
4654
    // init_fill_dispatch: Option<BindGroup>,
4655
}
4656

4657
/// Combination of a texture layout and the bound textures.
4658
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4659
struct Material {
4660
    layout: TextureLayout,
4661
    textures: Vec<AssetId<Image>>,
4662
}
4663

4664
impl Material {
4665
    /// Get the bind group entries to create a bind group.
4666
    pub fn make_entries<'a>(
×
4667
        &self,
4668
        gpu_images: &'a RenderAssets<GpuImage>,
4669
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4670
        if self.textures.is_empty() {
×
4671
            return Ok(vec![]);
×
4672
        }
4673

4674
        let entries: Vec<BindGroupEntry<'a>> = self
×
4675
            .textures
×
4676
            .iter()
4677
            .enumerate()
4678
            .flat_map(|(index, id)| {
×
4679
                let base_binding = index as u32 * 2;
×
4680
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4681
                    vec![
×
4682
                        BindGroupEntry {
×
4683
                            binding: base_binding,
×
4684
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4685
                        },
4686
                        BindGroupEntry {
×
4687
                            binding: base_binding + 1,
×
4688
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4689
                        },
4690
                    ]
4691
                } else {
4692
                    vec![]
×
4693
                }
4694
            })
4695
            .collect();
4696
        if entries.len() == self.textures.len() * 2 {
×
4697
            return Ok(entries);
×
4698
        }
4699
        Err(())
×
4700
    }
4701
}
4702

4703
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4704
struct BindingKey {
4705
    pub buffer_id: BufferId,
4706
    pub offset: u32,
4707
    pub size: NonZeroU32,
4708
}
4709

4710
impl<'a> From<BufferSlice<'a>> for BindingKey {
4711
    fn from(value: BufferSlice<'a>) -> Self {
×
4712
        Self {
4713
            buffer_id: value.buffer.id(),
×
4714
            offset: value.offset,
×
4715
            size: value.size,
×
4716
        }
4717
    }
4718
}
4719

4720
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4721
    fn from(value: &BufferSlice<'a>) -> Self {
×
4722
        Self {
4723
            buffer_id: value.buffer.id(),
×
4724
            offset: value.offset,
×
4725
            size: value.size,
×
4726
        }
4727
    }
4728
}
4729

4730
impl From<&BufferBindingSource> for BindingKey {
4731
    fn from(value: &BufferBindingSource) -> Self {
×
4732
        Self {
4733
            buffer_id: value.buffer.id(),
×
4734
            offset: value.offset,
×
4735
            size: value.size,
×
4736
        }
4737
    }
4738
}
4739

4740
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4741
struct ConsumeEventKey {
4742
    child_infos_buffer_id: BufferId,
4743
    events: BindingKey,
4744
}
4745

4746
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4747
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4748
        Self {
4749
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4750
            events: value.events.into(),
×
4751
        }
4752
    }
4753
}
4754

4755
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4756
struct InitMetadataBindGroupKey {
4757
    pub slab_id: SlabId,
4758
    pub effect_metadata_buffer: BufferId,
4759
    pub consume_event_key: Option<ConsumeEventKey>,
4760
}
4761

4762
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4763
struct UpdateMetadataBindGroupKey {
4764
    pub slab_id: SlabId,
4765
    pub effect_metadata_buffer: BufferId,
4766
    pub child_info_buffer_id: Option<BufferId>,
4767
    pub event_buffers_keys: Vec<BindingKey>,
4768
}
4769

4770
/// Bind group cached with an associated key.
4771
///
4772
/// The cached bind group is associated with the given key representing the
4773
/// inputs that the bind group depends on. When those inputs change, the key
4774
/// should change, indicating the bind group needs to be recreated.
4775
///
4776
/// This object manages a single bind group and its key.
4777
struct CachedBindGroup<K: Eq> {
4778
    /// Key the bind group was created from. Each time the key changes, the bind
4779
    /// group should be re-created.
4780
    key: K,
4781
    /// Bind group created from the key.
4782
    bind_group: BindGroup,
4783
}
4784

4785
#[derive(Debug, Clone, Copy)]
4786
struct BufferSlice<'a> {
4787
    pub buffer: &'a Buffer,
4788
    pub offset: u32,
4789
    pub size: NonZeroU32,
4790
}
4791

4792
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4793
    fn from(value: BufferSlice<'a>) -> Self {
×
4794
        Self {
4795
            buffer: value.buffer,
×
4796
            offset: value.offset.into(),
×
4797
            size: Some(value.size.into()),
×
4798
        }
4799
    }
4800
}
4801

4802
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4803
    fn from(value: &BufferSlice<'a>) -> Self {
×
4804
        Self {
4805
            buffer: value.buffer,
×
4806
            offset: value.offset.into(),
×
4807
            size: Some(value.size.into()),
×
4808
        }
4809
    }
4810
}
4811

4812
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4813
    fn from(value: &'a BufferBindingSource) -> Self {
×
4814
        Self {
4815
            buffer: &value.buffer,
×
4816
            offset: value.offset,
×
4817
            size: value.size,
×
4818
        }
4819
    }
4820
}
4821

4822
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4823
/// the init pass consumes GPU events as a mechanism to spawn particles.
4824
struct ConsumeEventBuffers<'a> {
4825
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4826
    /// This is dynamically indexed inside the shader.
4827
    child_infos_buffer: &'a Buffer,
4828
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4829
    events: BufferSlice<'a>,
4830
}
4831

4832
#[derive(Default, Resource)]
4833
pub struct EffectBindGroups {
4834
    /// Map from a slab ID to the bind groups shared among all effects that
4835
    /// use that particle slab.
4836
    particle_slabs: HashMap<SlabId, BufferBindGroups>,
4837
    /// Map of bind groups for image assets used as particle textures.
4838
    images: HashMap<AssetId<Image>, BindGroup>,
4839
    /// Map from buffer index to its metadata bind group (group 3) for the init
4840
    /// pass.
4841
    // FIXME - doesn't work with batching; this should be the instance ID
4842
    init_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<InitMetadataBindGroupKey>>,
4843
    /// Map from buffer index to its metadata bind group (group 3) for the
4844
    /// update pass.
4845
    // FIXME - doesn't work with batching; this should be the instance ID
4846
    update_metadata_bind_groups: HashMap<SlabId, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4847
    /// Map from an effect material to its bind group.
4848
    material_bind_groups: HashMap<Material, BindGroup>,
4849
}
4850

4851
impl EffectBindGroups {
4852
    pub fn particle_render(&self, slab_id: &SlabId) -> Option<&BindGroup> {
311✔
4853
        self.particle_slabs.get(slab_id).map(|bg| &bg.render)
1,244✔
4854
    }
4855

4856
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4857
    /// needed.
4858
    pub(self) fn get_or_create_init_metadata(
312✔
4859
        &mut self,
4860
        effect_batch: &EffectBatch,
4861
        render_device: &RenderDevice,
4862
        layout: &BindGroupLayout,
4863
        effect_metadata_buffer: &Buffer,
4864
        consume_event_buffers: Option<ConsumeEventBuffers>,
4865
    ) -> Result<&BindGroup, ()> {
4866
        assert!(effect_batch.metadata_table_id.is_valid());
936✔
4867

4868
        let key = InitMetadataBindGroupKey {
4869
            slab_id: effect_batch.slab_id,
624✔
4870
            effect_metadata_buffer: effect_metadata_buffer.id(),
936✔
4871
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
624✔
4872
        };
4873

4874
        let make_entry = || {
314✔
4875
            let mut entries = Vec::with_capacity(3);
4✔
4876
            entries.push(
4✔
4877
                // @group(3) @binding(0) var<storage, read_write> effect_metadatas :
4878
                // array<EffectMetadata>;
4879
                BindGroupEntry {
2✔
4880
                    binding: 0,
2✔
4881
                    resource: effect_metadata_buffer.as_entire_binding(),
2✔
4882
                },
4883
            );
4884
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
UNCOV
4885
                entries.push(
×
4886
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4887
                    // ChildInfoBuffer;
UNCOV
4888
                    BindGroupEntry {
×
UNCOV
4889
                        binding: 1,
×
UNCOV
4890
                        resource: BindingResource::Buffer(BufferBinding {
×
UNCOV
4891
                            buffer: consume_event_buffers.child_infos_buffer,
×
UNCOV
4892
                            offset: 0,
×
UNCOV
4893
                            size: None,
×
4894
                        }),
4895
                    },
4896
                );
UNCOV
4897
                entries.push(
×
4898
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
UNCOV
4899
                    BindGroupEntry {
×
UNCOV
4900
                        binding: 2,
×
UNCOV
4901
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
×
4902
                    },
4903
                );
4904
            }
4905

4906
            let bind_group = render_device.create_bind_group(
6✔
4907
                "hanabi:bind_group:init:metadata@3",
4908
                layout,
2✔
4909
                &entries[..],
2✔
4910
            );
4911

4912
            trace!(
2✔
4913
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
4914
                    effect_batch.slab_id.index(),
4✔
4915
                    effect_batch.metadata_table_id.0,
4916
                );
4917

4918
            bind_group
2✔
4919
        };
4920

4921
        Ok(&self
312✔
4922
            .init_metadata_bind_groups
312✔
4923
            .entry(effect_batch.slab_id)
624✔
4924
            .and_modify(|cbg| {
622✔
4925
                if cbg.key != key {
310✔
4926
                    trace!(
×
4927
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
4928
                        cbg.key,
4929
                        key
4930
                    );
4931
                    cbg.key = key;
×
4932
                    cbg.bind_group = make_entry();
×
4933
                }
4934
            })
4935
            .or_insert_with(|| {
314✔
4936
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
2✔
4937
                CachedBindGroup {
2✔
4938
                    key,
2✔
4939
                    bind_group: make_entry(),
2✔
4940
                }
4941
            })
4942
            .bind_group)
4943
    }
4944

4945
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4946
    /// needed.
4947
    pub(self) fn get_or_create_update_metadata(
312✔
4948
        &mut self,
4949
        effect_batch: &EffectBatch,
4950
        render_device: &RenderDevice,
4951
        layout: &BindGroupLayout,
4952
        effect_metadata_buffer: &Buffer,
4953
        child_info_buffer: Option<&Buffer>,
4954
        event_buffers: &[(Entity, BufferBindingSource)],
4955
    ) -> Result<&BindGroup, ()> {
4956
        assert!(effect_batch.metadata_table_id.is_valid());
936✔
4957

4958
        // Check arguments consistency
4959
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
1,560✔
4960
        let emits_gpu_spawn_events = !event_buffers.is_empty();
624✔
4961
        let child_info_buffer_id = if emits_gpu_spawn_events {
624✔
4962
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4963
        } else {
4964
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4965
            // if relevant, that is if the effect emits GPU spawn events.
4966
            None
312✔
4967
        };
4968
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
936✔
4969

4970
        let event_buffers_keys = event_buffers
624✔
4971
            .iter()
4972
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
312✔
4973
            .collect::<Vec<_>>();
4974

4975
        let key = UpdateMetadataBindGroupKey {
4976
            slab_id: effect_batch.slab_id,
624✔
4977
            effect_metadata_buffer: effect_metadata_buffer.id(),
936✔
4978
            child_info_buffer_id,
4979
            event_buffers_keys,
4980
        };
4981

4982
        let make_entry = || {
314✔
4983
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
6✔
4984
            // @group(3) @binding(0) var<storage, read_write> effect_metadatas :
4985
            // array<EffectMetadata>;
4986
            entries.push(BindGroupEntry {
6✔
4987
                binding: 0,
2✔
4988
                resource: effect_metadata_buffer.as_entire_binding(),
2✔
4989
            });
4990
            if emits_gpu_spawn_events {
2✔
4991
                let child_info_buffer = child_info_buffer.unwrap();
×
4992

4993
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4994
                // ChildInfoBuffer;
4995
                entries.push(BindGroupEntry {
×
4996
                    binding: 1,
×
4997
                    resource: BindingResource::Buffer(BufferBinding {
×
4998
                        buffer: child_info_buffer,
×
4999
                        offset: 0,
×
5000
                        size: None,
×
5001
                    }),
5002
                });
5003

5004
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
5005
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
5006
                    // EventBuffer;
5007
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
5008
                    // then moved to counting in bytes, so now need some conversion. Need to review
5009
                    // all of this...
UNCOV
5010
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
×
UNCOV
5011
                    buffer_binding.offset *= 4;
×
UNCOV
5012
                    buffer_binding.size = buffer_binding
×
UNCOV
5013
                        .size
×
5014
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
UNCOV
5015
                    entries.push(BindGroupEntry {
×
UNCOV
5016
                        binding: 2 + index as u32,
×
UNCOV
5017
                        resource: BindingResource::Buffer(buffer_binding),
×
5018
                    });
5019
                }
5020
            }
5021

5022
            let bind_group = render_device.create_bind_group(
6✔
5023
                "hanabi:bind_group:update:metadata@3",
5024
                layout,
2✔
5025
                &entries[..],
2✔
5026
            );
5027

5028
            trace!(
2✔
5029
                "Created new metadata@3 bind group for update pass and slab ID {}: effect_metadata={}",
5030
                effect_batch.slab_id.index(),
4✔
5031
                effect_batch.metadata_table_id.0,
5032
            );
5033

5034
            bind_group
2✔
5035
        };
5036

5037
        Ok(&self
312✔
5038
            .update_metadata_bind_groups
312✔
5039
            .entry(effect_batch.slab_id)
624✔
5040
            .and_modify(|cbg| {
622✔
5041
                if cbg.key != key {
310✔
5042
                    trace!(
×
5043
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
5044
                        cbg.key,
5045
                        key
5046
                    );
5047
                    cbg.key = key.clone();
×
5048
                    cbg.bind_group = make_entry();
×
5049
                }
5050
            })
5051
            .or_insert_with(|| {
314✔
5052
                trace!(
2✔
5053
                    "Inserting new bind group for update metadata@3 with key={:?}",
5054
                    key
5055
                );
5056
                CachedBindGroup {
2✔
5057
                    key: key.clone(),
4✔
5058
                    bind_group: make_entry(),
2✔
5059
                }
5060
            })
5061
            .bind_group)
5062
    }
5063
}
5064

5065
#[derive(SystemParam)]
5066
pub struct QueueEffectsReadOnlyParams<'w, 's> {
5067
    #[cfg(feature = "2d")]
5068
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
5069
    #[cfg(feature = "3d")]
5070
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
5071
    #[cfg(feature = "3d")]
5072
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
5073
    #[cfg(feature = "3d")]
5074
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
5075
    marker: PhantomData<&'s usize>,
5076
}
5077

5078
fn emit_sorted_draw<T, F>(
624✔
5079
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5080
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5081
    view_entities: &mut FixedBitSet,
5082
    sorted_effect_batches: &SortedEffectBatches,
5083
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5084
    render_pipeline: &mut ParticlesRenderPipeline,
5085
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5086
    render_meshes: &RenderAssets<RenderMesh>,
5087
    pipeline_cache: &PipelineCache,
5088
    make_phase_item: F,
5089
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5090
) where
5091
    T: SortedPhaseItem,
5092
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5093
{
5094
    trace!("emit_sorted_draw() {} views", views.iter().len());
1,872✔
5095

5096
    for (visible_entities, view, msaa) in views.iter() {
1,872✔
5097
        trace!(
×
UNCOV
5098
            "Process new sorted view with {} visible particle effect entities",
×
5099
            visible_entities.len::<CompiledParticleEffect>()
1,248✔
5100
        );
5101

5102
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
312✔
5103
            continue;
312✔
5104
        };
5105

5106
        {
5107
            #[cfg(feature = "trace")]
5108
            let _span = bevy::log::info_span!("collect_view_entities").entered();
936✔
5109

5110
            view_entities.clear();
624✔
5111
            view_entities.extend(
624✔
5112
                visible_entities
312✔
5113
                    .iter::<EffectVisibilityClass>()
312✔
5114
                    .map(|e| e.1.index_u32() as usize),
624✔
5115
            );
5116
        }
5117

5118
        // For each view, loop over all the effect batches to determine if the effect
5119
        // needs to be rendered for that view, and enqueue a view-dependent
5120
        // batch if so.
5121
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
936✔
5122
            #[cfg(feature = "trace")]
5123
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5124

5125
            trace!(
×
UNCOV
5126
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5127
                draw_entity,
×
5128
                draw_batch.effect_batch_index,
×
5129
            );
5130

5131
            // Get the EffectBatches this EffectDrawBatch is part of.
5132
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
312✔
5133
            else {
×
5134
                continue;
×
5135
            };
5136

5137
            trace!(
×
UNCOV
5138
                "-> EffectBach: slab_id={} spawner_base={} layout_flags={:?}",
×
5139
                effect_batch.slab_id.index(),
624✔
5140
                effect_batch.spawner_base,
×
5141
                effect_batch.layout_flags,
×
5142
            );
5143

5144
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5145
            if effect_batch
×
5146
                .layout_flags
×
5147
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5148
            {
5149
                trace!("Non-transparent batch. Skipped.");
×
5150
                continue;
×
5151
            }
5152

5153
            // Check if batch contains any entity visible in the current view. Otherwise we
5154
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5155
            // the Sprite renderer this is inspired from) we don't expect more than
5156
            // a handful of particle effect instances, so would rather not pay the memory
5157
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5158
            // TODO - Profile to confirm.
5159
            #[cfg(feature = "trace")]
5160
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5161
            let has_visible_entity = effect_batch
×
5162
                .entities
×
5163
                .iter()
5164
                .any(|index| view_entities.contains(*index as usize));
936✔
5165
            if !has_visible_entity {
×
5166
                trace!("No visible entity for view, not emitting any draw call.");
×
5167
                continue;
×
5168
            }
5169
            #[cfg(feature = "trace")]
5170
            _span_check_vis.exit();
624✔
5171

5172
            // Create and cache the bind group layout for this texture layout
5173
            render_pipeline.cache_material(&effect_batch.texture_layout);
936✔
5174

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

5178
            let local_space_simulation = effect_batch
624✔
5179
                .layout_flags
312✔
5180
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
312✔
5181
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
936✔
5182
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
936✔
5183
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
936✔
5184
            let needs_normal = effect_batch
624✔
5185
                .layout_flags
312✔
5186
                .contains(LayoutFlags::NEEDS_NORMAL);
312✔
5187
            let needs_particle_fragment = effect_batch
624✔
5188
                .layout_flags
312✔
5189
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
312✔
5190
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
936✔
5191
            let image_count = effect_batch.texture_layout.layout.len() as u8;
624✔
5192

5193
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5194
            // re-querying here...?
5195
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
936✔
5196
                trace!("Batch has no render mesh, skipped.");
×
5197
                continue;
×
5198
            };
5199
            let mesh_layout = render_mesh.layout.clone();
×
5200

5201
            // Specialize the render pipeline based on the effect batch
5202
            trace!(
×
UNCOV
5203
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5204
                effect_batch.render_shader,
×
5205
                image_count,
×
5206
                alpha_mask,
×
5207
                flipbook,
×
5208
                view.hdr
×
5209
            );
5210

5211
            // Add a draw pass for the effect batch
UNCOV
5212
            trace!("Emitting individual draw for batch");
×
5213

5214
            let alpha_mode = effect_batch.alpha_mode;
×
5215

5216
            #[cfg(feature = "trace")]
5217
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5218
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5219
                pipeline_cache,
×
5220
                render_pipeline,
×
5221
                ParticleRenderPipelineKey {
×
5222
                    shader: effect_batch.render_shader.clone(),
×
5223
                    mesh_layout: Some(mesh_layout),
×
5224
                    particle_layout: effect_batch.particle_layout.clone(),
×
5225
                    texture_layout: effect_batch.texture_layout.clone(),
×
5226
                    local_space_simulation,
×
5227
                    alpha_mask,
×
5228
                    alpha_mode,
×
5229
                    flipbook,
×
5230
                    needs_uv,
×
5231
                    needs_normal,
×
5232
                    needs_particle_fragment,
×
5233
                    ribbons,
×
5234
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5235
                    pipeline_mode,
×
5236
                    msaa_samples: msaa.samples(),
×
5237
                    hdr: view.hdr,
×
5238
                },
5239
            );
5240
            #[cfg(feature = "trace")]
5241
            _span_specialize.exit();
×
5242

UNCOV
5243
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5244
            trace!(
×
UNCOV
5245
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
×
UNCOV
5246
                spawner_base={} handle={:?}",
×
5247
                draw_entity,
×
5248
                effect_batch.slab_id.index(),
624✔
5249
                effect_batch.spawner_base,
×
5250
                effect_batch.handle
×
5251
            );
5252
            render_phase.add(make_phase_item(
×
5253
                render_pipeline_id,
×
5254
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5255
                draw_batch,
×
5256
                view,
×
5257
            ));
5258
        }
5259
    }
5260
}
5261

5262
#[cfg(feature = "3d")]
5263
fn emit_binned_draw<T, F, G>(
624✔
5264
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5265
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5266
    view_entities: &mut FixedBitSet,
5267
    sorted_effect_batches: &SortedEffectBatches,
5268
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5269
    render_pipeline: &mut ParticlesRenderPipeline,
5270
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5271
    pipeline_cache: &PipelineCache,
5272
    render_meshes: &RenderAssets<RenderMesh>,
5273
    make_batch_set_key: F,
5274
    make_bin_key: G,
5275
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5276
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5277
    change_tick: &mut Tick,
5278
) where
5279
    T: BinnedPhaseItem,
5280
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5281
    G: Fn() -> T::BinKey,
5282
{
5283
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5284

5285
    trace!("emit_binned_draw() {} views", views.iter().len());
1,872✔
5286

5287
    for (visible_entities, view, msaa) in views.iter() {
1,872✔
UNCOV
5288
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
×
5289

5290
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
624✔
5291
            continue;
×
5292
        };
5293

5294
        {
5295
            #[cfg(feature = "trace")]
5296
            let _span = bevy::log::info_span!("collect_view_entities").entered();
1,872✔
5297

5298
            view_entities.clear();
1,248✔
5299
            view_entities.extend(
1,248✔
5300
                visible_entities
624✔
5301
                    .iter::<EffectVisibilityClass>()
624✔
5302
                    .map(|e| e.1.index_u32() as usize),
1,248✔
5303
            );
5304
        }
5305

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

5313
            trace!(
×
UNCOV
5314
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
×
5315
                draw_entity,
×
5316
                draw_batch.effect_batch_index,
×
5317
            );
5318

5319
            // Get the EffectBatches this EffectDrawBatch is part of.
5320
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
624✔
5321
            else {
×
5322
                continue;
×
5323
            };
5324

5325
            trace!(
×
UNCOV
5326
                "-> EffectBaches: slab_id={} spawner_base={} layout_flags={:?}",
×
5327
                effect_batch.slab_id.index(),
1,248✔
5328
                effect_batch.spawner_base,
×
5329
                effect_batch.layout_flags,
×
5330
            );
5331

5332
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5333
                trace!(
624✔
UNCOV
5334
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
×
5335
                    effect_batch.layout_flags,
×
5336
                    alpha_mask
×
5337
                );
5338
                continue;
624✔
5339
            }
5340

5341
            // Check if batch contains any entity visible in the current view. Otherwise we
5342
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5343
            // the Sprite renderer this is inspired from) we don't expect more than
5344
            // a handful of particle effect instances, so would rather not pay the memory
5345
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5346
            // TODO - Profile to confirm.
5347
            #[cfg(feature = "trace")]
5348
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5349
            let has_visible_entity = effect_batch
×
5350
                .entities
×
5351
                .iter()
5352
                .any(|index| view_entities.contains(*index as usize));
×
5353
            if !has_visible_entity {
×
5354
                trace!("No visible entity for view, not emitting any draw call.");
×
5355
                continue;
×
5356
            }
5357
            #[cfg(feature = "trace")]
5358
            _span_check_vis.exit();
×
5359

5360
            // Create and cache the bind group layout for this texture layout
5361
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5362

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

5366
            let local_space_simulation = effect_batch
×
5367
                .layout_flags
×
5368
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5369
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5370
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5371
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5372
            let needs_normal = effect_batch
×
5373
                .layout_flags
×
5374
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5375
            let needs_particle_fragment = effect_batch
×
5376
                .layout_flags
×
5377
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5378
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5379
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5380
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5381

5382
            // Specialize the render pipeline based on the effect batch
5383
            trace!(
×
5384
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5385
                effect_batch.render_shader,
×
5386
                image_count,
×
5387
                alpha_mask,
×
5388
                flipbook,
×
5389
                view.hdr
×
5390
            );
5391

5392
            // Add a draw pass for the effect batch
5393
            trace!("Emitting individual draw for batch");
×
5394

5395
            let alpha_mode = effect_batch.alpha_mode;
×
5396

5397
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5398
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5399
                continue;
×
5400
            };
5401

5402
            #[cfg(feature = "trace")]
5403
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5404
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5405
                pipeline_cache,
×
5406
                render_pipeline,
×
5407
                ParticleRenderPipelineKey {
×
5408
                    shader: effect_batch.render_shader.clone(),
×
5409
                    mesh_layout: Some(mesh_layout),
×
5410
                    particle_layout: effect_batch.particle_layout.clone(),
×
5411
                    texture_layout: effect_batch.texture_layout.clone(),
×
5412
                    local_space_simulation,
×
5413
                    alpha_mask,
×
5414
                    alpha_mode,
×
5415
                    flipbook,
×
5416
                    needs_uv,
×
5417
                    needs_normal,
×
5418
                    needs_particle_fragment,
×
5419
                    ribbons,
×
5420
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5421
                    pipeline_mode,
×
5422
                    msaa_samples: msaa.samples(),
×
5423
                    hdr: view.hdr,
×
5424
                },
5425
            );
5426
            #[cfg(feature = "trace")]
5427
            _span_specialize.exit();
×
5428

5429
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5430
            trace!(
×
5431
                "+ Add Transparent for batch on draw_entity {:?}: slab_id={} \
×
5432
                spawner_base={} handle={:?}",
×
5433
                draw_entity,
×
5434
                effect_batch.slab_id.index(),
×
5435
                effect_batch.spawner_base,
×
5436
                effect_batch.handle
×
5437
            );
5438
            render_phase.add(
×
5439
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
5440
                make_bin_key(),
×
5441
                (draw_entity, draw_batch.main_entity),
×
5442
                InputUniformIndex::default(),
×
5443
                BinnedRenderPhaseType::NonMesh,
×
5444
                *change_tick,
×
5445
            );
5446
        }
5447
    }
5448
}
5449

5450
#[allow(clippy::too_many_arguments)]
5451
pub(crate) fn queue_effects(
330✔
5452
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5453
    effects_meta: Res<EffectsMeta>,
5454
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5455
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5456
    pipeline_cache: Res<PipelineCache>,
5457
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5458
    sorted_effect_batches: Res<SortedEffectBatches>,
5459
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5460
    events: Res<EffectAssetEvents>,
5461
    render_meshes: Res<RenderAssets<RenderMesh>>,
5462
    read_params: QueueEffectsReadOnlyParams,
5463
    mut view_entities: Local<FixedBitSet>,
5464
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5465
        ViewSortedRenderPhases<Transparent2d>,
5466
    >,
5467
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5468
        ViewSortedRenderPhases<Transparent3d>,
5469
    >,
5470
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5471
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5472
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5473
    ),
5474
    mut change_tick: Local<Tick>,
5475
) {
5476
    #[cfg(feature = "trace")]
5477
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
990✔
5478

5479
    trace!("queue_effects");
330✔
5480

5481
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5482
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5483
    // reuse stale data.
5484
    let next_change_tick = change_tick.get() + 1;
660✔
5485
    change_tick.set(next_change_tick);
660✔
5486

5487
    // If an image has changed, the GpuImage has (probably) changed
5488
    for event in &events.images {
363✔
UNCOV
5489
        match event {
×
5490
            AssetEvent::Added { .. } => (),
27✔
5491
            AssetEvent::LoadedWithDependencies { .. } => (),
6✔
5492
            AssetEvent::Unused { .. } => (),
×
5493
            AssetEvent::Modified { id } => {
×
5494
                if effect_bind_groups.images.remove(id).is_some() {
×
5495
                    trace!("Destroyed bind group of modified image asset {:?}", id);
×
5496
                }
5497
            }
UNCOV
5498
            AssetEvent::Removed { id } => {
×
UNCOV
5499
                if effect_bind_groups.images.remove(id).is_some() {
×
5500
                    trace!("Destroyes bind group of removed image asset {:?}", id);
×
5501
                }
5502
            }
5503
        };
5504
    }
5505

5506
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
978✔
5507
        // No spawners are active
5508
        return;
18✔
5509
    }
5510

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

UNCOV
5517
        let draw_effects_function_2d = read_params
×
UNCOV
5518
            .draw_functions_2d
×
5519
            .read()
5520
            .get_id::<DrawEffects>()
5521
            .unwrap();
5522

5523
        // Effects with full alpha blending
UNCOV
5524
        if !views.is_empty() {
×
5525
            trace!("Emit effect draw calls for alpha blended 2D views...");
312✔
5526
            emit_sorted_draw(
UNCOV
5527
                &views,
×
UNCOV
5528
                &mut transparent_2d_render_phases,
×
UNCOV
5529
                &mut view_entities,
×
UNCOV
5530
                &sorted_effect_batches,
×
UNCOV
5531
                &effect_draw_batches,
×
UNCOV
5532
                &mut render_pipeline,
×
UNCOV
5533
                specialized_render_pipelines.reborrow(),
×
UNCOV
5534
                &render_meshes,
×
UNCOV
5535
                &pipeline_cache,
×
5536
                |id, entity, draw_batch, _view| Transparent2d {
5537
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5538
                    entity,
×
5539
                    pipeline: id,
×
5540
                    draw_function: draw_effects_function_2d,
×
5541
                    batch_range: 0..1,
×
5542
                    extracted_index: 0, // ???
5543
                    extra_index: PhaseItemExtraIndex::None,
×
5544
                    indexed: true, // ???
5545
                },
5546
                #[cfg(feature = "3d")]
UNCOV
5547
                PipelineMode::Camera2d,
×
5548
            );
5549
        }
5550
    }
5551

5552
    // Loop over all 3D cameras/views that need to render effects
5553
    #[cfg(feature = "3d")]
5554
    {
5555
        #[cfg(feature = "trace")]
UNCOV
5556
        let _span_draw = bevy::log::info_span!("draw_3d").entered();
×
5557

5558
        // Effects with full alpha blending
UNCOV
5559
        if !views.is_empty() {
×
5560
            trace!("Emit effect draw calls for alpha blended 3D views...");
312✔
5561

UNCOV
5562
            let draw_effects_function_3d = read_params
×
UNCOV
5563
                .draw_functions_3d
×
5564
                .read()
5565
                .get_id::<DrawEffects>()
5566
                .unwrap();
5567

5568
            emit_sorted_draw(
UNCOV
5569
                &views,
×
UNCOV
5570
                &mut transparent_3d_render_phases,
×
UNCOV
5571
                &mut view_entities,
×
UNCOV
5572
                &sorted_effect_batches,
×
UNCOV
5573
                &effect_draw_batches,
×
UNCOV
5574
                &mut render_pipeline,
×
UNCOV
5575
                specialized_render_pipelines.reborrow(),
×
UNCOV
5576
                &render_meshes,
×
UNCOV
5577
                &pipeline_cache,
×
5578
                |id, entity, batch, view| Transparent3d {
5579
                    distance: view.rangefinder3d().distance(&batch.translation),
936✔
5580
                    pipeline: id,
312✔
5581
                    entity,
312✔
5582
                    draw_function: draw_effects_function_3d,
312✔
5583
                    batch_range: 0..1,
312✔
5584
                    extra_index: PhaseItemExtraIndex::None,
312✔
5585
                    indexed: true, // ???
5586
                },
5587
                #[cfg(feature = "2d")]
UNCOV
5588
                PipelineMode::Camera3d,
×
5589
            );
5590
        }
5591

5592
        // Effects with alpha mask
UNCOV
5593
        if !views.is_empty() {
×
5594
            #[cfg(feature = "trace")]
5595
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
312✔
5596

UNCOV
5597
            trace!("Emit effect draw calls for alpha masked 3D views...");
×
5598

UNCOV
5599
            let draw_effects_function_alpha_mask = read_params
×
UNCOV
5600
                .draw_functions_alpha_mask
×
5601
                .read()
5602
                .get_id::<DrawEffects>()
5603
                .unwrap();
5604

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

5633
        // Opaque particles
UNCOV
5634
        if !views.is_empty() {
×
5635
            #[cfg(feature = "trace")]
5636
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
312✔
5637

UNCOV
5638
            trace!("Emit effect draw calls for opaque 3D views...");
×
5639

UNCOV
5640
            let draw_effects_function_opaque = read_params
×
UNCOV
5641
                .draw_functions_opaque
×
5642
                .read()
5643
                .get_id::<DrawEffects>()
5644
                .unwrap();
5645

5646
            emit_binned_draw(
UNCOV
5647
                &views,
×
UNCOV
5648
                &mut opaque_3d_render_phases,
×
UNCOV
5649
                &mut view_entities,
×
UNCOV
5650
                &sorted_effect_batches,
×
UNCOV
5651
                &effect_draw_batches,
×
UNCOV
5652
                &mut render_pipeline,
×
UNCOV
5653
                specialized_render_pipelines.reborrow(),
×
UNCOV
5654
                &pipeline_cache,
×
UNCOV
5655
                &render_meshes,
×
5656
                |id, _batch, _view| Opaque3dBatchSetKey {
5657
                    pipeline: id,
×
5658
                    draw_function: draw_effects_function_opaque,
×
5659
                    material_bind_group_index: None,
×
5660
                    vertex_slab: default(),
×
5661
                    index_slab: None,
×
5662
                    lightmap_slab: None,
×
5663
                },
5664
                // Unused for now
5665
                || Opaque3dBinKey {
5666
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5667
                },
5668
                #[cfg(feature = "2d")]
UNCOV
5669
                PipelineMode::Camera3d,
×
UNCOV
5670
                ParticleRenderAlphaMaskPipelineKey::Opaque,
×
UNCOV
5671
                &mut change_tick,
×
5672
            );
5673
        }
5674
    }
5675
}
5676

5677
/// Once a child effect is batched, and therefore passed validations to be
5678
/// updated and rendered this frame, dispatch a new GPU operation to fill the
5679
/// indirect dispatch args of its init pass based on the number of GPU events
5680
/// emitted in the previous frame and stored in its event buffer.
5681
pub fn queue_init_indirect_workgroup_update(
330✔
5682
    q_cached_effects: Query<(
5683
        Entity,
5684
        &CachedChildInfo,
5685
        &CachedEffectEvents,
5686
        &CachedReadyState,
5687
    )>,
5688
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5689
) {
5690
    debug_assert_eq!(
330✔
5691
        GpuChildInfo::min_size().get() % 4,
330✔
5692
        0,
5693
        "Invalid GpuChildInfo alignment."
5694
    );
5695

5696
    // Schedule some GPU buffer operation to update the number of workgroups to
5697
    // dispatch during the indirect init pass of this effect based on the number of
5698
    // GPU spawn events written in its buffer.
5699
    for (entity, cached_child_info, cached_effect_events, cached_ready_state) in &q_cached_effects {
330✔
5700
        if !cached_ready_state.is_ready() {
×
5701
            trace!(
×
5702
                "[Effect {:?}] Skipping init_fill_dispatch.enqueue() because effect is not ready.",
5703
                entity
5704
            );
UNCOV
5705
            continue;
×
5706
        }
5707
        let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
×
5708
        let global_child_index = cached_child_info.global_child_index;
×
5709
        trace!(
×
5710
            "[Effect {:?}] init_fill_dispatch.enqueue(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
5711
            entity,
5712
            global_child_index,
5713
            init_indirect_dispatch_index,
5714
        );
5715
        assert!(global_child_index != u32::MAX);
×
5716
        init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
5717
    }
5718
}
5719

5720
/// Prepare GPU resources for effect rendering.
5721
///
5722
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5723
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5724
/// access to the current camera view.
5725
pub(crate) fn prepare_gpu_resources(
330✔
5726
    mut effects_meta: ResMut<EffectsMeta>,
5727
    //mut effect_cache: ResMut<EffectCache>,
5728
    mut event_cache: ResMut<EventCache>,
5729
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5730
    mut sort_bind_groups: ResMut<SortBindGroups>,
5731
    render_device: Res<RenderDevice>,
5732
    render_queue: Res<RenderQueue>,
5733
    view_uniforms: Res<ViewUniforms>,
5734
    render_pipeline: Res<ParticlesRenderPipeline>,
5735
    pipeline_cache: Res<PipelineCache>,
5736
) {
5737
    // Get the binding for the ViewUniform, the uniform data structure containing
5738
    // the Camera data for the current view. If not available, we cannot render
5739
    // anything.
5740
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
660✔
5741
        return;
×
5742
    };
5743

5744
    // Upload simulation parameters for this frame
5745
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
654✔
UNCOV
5746
    effects_meta
×
UNCOV
5747
        .sim_params_uniforms
×
UNCOV
5748
        .write_buffer(&render_device, &render_queue);
×
5749
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
663✔
5750
        // Buffer changed, invalidate bind groups
5751
        effects_meta.update_sim_params_bind_group = None;
9✔
5752
        effects_meta.indirect_sim_params_bind_group = None;
3✔
5753
    }
5754

5755
    // Create the bind group for the camera/view parameters
5756
    // FIXME - Not here!
UNCOV
5757
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
×
UNCOV
5758
        "hanabi:bind_group_camera_view",
×
NEW
5759
        &pipeline_cache.get_bind_group_layout(&render_pipeline.view_layout_desc),
×
UNCOV
5760
        &[
×
UNCOV
5761
            BindGroupEntry {
×
UNCOV
5762
                binding: 0,
×
UNCOV
5763
                resource: view_binding,
×
5764
            },
UNCOV
5765
            BindGroupEntry {
×
UNCOV
5766
                binding: 1,
×
UNCOV
5767
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
5768
            },
5769
        ],
5770
    ));
5771

5772
    // Re-/allocate the draw indirect args buffer if needed
UNCOV
5773
    if effects_meta
×
UNCOV
5774
        .draw_indirect_buffer
×
UNCOV
5775
        .allocate_gpu(&render_device, &render_queue)
×
5776
    {
5777
        // All those bind groups use the buffer so need to be re-created
5778
        trace!("*** Draw indirect args buffer re-allocated; clearing all bind groups using it.");
2✔
5779
        effects_meta.update_sim_params_bind_group = None;
4✔
5780
        effects_meta.indirect_metadata_bind_group = None;
4✔
5781
    }
5782

5783
    // Re-/allocate any GPU buffer if needed
5784
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5785
    // effect_bind_groups);
UNCOV
5786
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
×
UNCOV
5787
    sort_bind_groups.prepare_buffers(&render_device);
×
UNCOV
5788
    if effects_meta
×
UNCOV
5789
        .dispatch_indirect_buffer
×
UNCOV
5790
        .prepare_buffers(&render_device)
×
5791
    {
5792
        // All those bind groups use the buffer so need to be re-created
5793
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
2✔
5794
        effect_bind_groups.particle_slabs.clear();
4✔
5795
    }
5796
}
5797

5798
/// Update the [`GpuEffectMetadata`] of all the effects queued for update/render
5799
/// this frame.
5800
///
5801
/// By this point, all effects should have a [`CachedEffectMetadata`] with a
5802
/// valid allocation in the GPU table for a [`GpuEffectMetadata`] entry. This
5803
/// system actually synchronize the CPU value with the GPU one in case of
5804
/// change.
5805
pub(crate) fn prepare_effect_metadata(
330✔
5806
    render_device: Res<RenderDevice>,
5807
    render_queue: Res<RenderQueue>,
5808
    mut q_effects: Query<(
5809
        MainEntity,
5810
        Ref<ExtractedEffect>,
5811
        Ref<CachedEffect>,
5812
        Ref<DispatchBufferIndices>,
5813
        Option<Ref<CachedChildInfo>>,
5814
        Option<Ref<CachedParentInfo>>,
5815
        Option<Ref<CachedDrawIndirectArgs>>,
5816
        Option<Ref<CachedEffectEvents>>,
5817
        &mut CachedEffectMetadata,
5818
    )>,
5819
    mut effects_meta: ResMut<EffectsMeta>,
5820
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5821
) {
5822
    #[cfg(feature = "trace")]
5823
    let _span = bevy::log::info_span!("prepare_effect_metadata").entered();
990✔
5824
    trace!("prepare_effect_metadata");
330✔
5825

5826
    for (
5827
        main_entity,
314✔
5828
        extracted_effect,
314✔
5829
        cached_effect,
314✔
5830
        dispatch_buffer_indices,
314✔
5831
        maybe_cached_child_info,
314✔
5832
        maybe_cached_parent_info,
314✔
5833
        maybe_cached_draw_indirect_args,
314✔
5834
        maybe_cached_effect_events,
314✔
5835
        mut cached_effect_metadata,
314✔
5836
    ) in &mut q_effects
330✔
5837
    {
5838
        // Check if anything relevant to GpuEffectMetadata changed this frame; otherwise
5839
        // early out and skip this effect.
5840
        let is_changed_ee = extracted_effect.is_changed();
942✔
5841
        let is_changed_ce = cached_effect.is_changed();
942✔
5842
        let is_changed_dbi = dispatch_buffer_indices.is_changed();
942✔
5843
        let is_changed_cci = maybe_cached_child_info
628✔
5844
            .as_ref()
5845
            .map(|cci| cci.is_changed())
314✔
5846
            .unwrap_or(false);
5847
        let is_changed_cpi = maybe_cached_parent_info
628✔
5848
            .as_ref()
5849
            .map(|cpi| cpi.is_changed())
314✔
5850
            .unwrap_or(false);
5851
        let is_changed_cdia = maybe_cached_draw_indirect_args
628✔
5852
            .as_ref()
5853
            .map(|cdia| cdia.is_changed())
942✔
5854
            .unwrap_or(false);
5855
        let is_changed_cee = maybe_cached_effect_events
628✔
5856
            .as_ref()
5857
            .map(|cee| cee.is_changed())
314✔
5858
            .unwrap_or(false);
5859
        trace!(
314✔
5860
            "Preparting GpuEffectMetadata for effect {:?}: is_changed[] = {} {} {} {} {} {} {}",
5861
            main_entity,
5862
            is_changed_ee,
5863
            is_changed_ce,
5864
            is_changed_dbi,
5865
            is_changed_cci,
5866
            is_changed_cpi,
5867
            is_changed_cdia,
5868
            is_changed_cee
5869
        );
5870
        if !is_changed_ee
314✔
5871
            && !is_changed_ce
311✔
5872
            && !is_changed_dbi
311✔
5873
            && !is_changed_cci
311✔
5874
            && !is_changed_cpi
311✔
5875
            && !is_changed_cdia
311✔
5876
            && !is_changed_cee
311✔
5877
        {
5878
            continue;
311✔
5879
        }
5880

5881
        let capacity = cached_effect.slice.len();
9✔
5882

5883
        // Global and local indices of this effect as a child of another (parent) effect
5884
        let (global_child_index, local_child_index) = maybe_cached_child_info
9✔
5885
            .map(|cci| (cci.global_child_index, cci.local_child_index))
3✔
5886
            .unwrap_or((u32::MAX, u32::MAX));
6✔
5887

5888
        // Base index of all children of this (parent) effect
5889
        let base_child_index = maybe_cached_parent_info
6✔
5890
            .map(|cpi| {
3✔
5891
                debug_assert_eq!(
×
5892
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
5893
                    0
5894
                );
5895
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
5896
            })
5897
            .unwrap_or(u32::MAX);
3✔
5898

5899
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
9✔
5900
        let sort_key_offset = extracted_effect
6✔
5901
            .particle_layout
3✔
5902
            .byte_offset(Attribute::RIBBON_ID)
3✔
5903
            .map(|byte_offset| byte_offset / 4)
3✔
5904
            .unwrap_or(u32::MAX);
3✔
5905
        let sort_key2_offset = extracted_effect
6✔
5906
            .particle_layout
3✔
5907
            .byte_offset(Attribute::AGE)
3✔
5908
            .map(|byte_offset| byte_offset / 4)
3✔
5909
            .unwrap_or(u32::MAX);
3✔
5910

5911
        let gpu_effect_metadata = GpuEffectMetadata {
5912
            capacity,
5913
            alive_count: 0,
5914
            max_update: 0,
5915
            max_spawn: capacity,
5916
            indirect_write_index: 0,
5917
            indirect_dispatch_index: dispatch_buffer_indices
3✔
5918
                .update_dispatch_indirect_buffer_row_index,
5919
            indirect_draw_index: maybe_cached_draw_indirect_args
3✔
5920
                .map(|cdia| cdia.get_row().0)
5921
                .unwrap_or(u32::MAX),
5922
            init_indirect_dispatch_index: maybe_cached_effect_events
3✔
5923
                .map(|cee| cee.init_indirect_dispatch_index)
5924
                .unwrap_or(u32::MAX),
5925
            local_child_index,
5926
            global_child_index,
5927
            base_child_index,
5928
            particle_stride,
5929
            sort_key_offset,
5930
            sort_key2_offset,
5931
            ..default()
5932
        };
5933

5934
        // Insert of update entry in GPU buffer table
5935
        assert!(cached_effect_metadata.table_id.is_valid());
9✔
5936
        if gpu_effect_metadata != cached_effect_metadata.metadata {
3✔
5937
            effects_meta
2✔
5938
                .effect_metadata_buffer
2✔
5939
                .update(cached_effect_metadata.table_id, gpu_effect_metadata);
6✔
5940

5941
            cached_effect_metadata.metadata = gpu_effect_metadata;
2✔
5942

5943
            // This triggers on all new spawns and annoys everyone; silence until we can at
5944
            // least warn only on non-first-spawn, and ideally split indirect data from that
5945
            // struct so we don't overwrite it and solve the issue.
5946
            debug!(
2✔
5947
                "Updated metadata entry {} for effect {:?}, this will reset it.",
5948
                cached_effect_metadata.table_id.0, main_entity
2✔
5949
            );
5950
        }
5951
    }
5952

5953
    // Once all EffectMetadata values are written, schedule a GPU upload
5954
    if effects_meta
330✔
5955
        .effect_metadata_buffer
330✔
UNCOV
5956
        .allocate_gpu(render_device.as_ref(), render_queue.as_ref())
×
5957
    {
5958
        // All those bind groups use the buffer so need to be re-created
5959
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
2✔
5960
        effects_meta.indirect_metadata_bind_group = None;
4✔
5961
        effect_bind_groups.init_metadata_bind_groups.clear();
4✔
5962
        effect_bind_groups.update_metadata_bind_groups.clear();
4✔
5963
    }
5964
}
5965

5966
/// Read the queued init fill dispatch operations, batch them together by
5967
/// contiguous source and destination entries in the buffers, and enqueue
5968
/// corresponding GPU buffer fill dispatch operations for all batches.
5969
///
5970
/// This system runs after the GPU buffers have been (re-)allocated in
5971
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5972
/// reference them from the generic [`GpuBufferOperationQueue`].
5973
pub(crate) fn queue_init_fill_dispatch_ops(
330✔
5974
    event_cache: Res<EventCache>,
5975
    render_device: Res<RenderDevice>,
5976
    render_queue: Res<RenderQueue>,
5977
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5978
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5979
) {
5980
    // Submit all queued init fill dispatch operations with the proper buffers
5981
    if !init_fill_dispatch_queue.is_empty() {
330✔
5982
        let src_buffer = event_cache.child_infos().buffer();
×
UNCOV
5983
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
×
5984
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
UNCOV
5985
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
×
5986
        } else {
5987
            if src_buffer.is_none() {
×
5988
                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());
×
5989
            }
5990
            if dst_buffer.is_none() {
×
5991
                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());
×
5992
            }
5993
        }
5994
    }
5995

5996
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5997
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
990✔
5998
}
5999

6000
#[derive(SystemParam)]
6001
pub struct PipelineParams<'w, 's> {
6002
    dispatch_indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
6003
    utils_pipeline: Res<'w, UtilsPipeline>,
6004
    init_pipeline: Res<'w, ParticlesInitPipeline>,
6005
    update_pipeline: Res<'w, ParticlesUpdatePipeline>,
6006
    render_pipeline: ResMut<'w, ParticlesRenderPipeline>,
6007
    marker: PhantomData<&'s usize>,
6008
}
6009

6010
pub(crate) fn prepare_bind_groups(
330✔
6011
    mut effects_meta: ResMut<EffectsMeta>,
6012
    mut effect_cache: ResMut<EffectCache>,
6013
    mut event_cache: ResMut<EventCache>,
6014
    mut effect_bind_groups: ResMut<EffectBindGroups>,
6015
    mut property_bind_groups: ResMut<PropertyBindGroups>,
6016
    mut sort_bind_groups: ResMut<SortBindGroups>,
6017
    property_cache: Res<PropertyCache>,
6018
    sorted_effect_batched: Res<SortedEffectBatches>,
6019
    render_device: Res<RenderDevice>,
6020
    pipeline_cache: Res<PipelineCache>,
6021
    pipelines: PipelineParams,
6022
    gpu_images: Res<RenderAssets<GpuImage>>,
6023
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
6024
) {
6025
    // We can't simulate nor render anything without at least the spawner buffer
6026
    if effects_meta.spawner_buffer.is_empty() {
660✔
6027
        return;
18✔
6028
    }
6029
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
312✔
6030
        return;
×
6031
    };
6032

6033
    // Workaround for too many params in system (TODO: refactor to split work?)
NEW
6034
    let dispatch_indirect_pipeline = pipelines.dispatch_indirect_pipeline.into_inner();
×
NEW
6035
    let utils_pipeline = pipelines.utils_pipeline.into_inner();
×
NEW
6036
    let init_pipeline = pipelines.init_pipeline.into_inner();
×
NEW
6037
    let update_pipeline = pipelines.update_pipeline.into_inner();
×
NEW
6038
    let render_pipeline = pipelines.render_pipeline.into_inner();
×
6039

6040
    // Ensure child_infos@3 bind group for the indirect pass is available if needed.
6041
    // This returns `None` if the buffer is not ready, either because it's not
6042
    // created yet or because it's not needed (no child effect).
UNCOV
6043
    event_cache.ensure_indirect_child_info_buffer_bind_group(&render_device);
×
6044

6045
    {
6046
        #[cfg(feature = "trace")]
UNCOV
6047
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
×
6048

6049
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
6050
        // loop below. Also allows earlying out before doing any work in case some
6051
        // buffer is missing.
6052
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
312✔
6053
            return;
×
6054
        };
6055

6056
        // Create the sim_params@0 bind group for the global simulation parameters,
6057
        // which is shared by the init and update passes.
UNCOV
6058
        if effects_meta.update_sim_params_bind_group.is_none() {
×
6059
            if let Some(draw_indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() {
4✔
UNCOV
6060
                effects_meta.update_sim_params_bind_group = Some(render_device.create_bind_group(
×
UNCOV
6061
                    "hanabi:bind_group:vfx_update:sim_params@0",
×
NEW
6062
                    &pipeline_cache.get_bind_group_layout(&update_pipeline.sim_params_layout_desc),
×
UNCOV
6063
                    &[
×
6064
                        // @group(0) @binding(0) var<uniform> sim_params : SimParams;
UNCOV
6065
                        BindGroupEntry {
×
UNCOV
6066
                            binding: 0,
×
UNCOV
6067
                            resource: effects_meta.sim_params_uniforms.binding().unwrap(),
×
6068
                        },
6069
                        // @group(0) @binding(1) var<storage, read_write> draw_indirect_buffer :
6070
                        // array<DrawIndexedIndirectArgs>;
UNCOV
6071
                        BindGroupEntry {
×
UNCOV
6072
                            binding: 1,
×
UNCOV
6073
                            resource: draw_indirect_buffer.as_entire_binding(),
×
6074
                        },
6075
                    ],
6076
                ));
6077
            } else {
6078
                debug!("Cannot allocate bind group for vfx_update:sim_params@0 - draw_indirect_buffer not ready");
×
6079
            }
6080
        }
6081
        if effects_meta.indirect_sim_params_bind_group.is_none() {
2✔
6082
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
8✔
6083
                "hanabi:bind_group:vfx_indirect:sim_params@0",
2✔
6084
                &pipeline_cache.get_bind_group_layout(&init_pipeline.sim_params_layout_desc), // FIXME - Shared with init
6✔
6085
                &[
2✔
6086
                    // @group(0) @binding(0) var<uniform> sim_params : SimParams;
6087
                    BindGroupEntry {
2✔
6088
                        binding: 0,
2✔
6089
                        resource: effects_meta.sim_params_uniforms.binding().unwrap(),
4✔
6090
                    },
6091
                ],
6092
            ));
6093
        }
6094

6095
        // Create the @1 bind group for the indirect dispatch preparation pass of all
6096
        // effects at once
UNCOV
6097
        effects_meta.indirect_metadata_bind_group = match (
×
UNCOV
6098
            effects_meta.effect_metadata_buffer.buffer(),
×
UNCOV
6099
            effects_meta.dispatch_indirect_buffer.buffer(),
×
UNCOV
6100
            effects_meta.draw_indirect_buffer.buffer(),
×
6101
        ) {
6102
            (
6103
                Some(effect_metadata_buffer),
312✔
UNCOV
6104
                Some(dispatch_indirect_buffer),
×
UNCOV
6105
                Some(draw_indirect_buffer),
×
6106
            ) => {
6107
                // Base bind group for indirect pass
UNCOV
6108
                Some(render_device.create_bind_group(
×
UNCOV
6109
                    "hanabi:bind_group:vfx_indirect:metadata@1",
×
NEW
6110
                    &pipeline_cache.get_bind_group_layout(
×
NEW
6111
                        &dispatch_indirect_pipeline.effect_metadata_bind_group_layout_desc,
×
6112
                    ),
UNCOV
6113
                    &[
×
6114
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer :
6115
                        // array<u32>;
UNCOV
6116
                        BindGroupEntry {
×
UNCOV
6117
                            binding: 0,
×
UNCOV
6118
                            resource: effect_metadata_buffer.as_entire_binding(),
×
6119
                        },
6120
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer
6121
                        // : array<DispatchIndirectArgs>;
UNCOV
6122
                        BindGroupEntry {
×
UNCOV
6123
                            binding: 1,
×
UNCOV
6124
                            resource: dispatch_indirect_buffer.as_entire_binding(),
×
6125
                        },
6126
                        // @group(1) @binding(2) var<storage, read_write> draw_indirect_buffer :
6127
                        // array<u32>;
UNCOV
6128
                        BindGroupEntry {
×
UNCOV
6129
                            binding: 2,
×
UNCOV
6130
                            resource: draw_indirect_buffer.as_entire_binding(),
×
6131
                        },
6132
                    ],
6133
                ))
6134
            }
6135

6136
            // Some buffer is not yet available, can't create the bind group
6137
            _ => None,
×
6138
        };
6139

6140
        // Create the @2 bind group for the indirect dispatch preparation pass of all
6141
        // effects at once
6142
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
6143
            let bind_group = render_device.create_bind_group(
10✔
6144
                "hanabi:bind_group:vfx_indirect:spawner@2",
6145
                &pipeline_cache.get_bind_group_layout(
8✔
6146
                    &dispatch_indirect_pipeline.spawner_bind_group_layout_desc,
6✔
6147
                ),
6148
                &[
4✔
6149
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
6150
                    BindGroupEntry {
4✔
6151
                        binding: 0,
4✔
6152
                        resource: BindingResource::Buffer(BufferBinding {
4✔
6153
                            buffer: &spawner_buffer,
4✔
6154
                            offset: 0,
4✔
6155
                            size: None,
4✔
6156
                        }),
6157
                    },
6158
                ],
6159
            );
6160

6161
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
6162
        }
6163
    }
6164

6165
    // Create the per-slab bind groups
UNCOV
6166
    trace!("Create per-slab bind groups...");
×
6167
    for (slab_index, particle_slab) in effect_cache.slabs().iter().enumerate() {
312✔
6168
        #[cfg(feature = "trace")]
UNCOV
6169
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
×
6170

6171
        let Some(particle_slab) = particle_slab else {
312✔
6172
            trace!(
×
6173
                "Particle slab index #{} has no allocated EffectBuffer, skipped.",
6174
                slab_index
6175
            );
6176
            continue;
×
6177
        };
6178

6179
        // Ensure all effects in this batch have a bind group for the entire buffer of
6180
        // the group, since the update phase runs on an entire group/buffer at once,
6181
        // with all the effect instances in it batched together.
UNCOV
6182
        trace!("effect particle slab_index=#{}", slab_index);
×
UNCOV
6183
        effect_bind_groups
×
UNCOV
6184
            .particle_slabs
×
UNCOV
6185
            .entry(SlabId::new(slab_index as u32))
×
6186
            .or_insert_with(|| {
2✔
6187
                // Bind group particle@1 for render pass
6188
                trace!("Creating particle@1 bind group for buffer #{slab_index} in render pass");
2✔
6189
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
4✔
6190
                    render_device.limits().min_storage_buffer_offset_alignment,
2✔
6191
                );
6192
                let entries = [
4✔
6193
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
6194
                    BindGroupEntry {
4✔
6195
                        binding: 0,
4✔
6196
                        resource: particle_slab.as_entire_binding_particle(),
4✔
6197
                    },
6198
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
6199
                    BindGroupEntry {
4✔
6200
                        binding: 1,
4✔
6201
                        resource: particle_slab.as_entire_binding_indirect(),
4✔
6202
                    },
6203
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
6204
                    BindGroupEntry {
2✔
6205
                        binding: 2,
2✔
6206
                        resource: BindingResource::Buffer(BufferBinding {
2✔
6207
                            buffer: &spawner_buffer,
2✔
6208
                            offset: 0,
2✔
6209
                            size: Some(spawner_min_binding_size),
2✔
6210
                        }),
6211
                    },
6212
                ];
6213
                let render = render_device.create_bind_group(
8✔
6214
                    &format!("hanabi:bind_group:render:particles@1:vfx{slab_index}")[..],
4✔
6215
                    particle_slab.render_particles_buffer_layout(),
4✔
6216
                    &entries[..],
2✔
6217
                );
6218

6219
                BufferBindGroups { render }
2✔
6220
            });
6221
    }
6222

6223
    // Create bind groups for queued GPU buffer operations
NEW
6224
    gpu_buffer_operation_queue.create_bind_groups(&render_device, utils_pipeline);
×
6225

6226
    // Create the per-effect bind groups
UNCOV
6227
    let spawner_buffer_binding_size =
×
UNCOV
6228
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
×
6229
    for effect_batch in sorted_effect_batched.iter() {
312✔
6230
        #[cfg(feature = "trace")]
6231
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
936✔
6232

6233
        // Create the property bind group @2 if needed
6234
        if let Some(property_key) = &effect_batch.property_key {
325✔
6235
            if let Err(err) = property_bind_groups.ensure_exists(
×
UNCOV
6236
                property_key,
×
UNCOV
6237
                &property_cache,
×
UNCOV
6238
                &spawner_buffer,
×
UNCOV
6239
                spawner_buffer_binding_size,
×
UNCOV
6240
                &render_device,
×
NEW
6241
                &pipeline_cache,
×
6242
            ) {
6243
                error!("Failed to create property bind group for effect batch: {err:?}");
×
UNCOV
6244
                continue;
×
6245
            }
6246
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
897✔
6247
            &property_cache,
598✔
6248
            &spawner_buffer,
598✔
6249
            spawner_buffer_binding_size,
598✔
6250
            &render_device,
299✔
6251
            &pipeline_cache,
299✔
6252
        ) {
6253
            error!("Failed to create property bind group for effect batch: {err:?}");
×
UNCOV
6254
            continue;
×
6255
        }
6256

6257
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
6258
        // simulate particles.
6259
        if effect_cache
312✔
6260
            .create_particle_sim_bind_group(
UNCOV
6261
                &effect_batch.slab_id,
×
UNCOV
6262
                &render_device,
×
UNCOV
6263
                effect_batch.particle_layout.min_binding_size32(),
×
UNCOV
6264
                effect_batch.parent_min_binding_size,
×
UNCOV
6265
                effect_batch.parent_binding_source.as_ref(),
×
NEW
6266
                &pipeline_cache,
×
6267
            )
6268
            .is_err()
6269
        {
6270
            error!("No particle buffer allocated for effect batch.");
×
6271
            continue;
×
6272
        }
6273

6274
        // Bind group @3 of init pass
6275
        // FIXME - this is instance-dependent, not buffer-dependent
6276
        {
UNCOV
6277
            let consume_gpu_spawn_events = effect_batch
×
UNCOV
6278
                .layout_flags
×
UNCOV
6279
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
×
6280
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
312✔
UNCOV
6281
                effect_batch.spawn_info
×
6282
            {
6283
                assert!(consume_gpu_spawn_events);
×
6284
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
6285
                Some(ConsumeEventBuffers {
×
6286
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
6287
                    events: BufferSlice {
×
6288
                        buffer: event_cache
×
6289
                            .get_buffer(cached_effect_events.buffer_index)
×
6290
                            .unwrap(),
×
6291
                        // Note: event range is in u32 count, not bytes
6292
                        offset: cached_effect_events.range.start * 4,
×
6293
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
6294
                    },
6295
                })
6296
            } else {
6297
                assert!(!consume_gpu_spawn_events);
624✔
6298
                None
312✔
6299
            };
6300
            let Some(init_metadata_layout_desc) =
312✔
NEW
6301
                effect_cache.metadata_init_bind_group_layout_desc(consume_gpu_spawn_events)
×
6302
            else {
6303
                continue;
×
6304
            };
UNCOV
6305
            if effect_bind_groups
×
6306
                .get_or_create_init_metadata(
UNCOV
6307
                    effect_batch,
×
UNCOV
6308
                    &render_device,
×
NEW
6309
                    &pipeline_cache.get_bind_group_layout(init_metadata_layout_desc),
×
UNCOV
6310
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
×
UNCOV
6311
                    consume_event_buffers,
×
6312
                )
6313
                .is_err()
6314
            {
6315
                continue;
×
6316
            }
6317
        }
6318

6319
        // Bind group @3 of update pass
6320
        // FIXME - this is instance-dependent, not buffer-dependent#
6321
        {
UNCOV
6322
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
×
6323

6324
            let Some(update_metadata_layout_desc) =
312✔
NEW
6325
                effect_cache.metadata_update_bind_group_layout_desc(num_event_buffers)
×
6326
            else {
6327
                continue;
×
6328
            };
UNCOV
6329
            if effect_bind_groups
×
6330
                .get_or_create_update_metadata(
UNCOV
6331
                    effect_batch,
×
UNCOV
6332
                    &render_device,
×
NEW
6333
                    &pipeline_cache.get_bind_group_layout(update_metadata_layout_desc),
×
UNCOV
6334
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
×
UNCOV
6335
                    event_cache.child_infos_buffer(),
×
UNCOV
6336
                    &effect_batch.child_event_buffers[..],
×
6337
                )
6338
                .is_err()
6339
            {
6340
                continue;
×
6341
            }
6342
        }
6343

UNCOV
6344
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
×
6345
            let effect_buffer = effect_cache.get_slab(&effect_batch.slab_id).unwrap();
×
6346

6347
            // Bind group @0 of sort-fill pass
6348
            let particle_buffer = effect_buffer.particle_buffer();
×
6349
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6350
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6351
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
6352
                &effect_batch.particle_layout,
×
6353
                particle_buffer,
×
6354
                indirect_index_buffer,
×
6355
                effect_metadata_buffer,
×
6356
                &spawner_buffer,
×
NEW
6357
                &pipeline_cache,
×
6358
            ) {
UNCOV
6359
                error!(
×
6360
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
6361
                    err
6362
                );
UNCOV
6363
                continue;
×
6364
            }
6365

6366
            // Bind group @0 of sort pass
NEW
6367
            if let Err(err) = sort_bind_groups.ensure_sort_bind_group(&pipeline_cache) {
×
NEW
6368
                error!(
×
6369
                    "Failed to create sort bind group @0 for ribbon effect: {:?}",
6370
                    err
6371
                );
NEW
6372
                continue;
×
6373
            }
6374

6375
            // Bind group @0 of sort-copy pass
6376
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6377
            if let Err(err) = sort_bind_groups.ensure_sort_copy_bind_group(
×
6378
                indirect_index_buffer,
×
6379
                effect_metadata_buffer,
×
6380
                &spawner_buffer,
×
NEW
6381
                &pipeline_cache,
×
6382
            ) {
UNCOV
6383
                error!(
×
6384
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
6385
                    err
6386
                );
UNCOV
6387
                continue;
×
6388
            }
6389
        }
6390

6391
        // Ensure the particle texture(s) are available as GPU resources and that a bind
6392
        // group for them exists
6393
        // FIXME fix this insert+get below
6394
        if !effect_batch.texture_layout.layout.is_empty() {
312✔
6395
            // This should always be available, as this is cached into the render pipeline
6396
            // just before we start specializing it.
NEW
6397
            let Some(material_bind_group_layout_desc) =
×
6398
                render_pipeline.get_material(&effect_batch.texture_layout)
×
6399
            else {
6400
                error!(
×
6401
                    "Failed to find material bind group layout for particle slab #{}",
6402
                    effect_batch.slab_id.index()
×
6403
                );
6404
                continue;
×
6405
            };
6406

6407
            // TODO = move
6408
            let material = Material {
UNCOV
6409
                layout: effect_batch.texture_layout.clone(),
×
6410
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6411
            };
UNCOV
6412
            assert_eq!(material.layout.layout.len(), material.textures.len());
×
6413

6414
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6415
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6416
                trace!(
×
6417
                    "Temporarily ignoring material {:?} due to missing image(s)",
6418
                    material
6419
                );
6420
                continue;
×
6421
            };
6422

UNCOV
6423
            effect_bind_groups
×
UNCOV
6424
                .material_bind_groups
×
UNCOV
6425
                .entry(material.clone())
×
6426
                .or_insert_with(|| {
×
6427
                    debug!("Creating material bind group for material {:?}", material);
×
6428
                    render_device.create_bind_group(
×
6429
                        &format!(
×
6430
                            "hanabi:material_bind_group_{}",
×
6431
                            material.layout.layout.len()
×
6432
                        )[..],
×
NEW
6433
                        &pipeline_cache.get_bind_group_layout(material_bind_group_layout_desc),
×
6434
                        &bind_group_entries[..],
×
6435
                    )
6436
                });
6437
        }
6438
    }
6439
}
6440

6441
type DrawEffectsSystemState = SystemState<(
6442
    SRes<EffectsMeta>,
6443
    SRes<EffectBindGroups>,
6444
    SRes<PipelineCache>,
6445
    SRes<RenderAssets<RenderMesh>>,
6446
    SRes<MeshAllocator>,
6447
    SQuery<Read<ViewUniformOffset>>,
6448
    SRes<SortedEffectBatches>,
6449
    SQuery<Read<EffectDrawBatch>>,
6450
)>;
6451

6452
/// Draw function for rendering all active effects for the current frame.
6453
///
6454
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6455
/// and the [`Transparent3d`] phase of the main 3D pass.
6456
pub(crate) struct DrawEffects {
6457
    params: DrawEffectsSystemState,
6458
}
6459

6460
impl DrawEffects {
6461
    pub fn new(world: &mut World) -> Self {
12✔
6462
        Self {
6463
            params: SystemState::new(world),
12✔
6464
        }
6465
    }
6466
}
6467

6468
/// Draw all particles of a single effect in view, in 2D or 3D.
6469
///
6470
/// FIXME: use pipeline ID to look up which group index it is.
6471
fn draw<'w>(
311✔
6472
    world: &'w World,
6473
    pass: &mut TrackedRenderPass<'w>,
6474
    view: Entity,
6475
    entity: (Entity, MainEntity),
6476
    pipeline_id: CachedRenderPipelineId,
6477
    params: &mut DrawEffectsSystemState,
6478
) {
6479
    let (
×
6480
        effects_meta,
311✔
6481
        effect_bind_groups,
311✔
6482
        pipeline_cache,
311✔
6483
        meshes,
311✔
6484
        mesh_allocator,
311✔
6485
        views,
311✔
6486
        sorted_effect_batches,
311✔
6487
        effect_draw_batches,
311✔
6488
    ) = params.get(world);
622✔
6489
    let view_uniform = views.get(view).unwrap();
1,555✔
6490
    let effects_meta = effects_meta.into_inner();
933✔
6491
    let effect_bind_groups = effect_bind_groups.into_inner();
933✔
6492
    let meshes = meshes.into_inner();
933✔
6493
    let mesh_allocator = mesh_allocator.into_inner();
933✔
6494
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
1,555✔
6495
    let effect_batch = sorted_effect_batches
933✔
6496
        .get(effect_draw_batch.effect_batch_index)
311✔
6497
        .unwrap();
6498

6499
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
933✔
6500
        return;
×
6501
    };
6502

UNCOV
6503
    trace!("render pass");
×
6504

6505
    pass.set_render_pipeline(pipeline);
×
6506

6507
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
311✔
6508
        return;
×
6509
    };
6510
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
311✔
6511
        return;
×
6512
    };
6513

6514
    // Vertex buffer containing the particle model to draw. Generally a quad.
6515
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6516
    // "base_vertex" in the indirect struct...
6517
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6518

6519
    // View properties (camera matrix, etc.)
6520
    pass.set_bind_group(
×
6521
        0,
6522
        effects_meta.view_bind_group.as_ref().unwrap(),
×
6523
        &[view_uniform.offset],
×
6524
    );
6525

6526
    // Particles buffer
6527
    let spawner_base = effect_batch.spawner_base;
×
6528
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
6529
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6530
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
622✔
6531
    pass.set_bind_group(
622✔
6532
        1,
6533
        effect_bind_groups
622✔
6534
            .particle_render(&effect_batch.slab_id)
622✔
6535
            .unwrap(),
311✔
6536
        &[spawner_offset],
311✔
6537
    );
6538

6539
    // Particle texture
6540
    // TODO = move
6541
    let material = Material {
6542
        layout: effect_batch.texture_layout.clone(),
622✔
6543
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
933✔
6544
    };
6545
    if !effect_batch.texture_layout.layout.is_empty() {
311✔
6546
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
6547
            pass.set_bind_group(2, bind_group, &[]);
×
6548
        } else {
6549
            // Texture(s) not ready; skip this drawing for now
6550
            trace!(
×
6551
                "Particle material bind group not available for batch slab_id={}. Skipping draw call.",
×
6552
                effect_batch.slab_id.index(),
×
6553
            );
6554
            return;
×
6555
        }
6556
    }
6557

6558
    let draw_indirect_index = effect_batch.draw_indirect_buffer_row_index.0;
311✔
6559
    assert_eq!(GpuDrawIndexedIndirectArgs::SHADER_SIZE.get(), 20);
×
6560
    let draw_indirect_offset =
311✔
6561
        draw_indirect_index as u64 * GpuDrawIndexedIndirectArgs::SHADER_SIZE.get();
311✔
6562
    trace!(
311✔
UNCOV
6563
        "Draw up to {} particles with {} vertices per particle for batch from particle slab #{} \
×
UNCOV
6564
            (effect_metadata_index={}, draw_indirect_offset={}B).",
×
6565
        effect_batch.slice.len(),
622✔
6566
        render_mesh.vertex_count,
×
6567
        effect_batch.slab_id.index(),
622✔
6568
        draw_indirect_index,
×
6569
        draw_indirect_offset,
×
6570
    );
6571

6572
    let Some(indirect_buffer) = effects_meta.draw_indirect_buffer.buffer() else {
622✔
6573
        trace!(
×
6574
            "The draw indirect buffer containing the indirect draw args is not ready for batch slab_id=#{}. Skipping draw call.",
×
6575
            effect_batch.slab_id.index(),
×
6576
        );
6577
        return;
×
6578
    };
6579

6580
    match render_mesh.buffer_info {
×
6581
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
311✔
6582
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
311✔
6583
            else {
×
6584
                trace!(
×
6585
                    "The index buffer for indexed rendering is not ready for batch slab_id=#{}. Skipping draw call.",
×
6586
                    effect_batch.slab_id.index(),
×
6587
                );
6588
                return;
×
6589
            };
6590

NEW
6591
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), index_format);
×
6592
            pass.draw_indexed_indirect(indirect_buffer, draw_indirect_offset);
×
6593
        }
6594
        RenderMeshBufferInfo::NonIndexed => {
×
6595
            pass.draw_indirect(indirect_buffer, draw_indirect_offset);
×
6596
        }
6597
    }
6598
}
6599

6600
#[cfg(feature = "2d")]
6601
impl Draw<Transparent2d> for DrawEffects {
6602
    fn draw<'w>(
×
6603
        &mut self,
6604
        world: &'w World,
6605
        pass: &mut TrackedRenderPass<'w>,
6606
        view: Entity,
6607
        item: &Transparent2d,
6608
    ) -> Result<(), DrawError> {
6609
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6610
        draw(
6611
            world,
×
6612
            pass,
×
6613
            view,
×
6614
            item.entity,
×
6615
            item.pipeline,
×
6616
            &mut self.params,
×
6617
        );
6618
        Ok(())
×
6619
    }
6620
}
6621

6622
#[cfg(feature = "3d")]
6623
impl Draw<Transparent3d> for DrawEffects {
6624
    fn draw<'w>(
311✔
6625
        &mut self,
6626
        world: &'w World,
6627
        pass: &mut TrackedRenderPass<'w>,
6628
        view: Entity,
6629
        item: &Transparent3d,
6630
    ) -> Result<(), DrawError> {
6631
        trace!("Draw<Transparent3d>: view={:?}", view);
311✔
6632
        draw(
6633
            world,
311✔
6634
            pass,
311✔
6635
            view,
311✔
6636
            item.entity,
311✔
6637
            item.pipeline,
311✔
6638
            &mut self.params,
311✔
6639
        );
6640
        Ok(())
311✔
6641
    }
6642
}
6643

6644
#[cfg(feature = "3d")]
6645
impl Draw<AlphaMask3d> for DrawEffects {
6646
    fn draw<'w>(
×
6647
        &mut self,
6648
        world: &'w World,
6649
        pass: &mut TrackedRenderPass<'w>,
6650
        view: Entity,
6651
        item: &AlphaMask3d,
6652
    ) -> Result<(), DrawError> {
6653
        trace!("Draw<AlphaMask3d>: view={:?}", view);
×
6654
        draw(
6655
            world,
×
6656
            pass,
×
6657
            view,
×
6658
            item.representative_entity,
×
6659
            item.batch_set_key.pipeline,
×
6660
            &mut self.params,
×
6661
        );
6662
        Ok(())
×
6663
    }
6664
}
6665

6666
#[cfg(feature = "3d")]
6667
impl Draw<Opaque3d> for DrawEffects {
6668
    fn draw<'w>(
×
6669
        &mut self,
6670
        world: &'w World,
6671
        pass: &mut TrackedRenderPass<'w>,
6672
        view: Entity,
6673
        item: &Opaque3d,
6674
    ) -> Result<(), DrawError> {
6675
        trace!("Draw<Opaque3d>: view={:?}", view);
×
6676
        draw(
6677
            world,
×
6678
            pass,
×
6679
            view,
×
6680
            item.representative_entity,
×
6681
            item.batch_set_key.pipeline,
×
6682
            &mut self.params,
×
6683
        );
6684
        Ok(())
×
6685
    }
6686
}
6687

6688
/// Render node to run the simulation sub-graph once per frame.
6689
///
6690
/// This node doesn't simulate anything by itself, but instead schedules the
6691
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6692
/// actual simulation.
6693
///
6694
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6695
/// renders all the views, such that rendered views have access to the
6696
/// just-simulated particles to render them.
6697
///
6698
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6699
pub(crate) struct VfxSimulateDriverNode;
6700

6701
impl Node for VfxSimulateDriverNode {
6702
    fn run(
330✔
6703
        &self,
6704
        graph: &mut RenderGraphContext,
6705
        _render_context: &mut RenderContext,
6706
        _world: &World,
6707
    ) -> Result<(), NodeRunError> {
6708
        graph.run_sub_graph(
660✔
6709
            crate::plugin::simulate_graph::HanabiSimulateGraph,
330✔
6710
            vec![],
330✔
6711
            None,
330✔
6712
            Some("hanabi".to_string()),
330✔
6713
        )?;
6714
        Ok(())
330✔
6715
    }
6716
}
6717

6718
#[derive(Debug, Clone, PartialEq, Eq)]
6719
enum HanabiPipelineId {
6720
    Invalid,
6721
    Cached(CachedComputePipelineId),
6722
}
6723

6724
#[derive(Debug)]
6725
pub(crate) enum ComputePipelineError {
6726
    Queued,
6727
    Creating,
6728
    Error,
6729
}
6730

6731
impl From<&CachedPipelineState> for ComputePipelineError {
6732
    fn from(value: &CachedPipelineState) -> Self {
×
6733
        match value {
×
6734
            CachedPipelineState::Queued => Self::Queued,
×
6735
            CachedPipelineState::Creating(_) => Self::Creating,
×
6736
            CachedPipelineState::Err(_) => Self::Error,
×
6737
            _ => panic!("Trying to convert Ok state to error."),
×
6738
        }
6739
    }
6740
}
6741

6742
pub(crate) struct HanabiComputePass<'a> {
6743
    /// Pipeline cache to fetch cached compute pipelines by ID.
6744
    pipeline_cache: &'a PipelineCache,
6745
    /// WGPU compute pass.
6746
    compute_pass: ComputePass<'a>,
6747
    /// Current pipeline (cached).
6748
    pipeline_id: HanabiPipelineId,
6749
}
6750

6751
impl<'a> Deref for HanabiComputePass<'a> {
6752
    type Target = ComputePass<'a>;
6753

6754
    fn deref(&self) -> &Self::Target {
×
6755
        &self.compute_pass
×
6756
    }
6757
}
6758

6759
impl DerefMut for HanabiComputePass<'_> {
6760
    fn deref_mut(&mut self) -> &mut Self::Target {
4,308✔
6761
        &mut self.compute_pass
4,308✔
6762
    }
6763
}
6764

6765
impl<'a> HanabiComputePass<'a> {
6766
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
1,248✔
6767
        Self {
6768
            pipeline_cache,
6769
            compute_pass,
6770
            pipeline_id: HanabiPipelineId::Invalid,
6771
        }
6772
    }
6773

6774
    pub fn set_cached_compute_pipeline(
921✔
6775
        &mut self,
6776
        pipeline_id: CachedComputePipelineId,
6777
    ) -> Result<(), ComputePipelineError> {
6778
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
921✔
6779
            trace!("set_cached_compute_pipeline() id={pipeline_id:?} -> already set; skipped");
×
6780
            return Ok(());
×
6781
        }
UNCOV
6782
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
×
6783
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
921✔
6784
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6785
            if let CachedPipelineState::Err(err) = state {
×
6786
                error!(
×
6787
                    "Failed to find compute pipeline #{}: {:?}",
×
6788
                    pipeline_id.id(),
×
6789
                    err
×
6790
                );
6791
            } else {
6792
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6793
            }
6794
            return Err(state.into());
×
6795
        };
6796
        self.compute_pass.set_pipeline(pipeline);
×
6797
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6798
        Ok(())
×
6799
    }
6800
}
6801

6802
/// Render node to run the simulation of all effects once per frame.
6803
///
6804
/// Runs inside the simulation sub-graph, looping over all extracted effect
6805
/// batches to simulate them.
6806
pub(crate) struct VfxSimulateNode {}
6807

6808
impl VfxSimulateNode {
6809
    /// Create a new node for simulating the effects of the given world.
6810
    pub fn new(_world: &mut World) -> Self {
3✔
6811
        Self {}
6812
    }
6813

6814
    /// Begin a new compute pass and return a wrapper with extra
6815
    /// functionalities.
6816
    pub fn begin_compute_pass<'encoder>(
1,248✔
6817
        &self,
6818
        label: &str,
6819
        pipeline_cache: &'encoder PipelineCache,
6820
        render_context: &'encoder mut RenderContext,
6821
    ) -> HanabiComputePass<'encoder> {
6822
        let compute_pass =
1,248✔
6823
            render_context
1,248✔
6824
                .command_encoder()
6825
                .begin_compute_pass(&ComputePassDescriptor {
2,496✔
6826
                    label: Some(label),
1,248✔
6827
                    timestamp_writes: None,
1,248✔
6828
                });
6829
        HanabiComputePass::new(pipeline_cache, compute_pass)
3,744✔
6830
    }
6831
}
6832

6833
impl Node for VfxSimulateNode {
6834
    fn input(&self) -> Vec<SlotInfo> {
3✔
6835
        vec![]
3✔
6836
    }
6837

6838
    fn update(&mut self, _world: &mut World) {}
660✔
6839

6840
    fn run(
330✔
6841
        &self,
6842
        _graph: &mut RenderGraphContext,
6843
        render_context: &mut RenderContext,
6844
        world: &World,
6845
    ) -> Result<(), NodeRunError> {
6846
        trace!("VfxSimulateNode::run()");
330✔
6847

6848
        let pipeline_cache = world.resource::<PipelineCache>();
990✔
6849
        let effects_meta = world.resource::<EffectsMeta>();
990✔
6850
        let effect_bind_groups = world.resource::<EffectBindGroups>();
990✔
6851
        let property_bind_groups = world.resource::<PropertyBindGroups>();
990✔
6852
        let sort_bind_groups = world.resource::<SortBindGroups>();
990✔
6853
        let utils_pipeline = world.resource::<UtilsPipeline>();
990✔
6854
        let effect_cache = world.resource::<EffectCache>();
990✔
6855
        let event_cache = world.resource::<EventCache>();
990✔
6856
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
990✔
6857
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
990✔
6858
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
990✔
6859

6860
        // Make sure to schedule any buffer copy before accessing their content later in
6861
        // the GPU commands below.
6862
        {
6863
            let command_encoder = render_context.command_encoder();
1,320✔
6864
            effects_meta
660✔
6865
                .dispatch_indirect_buffer
660✔
6866
                .write_buffers(command_encoder);
990✔
6867
            effects_meta
660✔
6868
                .draw_indirect_buffer
660✔
6869
                .write_buffer(command_encoder);
990✔
6870
            effects_meta
660✔
6871
                .effect_metadata_buffer
660✔
6872
                .write_buffer(command_encoder);
990✔
6873
            event_cache.write_buffers(command_encoder);
1,320✔
6874
            sort_bind_groups.write_buffers(command_encoder);
660✔
6875
        }
6876

6877
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6878
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6879
        // the update pass of their parent effect during the previous frame.
6880
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
330✔
UNCOV
6881
            gpu_buffer_operations.dispatch(
×
UNCOV
6882
                *queue_index,
×
UNCOV
6883
                render_context,
×
UNCOV
6884
                utils_pipeline,
×
UNCOV
6885
                Some("hanabi:init_indirect_fill_dispatch"),
×
6886
            );
6887
        }
6888

6889
        // If there's no batch, there's nothing more to do. Avoid continuing because
6890
        // some GPU resources are missing, which is expected when there's no effect but
6891
        // is an error (and will log warnings/errors) otherwise.
6892
        if sorted_effect_batches.is_empty() {
660✔
6893
            return Ok(());
18✔
6894
        }
6895

6896
        // Compute init pass
6897
        {
UNCOV
6898
            trace!("init: loop over effect batches...");
×
6899

UNCOV
6900
            let mut compute_pass =
×
UNCOV
6901
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
×
6902

6903
            // Bind group simparams@0 is common to everything, only set once per init pass
UNCOV
6904
            compute_pass.set_bind_group(
×
6905
                0,
UNCOV
6906
                effects_meta
×
UNCOV
6907
                    .indirect_sim_params_bind_group
×
UNCOV
6908
                    .as_ref()
×
UNCOV
6909
                    .unwrap(),
×
UNCOV
6910
                &[],
×
6911
            );
6912

6913
            // Dispatch init compute jobs for all batches
6914
            for effect_batch in sorted_effect_batches.iter() {
312✔
6915
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6916
                // batch. Note that this hopefully should have been skipped earlier.
6917
                {
6918
                    let use_indirect_dispatch = effect_batch
624✔
6919
                        .layout_flags
312✔
6920
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
312✔
6921
                    match effect_batch.spawn_info {
312✔
6922
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
312✔
UNCOV
6923
                            assert!(!use_indirect_dispatch);
×
6924
                            if total_spawn_count == 0 {
312✔
6925
                                continue;
15✔
6926
                            }
6927
                        }
6928
                        BatchSpawnInfo::GpuSpawner { .. } => {
6929
                            assert!(use_indirect_dispatch);
×
6930
                        }
6931
                    }
6932
                }
6933

6934
                // Fetch bind group particle@1
6935
                let Some(particle_bind_group) =
297✔
6936
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
297✔
6937
                else {
6938
                    error!(
×
6939
                        "Failed to find init particle@1 bind group for slab #{}",
6940
                        effect_batch.slab_id.index()
×
6941
                    );
6942
                    continue;
×
6943
                };
6944

6945
                // Fetch bind group metadata@3
6946
                let Some(metadata_bind_group) = effect_bind_groups
297✔
UNCOV
6947
                    .init_metadata_bind_groups
×
UNCOV
6948
                    .get(&effect_batch.slab_id)
×
6949
                else {
6950
                    error!(
×
6951
                        "Failed to find init metadata@3 bind group for slab #{}",
6952
                        effect_batch.slab_id.index()
×
6953
                    );
6954
                    continue;
×
6955
                };
6956

UNCOV
6957
                if compute_pass
×
UNCOV
6958
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
×
6959
                    .is_err()
6960
                {
6961
                    continue;
×
6962
                }
6963

6964
                // Compute dynamic offsets
UNCOV
6965
                let spawner_base = effect_batch.spawner_base;
×
UNCOV
6966
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
UNCOV
6967
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
6968
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
594✔
6969
                let property_offset = effect_batch.property_offset;
594✔
6970

6971
                // Setup init pass
6972
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
891✔
6973
                let offsets = if let Some(property_offset) = property_offset {
594✔
UNCOV
6974
                    vec![spawner_offset, property_offset]
×
6975
                } else {
6976
                    vec![spawner_offset]
594✔
6977
                };
6978
                compute_pass.set_bind_group(
891✔
6979
                    2,
6980
                    property_bind_groups
594✔
6981
                        .get(effect_batch.property_key.as_ref())
1,188✔
6982
                        .unwrap(),
594✔
6983
                    &offsets[..],
297✔
6984
                );
6985
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
891✔
6986

6987
                // Dispatch init job
6988
                match effect_batch.spawn_info {
297✔
6989
                    // Indirect dispatch via GPU spawn events
6990
                    BatchSpawnInfo::GpuSpawner {
6991
                        init_indirect_dispatch_index,
×
6992
                        ..
6993
                    } => {
6994
                        assert!(effect_batch
×
6995
                            .layout_flags
×
6996
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6997

6998
                        // Note: the indirect offset of a dispatch workgroup only needs
6999
                        // 4-byte alignment
7000
                        assert_eq!(GpuDispatchIndirectArgs::min_size().get(), 12);
×
7001
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
7002

7003
                        trace!(
×
7004
                            "record commands for indirect init pipeline of effect {:?} \
7005
                                init_indirect_dispatch_index={} \
7006
                                indirect_offset={} \
7007
                                spawner_base={} \
7008
                                spawner_offset={} \
7009
                                property_key={:?}...",
7010
                            effect_batch.handle,
7011
                            init_indirect_dispatch_index,
7012
                            indirect_offset,
7013
                            spawner_base,
7014
                            spawner_offset,
7015
                            effect_batch.property_key,
7016
                        );
7017

7018
                        compute_pass.dispatch_workgroups_indirect(
×
7019
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
7020
                            indirect_offset,
×
7021
                        );
7022
                    }
7023

7024
                    // Direct dispatch via CPU spawn count
7025
                    BatchSpawnInfo::CpuSpawner {
7026
                        total_spawn_count: spawn_count,
297✔
7027
                    } => {
UNCOV
7028
                        assert!(!effect_batch
×
UNCOV
7029
                            .layout_flags
×
UNCOV
7030
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
7031

7032
                        const WORKGROUP_SIZE: u32 = 64;
7033
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
297✔
7034

UNCOV
7035
                        trace!(
×
7036
                            "record commands for init pipeline of effect {:?} \
7037
                                (spawn {} particles => {} workgroups) spawner_base={} \
7038
                                spawner_offset={} \
7039
                                property_key={:?}...",
7040
                            effect_batch.handle,
7041
                            spawn_count,
7042
                            workgroup_count,
7043
                            spawner_base,
7044
                            spawner_offset,
7045
                            effect_batch.property_key,
7046
                        );
7047

UNCOV
7048
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
7049
                    }
7050
                }
7051

7052
                trace!("init compute dispatched");
297✔
7053
            }
7054
        }
7055

7056
        // Compute indirect dispatch pass
7057
        if let (
7058
            Some(_),
7059
            true,
7060
            Some(indirect_metadata_bind_group),
312✔
7061
            Some(indirect_sim_params_bind_group),
312✔
7062
            Some(indirect_spawner_bind_group),
312✔
7063
        ) = (
7064
            effects_meta.spawner_buffer.buffer(),
312✔
NEW
7065
            !effects_meta.spawner_buffer.is_empty(),
×
NEW
7066
            &effects_meta.indirect_metadata_bind_group,
×
NEW
7067
            &effects_meta.indirect_sim_params_bind_group,
×
NEW
7068
            &effects_meta.indirect_spawner_bind_group,
×
7069
        ) {
7070
            // Only start a compute pass if there's an effect; makes things clearer in
7071
            // debugger.
7072
            let mut compute_pass =
312✔
7073
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
1,560✔
7074

7075
            // Dispatch indirect dispatch compute job
7076
            trace!("record commands for indirect dispatch pipeline...");
312✔
7077

7078
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
624✔
7079
            if has_gpu_spawn_events {
312✔
7080
                if let Some(indirect_child_info_buffer_bind_group) =
×
7081
                    event_cache.indirect_child_info_buffer_bind_group()
×
7082
                {
UNCOV
7083
                    assert!(has_gpu_spawn_events);
×
7084
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
7085
                } else {
7086
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
7087
                    // render_context
7088
                    //     .command_encoder()
7089
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
7090
                    // FIXME - Bevy doesn't allow returning custom errors here...
7091
                    return Ok(());
×
7092
                }
7093
            }
7094

7095
            if compute_pass
312✔
UNCOV
7096
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
×
7097
                .is_err()
7098
            {
7099
                // FIXME - Bevy doesn't allow returning custom errors here...
7100
                return Ok(());
×
7101
            }
7102

7103
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
7104
            // the size exluding gaps!");
7105
            const WORKGROUP_SIZE: u32 = 64;
7106
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
UNCOV
7107
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
×
UNCOV
7108
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
×
7109

7110
            // Setup vfx_indirect pass
NEW
7111
            compute_pass.set_bind_group(0, indirect_sim_params_bind_group, &[]);
×
NEW
7112
            compute_pass.set_bind_group(1, indirect_metadata_bind_group, &[]);
×
NEW
7113
            compute_pass.set_bind_group(2, indirect_spawner_bind_group, &[]);
×
UNCOV
7114
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
×
UNCOV
7115
            trace!(
×
7116
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
7117
                total_effect_count,
7118
                workgroup_count
7119
            );
7120
        }
7121

7122
        // Compute update pass
7123
        {
7124
            let Some(indirect_buffer) = effects_meta.dispatch_indirect_buffer.buffer() else {
624✔
7125
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
7126
                render_context
×
7127
                    .command_encoder()
7128
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
7129
                // FIXME - Bevy doesn't allow returning custom errors here...
7130
                return Ok(());
×
7131
            };
7132

UNCOV
7133
            let mut compute_pass =
×
UNCOV
7134
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
×
7135

7136
            // Bind group simparams@0 is common to everything, only set once per update pass
UNCOV
7137
            compute_pass.set_bind_group(
×
7138
                0,
UNCOV
7139
                effects_meta.update_sim_params_bind_group.as_ref().unwrap(),
×
UNCOV
7140
                &[],
×
7141
            );
7142

7143
            // Dispatch update compute jobs
7144
            for effect_batch in sorted_effect_batches.iter() {
312✔
7145
                // Fetch bind group particle@1
7146
                let Some(particle_bind_group) =
312✔
7147
                    effect_cache.particle_sim_bind_group(&effect_batch.slab_id)
624✔
7148
                else {
7149
                    error!(
×
7150
                        "Failed to find update particle@1 bind group for slab #{}",
7151
                        effect_batch.slab_id.index()
×
7152
                    );
7153
                    compute_pass.insert_debug_marker("ERROR:MissingParticleSimBindGroup");
×
7154
                    continue;
×
7155
                };
7156

7157
                // Fetch bind group metadata@3
7158
                let Some(metadata_bind_group) = effect_bind_groups
312✔
UNCOV
7159
                    .update_metadata_bind_groups
×
UNCOV
7160
                    .get(&effect_batch.slab_id)
×
7161
                else {
7162
                    error!(
×
7163
                        "Failed to find update metadata@3 bind group for slab #{}",
7164
                        effect_batch.slab_id.index()
×
7165
                    );
7166
                    compute_pass.insert_debug_marker("ERROR:MissingMetadataBindGroup");
×
7167
                    continue;
×
7168
                };
7169

7170
                // Fetch compute pipeline
7171
                if let Err(err) = compute_pass
×
UNCOV
7172
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
×
7173
                {
UNCOV
7174
                    compute_pass.insert_debug_marker(&format!(
×
UNCOV
7175
                        "ERROR:FailedToSetCachedUpdatePipeline:{:?}",
×
UNCOV
7176
                        err
×
7177
                    ));
UNCOV
7178
                    continue;
×
7179
                }
7180

7181
                // Compute dynamic offsets
7182
                let spawner_base = effect_batch.spawner_base;
624✔
7183
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
936✔
7184
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
936✔
7185
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
312✔
UNCOV
7186
                let property_offset = effect_batch.property_offset;
×
7187

UNCOV
7188
                trace!(
×
7189
                    "record commands for update pipeline of effect {:?} spawner_base={}",
7190
                    effect_batch.handle,
7191
                    spawner_base,
7192
                );
7193

7194
                // Setup update pass
UNCOV
7195
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
×
7196
                let offsets = if let Some(property_offset) = property_offset {
13✔
UNCOV
7197
                    vec![spawner_offset, property_offset]
×
7198
                } else {
7199
                    vec![spawner_offset]
598✔
7200
                };
UNCOV
7201
                compute_pass.set_bind_group(
×
7202
                    2,
UNCOV
7203
                    property_bind_groups
×
UNCOV
7204
                        .get(effect_batch.property_key.as_ref())
×
UNCOV
7205
                        .unwrap(),
×
UNCOV
7206
                    &offsets[..],
×
7207
                );
UNCOV
7208
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
×
7209

7210
                // Dispatch update job
UNCOV
7211
                let dispatch_indirect_offset = effect_batch
×
UNCOV
7212
                    .dispatch_buffer_indices
×
UNCOV
7213
                    .update_dispatch_indirect_buffer_row_index
×
UNCOV
7214
                    * 12;
×
UNCOV
7215
                trace!(
×
7216
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
7217
                    indirect_buffer,
7218
                    dispatch_indirect_offset,
7219
                );
UNCOV
7220
                compute_pass
×
UNCOV
7221
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
×
7222

UNCOV
7223
                trace!("update compute dispatched");
×
7224
            }
7225
        }
7226

7227
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
7228
        // batch of particles which needs sorting, based on the actual number of alive
7229
        // particles in the batch after their update in the compute update pass. Since
7230
        // particles may die during update, this may be different from the number of
7231
        // particles updated.
7232
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
312✔
UNCOV
7233
            gpu_buffer_operations.dispatch(
×
UNCOV
7234
                *queue_index,
×
UNCOV
7235
                render_context,
×
UNCOV
7236
                utils_pipeline,
×
UNCOV
7237
                Some("hanabi:sort_fill_dispatch"),
×
7238
            );
7239
        }
7240

7241
        // Compute sort pass
7242
        {
UNCOV
7243
            let mut compute_pass =
×
UNCOV
7244
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
×
7245

UNCOV
7246
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
UNCOV
7247
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
×
7248

7249
            // Loop on batches and find those which need sorting
7250
            for effect_batch in sorted_effect_batches.iter() {
312✔
7251
                trace!("Processing effect batch for sorting...");
312✔
7252
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
312✔
7253
                    continue;
312✔
7254
                }
7255
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
7256
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
7257

7258
                let Some(effect_buffer) = effect_cache.get_slab(&effect_batch.slab_id) else {
×
7259
                    warn!("Missing sort-fill effect buffer.");
×
7260
                    // render_context
7261
                    //     .command_encoder()
7262
                    //     .insert_debug_marker("ERROR:MissingEffectBatchBuffer");
7263
                    continue;
×
7264
                };
7265

UNCOV
7266
                let indirect_dispatch_index = *effect_batch
×
UNCOV
7267
                    .sort_fill_indirect_dispatch_index
×
UNCOV
7268
                    .as_ref()
×
UNCOV
7269
                    .unwrap();
×
UNCOV
7270
                let indirect_offset =
×
UNCOV
7271
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
×
7272

7273
                // Fill the sort buffer with the key-value pairs to sort
7274
                {
UNCOV
7275
                    compute_pass.push_debug_group("hanabi:sort_fill");
×
7276

7277
                    // Fetch compute pipeline
7278
                    let Some(pipeline_id) =
×
UNCOV
7279
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
×
7280
                    else {
7281
                        warn!("Missing sort-fill pipeline.");
×
7282
                        compute_pass.insert_debug_marker("ERROR:MissingSortFillPipeline");
×
7283
                        continue;
×
7284
                    };
UNCOV
7285
                    if compute_pass
×
UNCOV
7286
                        .set_cached_compute_pipeline(pipeline_id)
×
7287
                        .is_err()
7288
                    {
7289
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortFillPipeline");
×
7290
                        compute_pass.pop_debug_group();
×
7291
                        // FIXME - Bevy doesn't allow returning custom errors here...
7292
                        return Ok(());
×
7293
                    }
7294

UNCOV
7295
                    let spawner_base = effect_batch.spawner_base;
×
UNCOV
7296
                    let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
UNCOV
7297
                    assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
7298
                    let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
7299

7300
                    // Bind group sort_fill@0
7301
                    let particle_buffer = effect_buffer.particle_buffer();
×
7302
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
7303
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
7304
                        particle_buffer.id(),
×
7305
                        indirect_index_buffer.id(),
×
7306
                        effect_metadata_buffer.id(),
×
7307
                    ) else {
7308
                        warn!("Missing sort-fill bind group.");
×
7309
                        compute_pass.insert_debug_marker("ERROR:MissingSortFillBindGroup");
×
7310
                        continue;
×
7311
                    };
UNCOV
7312
                    let effect_metadata_offset = effects_meta
×
UNCOV
7313
                        .gpu_limits
×
UNCOV
7314
                        .effect_metadata_offset(effect_batch.metadata_table_id.0)
×
UNCOV
7315
                        as u32;
×
UNCOV
7316
                    compute_pass.set_bind_group(
×
7317
                        0,
UNCOV
7318
                        bind_group,
×
UNCOV
7319
                        &[effect_metadata_offset, spawner_offset],
×
7320
                    );
7321

UNCOV
7322
                    compute_pass
×
UNCOV
7323
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
7324
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
7325

UNCOV
7326
                    compute_pass.pop_debug_group();
×
7327
                }
7328

7329
                // Do the actual sort
7330
                {
UNCOV
7331
                    compute_pass.push_debug_group("hanabi:sort");
×
7332

UNCOV
7333
                    if compute_pass
×
UNCOV
7334
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
×
7335
                        .is_err()
7336
                    {
7337
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortPipeline");
×
7338
                        compute_pass.pop_debug_group();
×
7339
                        // FIXME - Bevy doesn't allow returning custom errors here...
7340
                        return Ok(());
×
7341
                    }
7342

NEW
7343
                    let Some(bind_group) = sort_bind_groups.sort_bind_group() else {
×
NEW
7344
                        warn!("Missing sort bind group.");
×
NEW
7345
                        compute_pass.insert_debug_marker("ERROR:MissingSortBindGroup");
×
NEW
7346
                        continue;
×
7347
                    };
NEW
7348
                    compute_pass.set_bind_group(0, bind_group, &[]);
×
UNCOV
7349
                    compute_pass
×
UNCOV
7350
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
7351
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7352

UNCOV
7353
                    compute_pass.pop_debug_group();
×
7354
                }
7355

7356
                // Copy the sorted particle indices back into the indirect index buffer, where
7357
                // the render pass will read them.
7358
                {
UNCOV
7359
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
×
7360

7361
                    // Fetch compute pipeline
UNCOV
7362
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
×
UNCOV
7363
                    if compute_pass
×
UNCOV
7364
                        .set_cached_compute_pipeline(pipeline_id)
×
7365
                        .is_err()
7366
                    {
7367
                        compute_pass.insert_debug_marker("ERROR:FailedToSetSortCopyPipeline");
×
7368
                        compute_pass.pop_debug_group();
×
7369
                        // FIXME - Bevy doesn't allow returning custom errors here...
7370
                        return Ok(());
×
7371
                    }
7372

UNCOV
7373
                    let spawner_base = effect_batch.spawner_base;
×
UNCOV
7374
                    let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
×
UNCOV
7375
                    assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
×
7376
                    let spawner_offset = spawner_base * spawner_aligned_size as u32;
×
7377

7378
                    // Bind group sort_copy@0
UNCOV
7379
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
7380
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
UNCOV
7381
                        indirect_index_buffer.id(),
×
UNCOV
7382
                        effect_metadata_buffer.id(),
×
7383
                    ) else {
7384
                        warn!("Missing sort-copy bind group.");
×
7385
                        compute_pass.insert_debug_marker("ERROR:MissingSortCopyBindGroup");
×
7386
                        continue;
×
7387
                    };
UNCOV
7388
                    let effect_metadata_offset = effects_meta
×
UNCOV
7389
                        .effect_metadata_buffer
×
UNCOV
7390
                        .dynamic_offset(effect_batch.metadata_table_id);
×
UNCOV
7391
                    compute_pass.set_bind_group(
×
7392
                        0,
UNCOV
7393
                        bind_group,
×
UNCOV
7394
                        &[effect_metadata_offset, spawner_offset],
×
7395
                    );
7396

UNCOV
7397
                    compute_pass
×
UNCOV
7398
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
7399
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7400

UNCOV
7401
                    compute_pass.pop_debug_group();
×
7402
                }
7403
            }
7404
        }
7405

7406
        Ok(())
312✔
7407
    }
7408
}
7409

7410
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7411
    fn from(layout_flags: LayoutFlags) -> Self {
936✔
7412
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
1,872✔
7413
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7414
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
936✔
7415
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7416
        } else {
7417
            ParticleRenderAlphaMaskPipelineKey::Blend
936✔
7418
        }
7419
    }
7420
}
7421

7422
#[cfg(test)]
7423
mod tests {
7424
    use super::*;
7425

7426
    #[test]
7427
    fn layout_flags() {
7428
        let flags = LayoutFlags::default();
7429
        assert_eq!(flags, LayoutFlags::NONE);
7430
    }
7431

7432
    #[cfg(feature = "gpu_tests")]
7433
    #[test]
7434
    fn gpu_limits() {
7435
        use crate::test_utils::MockRenderer;
7436

7437
        let renderer = MockRenderer::new();
7438
        let device = renderer.device();
7439
        let limits = GpuLimits::from_device(&device);
7440

7441
        // assert!(limits.storage_buffer_align().get() >= 1);
7442
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7443
    }
7444

7445
    #[cfg(feature = "gpu_tests")]
7446
    #[test]
7447
    fn gpu_ops_ifda() {
7448
        use crate::test_utils::MockRenderer;
7449

7450
        let renderer = MockRenderer::new();
7451
        let device = renderer.device();
7452
        let render_queue = renderer.queue();
7453

7454
        let mut world = World::new();
7455
        world.insert_resource(device.clone());
7456
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7457

7458
        let src_buffer = device.create_buffer(&BufferDescriptor {
7459
            label: None,
7460
            size: 256,
7461
            usage: BufferUsages::STORAGE,
7462
            mapped_at_creation: false,
7463
        });
7464
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7465
            label: None,
7466
            size: 256,
7467
            usage: BufferUsages::STORAGE,
7468
            mapped_at_creation: false,
7469
        });
7470

7471
        // Two consecutive ops can be merged. This includes having contiguous slices
7472
        // both in source and destination.
7473
        buffer_ops.begin_frame();
7474
        {
7475
            let mut q = InitFillDispatchQueue::default();
7476
            q.enqueue(0, 0);
7477
            assert_eq!(q.queue.len(), 1);
7478
            q.enqueue(1, 1);
7479
            // Ops are not batched yet
7480
            assert_eq!(q.queue.len(), 2);
7481
            // On submit, the ops get batched together
7482
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7483
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7484
        }
7485
        buffer_ops.end_frame(&device, &render_queue);
7486

7487
        // Even if out of order, the init fill dispatch ops are batchable. Here the
7488
        // offsets are enqueued inverted.
7489
        buffer_ops.begin_frame();
7490
        {
7491
            let mut q = InitFillDispatchQueue::default();
7492
            q.enqueue(1, 1);
7493
            assert_eq!(q.queue.len(), 1);
7494
            q.enqueue(0, 0);
7495
            // Ops are not batched yet
7496
            assert_eq!(q.queue.len(), 2);
7497
            // On submit, the ops get batched together
7498
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7499
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7500
        }
7501
        buffer_ops.end_frame(&device, &render_queue);
7502

7503
        // However, both the source and destination need to be contiguous at the same
7504
        // time. Here they are mixed so we can't batch.
7505
        buffer_ops.begin_frame();
7506
        {
7507
            let mut q = InitFillDispatchQueue::default();
7508
            q.enqueue(0, 1);
7509
            assert_eq!(q.queue.len(), 1);
7510
            q.enqueue(1, 0);
7511
            // Ops are not batched yet
7512
            assert_eq!(q.queue.len(), 2);
7513
            // On submit, the ops cannot get batched together
7514
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7515
            assert_eq!(buffer_ops.args_buffer.len(), 2);
7516
        }
7517
        buffer_ops.end_frame(&device, &render_queue);
7518
    }
7519
}
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