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

djeedai / bevy_hanabi / 15811017124

22 Jun 2025 09:34PM UTC coverage: 66.748% (+0.2%) from 66.537%
15811017124

Pull #480

github

web-flow
Merge 2cdfbbc21 into aa073c5e6
Pull Request #480: Allow multiple effects to be packed into a single buffer again.

133 of 152 new or added lines in 4 files covered. (87.5%)

298 existing lines in 7 files now uncovered.

5191 of 7777 relevant lines covered (66.75%)

357.91 hits per line

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

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

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

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

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

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

103
use self::batch::EffectBatch;
104

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

519
        let mut fill_queue = GpuBufferOperationQueue::new();
520

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

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

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

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

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

636
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
3✔
637
        let render_effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
3✔
638
        let spawner_min_binding_size = GpuSpawnerParams::aligned_size(storage_alignment);
3✔
639

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

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

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

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

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

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

746
impl SpecializedComputePipeline for DispatchIndirectPipeline {
747
    type Key = DispatchIndirectPipelineKey;
748

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1006
        // Upload to GPU buffer
1007
        self.args_buffer.write_buffer(device, render_queue);
1,033✔
1008
    }
1009

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

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

1107
        if queue.is_empty() {
×
1108
            return;
×
1109
        }
1110

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

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

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

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

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

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

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

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

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

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

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

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

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

1294
        let shader_code = include_str!("vfx_utils.wgsl");
3✔
1295

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

1301
            let shader_defs = default();
3✔
1302

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

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

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

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

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

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

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

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

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

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

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

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

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

1493
impl SpecializedComputePipeline for ParticlesInitPipeline {
1494
    type Key = ParticleInitPipelineKey;
1495

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

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

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

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

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

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

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

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

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

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

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

1639
impl SpecializedComputePipeline for ParticlesUpdatePipeline {
1640
    type Key = ParticleUpdatePipelineKey;
1641

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

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

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

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

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

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

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

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

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

1767
        self.material_layouts
1768
            .insert(layout.clone(), material_bind_group_layout);
1769
    }
1770

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

1778
        self.material_layouts.get(layout)
×
1779
    }
1780
}
1781

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

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

1814
        let storage_alignment = render_device.limits().min_storage_buffer_offset_alignment;
3✔
1815
        let effect_metadata_size = GpuEffectMetadata::aligned_size(storage_alignment);
3✔
1816

1817
        let effect_metadata_bind_group_layout = render_device.create_bind_group_layout(
3✔
1818
            "hanabi:bind_group_layout:render:effect_metadata",
1819
            &[
3✔
1820
                // @group(2) @binding(0) var<storage, read> effect_metadata : EffectMetadata;
1821
                BindGroupLayoutEntry {
3✔
1822
                    binding: 0,
3✔
1823
                    visibility: ShaderStages::VERTEX,
3✔
1824
                    ty: BindingType::Buffer {
3✔
1825
                        ty: BufferBindingType::Storage { read_only: true },
3✔
1826
                        has_dynamic_offset: false,
3✔
1827
                        // This WGSL struct is manually padded, so the Rust type GpuEffectMetadata doesn't
1828
                        // reflect its true min size.
1829
                        min_binding_size: Some(effect_metadata_size),
3✔
1830
                    },
1831
                    count: None,
3✔
1832
                },
1833
            ],
1834
        );
1835

1836
        Self {
1837
            render_device: render_device.clone(),
3✔
1838
            view_layout,
1839
            effect_metadata_bind_group_layout,
1840
            material_layouts: default(),
3✔
1841
        }
1842
    }
1843
}
1844

1845
#[cfg(all(feature = "2d", feature = "3d"))]
1846
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1847
enum PipelineMode {
1848
    Camera2d,
1849
    Camera3d,
1850
}
1851

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

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

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

1933
impl SpecializedRenderPipeline for ParticlesRenderPipeline {
1934
    type Key = ParticleRenderPipelineKey;
1935

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

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

1984
        let mut layout = vec![
2✔
1985
            self.view_layout.clone(),
2✔
1986
            particle_bind_group_layout,
2✔
1987
            self.effect_metadata_bind_group_layout.clone(),
2✔
1988
        ];
1989
        let mut shader_defs = vec![];
2✔
1990

1991
        let vertex_buffer_layout = key.mesh_layout.as_ref().and_then(|mesh_layout| {
4✔
1992
            mesh_layout
2✔
1993
                .0
2✔
1994
                .get_layout(&[
2✔
1995
                    Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
2✔
1996
                    Mesh::ATTRIBUTE_UV_0.at_shader_location(1),
2✔
1997
                    Mesh::ATTRIBUTE_NORMAL.at_shader_location(2),
2✔
1998
                ])
1999
                .ok()
2✔
2000
        });
2001

2002
        // Bind group 3, if applicable.
2003
        if let Some(material_bind_group_layout) = self.get_material(&key.texture_layout) {
2✔
2004
            layout.push(material_bind_group_layout.clone());
2005
        }
2006

2007
        // Key: LOCAL_SPACE_SIMULATION
2008
        if key.local_space_simulation {
2✔
2009
            shader_defs.push("LOCAL_SPACE_SIMULATION".into());
×
2010
        }
2011

2012
        match key.alpha_mask {
2✔
2013
            ParticleRenderAlphaMaskPipelineKey::Blend => {}
2✔
2014
            ParticleRenderAlphaMaskPipelineKey::AlphaMask => {
2015
                // Key: USE_ALPHA_MASK
2016
                shader_defs.push("USE_ALPHA_MASK".into())
×
2017
            }
2018
            ParticleRenderAlphaMaskPipelineKey::Opaque => {
2019
                // Key: OPAQUE
2020
                shader_defs.push("OPAQUE".into())
×
2021
            }
2022
        }
2023

2024
        // Key: FLIPBOOK
2025
        if key.flipbook {
2✔
2026
            shader_defs.push("FLIPBOOK".into());
×
2027
        }
2028

2029
        // Key: NEEDS_UV
2030
        if key.needs_uv {
2✔
2031
            shader_defs.push("NEEDS_UV".into());
×
2032
        }
2033

2034
        // Key: NEEDS_NORMAL
2035
        if key.needs_normal {
2✔
2036
            shader_defs.push("NEEDS_NORMAL".into());
×
2037
        }
2038

2039
        if key.needs_particle_fragment {
2✔
2040
            shader_defs.push("NEEDS_PARTICLE_FRAGMENT".into());
×
2041
        }
2042

2043
        // Key: RIBBONS
2044
        if key.ribbons {
2✔
2045
            shader_defs.push("RIBBONS".into());
×
2046
        }
2047

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

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

2075
        #[cfg(all(feature = "2d", feature = "3d"))]
2076
        assert_eq!(CORE_2D_DEPTH_FORMAT, CORE_3D_DEPTH_FORMAT);
2✔
2077
        #[cfg(all(feature = "2d", feature = "3d"))]
2078
        let depth_stencil = match key.pipeline_mode {
4✔
2079
            PipelineMode::Camera2d => Some(depth_stencil_2d),
×
2080
            PipelineMode::Camera3d => Some(depth_stencil_3d),
2✔
2081
        };
2082

2083
        #[cfg(all(feature = "2d", not(feature = "3d")))]
2084
        let depth_stencil = Some(depth_stencil_2d);
2085

2086
        #[cfg(all(feature = "3d", not(feature = "2d")))]
2087
        let depth_stencil = Some(depth_stencil_3d);
2088

2089
        let format = if key.hdr {
4✔
2090
            ViewTarget::TEXTURE_FORMAT_HDR
×
2091
        } else {
2092
            TextureFormat::bevy_default()
2✔
2093
        };
2094

2095
        let hash = calc_func_id(&key);
2✔
2096
        let label = format!("hanabi:pipeline:render_{hash:016X}");
2✔
2097
        trace!(
2✔
2098
            "-> creating pipeline '{}' with shader defs:{}",
2✔
2099
            label,
2✔
2100
            shader_defs
2✔
2101
                .iter()
2✔
2102
                .fold(String::new(), |acc, x| acc + &format!(" {x:?}"))
4✔
2103
        );
2104

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

2145
/// A single effect instance extracted from a [`ParticleEffect`] as a
2146
/// render world item.
2147
///
2148
/// [`ParticleEffect`]: crate::ParticleEffect
2149
#[derive(Debug)]
2150
pub(crate) struct ExtractedEffect {
2151
    /// Main world entity owning the [`CompiledParticleEffect`] this effect was
2152
    /// extracted from. Mainly used for visibility.
2153
    pub main_entity: MainEntity,
2154
    /// Render world entity, if any, where the [`CachedEffect`] component
2155
    /// caching this extracted effect resides. If this component was never
2156
    /// cached in the render world, this is `None`. In that case a new
2157
    /// [`CachedEffect`] will be spawned automatically.
2158
    pub render_entity: RenderEntity,
2159
    /// Handle to the effect asset this instance is based on.
2160
    /// The handle is weak to prevent refcount cycles and gracefully handle
2161
    /// assets unloaded or destroyed after a draw call has been submitted.
2162
    pub handle: Handle<EffectAsset>,
2163
    /// Particle layout for the effect.
2164
    #[allow(dead_code)]
2165
    pub particle_layout: ParticleLayout,
2166
    /// Property layout for the effect.
2167
    pub property_layout: PropertyLayout,
2168
    /// Values of properties written in a binary blob according to
2169
    /// [`property_layout`].
2170
    ///
2171
    /// This is `Some(blob)` if the data needs to be (re)uploaded to GPU, or
2172
    /// `None` if nothing needs to be done for this frame.
2173
    ///
2174
    /// [`property_layout`]: crate::render::ExtractedEffect::property_layout
2175
    pub property_data: Option<Vec<u8>>,
2176
    /// Number of particles to spawn this frame.
2177
    ///
2178
    /// This is ignored if the effect is a child effect consuming GPU spawn
2179
    /// events.
2180
    pub spawn_count: u32,
2181
    /// PRNG seed.
2182
    pub prng_seed: u32,
2183
    /// Global transform of the effect origin.
2184
    pub transform: GlobalTransform,
2185
    /// Layout flags.
2186
    pub layout_flags: LayoutFlags,
2187
    /// Texture layout.
2188
    pub texture_layout: TextureLayout,
2189
    /// Textures.
2190
    pub textures: Vec<Handle<Image>>,
2191
    /// Alpha mode.
2192
    pub alpha_mode: AlphaMode,
2193
    /// Effect shaders.
2194
    pub effect_shaders: EffectShader,
2195
}
2196

2197
pub struct AddedEffectParent {
2198
    pub entity: MainEntity,
2199
    pub layout: ParticleLayout,
2200
    /// GPU spawn event count to allocate for this effect.
2201
    pub event_count: u32,
2202
}
2203

2204
/// Extracted data for newly-added [`ParticleEffect`] component requiring a new
2205
/// GPU allocation.
2206
///
2207
/// [`ParticleEffect`]: crate::ParticleEffect
2208
pub struct AddedEffect {
2209
    /// Entity with a newly-added [`ParticleEffect`] component.
2210
    ///
2211
    /// [`ParticleEffect`]: crate::ParticleEffect
2212
    pub entity: MainEntity,
2213
    #[allow(dead_code)]
2214
    pub render_entity: RenderEntity,
2215
    /// Capacity, in number of particles, of the effect.
2216
    pub capacity: u32,
2217
    /// Resolved particle mesh, either the one provided by the user or the
2218
    /// default one. This should always be valid.
2219
    pub mesh: Handle<Mesh>,
2220
    /// Parent effect, if any.
2221
    pub parent: Option<AddedEffectParent>,
2222
    /// Layout of particle attributes.
2223
    pub particle_layout: ParticleLayout,
2224
    /// Layout of properties for the effect, if properties are used at all, or
2225
    /// an empty layout.
2226
    pub property_layout: PropertyLayout,
2227
    /// Effect flags.
2228
    pub layout_flags: LayoutFlags,
2229
    /// Handle of the effect asset.
2230
    pub handle: Handle<EffectAsset>,
2231
}
2232

2233
/// Collection of all extracted effects for this frame, inserted into the
2234
/// render world as a render resource.
2235
#[derive(Default, Resource)]
2236
pub(crate) struct ExtractedEffects {
2237
    /// Extracted effects this frame.
2238
    pub effects: Vec<ExtractedEffect>,
2239
    /// Newly added effects without a GPU allocation yet.
2240
    pub added_effects: Vec<AddedEffect>,
2241
}
2242

2243
#[derive(Default, Resource)]
2244
pub(crate) struct EffectAssetEvents {
2245
    pub images: Vec<AssetEvent<Image>>,
2246
}
2247

2248
/// System extracting all the asset events for the [`Image`] assets to enable
2249
/// dynamic update of images bound to any effect.
2250
///
2251
/// This system runs in parallel of [`extract_effects`].
2252
pub(crate) fn extract_effect_events(
1,030✔
2253
    mut events: ResMut<EffectAssetEvents>,
2254
    mut image_events: Extract<EventReader<AssetEvent<Image>>>,
2255
) {
2256
    #[cfg(feature = "trace")]
2257
    let _span = bevy::log::info_span!("extract_effect_events").entered();
3,090✔
2258
    trace!("extract_effect_events()");
2,050✔
2259

2260
    let EffectAssetEvents { ref mut images } = *events;
1,030✔
2261
    *images = image_events.read().copied().collect();
1,030✔
2262
}
2263

2264
/// Debugging settings.
2265
///
2266
/// Settings used to debug Hanabi. These have no effect on the actual behavior
2267
/// of Hanabi, but may affect its performance.
2268
///
2269
/// # Example
2270
///
2271
/// ```
2272
/// # use bevy::prelude::*;
2273
/// # use bevy_hanabi::*;
2274
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
2275
///     // Each time a new effect is spawned, capture 2 frames
2276
///     debug_settings.start_capture_on_new_effect = true;
2277
///     debug_settings.capture_frame_count = 2;
2278
/// }
2279
/// ```
2280
#[derive(Debug, Default, Clone, Copy, Resource)]
2281
pub struct DebugSettings {
2282
    /// Enable automatically starting a GPU debugger capture as soon as this
2283
    /// frame starts rendering (extract phase).
2284
    ///
2285
    /// Enable this feature to automatically capture one or more GPU frames when
2286
    /// the `extract_effects()` system runs next. This instructs any attached
2287
    /// GPU debugger to start a capture; this has no effect if no debugger
2288
    /// is attached.
2289
    ///
2290
    /// If a capture is already on-going this has no effect; the on-going
2291
    /// capture needs to be terminated first. Note however that a capture can
2292
    /// stop and another start in the same frame.
2293
    ///
2294
    /// This value is not reset automatically. If you set this to `true`, you
2295
    /// should set it back to `false` on next frame to avoid capturing forever.
2296
    pub start_capture_this_frame: bool,
2297

2298
    /// Enable automatically starting a GPU debugger capture when one or more
2299
    /// effects are spawned.
2300
    ///
2301
    /// Enable this feature to automatically capture one or more GPU frames when
2302
    /// a new effect is spawned (as detected by ECS change detection). This
2303
    /// instructs any attached GPU debugger to start a capture; this has no
2304
    /// effect if no debugger is attached.
2305
    pub start_capture_on_new_effect: bool,
2306

2307
    /// Number of frames to capture with a GPU debugger.
2308
    ///
2309
    /// By default this value is zero, and a GPU debugger capture runs for a
2310
    /// single frame. If a non-zero frame count is specified here, the capture
2311
    /// will instead stop once the specified number of frames has been recorded.
2312
    ///
2313
    /// You should avoid setting this to a value too large, to prevent the
2314
    /// capture size from getting out of control. A typical value is 1 to 3
2315
    /// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
2316
    /// debuggers or graphics APIs might further limit this value on their own,
2317
    /// so there's no guarantee the graphics API will honor this value.
2318
    pub capture_frame_count: u32,
2319
}
2320

2321
#[derive(Debug, Default, Clone, Copy, Resource)]
2322
pub(crate) struct RenderDebugSettings {
2323
    /// Is a GPU debugger capture on-going?
2324
    is_capturing: bool,
2325
    /// Start time of any on-going GPU debugger capture.
2326
    capture_start: Duration,
2327
    /// Number of frames captured so far for on-going GPU debugger capture.
2328
    captured_frames: u32,
2329
}
2330

2331
/// System extracting data for rendering of all active [`ParticleEffect`]
2332
/// components.
2333
///
2334
/// Extract rendering data for all [`ParticleEffect`] components in the world
2335
/// which are visible ([`ComputedVisibility::is_visible`] is `true`), and wrap
2336
/// the data into a new [`ExtractedEffect`] instance added to the
2337
/// [`ExtractedEffects`] resource.
2338
///
2339
/// This system runs in parallel of [`extract_effect_events`].
2340
///
2341
/// If any GPU debug capture is configured to start or stop in
2342
/// [`DebugSettings`], they do so at the beginning of this system. This ensures
2343
/// that all GPU commands produced by Hanabi are recorded (but may miss some
2344
/// from Bevy itself, if another Bevy system runs before this one).
2345
///
2346
/// [`ParticleEffect`]: crate::ParticleEffect
2347
pub(crate) fn extract_effects(
1,030✔
2348
    real_time: Extract<Res<Time<Real>>>,
2349
    virtual_time: Extract<Res<Time<Virtual>>>,
2350
    time: Extract<Res<Time<EffectSimulation>>>,
2351
    effects: Extract<Res<Assets<EffectAsset>>>,
2352
    q_added_effects: Extract<
2353
        Query<
2354
            (Entity, &RenderEntity, &CompiledParticleEffect),
2355
            (Added<CompiledParticleEffect>, With<GlobalTransform>),
2356
        >,
2357
    >,
2358
    q_effects: Extract<
2359
        Query<(
2360
            Entity,
2361
            &RenderEntity,
2362
            Option<&InheritedVisibility>,
2363
            Option<&ViewVisibility>,
2364
            &EffectSpawner,
2365
            &CompiledParticleEffect,
2366
            Option<Ref<EffectProperties>>,
2367
            &GlobalTransform,
2368
        )>,
2369
    >,
2370
    q_all_effects: Extract<Query<(&RenderEntity, &CompiledParticleEffect), With<GlobalTransform>>>,
2371
    mut pending_effects: Local<Vec<MainEntity>>,
2372
    render_device: Res<RenderDevice>,
2373
    debug_settings: Extract<Res<DebugSettings>>,
2374
    default_mesh: Extract<Res<DefaultMesh>>,
2375
    mut sim_params: ResMut<SimParams>,
2376
    mut extracted_effects: ResMut<ExtractedEffects>,
2377
    mut render_debug_settings: ResMut<RenderDebugSettings>,
2378
) {
2379
    #[cfg(feature = "trace")]
2380
    let _span = bevy::log::info_span!("extract_effects").entered();
3,090✔
2381
    trace!("extract_effects()");
2,050✔
2382

2383
    // Manage GPU debug capture
2384
    if render_debug_settings.is_capturing {
1,030✔
2385
        render_debug_settings.captured_frames += 1;
×
2386

2387
        // Stop any pending capture if needed
2388
        if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
×
2389
            render_device.wgpu_device().stop_capture();
×
2390
            render_debug_settings.is_capturing = false;
×
2391
            warn!(
×
2392
                "Stopped GPU debug capture after {} frames, at t={}s.",
×
2393
                render_debug_settings.captured_frames,
×
2394
                real_time.elapsed().as_secs_f64()
×
2395
            );
2396
        }
2397
    }
2398
    if !render_debug_settings.is_capturing {
1,030✔
2399
        // If no pending capture, consider starting a new one
2400
        if debug_settings.start_capture_this_frame
1,030✔
2401
            || (debug_settings.start_capture_on_new_effect && !q_added_effects.is_empty())
1,030✔
2402
        {
2403
            render_device.wgpu_device().start_capture();
×
2404
            render_debug_settings.is_capturing = true;
×
2405
            render_debug_settings.capture_start = real_time.elapsed();
×
2406
            render_debug_settings.captured_frames = 0;
×
2407
            warn!(
×
2408
                "Started GPU debug capture at t={}s.",
×
2409
                render_debug_settings.capture_start.as_secs_f64()
×
2410
            );
2411
        }
2412
    }
2413

2414
    // Save simulation params into render world
2415
    sim_params.time = time.elapsed_secs_f64();
1,030✔
2416
    sim_params.delta_time = time.delta_secs();
1,030✔
2417
    sim_params.virtual_time = virtual_time.elapsed_secs_f64();
1,030✔
2418
    sim_params.virtual_delta_time = virtual_time.delta_secs();
1,030✔
2419
    sim_params.real_time = real_time.elapsed_secs_f64();
1,030✔
2420
    sim_params.real_delta_time = real_time.delta_secs();
1,030✔
2421

2422
    // Collect added effects for later GPU data allocation
2423
    extracted_effects.added_effects = q_added_effects
1,030✔
2424
        .iter()
1,030✔
2425
        .chain(mem::take(&mut *pending_effects).into_iter().filter_map(|main_entity| {
1,039✔
2426
            q_all_effects.get(main_entity.id()).ok().map(|(render_entity, compiled_particle_effect)| {
18✔
2427
                (main_entity.id(), render_entity, compiled_particle_effect)
9✔
2428
            })
2429
        }))
2430
        .filter_map(|(entity, render_entity, compiled_effect)| {
1,042✔
2431
            let handle = compiled_effect.asset.clone_weak();
12✔
2432
            let asset = match effects.get(&compiled_effect.asset) {
14✔
2433
                None => {
2434
                    // The effect wasn't ready yet. Retry on subsequent frames.
2435
                    trace!("Failed to find asset for {:?}/{:?}, deferring to next frame", entity, render_entity);
10✔
2436
                    pending_effects.push(entity.into());
2437
                    return None;
2438
                }
2439
                Some(asset) => asset,
2✔
2440
            };
2441
            let particle_layout = asset.particle_layout();
2✔
2442
            assert!(
2✔
2443
                particle_layout.size() > 0,
2✔
2444
                "Invalid empty particle layout for effect '{}' on entity {:?} (render entity {:?}). Did you forget to add some modifier to the asset?",
×
2445
                asset.name,
×
2446
                entity,
×
2447
                render_entity.id(),
×
2448
            );
2449
            let property_layout = asset.property_layout();
2✔
2450
            let mesh = compiled_effect
2✔
2451
                .mesh
2✔
2452
                .clone()
2✔
2453
                .unwrap_or(default_mesh.0.clone());
2✔
2454

2455
            trace!(
2✔
2456
                "Found new effect: entity {:?} | render entity {:?} | capacity {:?} | particle_layout {:?} | \
2✔
2457
                 property_layout {:?} | layout_flags {:?} | mesh {:?}",
2✔
2458
                 entity,
2✔
2459
                 render_entity.id(),
2✔
2460
                 asset.capacity(),
2✔
2461
                 particle_layout,
2462
                 property_layout,
2463
                 compiled_effect.layout_flags,
2464
                 mesh);
2465

2466
            // FIXME - fixed 256 events per child (per frame) for now... this neatly avoids any issue with alignment 32/256 byte storage buffer align for bind groups
2467
            const FIXME_HARD_CODED_EVENT_COUNT: u32 = 256;
2468
            let parent = compiled_effect.parent.map(|entity| AddedEffectParent {
2✔
2469
                entity: entity.into(),
×
2470
                layout: compiled_effect.parent_particle_layout.as_ref().unwrap().clone(),
×
2471
                event_count: FIXME_HARD_CODED_EVENT_COUNT,
×
2472
            });
2473

2474
            trace!("Found new effect: entity {:?} | capacity {:?} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, compiled_effect.layout_flags);
4✔
2475
            Some(AddedEffect {
2✔
2476
                entity: MainEntity::from(entity),
2✔
2477
                render_entity: *render_entity,
2✔
2478
                capacity: asset.capacity(),
2✔
2479
                mesh,
2✔
2480
                parent,
2✔
2481
                particle_layout,
2✔
2482
                property_layout,
2✔
2483
                layout_flags: compiled_effect.layout_flags,
2✔
2484
                handle,
2✔
2485
            })
2486
        })
2487
        .collect();
1,030✔
2488

2489
    // Loop over all existing effects to extract them
2490
    extracted_effects.effects.clear();
1,030✔
2491
    for (
2492
        main_entity,
1,014✔
2493
        render_entity,
1,014✔
2494
        maybe_inherited_visibility,
1,014✔
2495
        maybe_view_visibility,
1,014✔
2496
        effect_spawner,
1,014✔
2497
        compiled_effect,
1,014✔
2498
        maybe_properties,
1,014✔
2499
        transform,
1,014✔
2500
    ) in q_effects.iter()
1,030✔
2501
    {
2502
        // Check if shaders are configured
2503
        let Some(effect_shaders) = compiled_effect.get_configured_shaders() else {
1,014✔
2504
            continue;
×
2505
        };
2506

2507
        // Check if hidden, unless always simulated
2508
        if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
2509
            && !maybe_inherited_visibility
1,014✔
2510
                .map(|cv| cv.get())
3,042✔
2511
                .unwrap_or(true)
1,014✔
2512
            && !maybe_view_visibility.map(|cv| cv.get()).unwrap_or(true)
×
2513
        {
2514
            continue;
×
2515
        }
2516

2517
        // Check if asset is available, otherwise silently ignore
2518
        let Some(asset) = effects.get(&compiled_effect.asset) else {
1,014✔
2519
            trace!(
×
2520
                "EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
×
2521
                main_entity
2522
            );
2523
            continue;
×
2524
        };
2525

2526
        // Resolve the render entity of the parent, if any
2527
        let _parent = if let Some(main_entity) = compiled_effect.parent {
1,014✔
2528
            let Ok((_, render_entity, _, _, _, _, _, _)) = q_effects.get(main_entity) else {
×
2529
                error!(
×
2530
                    "Failed to resolve render entity of parent with main entity {:?}.",
×
2531
                    main_entity
2532
                );
2533
                continue;
×
2534
            };
2535
            Some(*render_entity)
2536
        } else {
2537
            None
1,014✔
2538
        };
2539

2540
        let property_layout = asset.property_layout();
2541
        let property_data = if let Some(properties) = maybe_properties {
×
2542
            // Note: must check that property layout is not empty, because the
2543
            // EffectProperties component is marked as changed when added but contains an
2544
            // empty Vec if there's no property, which would later raise an error if we
2545
            // don't return None here.
2546
            if properties.is_changed() && !property_layout.is_empty() {
×
2547
                trace!("Detected property change, re-serializing...");
×
2548
                Some(properties.serialize(&property_layout))
2549
            } else {
2550
                None
×
2551
            }
2552
        } else {
2553
            None
1,014✔
2554
        };
2555

2556
        let texture_layout = asset.module().texture_layout();
2557
        let layout_flags = compiled_effect.layout_flags;
2558
        // let mesh = compiled_effect
2559
        //     .mesh
2560
        //     .clone()
2561
        //     .unwrap_or(default_mesh.0.clone());
2562
        let alpha_mode = compiled_effect.alpha_mode;
2563

2564
        trace!(
2565
            "Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
1,014✔
2566
            asset.name,
1,014✔
2567
            main_entity,
1,014✔
2568
            render_entity.id(),
1,014✔
2569
            texture_layout.layout.len(),
1,014✔
2570
            compiled_effect.textures.len(),
1,014✔
2571
            layout_flags,
2572
        );
2573

2574
        extracted_effects.effects.push(ExtractedEffect {
2575
            render_entity: *render_entity,
2576
            main_entity: main_entity.into(),
2577
            handle: compiled_effect.asset.clone_weak(),
2578
            particle_layout: asset.particle_layout().clone(),
2579
            property_layout,
2580
            property_data,
2581
            spawn_count: effect_spawner.spawn_count,
2582
            prng_seed: compiled_effect.prng_seed,
2583
            transform: *transform,
2584
            layout_flags,
2585
            texture_layout,
2586
            textures: compiled_effect.textures.clone(),
2587
            alpha_mode,
2588
            effect_shaders: effect_shaders.clone(),
2589
        });
2590
    }
2591
}
2592

2593
/// Various GPU limits and aligned sizes computed once and cached.
2594
struct GpuLimits {
2595
    /// Value of [`WgpuLimits::min_storage_buffer_offset_alignment`].
2596
    ///
2597
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2598
    storage_buffer_align: NonZeroU32,
2599

2600
    /// Size of [`GpuEffectMetadata`] aligned to the contraint of
2601
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`].
2602
    ///
2603
    /// [`WgpuLimits::min_storage_buffer_offset_alignment`]: bevy::render::settings::WgpuLimits::min_storage_buffer_offset_alignment
2604
    effect_metadata_aligned_size: NonZeroU32,
2605
}
2606

2607
impl GpuLimits {
2608
    pub fn from_device(render_device: &RenderDevice) -> Self {
4✔
2609
        let storage_buffer_align =
4✔
2610
            render_device.limits().min_storage_buffer_offset_alignment as u64;
4✔
2611

2612
        let effect_metadata_aligned_size = NonZeroU32::new(
2613
            GpuEffectMetadata::min_size()
4✔
2614
                .get()
4✔
2615
                .next_multiple_of(storage_buffer_align) as u32,
4✔
2616
        )
2617
        .unwrap();
2618

2619
        trace!(
4✔
2620
            "GPU-aligned sizes (align: {} B):\n- GpuEffectMetadata: {} B -> {} B",
2✔
2621
            storage_buffer_align,
2✔
2622
            GpuEffectMetadata::min_size().get(),
2✔
2623
            effect_metadata_aligned_size.get(),
2✔
2624
        );
2625

2626
        Self {
2627
            storage_buffer_align: NonZeroU32::new(storage_buffer_align as u32).unwrap(),
4✔
2628
            effect_metadata_aligned_size,
2629
        }
2630
    }
2631

2632
    /// Byte alignment for any storage buffer binding.
2633
    pub fn storage_buffer_align(&self) -> NonZeroU32 {
3✔
2634
        self.storage_buffer_align
3✔
2635
    }
2636

2637
    /// Byte offset of the [`GpuEffectMetadata`] of a given buffer.
2638
    pub fn effect_metadata_offset(&self, buffer_index: u32) -> u64 {
3,043✔
2639
        self.effect_metadata_aligned_size.get() as u64 * buffer_index as u64
3,043✔
2640
    }
2641

2642
    /// Byte alignment for [`GpuEffectMetadata`].
2643
    pub fn effect_metadata_size(&self) -> NonZeroU64 {
2✔
2644
        NonZeroU64::new(self.effect_metadata_aligned_size.get() as u64).unwrap()
2✔
2645
    }
2646
}
2647

2648
/// Global resource containing the GPU data to draw all the particle effects in
2649
/// all views.
2650
///
2651
/// The resource is populated by [`prepare_effects()`] with all the effects to
2652
/// render for the current frame, for all views in the frame, and consumed by
2653
/// [`queue_effects()`] to actually enqueue the drawning commands to draw those
2654
/// effects.
2655
#[derive(Resource)]
2656
pub struct EffectsMeta {
2657
    /// Bind group for the camera view, containing the camera projection and
2658
    /// other uniform values related to the camera.
2659
    view_bind_group: Option<BindGroup>,
2660
    /// Bind group #0 of the vfx_indirect shader, for the simulation parameters
2661
    /// like the current time and frame delta time.
2662
    indirect_sim_params_bind_group: Option<BindGroup>,
2663
    /// Bind group #1 of the vfx_indirect shader, containing both the indirect
2664
    /// compute dispatch and render buffers.
2665
    indirect_metadata_bind_group: Option<BindGroup>,
2666
    /// Bind group #2 of the vfx_indirect shader, containing the spawners.
2667
    indirect_spawner_bind_group: Option<BindGroup>,
2668
    /// Global shared GPU uniform buffer storing the simulation parameters,
2669
    /// uploaded each frame from CPU to GPU.
2670
    sim_params_uniforms: UniformBuffer<GpuSimParams>,
2671
    /// Global shared GPU buffer storing the various spawner parameter structs
2672
    /// for the active effect instances.
2673
    spawner_buffer: AlignedBufferVec<GpuSpawnerParams>,
2674
    /// Global shared GPU buffer storing the various indirect dispatch structs
2675
    /// for the indirect dispatch of the Update pass.
2676
    update_dispatch_indirect_buffer: GpuBuffer<GpuDispatchIndirect>,
2677
    /// Global shared GPU buffer storing the various `EffectMetadata`
2678
    /// structs for the active effect instances.
2679
    effect_metadata_buffer: BufferTable<GpuEffectMetadata>,
2680
    /// Various GPU limits and aligned sizes lazily allocated and cached for
2681
    /// convenience.
2682
    gpu_limits: GpuLimits,
2683
    indirect_shader_noevent: Handle<Shader>,
2684
    indirect_shader_events: Handle<Shader>,
2685
    /// Pipeline cache ID of the two indirect dispatch pass pipelines (the
2686
    /// -noevent and -events variants).
2687
    indirect_pipeline_ids: [CachedComputePipelineId; 2],
2688
    /// Pipeline cache ID of the active indirect dispatch pass pipeline, which
2689
    /// is either the -noevent or -events variant depending on whether there's
2690
    /// any child effect with GPU events currently active.
2691
    active_indirect_pipeline_id: CachedComputePipelineId,
2692
}
2693

2694
impl EffectsMeta {
2695
    pub fn new(
3✔
2696
        device: RenderDevice,
2697
        indirect_shader_noevent: Handle<Shader>,
2698
        indirect_shader_events: Handle<Shader>,
2699
    ) -> Self {
2700
        let gpu_limits = GpuLimits::from_device(&device);
3✔
2701

2702
        // Ensure individual GpuSpawnerParams elements are properly aligned so they can
2703
        // be addressed individually by the computer shaders.
2704
        let item_align = gpu_limits.storage_buffer_align().get() as u64;
3✔
2705
        trace!(
3✔
2706
            "Aligning storage buffers to {} bytes as device limits requires.",
2✔
2707
            item_align
2708
        );
2709

2710
        Self {
2711
            view_bind_group: None,
2712
            indirect_sim_params_bind_group: None,
2713
            indirect_metadata_bind_group: None,
2714
            indirect_spawner_bind_group: None,
2715
            sim_params_uniforms: UniformBuffer::default(),
3✔
2716
            spawner_buffer: AlignedBufferVec::new(
3✔
2717
                BufferUsages::STORAGE,
2718
                NonZeroU64::new(item_align),
2719
                Some("hanabi:buffer:spawner".to_string()),
2720
            ),
2721
            update_dispatch_indirect_buffer: GpuBuffer::new(
3✔
2722
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2723
                Some("hanabi:buffer:update_dispatch_indirect".to_string()),
2724
            ),
2725
            effect_metadata_buffer: BufferTable::new(
3✔
2726
                BufferUsages::STORAGE | BufferUsages::INDIRECT,
2727
                NonZeroU64::new(item_align),
2728
                Some("hanabi:buffer:effect_metadata".to_string()),
2729
            ),
2730
            gpu_limits,
2731
            indirect_shader_noevent,
2732
            indirect_shader_events,
2733
            indirect_pipeline_ids: [
3✔
2734
                CachedComputePipelineId::INVALID,
2735
                CachedComputePipelineId::INVALID,
2736
            ],
2737
            active_indirect_pipeline_id: CachedComputePipelineId::INVALID,
2738
        }
2739
    }
2740

2741
    /// Allocate internal resources for newly spawned effects.
2742
    ///
2743
    /// After this system ran, all valid extracted effects from the main world
2744
    /// have a corresponding entity with a [`CachedEffect`] component in the
2745
    /// render world. An extracted effect is considered valid if it passed some
2746
    /// basic checks, like having a valid mesh. Note however that the main
2747
    /// world's entity might still be missing its [`RenderEntity`]
2748
    /// reference, since we cannot yet write into the main world.
2749
    pub fn add_effects(
1,030✔
2750
        &mut self,
2751
        mut commands: Commands,
2752
        mut added_effects: Vec<AddedEffect>,
2753
        effect_cache: &mut ResMut<EffectCache>,
2754
        property_cache: &mut ResMut<PropertyCache>,
2755
        event_cache: &mut ResMut<EventCache>,
2756
    ) {
2757
        // FIXME - We delete a buffer above, and have a chance to immediatly re-create
2758
        // it below. We should keep the GPU buffer around until the end of this method.
2759
        // On the other hand, we should also be careful that allocated buffers need to
2760
        // be tightly packed because 'vfx_indirect.wgsl' index them by buffer index in
2761
        // order, so doesn't support offset.
2762

2763
        trace!("Adding {} newly spawned effects", added_effects.len());
2,050✔
2764
        for added_effect in added_effects.drain(..) {
1,032✔
2765
            trace!("+ added effect: capacity={}", added_effect.capacity);
2✔
2766

2767
            // Allocate an indirect dispatch arguments struct for this instance
2768
            let update_dispatch_indirect_buffer_row_index =
2769
                self.update_dispatch_indirect_buffer.allocate();
2770

2771
            // Allocate per-effect metadata.
2772
            let gpu_effect_metadata = GpuEffectMetadata {
2773
                alive_count: 0,
2774
                max_update: 0,
2775
                dead_count: added_effect.capacity,
2776
                max_spawn: added_effect.capacity,
2777
                ..default()
2778
            };
2779
            trace!("+ Effect: {:?}", gpu_effect_metadata);
2✔
2780
            let effect_metadata_buffer_table_id =
2781
                self.effect_metadata_buffer.insert(gpu_effect_metadata);
2782
            let dispatch_buffer_indices = DispatchBufferIndices {
2783
                update_dispatch_indirect_buffer_row_index,
2784
                effect_metadata_buffer_table_id,
2785
            };
2786

2787
            // Insert the effect into the cache. This will allocate all the necessary
2788
            // mandatory GPU resources as needed.
2789
            let cached_effect = effect_cache.insert(
2790
                added_effect.handle,
2791
                added_effect.capacity,
2792
                &added_effect.particle_layout,
2793
                added_effect.layout_flags,
2794
            );
2795
            let mut cmd = commands.entity(added_effect.render_entity.id());
2796
            cmd.insert((
2797
                added_effect.entity,
2798
                cached_effect,
2799
                dispatch_buffer_indices,
2800
                CachedMesh {
2801
                    mesh: added_effect.mesh.id(),
2802
                },
2803
            ));
2804

2805
            // Allocate storage for properties if needed
2806
            if !added_effect.property_layout.is_empty() {
1✔
2807
                let cached_effect_properties = property_cache.insert(&added_effect.property_layout);
1✔
2808
                cmd.insert(cached_effect_properties);
1✔
2809
            } else {
2810
                cmd.remove::<CachedEffectProperties>();
1✔
2811
            }
2812

2813
            // Allocate storage for the reference to the parent effect if needed. Note that
2814
            // we cannot yet allocate the complete parent info (CachedChildInfo) because it
2815
            // depends on the list of children, which we can't resolve until all
2816
            // effects have been added/removed this frame. This will be done later in
2817
            // resolve_parents().
2818
            if let Some(parent) = added_effect.parent.as_ref() {
×
2819
                let cached_parent: CachedParentRef = CachedParentRef {
2820
                    entity: parent.entity,
2821
                };
2822
                cmd.insert(cached_parent);
2823
                trace!("+ new effect declares parent entity {:?}", parent.entity);
×
2824
            } else {
2825
                cmd.remove::<CachedParentRef>();
2✔
2826
                trace!("+ new effect declares no parent");
4✔
2827
            }
2828

2829
            // Allocate storage for GPU spawn events if needed
2830
            if let Some(parent) = added_effect.parent.as_ref() {
×
2831
                let cached_events = event_cache.allocate(parent.event_count);
2832
                cmd.insert(cached_events);
2833
            } else {
2834
                cmd.remove::<CachedEffectEvents>();
2✔
2835
            }
2836

2837
            // Ensure the particle@1 bind group layout exists for the given configuration of
2838
            // particle layout and (optionally) parent particle layout.
2839
            {
2840
                let parent_min_binding_size = added_effect
2841
                    .parent
2842
                    .map(|added_parent| added_parent.layout.min_binding_size32());
×
2843
                effect_cache.ensure_particle_bind_group_layout(
2844
                    added_effect.particle_layout.min_binding_size32(),
2845
                    parent_min_binding_size,
2846
                );
2847
            }
2848

2849
            // Ensure the metadata@3 bind group layout exists for init pass.
2850
            {
2851
                let consume_gpu_spawn_events = added_effect
2852
                    .layout_flags
2853
                    .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
2854
                effect_cache.ensure_metadata_init_bind_group_layout(consume_gpu_spawn_events);
2855
            }
2856

2857
            // We cannot yet determine the layout of the metadata@3 bind group for the
2858
            // update pass, because it depends on the number of children, and
2859
            // this is encoded indirectly via the number of child effects
2860
            // pointing to this parent, and only calculated later in
2861
            // resolve_parents().
2862

2863
            trace!(
2864
                "+ added effect entity {:?}: main_entity={:?} \
2✔
2865
                first_update_group_dispatch_buffer_index={} \
2✔
2866
                render_effect_dispatch_buffer_id={}",
2✔
2867
                added_effect.render_entity,
2868
                added_effect.entity,
2869
                update_dispatch_indirect_buffer_row_index,
2870
                effect_metadata_buffer_table_id.0
2871
            );
2872
        }
2873
    }
2874

2875
    pub fn allocate_spawner(
1,014✔
2876
        &mut self,
2877
        global_transform: &GlobalTransform,
2878
        spawn_count: u32,
2879
        prng_seed: u32,
2880
        effect_metadata_buffer_table_id: BufferTableId,
2881
    ) -> u32 {
2882
        let spawner_base = self.spawner_buffer.len() as u32;
1,014✔
2883
        let transform = global_transform.compute_matrix().into();
1,014✔
2884
        let inverse_transform = Mat4::from(
2885
            // Inverse the Affine3A first, then convert to Mat4. This is a lot more
2886
            // efficient than inversing the Mat4.
2887
            global_transform.affine().inverse(),
1,014✔
2888
        )
2889
        .into();
2890
        let spawner_params = GpuSpawnerParams {
2891
            transform,
2892
            inverse_transform,
2893
            spawn: spawn_count as i32,
1,014✔
2894
            seed: prng_seed,
2895
            effect_metadata_index: effect_metadata_buffer_table_id.0,
1,014✔
2896
            ..default()
2897
        };
2898
        trace!("spawner params = {:?}", spawner_params);
2,028✔
2899
        self.spawner_buffer.push(spawner_params);
1,014✔
2900
        spawner_base
1,014✔
2901
    }
2902
}
2903

2904
bitflags! {
2905
    /// Effect flags.
2906
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2907
    pub struct LayoutFlags: u32 {
2908
        /// No flags.
2909
        const NONE = 0;
2910
        // DEPRECATED - The effect uses an image texture.
2911
        //const PARTICLE_TEXTURE = (1 << 0);
2912
        /// The effect is simulated in local space.
2913
        const LOCAL_SPACE_SIMULATION = (1 << 2);
2914
        /// The effect uses alpha masking instead of alpha blending. Only used for 3D.
2915
        const USE_ALPHA_MASK = (1 << 3);
2916
        /// The effect is rendered with flipbook texture animation based on the
2917
        /// [`Attribute::SPRITE_INDEX`] of each particle.
2918
        const FLIPBOOK = (1 << 4);
2919
        /// The effect needs UVs.
2920
        const NEEDS_UV = (1 << 5);
2921
        /// The effect has ribbons.
2922
        const RIBBONS = (1 << 6);
2923
        /// The effects needs normals.
2924
        const NEEDS_NORMAL = (1 << 7);
2925
        /// The effect is fully-opaque.
2926
        const OPAQUE = (1 << 8);
2927
        /// The (update) shader emits GPU spawn events to instruct another effect to spawn particles.
2928
        const EMIT_GPU_SPAWN_EVENTS = (1 << 9);
2929
        /// The (init) shader spawns particles by consuming GPU spawn events, instead of
2930
        /// a single CPU spawn count.
2931
        const CONSUME_GPU_SPAWN_EVENTS = (1 << 10);
2932
        /// The (init or update) shader needs access to its parent particle. This allows
2933
        /// a particle init or update pass to read the data of a parent particle, for
2934
        /// example to inherit some of the attributes.
2935
        const READ_PARENT_PARTICLE = (1 << 11);
2936
        /// The effect access to the particle data in the fragment shader.
2937
        const NEEDS_PARTICLE_FRAGMENT = (1 << 12);
2938
    }
2939
}
2940

2941
impl Default for LayoutFlags {
2942
    fn default() -> Self {
1✔
2943
        Self::NONE
1✔
2944
    }
2945
}
2946

2947
/// Observer raised when the [`CachedEffect`] component is removed, which
2948
/// indicates that the effect instance was despawned.
2949
pub(crate) fn on_remove_cached_effect(
1✔
2950
    trigger: Trigger<OnRemove, CachedEffect>,
2951
    query: Query<(
2952
        Entity,
2953
        MainEntity,
2954
        &CachedEffect,
2955
        &DispatchBufferIndices,
2956
        Option<&CachedEffectProperties>,
2957
        Option<&CachedParentInfo>,
2958
        Option<&CachedEffectEvents>,
2959
    )>,
2960
    mut effect_cache: ResMut<EffectCache>,
2961
    mut effect_bind_groups: ResMut<EffectBindGroups>,
2962
    mut effects_meta: ResMut<EffectsMeta>,
2963
    mut event_cache: ResMut<EventCache>,
2964
) {
2965
    #[cfg(feature = "trace")]
2966
    let _span = bevy::log::info_span!("on_remove_cached_effect").entered();
3✔
2967

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

2971
    // Fecth the components of the effect being destroyed. Note that the despawn
2972
    // command above is not yet applied, so this query should always succeed.
2973
    let Ok((
2974
        render_entity,
1✔
2975
        main_entity,
1✔
2976
        cached_effect,
1✔
2977
        dispatch_buffer_indices,
1✔
2978
        _opt_props,
1✔
2979
        _opt_parent,
1✔
2980
        opt_cached_effect_events,
1✔
2981
    )) = query.get(trigger.target())
1✔
2982
    else {
2983
        return;
×
2984
    };
2985

2986
    // Dealllocate the effect slice in the event buffer, if any.
2987
    if let Some(cached_effect_events) = opt_cached_effect_events {
×
2988
        match event_cache.free(cached_effect_events) {
2989
            Err(err) => {
×
2990
                error!("Error while freeing effect event slice: {err:?}");
×
2991
            }
2992
            Ok(buffer_state) => {
×
2993
                if buffer_state != BufferState::Used {
×
2994
                    // Clear bind groups associated with the old buffer
2995
                    effect_bind_groups.init_metadata_bind_groups.clear();
×
2996
                    effect_bind_groups.update_metadata_bind_groups.clear();
×
NEW
2997
                    effect_bind_groups.render_metadata_bind_groups.clear();
×
2998
                }
2999
            }
3000
        }
3001
    }
3002

3003
    // Deallocate the effect slice in the GPU effect buffer, and if this was the
3004
    // last slice, also deallocate the GPU buffer itself.
3005
    trace!(
3006
        "=> ParticleEffect on render entity {:?} associated with main entity {:?}, removing...",
1✔
3007
        render_entity,
3008
        main_entity,
3009
    );
3010
    let Ok(BufferState::Free) = effect_cache.remove(cached_effect) else {
3011
        // Buffer was not affected, so all bind groups are still valid. Nothing else to
3012
        // do.
3013
        return;
×
3014
    };
3015

3016
    // Clear bind groups associated with the removed buffer
3017
    trace!(
1✔
3018
        "=> GPU buffer #{} gone, destroying its bind groups...",
1✔
3019
        cached_effect.buffer_index
3020
    );
3021
    effect_bind_groups
3022
        .particle_buffers
3023
        .remove(&cached_effect.buffer_index);
3024
    effects_meta
3025
        .update_dispatch_indirect_buffer
3026
        .free(dispatch_buffer_indices.update_dispatch_indirect_buffer_row_index);
3027
    effects_meta
3028
        .effect_metadata_buffer
3029
        .remove(dispatch_buffer_indices.effect_metadata_buffer_table_id);
3030
}
3031

3032
/// Update the [`CachedEffect`] component for any newly allocated effect.
3033
///
3034
/// After this system ran, and its commands are applied, all valid extracted
3035
/// effects have a corresponding entity in the render world, with a
3036
/// [`CachedEffect`] component. From there, we operate on those exclusively.
3037
pub(crate) fn add_effects(
1,030✔
3038
    commands: Commands,
3039
    mut effects_meta: ResMut<EffectsMeta>,
3040
    mut effect_cache: ResMut<EffectCache>,
3041
    mut property_cache: ResMut<PropertyCache>,
3042
    mut event_cache: ResMut<EventCache>,
3043
    mut extracted_effects: ResMut<ExtractedEffects>,
3044
    mut sort_bind_groups: ResMut<SortBindGroups>,
3045
) {
3046
    #[cfg(feature = "trace")]
3047
    let _span = bevy::log::info_span!("add_effects").entered();
3,090✔
3048
    trace!("add_effects");
2,050✔
3049

3050
    // Clear last frame's buffer resizes which may have occured during last frame,
3051
    // during `Node::run()` while the `BufferTable` could not be mutated. This is
3052
    // the first point at which we can do that where we're not blocking the main
3053
    // world (so, excluding the extract system).
3054
    effects_meta
1,030✔
3055
        .update_dispatch_indirect_buffer
1,030✔
3056
        .clear_previous_frame_resizes();
3057
    effects_meta
1,030✔
3058
        .effect_metadata_buffer
1,030✔
3059
        .clear_previous_frame_resizes();
3060
    sort_bind_groups.clear_previous_frame_resizes();
1,030✔
3061
    event_cache.clear_previous_frame_resizes();
1,030✔
3062

3063
    // Allocate new effects
3064
    effects_meta.add_effects(
1,030✔
3065
        commands,
1,030✔
3066
        std::mem::take(&mut extracted_effects.added_effects),
1,030✔
3067
        &mut effect_cache,
1,030✔
3068
        &mut property_cache,
1,030✔
3069
        &mut event_cache,
1,030✔
3070
    );
3071

3072
    // Note: we don't need to explicitly allocate GPU buffers for effects,
3073
    // because EffectBuffer already contains a reference to the
3074
    // RenderDevice, so has done so internally. This is not ideal
3075
    // design-wise, but works.
3076
}
3077

3078
/// Check if two lists of entities are equal.
3079
fn is_child_list_changed(
×
3080
    parent_entity: Entity,
3081
    old: impl ExactSizeIterator<Item = Entity>,
3082
    new: impl ExactSizeIterator<Item = Entity>,
3083
) -> bool {
3084
    if old.len() != new.len() {
×
3085
        trace!(
×
3086
            "Child list changed for effect {:?}: old #{} != new #{}",
×
3087
            parent_entity,
×
3088
            old.len(),
×
3089
            new.len()
×
3090
        );
3091
        return true;
×
3092
    }
3093

3094
    // TODO - this value is arbitrary
3095
    if old.len() >= 16 {
×
3096
        // For large-ish lists, use a hash set.
3097
        let old = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(old);
×
3098
        let new = HashSet::<Entity, bevy::platform::hash::FixedHasher>::from_iter(new);
×
3099
        if old != new {
×
3100
            trace!(
×
3101
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3102
            );
3103
            true
×
3104
        } else {
3105
            false
×
3106
        }
3107
    } else {
3108
        // For small lists, just use a linear array and sort it
3109
        let mut old = old.collect::<Vec<_>>();
×
3110
        let mut new = new.collect::<Vec<_>>();
×
3111
        old.sort_unstable();
×
3112
        new.sort_unstable();
×
3113
        if old != new {
×
3114
            trace!(
×
3115
                "Child list changed for effect {parent_entity:?}: old [{old:?}] != new [{new:?}]"
×
3116
            );
3117
            true
×
3118
        } else {
3119
            false
×
3120
        }
3121
    }
3122
}
3123

3124
/// Resolve parents and children, updating their [`CachedParent`] and
3125
/// [`CachedChild`] components, as well as (re-)allocating any [`GpuChildInfo`]
3126
/// slice for all children of each parent.
3127
pub(crate) fn resolve_parents(
1,030✔
3128
    mut commands: Commands,
3129
    q_child_effects: Query<
3130
        (
3131
            Entity,
3132
            &CachedParentRef,
3133
            &CachedEffectEvents,
3134
            Option<&CachedChildInfo>,
3135
        ),
3136
        With<CachedEffect>,
3137
    >,
3138
    q_cached_effects: Query<(Entity, MainEntity, &CachedEffect)>,
3139
    effect_cache: Res<EffectCache>,
3140
    mut q_parent_effects: Query<(Entity, &mut CachedParentInfo), With<CachedEffect>>,
3141
    mut event_cache: ResMut<EventCache>,
3142
    mut children_from_parent: Local<
3143
        HashMap<Entity, (Vec<(Entity, BufferBindingSource)>, Vec<GpuChildInfo>)>,
3144
    >,
3145
) {
3146
    #[cfg(feature = "trace")]
3147
    let _span = bevy::log::info_span!("resolve_parents").entered();
3,090✔
3148
    let num_parent_effects = q_parent_effects.iter().len();
1,030✔
3149
    trace!("resolve_parents: num_parents={num_parent_effects}");
2,050✔
3150

3151
    // Build map of render entity from main entity for all cached effects.
3152
    let render_from_main_entity = q_cached_effects
1,030✔
3153
        .iter()
3154
        .map(|(render_entity, main_entity, _)| (main_entity, render_entity))
3,074✔
3155
        .collect::<HashMap<_, _>>();
3156

3157
    // Record all parents with children that changed so that we can mark those
3158
    // parents' `CachedParentInfo` as changed. See the comment in the
3159
    // `q_parent_effects` loop for more information.
3160
    let mut parents_with_dirty_children = EntityHashSet::default();
1,030✔
3161

3162
    // Group child effects by parent, building a list of children for each parent,
3163
    // solely based on the declaration each child makes of its parent. This doesn't
3164
    // mean yet that the parent exists.
3165
    if children_from_parent.capacity() < num_parent_effects {
1,030✔
3166
        let extra = num_parent_effects - children_from_parent.capacity();
×
3167
        children_from_parent.reserve(extra);
×
3168
    }
3169
    for (child_entity, cached_parent_ref, cached_effect_events, cached_child_info) in
×
3170
        q_child_effects.iter()
1,030✔
3171
    {
3172
        // Resolve the parent reference into the render world
3173
        let parent_main_entity = cached_parent_ref.entity;
3174
        let Some(parent_entity) = render_from_main_entity.get(&parent_main_entity.id()) else {
×
3175
            warn!(
×
3176
                "Cannot resolve parent render entity for parent main entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3177
                parent_main_entity, child_entity
3178
            );
3179
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3180
            continue;
×
3181
        };
3182
        let parent_entity = *parent_entity;
3183

3184
        // Resolve the parent
3185
        let Ok((_, _, parent_cached_effect)) = q_cached_effects.get(parent_entity) else {
×
3186
            // Since we failed to resolve, remove this component so the next systems ignore
3187
            // this effect.
3188
            warn!(
×
3189
                "Unknown parent render entity {:?}, removing CachedChildInfo from child entity {:?}.",
×
3190
                parent_entity, child_entity
3191
            );
3192
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3193
            continue;
×
3194
        };
3195
        let Some(parent_buffer_binding_source) = effect_cache
×
3196
            .get_buffer(parent_cached_effect.buffer_index)
3197
            .map(|effect_buffer| effect_buffer.max_binding_source())
×
3198
        else {
3199
            // Since we failed to resolve, remove this component so the next systems ignore
3200
            // this effect.
3201
            warn!(
×
3202
                "Unknown parent buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3203
                parent_cached_effect.buffer_index, child_entity
3204
            );
3205
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3206
            continue;
×
3207
        };
3208

3209
        let Some(child_event_buffer) = event_cache.get_buffer(cached_effect_events.buffer_index)
×
3210
        else {
3211
            // Since we failed to resolve, remove this component so the next systems ignore
3212
            // this effect.
3213
            warn!(
×
3214
                "Unknown child event buffer #{} on entity {:?}, removing CachedChildInfo.",
×
3215
                cached_effect_events.buffer_index, child_entity
3216
            );
3217
            commands.entity(child_entity).remove::<CachedChildInfo>();
×
3218
            continue;
×
3219
        };
3220
        let child_buffer_binding_source = BufferBindingSource {
3221
            buffer: child_event_buffer.clone(),
3222
            offset: cached_effect_events.range.start,
3223
            size: NonZeroU32::new(cached_effect_events.range.len() as u32).unwrap(),
3224
        };
3225

3226
        // Push the child entity into the children list
3227
        let (child_vec, child_infos) = children_from_parent.entry(parent_entity).or_default();
3228
        let local_child_index = child_vec.len() as u32;
3229
        child_vec.push((child_entity, child_buffer_binding_source));
3230
        child_infos.push(GpuChildInfo {
3231
            event_count: 0,
3232
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
3233
        });
3234

3235
        // Check if child info changed. Avoid overwriting if no change.
3236
        if let Some(old_cached_child_info) = cached_child_info {
×
3237
            if parent_entity == old_cached_child_info.parent
3238
                && parent_cached_effect.slice.particle_layout
×
3239
                    == old_cached_child_info.parent_particle_layout
×
3240
                && parent_buffer_binding_source
×
3241
                    == old_cached_child_info.parent_buffer_binding_source
×
3242
                // Note: if local child index didn't change, then keep global one too for now. Chances are the parent didn't change, but anyway we can't know for now without inspecting all its children.
3243
                && local_child_index == old_cached_child_info.local_child_index
×
3244
                && cached_effect_events.init_indirect_dispatch_index
×
3245
                    == old_cached_child_info.init_indirect_dispatch_index
×
3246
            {
3247
                trace!(
×
3248
                    "ChildInfo didn't change for child entity {:?}, skipping component write.",
×
3249
                    child_entity
3250
                );
3251
                continue;
×
3252
            }
3253
        }
3254

3255
        // Allocate (or overwrite, if already existing) the child info, now that the
3256
        // parent is resolved.
3257
        let cached_child_info = CachedChildInfo {
3258
            parent: parent_entity,
3259
            parent_particle_layout: parent_cached_effect.slice.particle_layout.clone(),
×
3260
            parent_buffer_binding_source,
3261
            local_child_index,
3262
            global_child_index: u32::MAX, // fixed up later by fixup_parents()
3263
            init_indirect_dispatch_index: cached_effect_events.init_indirect_dispatch_index,
×
3264
        };
3265
        commands.entity(child_entity).insert(cached_child_info);
×
3266
        trace!("Spawned CachedChildInfo on child entity {:?}", child_entity);
×
3267

3268
        // Make a note of the parent entity so that we remember to mark its
3269
        // `CachedParentInfo` as changed below.
3270
        parents_with_dirty_children.insert(parent_entity);
3271
    }
3272

3273
    // Once all parents are resolved, diff all children of already-cached parents,
3274
    // and re-allocate their GpuChildInfo if needed.
3275
    for (parent_entity, mut cached_parent_info) in q_parent_effects.iter_mut() {
1,030✔
3276
        // Fetch the newly extracted list of children
3277
        let Some((_, (children, child_infos))) = children_from_parent.remove_entry(&parent_entity)
×
3278
        else {
3279
            trace!("Entity {parent_entity:?} is no more a parent, removing CachedParentInfo component...");
×
3280
            commands.entity(parent_entity).remove::<CachedParentInfo>();
×
3281
            continue;
×
3282
        };
3283

3284
        // If we updated `CachedChildInfo` for any of this entity's children,
3285
        // then even if the check below passes, we must still set the change
3286
        // flag on this entity's `CachedParentInfo`. That's because the
3287
        // `fixup_parents` system looks at the change flag for the parent in
3288
        // order to determine which `CachedChildInfo` it needs to update, and
3289
        // that system must process all newly-added `CachedChildInfo`s.
3290
        if parents_with_dirty_children.contains(&parent_entity) {
×
3291
            cached_parent_info.set_changed();
×
3292
        }
3293

3294
        // Check if any child changed compared to the existing CachedChildren component
3295
        if !is_child_list_changed(
3296
            parent_entity,
3297
            cached_parent_info
3298
                .children
3299
                .iter()
3300
                .map(|(entity, _)| *entity),
×
3301
            children.iter().map(|(entity, _)| *entity),
×
3302
        ) {
3303
            continue;
×
3304
        }
3305

3306
        event_cache.reallocate_child_infos(
×
3307
            parent_entity,
×
3308
            children,
×
3309
            &child_infos[..],
×
3310
            cached_parent_info.deref_mut(),
×
3311
        );
3312
    }
3313

3314
    // Once this is done, the children hash map contains all entries which don't
3315
    // already have a CachedParentInfo component. That is, all entities which are
3316
    // new parents.
3317
    for (parent_entity, (children, child_infos)) in children_from_parent.drain() {
1,030✔
3318
        let cached_parent_info =
3319
            event_cache.allocate_child_infos(parent_entity, children, &child_infos[..]);
3320
        commands.entity(parent_entity).insert(cached_parent_info);
3321
    }
3322

3323
    // // Once all changes are applied, immediately schedule any GPU buffer
3324
    // // (re)allocation based on the new buffer size. The actual GPU buffer
3325
    // content // will be written later.
3326
    // if event_cache
3327
    //     .child_infos()
3328
    //     .allocate_gpu(render_device, render_queue)
3329
    // {
3330
    //     // All those bind groups use the buffer so need to be re-created
3331
    //     effect_bind_groups.particle_buffers.clear();
3332
    // }
3333
}
3334

3335
pub fn fixup_parents(
1,030✔
3336
    q_changed_parents: Query<(Entity, &CachedParentInfo), Changed<CachedParentInfo>>,
3337
    mut q_children: Query<&mut CachedChildInfo>,
3338
) {
3339
    #[cfg(feature = "trace")]
3340
    let _span = bevy::log::info_span!("fixup_parents").entered();
3,090✔
3341
    trace!("fixup_parents");
2,050✔
3342

3343
    // Once all parents are (re-)allocated, fix up the global index of all
3344
    // children if the parent base index changed.
3345
    trace!(
1,030✔
3346
        "Updating the global index of children of parent effects whose child list just changed..."
1,020✔
3347
    );
3348
    for (parent_entity, cached_parent_info) in q_changed_parents.iter() {
1,030✔
3349
        let base_index =
3350
            cached_parent_info.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32;
3351
        trace!(
3352
            "Updating {} children of parent effect {:?} with base child index {}...",
×
3353
            cached_parent_info.children.len(),
×
3354
            parent_entity,
3355
            base_index
3356
        );
3357
        for (child_entity, _) in &cached_parent_info.children {
×
3358
            let Ok(mut cached_child_info) = q_children.get_mut(*child_entity) else {
×
3359
                continue;
×
3360
            };
3361
            cached_child_info.global_child_index = base_index + cached_child_info.local_child_index;
×
3362
            trace!(
×
3363
                "+ Updated global index for child ID {:?} of parent {:?}: local={}, global={}",
×
3364
                child_entity,
×
3365
                parent_entity,
×
3366
                cached_child_info.local_child_index,
×
3367
                cached_child_info.global_child_index
×
3368
            );
3369
        }
3370
    }
3371
}
3372

3373
/// Update any cached mesh info based on any relocation done by Bevy itself.
3374
pub fn update_mesh_locations(
1,030✔
3375
    mut commands: Commands,
3376
    mesh_allocator: Res<MeshAllocator>,
3377
    render_meshes: Res<RenderAssets<RenderMesh>>,
3378
    mut q_cached_effects: Query<
3379
        (Entity, &CachedMesh, Option<&mut CachedMeshLocation>),
3380
        With<CachedEffect>,
3381
    >,
3382
) {
3383
    for (entity, cached_mesh, maybe_cached_mesh_location) in &mut q_cached_effects {
3,058✔
3384
        // Resolve the render mesh
3385
        let Some(render_mesh) = render_meshes.get(cached_mesh.mesh) else {
1,014✔
3386
            warn!(
×
3387
                "Cannot find render mesh of particle effect instance on entity {:?}, despite applying default mesh. Invalid asset handle: {:?}",
×
3388
                entity, cached_mesh.mesh
3389
            );
3390
            continue;
×
3391
        };
3392

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

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

3448
        // Compare to any cached data and update if necessary, or insert if missing.
3449
        // This will trigger change detection in the ECS, which will in turn trigger
3450
        // GpuEffectMetadata re-upload.
3451
        if let Some(mut old_mesh_location) = maybe_cached_mesh_location {
1,012✔
3452
            #[cfg(debug_assertions)]
3453
            if *old_mesh_location.deref() != new_mesh_location {
3454
                debug!(
×
3455
                    "Mesh location changed for asset {:?}\nold:{:?}\nnew:{:?}",
×
3456
                    entity, old_mesh_location, new_mesh_location
3457
                );
3458
            }
3459

3460
            old_mesh_location.set_if_neq(new_mesh_location);
3461
        } else {
3462
            commands.entity(entity).insert(new_mesh_location);
2✔
3463
        }
3464
    }
3465
}
3466

3467
// TEMP - Mark all cached effects as invalid for this frame until another system
3468
// explicitly marks them as valid. Otherwise we early out in some parts, and
3469
// reuse by mistake the previous frame's extraction.
3470
pub fn clear_transient_batch_inputs(
1,030✔
3471
    mut commands: Commands,
3472
    mut q_cached_effects: Query<Entity, With<BatchInput>>,
3473
) {
3474
    for entity in &mut q_cached_effects {
3,054✔
3475
        if let Ok(mut cmd) = commands.get_entity(entity) {
1,012✔
3476
            cmd.remove::<BatchInput>();
3477
        }
3478
    }
3479
}
3480

3481
/// Render world cached mesh infos for a single effect instance.
3482
#[derive(Debug, Clone, Copy, Component)]
3483
pub(crate) struct CachedMesh {
3484
    /// Asset of the effect mesh to draw.
3485
    pub mesh: AssetId<Mesh>,
3486
}
3487

3488
/// Indexed mesh metadata for [`CachedMesh`].
3489
#[derive(Debug, Clone)]
3490
#[allow(dead_code)]
3491
pub(crate) struct MeshIndexSlice {
3492
    /// Index format.
3493
    pub format: IndexFormat,
3494
    /// GPU buffer containing the indices.
3495
    pub buffer: Buffer,
3496
    /// Range inside [`Self::buffer`] where the indices are.
3497
    pub range: Range<u32>,
3498
}
3499

3500
impl PartialEq for MeshIndexSlice {
3501
    fn eq(&self, other: &Self) -> bool {
2,024✔
3502
        self.format == other.format
2,024✔
3503
            && self.buffer.id() == other.buffer.id()
2,024✔
3504
            && self.range == other.range
2,024✔
3505
    }
3506
}
3507

3508
impl Eq for MeshIndexSlice {}
3509

3510
/// Cached info about a mesh location in a Bevy buffer. This information is
3511
/// uploaded to GPU into [`GpuEffectMetadata`] for indirect rendering, but is
3512
/// also kept CPU side in this component to detect when Bevy relocated a mesh,
3513
/// so we can invalidate that GPU data.
3514
#[derive(Debug, Clone, PartialEq, Eq, Component)]
3515
pub(crate) struct CachedMeshLocation {
3516
    /// Vertex buffer.
3517
    pub vertex_buffer: BufferId,
3518
    /// See [`GpuEffectMetadata::vertex_or_index_count`].
3519
    pub vertex_or_index_count: u32,
3520
    /// See [`GpuEffectMetadata::first_index_or_vertex_offset`].
3521
    pub first_index_or_vertex_offset: u32,
3522
    /// See [`GpuEffectMetadata::vertex_offset_or_base_instance`].
3523
    pub vertex_offset_or_base_instance: i32,
3524
    /// Indexed rendering metadata.
3525
    pub indexed: Option<MeshIndexSlice>,
3526
}
3527

3528
/// Render world cached properties info for a single effect instance.
3529
#[allow(unused)]
3530
#[derive(Debug, Component)]
3531
pub(crate) struct CachedProperties {
3532
    /// Layout of the effect properties.
3533
    pub layout: PropertyLayout,
3534
    /// Index of the buffer in the [`EffectCache`].
3535
    pub buffer_index: u32,
3536
    /// Offset in bytes inside the buffer.
3537
    pub offset: u32,
3538
    /// Binding size in bytes of the property struct.
3539
    pub binding_size: u32,
3540
}
3541

3542
#[derive(SystemParam)]
3543
pub struct PrepareEffectsReadOnlyParams<'w, 's> {
3544
    sim_params: Res<'w, SimParams>,
3545
    render_device: Res<'w, RenderDevice>,
3546
    render_queue: Res<'w, RenderQueue>,
3547
    marker: PhantomData<&'s usize>,
3548
}
3549

3550
#[derive(SystemParam)]
3551
pub struct PipelineSystemParams<'w, 's> {
3552
    pipeline_cache: Res<'w, PipelineCache>,
3553
    init_pipeline: ResMut<'w, ParticlesInitPipeline>,
3554
    indirect_pipeline: Res<'w, DispatchIndirectPipeline>,
3555
    update_pipeline: ResMut<'w, ParticlesUpdatePipeline>,
3556
    specialized_init_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesInitPipeline>>,
3557
    specialized_update_pipelines: ResMut<'w, SpecializedComputePipelines<ParticlesUpdatePipeline>>,
3558
    specialized_indirect_pipelines:
3559
        ResMut<'w, SpecializedComputePipelines<DispatchIndirectPipeline>>,
3560
    marker: PhantomData<&'s usize>,
3561
}
3562

3563
pub(crate) fn prepare_effects(
1,030✔
3564
    mut commands: Commands,
3565
    read_only_params: PrepareEffectsReadOnlyParams,
3566
    mut pipelines: PipelineSystemParams,
3567
    mut property_cache: ResMut<PropertyCache>,
3568
    event_cache: Res<EventCache>,
3569
    mut effect_cache: ResMut<EffectCache>,
3570
    mut effects_meta: ResMut<EffectsMeta>,
3571
    mut effect_bind_groups: ResMut<EffectBindGroups>,
3572
    mut extracted_effects: ResMut<ExtractedEffects>,
3573
    mut property_bind_groups: ResMut<PropertyBindGroups>,
3574
    q_cached_effects: Query<(
3575
        MainEntity,
3576
        &CachedEffect,
3577
        Ref<CachedMesh>,
3578
        Ref<CachedMeshLocation>,
3579
        &DispatchBufferIndices,
3580
        Option<&CachedEffectProperties>,
3581
        Option<&CachedParentInfo>,
3582
        Option<&CachedChildInfo>,
3583
        Option<&CachedEffectEvents>,
3584
    )>,
3585
    q_debug_all_entities: Query<MainEntity>,
3586
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
3587
    mut sort_bind_groups: ResMut<SortBindGroups>,
3588
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
3589
) {
3590
    #[cfg(feature = "trace")]
3591
    let _span = bevy::log::info_span!("prepare_effects").entered();
3,090✔
3592
    trace!("prepare_effects");
2,050✔
3593

3594
    init_fill_dispatch_queue.clear();
1,030✔
3595

3596
    // Workaround for too many params in system (TODO: refactor to split work?)
3597
    let sim_params = read_only_params.sim_params.into_inner();
1,030✔
3598
    let render_device = read_only_params.render_device.into_inner();
1,030✔
3599
    let render_queue = read_only_params.render_queue.into_inner();
1,030✔
3600
    let pipeline_cache = pipelines.pipeline_cache.into_inner();
1,030✔
3601
    let specialized_init_pipelines = pipelines.specialized_init_pipelines.into_inner();
1,030✔
3602
    let specialized_update_pipelines = pipelines.specialized_update_pipelines.into_inner();
1,030✔
3603
    let specialized_indirect_pipelines = pipelines.specialized_indirect_pipelines.into_inner();
1,030✔
3604

3605
    // // sort first by z and then by handle. this ensures that, when possible,
3606
    // batches span multiple z layers // batches won't span z-layers if there is
3607
    // another batch between them extracted_effects.effects.sort_by(|a, b| {
3608
    //     match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.
3609
    // w_axis[2])) {         Ordering::Equal => a.handle.cmp(&b.handle),
3610
    //         other => other,
3611
    //     }
3612
    // });
3613

3614
    // Ensure the indirect pipelines are created
3615
    if effects_meta.indirect_pipeline_ids[0] == CachedComputePipelineId::INVALID {
1,033✔
3616
        effects_meta.indirect_pipeline_ids[0] = specialized_indirect_pipelines.specialize(
3✔
3617
            pipeline_cache,
3✔
3618
            &pipelines.indirect_pipeline,
3✔
3619
            DispatchIndirectPipelineKey { has_events: false },
3✔
3620
        );
3621
    }
3622
    if effects_meta.indirect_pipeline_ids[1] == CachedComputePipelineId::INVALID {
1,033✔
3623
        effects_meta.indirect_pipeline_ids[1] = specialized_indirect_pipelines.specialize(
3✔
3624
            pipeline_cache,
3✔
3625
            &pipelines.indirect_pipeline,
3✔
3626
            DispatchIndirectPipelineKey { has_events: true },
3✔
3627
        );
3628
    }
3629
    if effects_meta.active_indirect_pipeline_id == CachedComputePipelineId::INVALID {
1,033✔
3630
        effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
3✔
3631
    } else {
3632
        // If this is the first time we insert an event buffer, we need to switch the
3633
        // indirect pass from non-event to event mode. That is, we need to re-allocate
3634
        // the pipeline with the child infos buffer binding. Conversely, if there's no
3635
        // more effect using GPU spawn events, we can deallocate.
3636
        let was_empty =
1,027✔
3637
            effects_meta.active_indirect_pipeline_id == effects_meta.indirect_pipeline_ids[0];
1,027✔
3638
        let is_empty = event_cache.child_infos().is_empty();
1,027✔
3639
        if was_empty && !is_empty {
2,054✔
3640
            trace!("First event buffer inserted; switching indirect pass to event mode...");
×
3641
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[1];
×
3642
        } else if is_empty && !was_empty {
2,054✔
3643
            trace!("Last event buffer removed; switching indirect pass to no-event mode...");
×
3644
            effects_meta.active_indirect_pipeline_id = effects_meta.indirect_pipeline_ids[0];
×
3645
        }
3646
    }
3647

3648
    gpu_buffer_operations.begin_frame();
1,030✔
3649

3650
    // Clear per-instance buffers, which are filled below and re-uploaded each frame
3651
    effects_meta.spawner_buffer.clear();
1,030✔
3652

3653
    // Build batcher inputs from extracted effects, updating all cached components
3654
    // for each effect on the fly.
3655
    let effects = std::mem::take(&mut extracted_effects.effects);
1,030✔
3656
    let extracted_effect_count = effects.len();
1,030✔
3657
    let mut prepared_effect_count = 0;
1,030✔
3658
    for extracted_effect in effects.into_iter() {
2,044✔
3659
        // Skip effects not cached. Since we're iterating over the extracted effects
3660
        // instead of the cached ones, it might happen we didn't cache some effect on
3661
        // purpose because they failed earlier validations.
3662
        // FIXME - extract into ECS directly so we don't have to do that?
3663
        let Ok((
3664
            main_entity,
1,014✔
3665
            cached_effect,
1,014✔
3666
            cached_mesh,
1,014✔
3667
            cached_mesh_location,
1,014✔
3668
            dispatch_buffer_indices,
1,014✔
3669
            cached_effect_properties,
1,014✔
3670
            cached_parent_info,
1,014✔
3671
            cached_child_info,
1,014✔
3672
            cached_effect_events,
1,014✔
3673
        )) = q_cached_effects.get(extracted_effect.render_entity.id())
1,014✔
3674
        else {
3675
            warn!(
×
3676
                "Unknown render entity {:?} for extracted effect.",
×
3677
                extracted_effect.render_entity.id()
×
3678
            );
3679
            if let Ok(main_entity) = q_debug_all_entities.get(extracted_effect.render_entity.id()) {
×
3680
                info!(
3681
                    "Render entity {:?} exists with main entity {:?}, some component missing!",
×
3682
                    extracted_effect.render_entity.id(),
×
3683
                    main_entity
3684
                );
3685
            } else {
3686
                info!(
×
3687
                    "Render entity {:?} does not exists with a MainEntity.",
×
3688
                    extracted_effect.render_entity.id()
×
3689
                );
3690
            }
3691
            continue;
×
3692
        };
3693

3694
        let effect_slice = EffectSlice {
3695
            slice: cached_effect.slice.range(),
3696
            buffer_index: cached_effect.buffer_index,
3697
            particle_layout: cached_effect.slice.particle_layout.clone(),
3698
        };
3699

3700
        let has_event_buffer = cached_child_info.is_some();
3701
        // FIXME: decouple "consumes event" from "reads parent particle" (here, p.layout
3702
        // should be Option<T>, not T)
3703
        let property_layout_min_binding_size = if extracted_effect.property_layout.is_empty() {
3704
            None
1,005✔
3705
        } else {
3706
            Some(extracted_effect.property_layout.min_binding_size())
9✔
3707
        };
3708

3709
        // Schedule some GPU buffer operation to update the number of workgroups to
3710
        // dispatch during the indirect init pass of this effect based on the number of
3711
        // GPU spawn events written in its buffer.
3712
        if let (Some(cached_effect_events), Some(cached_child_info)) =
×
3713
            (cached_effect_events, cached_child_info)
3714
        {
3715
            debug_assert_eq!(
3716
                GpuChildInfo::min_size().get() % 4,
3717
                0,
3718
                "Invalid GpuChildInfo alignment."
×
3719
            );
3720

3721
            // Resolve parent entry
3722
            let Ok((_, _, _, _, _, _, cached_parent_info, _, _)) =
×
3723
                q_cached_effects.get(cached_child_info.parent)
×
3724
            else {
3725
                continue;
×
3726
            };
3727
            let Some(cached_parent_info) = cached_parent_info else {
×
3728
                error!("Effect {:?} indicates its parent is {:?}, but that parent effect is missing a CachedParentInfo component. This is a bug.", extracted_effect.render_entity.id(), cached_child_info.parent);
×
3729
                continue;
×
3730
            };
3731

3732
            let init_indirect_dispatch_index = cached_effect_events.init_indirect_dispatch_index;
3733
            assert_eq!(0, cached_parent_info.byte_range.start % 4);
3734
            let global_child_index = cached_child_info.global_child_index;
×
3735

3736
            // Schedule a fill dispatch
3737
            trace!(
×
3738
                "init_fill_dispatch.push(): src:global_child_index={} dst:init_indirect_dispatch_index={}",
×
3739
                global_child_index,
3740
                init_indirect_dispatch_index,
3741
            );
3742
            init_fill_dispatch_queue.enqueue(global_child_index, init_indirect_dispatch_index);
×
3743
        }
3744

3745
        // Create init pipeline key flags.
3746
        let init_pipeline_key_flags = {
1,014✔
3747
            let mut flags = ParticleInitPipelineKeyFlags::empty();
1,014✔
3748
            flags.set(
1,014✔
3749
                ParticleInitPipelineKeyFlags::ATTRIBUTE_PREV,
1,014✔
3750
                effect_slice.particle_layout.contains(Attribute::PREV),
1,014✔
3751
            );
3752
            flags.set(
1,014✔
3753
                ParticleInitPipelineKeyFlags::ATTRIBUTE_NEXT,
1,014✔
3754
                effect_slice.particle_layout.contains(Attribute::NEXT),
1,014✔
3755
            );
3756
            flags.set(
1,014✔
3757
                ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS,
1,014✔
3758
                has_event_buffer,
1,014✔
3759
            );
3760
            flags
1,014✔
3761
        };
3762

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

3780
        // Fetch the bind group layouts from the cache
3781
        trace!("cached_child_info={:?}", cached_child_info);
1,014✔
3782
        let parent_particle_layout_min_binding_size =
1,014✔
3783
            if let Some(cached_child) = cached_child_info.as_ref() {
×
3784
                let Ok((_, parent_cached_effect, _, _, _, _, _, _, _)) =
×
3785
                    q_cached_effects.get(cached_child.parent)
3786
                else {
3787
                    // At this point we should have discarded invalid effects with a missing parent,
3788
                    // so if the parent is not found this is a bug.
3789
                    error!(
×
3790
                        "Effect main_entity {:?}: parent render entity {:?} not found.",
×
3791
                        main_entity, cached_child.parent
3792
                    );
3793
                    continue;
×
3794
                };
3795
                Some(
3796
                    parent_cached_effect
3797
                        .slice
3798
                        .particle_layout
3799
                        .min_binding_size32(),
3800
                )
3801
            } else {
3802
                None
1,014✔
3803
            };
3804
        let Some(particle_bind_group_layout) = effect_cache.particle_bind_group_layout(
1,014✔
3805
            effect_slice.particle_layout.min_binding_size32(),
3806
            parent_particle_layout_min_binding_size,
3807
        ) else {
3808
            error!("Failed to find particle sim bind group @1 for min_binding_size={} parent_min_binding_size={:?}", 
×
3809
            effect_slice.particle_layout.min_binding_size32(), parent_particle_layout_min_binding_size);
×
3810
            continue;
×
3811
        };
3812
        let particle_bind_group_layout = particle_bind_group_layout.clone();
3813
        trace!(
3814
            "Retrieved particle@1 bind group layout {:?} for particle binding size {:?} and parent binding size {:?}.",
1,014✔
3815
            particle_bind_group_layout.id(),
1,014✔
3816
            effect_slice.particle_layout.min_binding_size32(),
1,014✔
3817
            parent_particle_layout_min_binding_size,
3818
        );
3819

3820
        let particle_layout_min_binding_size = effect_slice.particle_layout.min_binding_size32();
3821
        let spawner_bind_group_layout = spawner_bind_group_layout.clone();
3822

3823
        // Specialize the init pipeline based on the effect.
3824
        let init_pipeline_id = {
3825
            let consume_gpu_spawn_events = init_pipeline_key_flags
3826
                .contains(ParticleInitPipelineKeyFlags::CONSUME_GPU_SPAWN_EVENTS);
3827

3828
            // Fetch the metadata@3 bind group layout from the cache
3829
            let metadata_bind_group_layout = effect_cache
3830
                .metadata_init_bind_group_layout(consume_gpu_spawn_events)
3831
                .unwrap()
3832
                .clone();
3833

3834
            // https://github.com/bevyengine/bevy/issues/17132
3835
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3836
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3837
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3838
            pipelines.init_pipeline.temp_particle_bind_group_layout =
3839
                Some(particle_bind_group_layout.clone());
3840
            pipelines.init_pipeline.temp_spawner_bind_group_layout =
3841
                Some(spawner_bind_group_layout.clone());
3842
            pipelines.init_pipeline.temp_metadata_bind_group_layout =
3843
                Some(metadata_bind_group_layout);
3844
            let init_pipeline_id: CachedComputePipelineId = specialized_init_pipelines.specialize(
3845
                pipeline_cache,
3846
                &pipelines.init_pipeline,
3847
                ParticleInitPipelineKey {
3848
                    shader: extracted_effect.effect_shaders.init.clone(),
3849
                    particle_layout_min_binding_size,
3850
                    parent_particle_layout_min_binding_size,
3851
                    flags: init_pipeline_key_flags,
3852
                    particle_bind_group_layout_id,
3853
                    spawner_bind_group_layout_id,
3854
                    metadata_bind_group_layout_id,
3855
                },
3856
            );
3857
            // keep things tidy; this is just a hack, should not persist
3858
            pipelines.init_pipeline.temp_particle_bind_group_layout = None;
3859
            pipelines.init_pipeline.temp_spawner_bind_group_layout = None;
3860
            pipelines.init_pipeline.temp_metadata_bind_group_layout = None;
3861
            trace!("Init pipeline specialized: id={:?}", init_pipeline_id);
1,014✔
3862

3863
            init_pipeline_id
3864
        };
3865

3866
        let update_pipeline_id = {
3867
            let num_event_buffers = cached_parent_info
3868
                .map(|p| p.children.len() as u32)
×
3869
                .unwrap_or_default();
3870

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

3878
            // Fetch the bind group layouts from the cache
3879
            let metadata_bind_group_layout = effect_cache
3880
                .metadata_update_bind_group_layout(num_event_buffers)
3881
                .unwrap()
3882
                .clone();
3883

3884
            // https://github.com/bevyengine/bevy/issues/17132
3885
            let particle_bind_group_layout_id = particle_bind_group_layout.id();
3886
            let spawner_bind_group_layout_id = spawner_bind_group_layout.id();
3887
            let metadata_bind_group_layout_id = metadata_bind_group_layout.id();
3888
            pipelines.update_pipeline.temp_particle_bind_group_layout =
3889
                Some(particle_bind_group_layout);
3890
            pipelines.update_pipeline.temp_spawner_bind_group_layout =
3891
                Some(spawner_bind_group_layout);
3892
            pipelines.update_pipeline.temp_metadata_bind_group_layout =
3893
                Some(metadata_bind_group_layout);
3894
            let update_pipeline_id = specialized_update_pipelines.specialize(
3895
                pipeline_cache,
3896
                &pipelines.update_pipeline,
3897
                ParticleUpdatePipelineKey {
3898
                    shader: extracted_effect.effect_shaders.update.clone(),
3899
                    particle_layout: effect_slice.particle_layout.clone(),
3900
                    parent_particle_layout_min_binding_size,
3901
                    num_event_buffers,
3902
                    particle_bind_group_layout_id,
3903
                    spawner_bind_group_layout_id,
3904
                    metadata_bind_group_layout_id,
3905
                },
3906
            );
3907
            // keep things tidy; this is just a hack, should not persist
3908
            pipelines.update_pipeline.temp_particle_bind_group_layout = None;
3909
            pipelines.update_pipeline.temp_spawner_bind_group_layout = None;
3910
            pipelines.update_pipeline.temp_metadata_bind_group_layout = None;
3911
            trace!("Update pipeline specialized: id={:?}", update_pipeline_id);
1,014✔
3912

3913
            update_pipeline_id
3914
        };
3915

3916
        let init_and_update_pipeline_ids = InitAndUpdatePipelineIds {
3917
            init: init_pipeline_id,
3918
            update: update_pipeline_id,
3919
        };
3920

3921
        // For ribbons, which need particle sorting, create a bind group layout for
3922
        // sorting the effect, based on its particle layout.
3923
        if extracted_effect.layout_flags.contains(LayoutFlags::RIBBONS) {
3924
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group_layout(
×
3925
                pipeline_cache,
×
3926
                &extracted_effect.particle_layout,
×
3927
            ) {
3928
                error!(
3929
                    "Failed to create bind group for ribbon effect sorting: {:?}",
×
3930
                    err
3931
                );
3932
                continue;
3933
            }
3934
        }
3935

3936
        // Create the metadata bind group layout for the render phase.
3937
        effect_cache.ensure_metadata_render_bind_group_layout();
1,014✔
3938

3939
        // Output some debug info
3940
        trace!("init_shader = {:?}", extracted_effect.effect_shaders.init);
2,028✔
3941
        trace!(
3942
            "update_shader = {:?}",
1,014✔
3943
            extracted_effect.effect_shaders.update
3944
        );
3945
        trace!(
3946
            "render_shader = {:?}",
1,014✔
3947
            extracted_effect.effect_shaders.render
3948
        );
3949
        trace!("layout_flags = {:?}", extracted_effect.layout_flags);
1,014✔
3950
        trace!("particle_layout = {:?}", effect_slice.particle_layout);
1,014✔
3951

3952
        let spawner_index = effects_meta.allocate_spawner(
3953
            &extracted_effect.transform,
3954
            extracted_effect.spawn_count,
3955
            extracted_effect.prng_seed,
3956
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
3957
        );
3958

3959
        trace!(
3960
            "Updating cached effect at entity {:?}...",
1,014✔
3961
            extracted_effect.render_entity.id()
1,014✔
3962
        );
3963
        let mut cmd = commands.entity(extracted_effect.render_entity.id());
3964
        cmd.insert(BatchInput {
3965
            handle: extracted_effect.handle,
3966
            entity: extracted_effect.render_entity.id(),
3967
            main_entity: extracted_effect.main_entity,
3968
            effect_slice: effect_slice.clone(),
3969
            init_and_update_pipeline_ids,
UNCOV
3970
            event_buffer_index: cached_effect_events.map(|cee| cee.buffer_index),
×
3971
            child_effects: cached_parent_info
3972
                .map(|cp| cp.children.clone())
×
3973
                .unwrap_or_default(),
3974
            layout_flags: extracted_effect.layout_flags,
3975
            texture_layout: extracted_effect.texture_layout.clone(),
3976
            textures: extracted_effect.textures.clone(),
3977
            alpha_mode: extracted_effect.alpha_mode,
3978
            particle_layout: extracted_effect.particle_layout.clone(),
3979
            shaders: extracted_effect.effect_shaders,
3980
            spawner_index,
3981
            spawn_count: extracted_effect.spawn_count,
3982
            position: extracted_effect.transform.translation(),
3983
            init_indirect_dispatch_index: cached_child_info
3984
                .map(|cc| cc.init_indirect_dispatch_index),
×
3985
        });
3986

3987
        // Update properties
3988
        if let Some(cached_effect_properties) = cached_effect_properties {
10✔
3989
            // Because the component is persisted, it may be there from a previous version
3990
            // of the asset. And add_remove_effects() only add new instances or remove old
3991
            // ones, but doesn't update existing ones. Check if it needs to be removed.
3992
            // FIXME - Dedupe with add_remove_effect(), we shouldn't have 2 codepaths doing
3993
            // the same thing at 2 different times.
3994
            if extracted_effect.property_layout.is_empty() {
3995
                trace!(
1✔
3996
                    "Render entity {:?} had CachedEffectProperties component, but newly extracted property layout is empty. Removing component...",
1✔
3997
                    extracted_effect.render_entity.id(),
1✔
3998
                );
3999
                cmd.remove::<CachedEffectProperties>();
1✔
4000
                // Also remove the other one. FIXME - dedupe those two...
4001
                cmd.remove::<CachedProperties>();
1✔
4002

4003
                if extracted_effect.property_data.is_some() {
1✔
4004
                    warn!(
×
4005
                        "Effect on entity {:?} doesn't declare any property in its Module, but some property values were provided. Those values will be discarded.",
×
4006
                        extracted_effect.main_entity.id(),
×
4007
                    );
4008
                }
4009
            } else {
4010
                // Insert a new component or overwrite the existing one
4011
                cmd.insert(CachedProperties {
9✔
4012
                    layout: extracted_effect.property_layout.clone(),
9✔
4013
                    buffer_index: cached_effect_properties.buffer_index,
9✔
4014
                    offset: cached_effect_properties.range.start,
9✔
4015
                    binding_size: cached_effect_properties.range.len() as u32,
9✔
4016
                });
4017

4018
                // Write properties for this effect if they were modified.
4019
                // FIXME - This doesn't work with batching!
4020
                if let Some(property_data) = &extracted_effect.property_data {
×
4021
                    trace!(
4022
                    "Properties changed; (re-)uploading to GPU... New data: {} bytes. Capacity: {} bytes.",
×
4023
                    property_data.len(),
×
4024
                    cached_effect_properties.range.len(),
×
4025
                );
4026
                    if property_data.len() <= cached_effect_properties.range.len() {
×
4027
                        let property_buffer = property_cache.buffers_mut()
×
4028
                            [cached_effect_properties.buffer_index as usize]
×
4029
                            .as_mut()
4030
                            .unwrap();
4031
                        property_buffer.write(cached_effect_properties.range.start, property_data);
×
4032
                    } else {
4033
                        error!(
×
4034
                            "Cannot upload properties: existing property slice in property buffer #{} is too small ({} bytes) for the new data ({} bytes).",
×
4035
                            cached_effect_properties.buffer_index,
×
4036
                            cached_effect_properties.range.len(),
×
4037
                            property_data.len()
×
4038
                        );
4039
                    }
4040
                }
4041
            }
4042
        } else {
4043
            // No property on the effect; remove the component
4044
            trace!(
1,004✔
4045
                "No CachedEffectProperties on render entity {:?}, remove any CachedProperties component too.",
1,004✔
4046
                extracted_effect.render_entity.id()
1,004✔
4047
            );
4048
            cmd.remove::<CachedProperties>();
1,004✔
4049
        }
4050

4051
        // Now that the effect is entirely prepared and all GPU resources are allocated,
4052
        // update its GpuEffectMetadata with all those infos.
4053
        // FIXME - should do this only when the below changes (not only the mesh), via
4054
        // some invalidation mechanism and ECS change detection.
4055
        if !cached_mesh.is_changed() && !cached_mesh_location.is_changed() {
1,012✔
4056
            prepared_effect_count += 1;
1,012✔
4057
            continue;
1,012✔
4058
        }
4059

4060
        let capacity = cached_effect.slice.len();
2✔
4061

4062
        // Global and local indices of this effect as a child of another (parent) effect
4063
        let (global_child_index, local_child_index) = cached_child_info
2✔
4064
            .map(|cci| (cci.global_child_index, cci.local_child_index))
2✔
4065
            .unwrap_or_default();
4066

4067
        // Base index of all children of this (parent) effect
4068
        let base_child_index = cached_parent_info
4069
            .map(|cpi| {
×
4070
                debug_assert_eq!(
×
4071
                    cpi.byte_range.start % GpuChildInfo::SHADER_SIZE.get() as u32,
×
4072
                    0
4073
                );
4074
                cpi.byte_range.start / GpuChildInfo::SHADER_SIZE.get() as u32
×
4075
            })
4076
            .unwrap_or_default();
4077

4078
        let particle_stride = extracted_effect.particle_layout.min_binding_size32().get() / 4;
4079
        let sort_key_offset = extracted_effect
4080
            .particle_layout
4081
            .offset(Attribute::RIBBON_ID)
4082
            .unwrap_or_default()
4083
            / 4;
4084
        let sort_key2_offset = extracted_effect
4085
            .particle_layout
4086
            .offset(Attribute::AGE)
4087
            .unwrap_or_default()
4088
            / 4;
4089

4090
        let gpu_effect_metadata = GpuEffectMetadata {
4091
            vertex_or_index_count: cached_mesh_location.vertex_or_index_count,
4092
            instance_count: 0,
4093
            first_index_or_vertex_offset: cached_mesh_location.first_index_or_vertex_offset,
4094
            // This is the vertex offset when indexed and base instance when
4095
            // not. So we should fill in the base instance if this is a
4096
            // non-indexed mesh.
4097
            vertex_offset_or_base_instance: if cached_mesh_location.indexed.is_some() {
4098
                cached_mesh_location.vertex_offset_or_base_instance
4099
            } else {
4100
                effect_slice.slice.start as i32
4101
            },
4102
            base_instance: effect_slice.slice.start,
4103
            alive_count: 0,
4104
            max_update: 0,
4105
            dead_count: capacity,
4106
            max_spawn: capacity,
4107
            ping: 0,
4108
            indirect_dispatch_index: dispatch_buffer_indices
4109
                .update_dispatch_indirect_buffer_row_index,
4110
            // Note: the indirect draw args are at the start of the GpuEffectMetadata struct
4111
            indirect_render_index: dispatch_buffer_indices.effect_metadata_buffer_table_id.0,
4112
            init_indirect_dispatch_index: cached_effect_events
4113
                .map(|cee| cee.init_indirect_dispatch_index)
4114
                .unwrap_or_default(),
4115
            local_child_index,
4116
            global_child_index,
4117
            base_child_index,
4118
            particle_stride,
4119
            sort_key_offset,
4120
            sort_key2_offset,
4121
            ..default()
4122
        };
4123

4124
        assert!(dispatch_buffer_indices
4125
            .effect_metadata_buffer_table_id
4126
            .is_valid());
4127
        effects_meta.effect_metadata_buffer.update(
2✔
4128
            dispatch_buffer_indices.effect_metadata_buffer_table_id,
2✔
4129
            gpu_effect_metadata,
2✔
4130
        );
4131

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

4140
        prepared_effect_count += 1;
4141
    }
4142
    trace!("Prepared {prepared_effect_count}/{extracted_effect_count} extracted effect(s)");
2,050✔
4143

4144
    // Once all EffectMetadata values are written, schedule a GPU upload
4145
    if effects_meta
4146
        .effect_metadata_buffer
4147
        .allocate_gpu(render_device, render_queue)
4148
    {
4149
        // All those bind groups use the buffer so need to be re-created
4150
        trace!("*** Effect metadata buffer re-allocated; clearing all bind groups using it.");
4✔
4151
        effects_meta.indirect_metadata_bind_group = None;
2✔
4152
        effect_bind_groups.init_metadata_bind_groups.clear();
2✔
4153
        effect_bind_groups.update_metadata_bind_groups.clear();
2✔
4154
        effect_bind_groups.render_metadata_bind_groups.clear();
2✔
4155
    }
4156

4157
    // Write the entire spawner buffer for this frame, for all effects combined
4158
    assert_eq!(
4159
        prepared_effect_count,
4160
        effects_meta.spawner_buffer.len() as u32
4161
    );
4162
    if effects_meta
1,030✔
4163
        .spawner_buffer
1,030✔
4164
        .write_buffer(render_device, render_queue)
1,030✔
4165
    {
4166
        // All property bind groups use the spawner buffer, which was reallocate
4167
        effect_bind_groups.particle_buffers.clear();
2✔
4168
        property_bind_groups.clear(true);
2✔
4169
        effects_meta.indirect_spawner_bind_group = None;
2✔
4170
    }
4171

4172
    // Update simulation parameters
4173
    effects_meta.sim_params_uniforms.set(sim_params.into());
1,030✔
4174
    {
4175
        let gpu_sim_params = effects_meta.sim_params_uniforms.get_mut();
1,030✔
4176
        gpu_sim_params.num_effects = prepared_effect_count;
1,030✔
4177

4178
        trace!(
1,030✔
4179
            "Simulation parameters: time={} delta_time={} virtual_time={} \
1,020✔
4180
                virtual_delta_time={} real_time={} real_delta_time={} num_effects={}",
1,020✔
4181
            gpu_sim_params.time,
4182
            gpu_sim_params.delta_time,
4183
            gpu_sim_params.virtual_time,
4184
            gpu_sim_params.virtual_delta_time,
4185
            gpu_sim_params.real_time,
4186
            gpu_sim_params.real_delta_time,
4187
            gpu_sim_params.num_effects,
4188
        );
4189
    }
4190
    let prev_buffer_id = effects_meta.sim_params_uniforms.buffer().map(|b| b.id());
3,087✔
4191
    effects_meta
1,030✔
4192
        .sim_params_uniforms
1,030✔
4193
        .write_buffer(render_device, render_queue);
1,030✔
4194
    if prev_buffer_id != effects_meta.sim_params_uniforms.buffer().map(|b| b.id()) {
3,093✔
4195
        // Buffer changed, invalidate bind groups
4196
        effects_meta.indirect_sim_params_bind_group = None;
3✔
4197
    }
4198
}
4199

4200
pub(crate) fn batch_effects(
1,030✔
4201
    mut commands: Commands,
4202
    effects_meta: Res<EffectsMeta>,
4203
    mut sort_bind_groups: ResMut<SortBindGroups>,
4204
    mut q_cached_effects: Query<(
4205
        Entity,
4206
        &MainEntity,
4207
        &CachedMesh,
4208
        Option<&CachedEffectEvents>,
4209
        Option<&CachedChildInfo>,
4210
        Option<&CachedProperties>,
4211
        &mut DispatchBufferIndices,
4212
        &mut BatchInput,
4213
    )>,
4214
    mut sorted_effect_batches: ResMut<SortedEffectBatches>,
4215
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
4216
) {
4217
    trace!("batch_effects");
2,050✔
4218

4219
    // Sort first by effect buffer index, then by slice range (see EffectSlice)
4220
    // inside that buffer. This is critical for batching to work, because
4221
    // batching effects is based on compatible items, which implies same GPU
4222
    // buffer and continuous slice ranges (the next slice start must be equal to
4223
    // the previous start end, without gap). EffectSlice already contains both
4224
    // information, and the proper ordering implementation.
4225
    // effect_entity_list.sort_by_key(|a| a.effect_slice.clone());
4226

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

4231
    let mut sort_queue = GpuBufferOperationQueue::new();
1,030✔
4232

4233
    // Sort all extracted effects.
4234

4235
    let mut effect_sorter = EffectSorter::new();
1,030✔
4236

4237
    for (entity, _, _, _, cached_child_info, _, _, input) in &q_cached_effects {
3,058✔
4238
        effect_sorter.effects.push(EffectToBeSorted {
4239
            entity,
4240
            buffer_index: input.effect_slice.buffer_index,
4241
            base_instance: input.effect_slice.slice.start,
4242
        });
NEW
4243
        if let Some(child_info) = cached_child_info {
×
4244
            effect_sorter
4245
                .child_to_parent
4246
                .insert(entity, child_info.parent);
4247
        }
4248
    }
4249

4250
    effect_sorter.sort();
1,030✔
4251

4252
    // Loop on all extracted effects in the order we determined above, and try
4253
    // to batch them together to reduce draw calls. -- currently does nothing,
4254
    // batching was broken and never fixed.
4255
    trace!("Batching {} effects...", q_cached_effects.iter().len());
2,050✔
4256

4257
    sorted_effect_batches.clear();
1,030✔
4258

4259
    for entity in effect_sorter
2,044✔
4260
        .effects
1,030✔
4261
        .into_iter()
1,030✔
4262
        .map(|effect_to_be_sorted| effect_to_be_sorted.entity)
3,074✔
4263
    {
4264
        let Ok((
4265
            _,
4266
            main_entity,
1,014✔
4267
            cached_mesh,
1,014✔
4268
            cached_effect_events,
1,014✔
4269
            cached_child_info,
1,014✔
4270
            cached_properties,
1,014✔
4271
            dispatch_buffer_indices,
1,014✔
4272
            mut input,
1,014✔
4273
        )) = q_cached_effects.get_mut(entity)
1,014✔
4274
        else {
NEW
4275
            continue;
×
4276
        };
4277
        // Detect if this cached effect was not updated this frame by a new extracted
4278
        // effect. This happens when e.g. the effect is invisible and not simulated, or
4279
        // some error prevented it from being extracted. We use the pipeline IDs vector
4280
        // as a marker, because each frame we move it out of the CachedGroup
4281
        // component during batching, so if empty this means a new one was not created
4282
        // this frame.
4283
        // if input.init_and_update_pipeline_ids.is_empty() {
4284
        //     trace!(
4285
        //         "Skipped cached effect on render entity {:?}: not extracted this
4286
        // frame.",         entity
4287
        //     );
4288
        //     continue;
4289
        // }
4290

4291
        let translation = input.position;
4292

4293
        // Spawn one EffectBatch per instance (no batching; TODO). This contains
4294
        // most of the data needed to drive rendering. However this doesn't drive
4295
        // rendering; this is just storage.
4296
        let mut effect_batch = EffectBatch::from_input(
4297
            cached_mesh,
4298
            cached_effect_events,
4299
            cached_child_info,
4300
            &mut input,
4301
            *dispatch_buffer_indices.as_ref(),
4302
            cached_properties.map(|cp| PropertyBindGroupKey {
9✔
4303
                buffer_index: cp.buffer_index,
9✔
4304
                binding_size: cp.binding_size,
9✔
4305
            }),
4306
            cached_properties.map(|cp| cp.offset),
9✔
4307
        );
4308

4309
        // If the batch has ribbons, we need to sort the particles by RIBBON_ID and AGE
4310
        // for ribbon meshing, in order to avoid gaps when some particles in the middle
4311
        // of the ribbon die (since we can't guarantee a linear lifetime through the
4312
        // ribbon).
4313
        if input.layout_flags.contains(LayoutFlags::RIBBONS) {
4314
            // This buffer is allocated in prepare_effects(), so should always be available
4315
            let Some(effect_metadata_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
×
4316
                error!("Failed to find effect metadata buffer. This is a bug.");
×
4317
                continue;
×
4318
            };
4319

4320
            // Allocate a GpuDispatchIndirect entry
4321
            let sort_fill_indirect_dispatch_index = sort_bind_groups.allocate_indirect_dispatch();
4322
            effect_batch.sort_fill_indirect_dispatch_index =
4323
                Some(sort_fill_indirect_dispatch_index);
4324

4325
            // Enqueue a fill dispatch operation which reads GpuEffectMetadata::alive_count,
4326
            // compute a number of workgroups to dispatch based on that particle count, and
4327
            // store the result into a GpuDispatchIndirect struct which will be used to
4328
            // dispatch the fill-sort pass.
4329
            {
4330
                let src_buffer = effect_metadata_buffer.clone();
4331
                let src_binding_offset = effects_meta.effect_metadata_buffer.dynamic_offset(
4332
                    effect_batch
4333
                        .dispatch_buffer_indices
4334
                        .effect_metadata_buffer_table_id,
4335
                );
4336
                let src_binding_size = effects_meta.gpu_limits.effect_metadata_aligned_size;
4337
                let Some(dst_buffer) = sort_bind_groups.indirect_buffer() else {
×
4338
                    error!("Missing indirect dispatch buffer for sorting, cannot schedule particle sort for ribbon. This is a bug.");
×
4339
                    continue;
×
4340
                };
4341
                let dst_buffer = dst_buffer.clone();
4342
                let dst_binding_offset = 0; // see dst_offset below
4343
                                            //let dst_binding_size = NonZeroU32::new(12).unwrap();
4344
                trace!(
4345
                    "queue_fill_dispatch(): src#{:?}@+{}B ({}B) -> dst#{:?}@+{}B ({}B)",
×
4346
                    src_buffer.id(),
×
4347
                    src_binding_offset,
×
4348
                    src_binding_size.get(),
×
4349
                    dst_buffer.id(),
×
4350
                    dst_binding_offset,
4351
                    -1, //dst_binding_size.get(),
4352
                );
4353
                let src_offset = std::mem::offset_of!(GpuEffectMetadata, alive_count) as u32 / 4;
4354
                debug_assert_eq!(
4355
                    src_offset, 5,
4356
                    "GpuEffectMetadata changed, update this assert."
×
4357
                );
4358
                // FIXME - This is a quick fix to get 0.15 out. The previous code used the
4359
                // dynamic binding offset, but the indirect dispatch structs are only 12 bytes,
4360
                // so are not aligned to min_storage_buffer_offset_alignment. The fix uses a
4361
                // binding offset of 0 and binds the entire destination buffer,
4362
                // then use the dst_offset value embedded inside the GpuBufferOperationArgs to
4363
                // index the proper offset in the buffer. This requires of
4364
                // course binding the entire buffer, or at least enough to index all operations
4365
                // (hence the None below). This is not really a general solution, so should be
4366
                // reviewed.
4367
                let dst_offset = sort_bind_groups
×
4368
                    .get_indirect_dispatch_byte_offset(sort_fill_indirect_dispatch_index)
×
4369
                    / 4;
×
4370
                sort_queue.enqueue(
×
4371
                    GpuBufferOperationType::FillDispatchArgs,
×
4372
                    GpuBufferOperationArgs {
×
4373
                        src_offset,
×
4374
                        src_stride: effects_meta.gpu_limits.effect_metadata_aligned_size.get() / 4,
×
4375
                        dst_offset,
×
4376
                        dst_stride: GpuDispatchIndirect::SHADER_SIZE.get() as u32 / 4,
×
4377
                        count: 1,
×
4378
                    },
4379
                    src_buffer,
×
4380
                    src_binding_offset,
×
4381
                    Some(src_binding_size),
×
4382
                    dst_buffer,
×
4383
                    dst_binding_offset,
×
4384
                    None, //Some(dst_binding_size),
×
4385
                );
4386
            }
4387
        }
4388

4389
        let effect_batch_index = sorted_effect_batches.push(effect_batch);
1,014✔
4390
        trace!(
1,014✔
4391
            "Spawned effect batch #{:?} from cached instance on entity {:?}.",
1,014✔
4392
            effect_batch_index,
4393
            entity,
4394
        );
4395

4396
        // Spawn an EffectDrawBatch, to actually drive rendering.
4397
        commands
4398
            .spawn(EffectDrawBatch {
4399
                effect_batch_index,
4400
                translation,
4401
                main_entity: *main_entity,
4402
            })
4403
            .insert(TemporaryRenderEntity);
4404
    }
4405

4406
    debug_assert!(sorted_effect_batches.dispatch_queue_index.is_none());
1,030✔
4407
    if !sort_queue.operation_queue.is_empty() {
1,030✔
4408
        sorted_effect_batches.dispatch_queue_index = Some(gpu_buffer_operations.submit(sort_queue));
×
4409
    }
4410
}
4411

4412
/// Per-buffer bind groups for a GPU effect buffer.
4413
///
4414
/// This contains all bind groups specific to a single [`EffectBuffer`].
4415
///
4416
/// [`EffectBuffer`]: crate::render::effect_cache::EffectBuffer
4417
pub(crate) struct BufferBindGroups {
4418
    /// Bind group for the render shader.
4419
    ///
4420
    /// ```wgsl
4421
    /// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
4422
    /// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
4423
    /// @binding(2) var<storage, read> spawner : Spawner;
4424
    /// ```
4425
    render: BindGroup,
4426
    // /// Bind group for filling the indirect dispatch arguments of any child init
4427
    // /// pass.
4428
    // ///
4429
    // /// This bind group is optional; it's only created if the current effect has
4430
    // /// a GPU spawn event buffer, irrelevant of whether it has child effects
4431
    // /// (although normally the event buffer is not created if there's no
4432
    // /// children).
4433
    // ///
4434
    // /// The source buffer is always the current effect's event buffer. The
4435
    // /// destination buffer is the global shared buffer for indirect fill args
4436
    // /// operations owned by the [`EffectCache`]. The uniform buffer of operation
4437
    // /// args contains the data to index the relevant part of the global shared
4438
    // /// buffer for this effect buffer; it may contain multiple entries in case
4439
    // /// multiple effects are batched inside the current effect buffer.
4440
    // ///
4441
    // /// ```wgsl
4442
    // /// @group(0) @binding(0) var<uniform> args : BufferOperationArgs;
4443
    // /// @group(0) @binding(1) var<storage, read> src_buffer : array<u32>;
4444
    // /// @group(0) @binding(2) var<storage, read_write> dst_buffer : array<u32>;
4445
    // /// ```
4446
    // init_fill_dispatch: Option<BindGroup>,
4447
}
4448

4449
/// Combination of a texture layout and the bound textures.
4450
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
4451
struct Material {
4452
    layout: TextureLayout,
4453
    textures: Vec<AssetId<Image>>,
4454
}
4455

4456
impl Material {
4457
    /// Get the bind group entries to create a bind group.
4458
    pub fn make_entries<'a>(
×
4459
        &self,
4460
        gpu_images: &'a RenderAssets<GpuImage>,
4461
    ) -> Result<Vec<BindGroupEntry<'a>>, ()> {
4462
        if self.textures.is_empty() {
×
4463
            return Ok(vec![]);
×
4464
        }
4465

4466
        let entries: Vec<BindGroupEntry<'a>> = self
×
4467
            .textures
×
4468
            .iter()
4469
            .enumerate()
4470
            .flat_map(|(index, id)| {
×
4471
                let base_binding = index as u32 * 2;
×
4472
                if let Some(gpu_image) = gpu_images.get(*id) {
×
4473
                    vec![
×
4474
                        BindGroupEntry {
×
4475
                            binding: base_binding,
×
4476
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
×
4477
                        },
4478
                        BindGroupEntry {
×
4479
                            binding: base_binding + 1,
×
4480
                            resource: BindingResource::Sampler(&gpu_image.sampler),
×
4481
                        },
4482
                    ]
4483
                } else {
4484
                    vec![]
×
4485
                }
4486
            })
4487
            .collect();
4488
        if entries.len() == self.textures.len() * 2 {
×
4489
            return Ok(entries);
×
4490
        }
4491
        Err(())
×
4492
    }
4493
}
4494

4495
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4496
struct BindingKey {
4497
    pub buffer_id: BufferId,
4498
    pub offset: u32,
4499
    pub size: NonZeroU32,
4500
}
4501

4502
impl<'a> From<BufferSlice<'a>> for BindingKey {
4503
    fn from(value: BufferSlice<'a>) -> Self {
×
4504
        Self {
4505
            buffer_id: value.buffer.id(),
×
4506
            offset: value.offset,
×
4507
            size: value.size,
×
4508
        }
4509
    }
4510
}
4511

4512
impl<'a> From<&BufferSlice<'a>> for BindingKey {
4513
    fn from(value: &BufferSlice<'a>) -> Self {
×
4514
        Self {
4515
            buffer_id: value.buffer.id(),
×
4516
            offset: value.offset,
×
4517
            size: value.size,
×
4518
        }
4519
    }
4520
}
4521

4522
impl From<&BufferBindingSource> for BindingKey {
4523
    fn from(value: &BufferBindingSource) -> Self {
×
4524
        Self {
4525
            buffer_id: value.buffer.id(),
×
4526
            offset: value.offset,
×
4527
            size: value.size,
×
4528
        }
4529
    }
4530
}
4531

4532
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4533
struct ConsumeEventKey {
4534
    child_infos_buffer_id: BufferId,
4535
    events: BindingKey,
4536
}
4537

4538
impl From<&ConsumeEventBuffers<'_>> for ConsumeEventKey {
4539
    fn from(value: &ConsumeEventBuffers) -> Self {
×
4540
        Self {
4541
            child_infos_buffer_id: value.child_infos_buffer.id(),
×
4542
            events: value.events.into(),
×
4543
        }
4544
    }
4545
}
4546

4547
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4548
struct InitMetadataBindGroupKey {
4549
    pub buffer_index: u32,
4550
    pub effect_metadata_buffer: BufferId,
4551
    pub effect_metadata_offset: u32,
4552
    pub consume_event_key: Option<ConsumeEventKey>,
4553
}
4554

4555
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4556
struct UpdateMetadataBindGroupKey {
4557
    pub buffer_index: u32,
4558
    pub effect_metadata_buffer: BufferId,
4559
    pub effect_metadata_offset: u32,
4560
    pub child_info_buffer_id: Option<BufferId>,
4561
    pub event_buffers_keys: Vec<BindingKey>,
4562
}
4563

4564
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4565
struct RenderMetadataBindGroupKey {
4566
    pub buffer_index: u32,
4567
    pub effect_metadata_buffer: BufferId,
4568
    pub effect_metadata_offset: u32,
4569
}
4570

4571
struct CachedBindGroup<K: Eq> {
4572
    /// Key the bind group was created from. Each time the key changes, the bind
4573
    /// group should be re-created.
4574
    key: K,
4575
    /// Bind group created from the key.
4576
    bind_group: BindGroup,
4577
}
4578

4579
#[derive(Debug, Clone, Copy)]
4580
struct BufferSlice<'a> {
4581
    pub buffer: &'a Buffer,
4582
    pub offset: u32,
4583
    pub size: NonZeroU32,
4584
}
4585

4586
impl<'a> From<BufferSlice<'a>> for BufferBinding<'a> {
4587
    fn from(value: BufferSlice<'a>) -> Self {
×
4588
        Self {
4589
            buffer: value.buffer,
×
4590
            offset: value.offset.into(),
×
4591
            size: Some(value.size.into()),
×
4592
        }
4593
    }
4594
}
4595

4596
impl<'a> From<&BufferSlice<'a>> for BufferBinding<'a> {
4597
    fn from(value: &BufferSlice<'a>) -> Self {
×
4598
        Self {
4599
            buffer: value.buffer,
×
4600
            offset: value.offset.into(),
×
4601
            size: Some(value.size.into()),
×
4602
        }
4603
    }
4604
}
4605

4606
impl<'a> From<&'a BufferBindingSource> for BufferSlice<'a> {
4607
    fn from(value: &'a BufferBindingSource) -> Self {
×
4608
        Self {
4609
            buffer: &value.buffer,
×
4610
            offset: value.offset,
×
4611
            size: value.size,
×
4612
        }
4613
    }
4614
}
4615

4616
/// Optional input to [`EffectBindGroups::get_or_create_init_metadata()`] when
4617
/// the init pass consumes GPU events as a mechanism to spawn particles.
4618
struct ConsumeEventBuffers<'a> {
4619
    /// Entire buffer containing the [`GpuChildInfo`] entries for all effects.
4620
    /// This is dynamically indexed inside the shader.
4621
    child_infos_buffer: &'a Buffer,
4622
    /// Slice of the [`EventBuffer`] where the GPU spawn events are stored.
4623
    events: BufferSlice<'a>,
4624
}
4625

4626
#[derive(Default, Resource)]
4627
pub struct EffectBindGroups {
4628
    /// Map from buffer index to the bind groups shared among all effects that
4629
    /// use that buffer.
4630
    particle_buffers: HashMap<u32, BufferBindGroups>,
4631
    /// Map of bind groups for image assets used as particle textures.
4632
    images: HashMap<AssetId<Image>, BindGroup>,
4633
    /// Map from buffer index to its metadata bind group (group 3) for the init
4634
    /// pass.
4635
    // FIXME - prevents batching; this should be keyed off the buffer index
4636
    init_metadata_bind_groups:
4637
        HashMap<EffectMetadataBindGroupKey, CachedBindGroup<InitMetadataBindGroupKey>>,
4638
    /// Map from buffer index to its metadata bind group (group 3) for the
4639
    /// update pass.
4640
    // FIXME - prevents batching; this should be keyed off the buffer index
4641
    update_metadata_bind_groups:
4642
        HashMap<EffectMetadataBindGroupKey, CachedBindGroup<UpdateMetadataBindGroupKey>>,
4643
    /// Map from buffer index to its metadata bind group (group 2) for the
4644
    /// render pass.
4645
    // FIXME - prevents batching; this should be keyed off the buffer index
4646
    render_metadata_bind_groups:
4647
        HashMap<EffectMetadataBindGroupKey, CachedBindGroup<RenderMetadataBindGroupKey>>,
4648
    /// Map from an effect material to its bind group.
4649
    material_bind_groups: HashMap<Material, BindGroup>,
4650
}
4651

4652
/// Identifies a bind group for effect metadata.
4653
///
4654
/// FIXME: This should eventually go away once we can batch init, update, and
4655
/// render. Then we'll have only one bind group per buffer.
4656
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
4657
pub struct EffectMetadataBindGroupKey {
4658
    /// The index of the buffer.
4659
    pub buffer_index: u32,
4660
    /// The offset of the first particle index for this effect in the indirect
4661
    /// index buffer.
4662
    pub base_instance: u32,
4663
}
4664

4665
impl EffectBindGroups {
4666
    pub fn particle_render(&self, buffer_index: u32) -> Option<&BindGroup> {
1,013✔
4667
        self.particle_buffers
1,013✔
4668
            .get(&buffer_index)
1,013✔
4669
            .map(|bg| &bg.render)
3,039✔
4670
    }
4671

4672
    /// Retrieve the metadata@3 bind group for the init pass, creating it if
4673
    /// needed.
4674
    pub(self) fn get_or_create_init_metadata(
1,014✔
4675
        &mut self,
4676
        effect_batch: &EffectBatch,
4677
        gpu_limits: &GpuLimits,
4678
        render_device: &RenderDevice,
4679
        layout: &BindGroupLayout,
4680
        effect_metadata_buffer: &Buffer,
4681
        consume_event_buffers: Option<ConsumeEventBuffers>,
4682
    ) -> Result<&BindGroup, ()> {
4683
        let DispatchBufferIndices {
1,014✔
4684
            effect_metadata_buffer_table_id,
1,014✔
4685
            ..
1,014✔
4686
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4687

4688
        let effect_metadata_offset =
1,014✔
4689
            gpu_limits.effect_metadata_offset(effect_metadata_buffer_table_id.0) as u32;
1,014✔
4690
        let key = InitMetadataBindGroupKey {
4691
            buffer_index: effect_batch.buffer_index,
1,014✔
4692
            effect_metadata_buffer: effect_metadata_buffer.id(),
1,014✔
4693
            effect_metadata_offset,
4694
            consume_event_key: consume_event_buffers.as_ref().map(Into::into),
1,014✔
4695
        };
4696

4697
        let make_entry = || {
1,016✔
4698
            let mut entries = Vec::with_capacity(3);
2✔
4699
            entries.push(
2✔
4700
                // @group(3) @binding(0) var<storage, read_write> effect_metadata : EffectMetadata;
4701
                BindGroupEntry {
2✔
4702
                    binding: 0,
2✔
4703
                    resource: BindingResource::Buffer(BufferBinding {
2✔
4704
                        buffer: effect_metadata_buffer,
2✔
4705
                        offset: key.effect_metadata_offset as u64,
2✔
4706
                        size: Some(gpu_limits.effect_metadata_size()),
2✔
4707
                    }),
4708
                },
4709
            );
4710
            if let Some(consume_event_buffers) = consume_event_buffers.as_ref() {
2✔
4711
                entries.push(
4712
                    // @group(3) @binding(1) var<storage, read> child_info_buffer :
4713
                    // ChildInfoBuffer;
4714
                    BindGroupEntry {
4715
                        binding: 1,
4716
                        resource: BindingResource::Buffer(BufferBinding {
4717
                            buffer: consume_event_buffers.child_infos_buffer,
4718
                            offset: 0,
4719
                            size: None,
4720
                        }),
4721
                    },
4722
                );
4723
                entries.push(
4724
                    // @group(3) @binding(2) var<storage, read> event_buffer : EventBuffer;
4725
                    BindGroupEntry {
4726
                        binding: 2,
4727
                        resource: BindingResource::Buffer(consume_event_buffers.events.into()),
4728
                    },
4729
                );
4730
            }
4731

4732
            let bind_group = render_device.create_bind_group(
2✔
4733
                "hanabi:bind_group:init:metadata@3",
4734
                layout,
2✔
4735
                &entries[..],
2✔
4736
            );
4737

4738
            trace!(
2✔
4739
                    "Created new metadata@3 bind group for init pass and buffer index {}: effect_metadata=#{}",
2✔
4740
                    effect_batch.buffer_index,
4741
                    effect_metadata_buffer_table_id.0,
4742
                );
4743

4744
            bind_group
2✔
4745
        };
4746

4747
        Ok(&self
1,014✔
4748
            .init_metadata_bind_groups
1,014✔
4749
            .entry(EffectMetadataBindGroupKey {
1,014✔
4750
                buffer_index: effect_batch.buffer_index,
1,014✔
4751
                base_instance: effect_batch.slice.start,
1,014✔
4752
            })
4753
            .and_modify(|cbg| {
2,026✔
4754
                if cbg.key != key {
1,012✔
4755
                    trace!(
×
4756
                        "Bind group key changed for init metadata@3, re-creating bind group... old={:?} new={:?}",
×
4757
                        cbg.key,
4758
                        key
4759
                    );
4760
                    cbg.key = key;
×
4761
                    cbg.bind_group = make_entry();
×
4762
                }
4763
            })
4764
            .or_insert_with(|| {
1,016✔
4765
                trace!("Inserting new bind group for init metadata@3 with key={:?}", key);
4✔
4766
                CachedBindGroup {
2✔
4767
                    key,
2✔
4768
                    bind_group: make_entry(),
2✔
4769
                }
4770
            })
4771
            .bind_group)
1,014✔
4772
    }
4773

4774
    /// Retrieve the metadata@3 bind group for the update pass, creating it if
4775
    /// needed.
4776
    pub(self) fn get_or_create_update_metadata(
1,014✔
4777
        &mut self,
4778
        effect_batch: &EffectBatch,
4779
        gpu_limits: &GpuLimits,
4780
        render_device: &RenderDevice,
4781
        layout: &BindGroupLayout,
4782
        effect_metadata_buffer: &Buffer,
4783
        child_info_buffer: Option<&Buffer>,
4784
        event_buffers: &[(Entity, BufferBindingSource)],
4785
    ) -> Result<&BindGroup, ()> {
4786
        let DispatchBufferIndices {
1,014✔
4787
            effect_metadata_buffer_table_id,
1,014✔
4788
            ..
1,014✔
4789
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4790

4791
        // Check arguments consistency
4792
        assert_eq!(effect_batch.child_event_buffers.len(), event_buffers.len());
1,014✔
4793
        let emits_gpu_spawn_events = !event_buffers.is_empty();
1,014✔
4794
        let child_info_buffer_id = if emits_gpu_spawn_events {
2,028✔
4795
            child_info_buffer.as_ref().map(|buffer| buffer.id())
×
4796
        } else {
4797
            // Note: child_info_buffer can be Some() if allocated, but we only consider it
4798
            // if relevant, that is if the effect emits GPU spawn events.
4799
            None
1,014✔
4800
        };
4801
        assert_eq!(emits_gpu_spawn_events, child_info_buffer_id.is_some());
1,014✔
4802

4803
        let event_buffers_keys = event_buffers
1,014✔
4804
            .iter()
4805
            .map(|(_, buffer_binding_source)| buffer_binding_source.into())
2,028✔
4806
            .collect::<Vec<_>>();
4807

4808
        let key = UpdateMetadataBindGroupKey {
4809
            buffer_index: effect_batch.buffer_index,
1,014✔
4810
            effect_metadata_buffer: effect_metadata_buffer.id(),
1,014✔
4811
            effect_metadata_offset: gpu_limits
1,014✔
4812
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4813
                as u32,
4814
            child_info_buffer_id,
4815
            event_buffers_keys,
4816
        };
4817

4818
        let make_entry = || {
1,016✔
4819
            let mut entries = Vec::with_capacity(2 + event_buffers.len());
2✔
4820
            // @group(3) @binding(0) var<storage, read_write> effect_metadata :
4821
            // EffectMetadata;
4822
            entries.push(BindGroupEntry {
2✔
4823
                binding: 0,
2✔
4824
                resource: BindingResource::Buffer(BufferBinding {
2✔
4825
                    buffer: effect_metadata_buffer,
2✔
4826
                    offset: key.effect_metadata_offset as u64,
2✔
4827
                    size: Some(gpu_limits.effect_metadata_aligned_size.into()),
2✔
4828
                }),
4829
            });
4830
            if emits_gpu_spawn_events {
2✔
4831
                let child_info_buffer = child_info_buffer.unwrap();
×
4832

4833
                // @group(3) @binding(1) var<storage, read_write> child_info_buffer :
4834
                // ChildInfoBuffer;
4835
                entries.push(BindGroupEntry {
×
4836
                    binding: 1,
×
4837
                    resource: BindingResource::Buffer(BufferBinding {
×
4838
                        buffer: child_info_buffer,
×
4839
                        offset: 0,
×
4840
                        size: None,
×
4841
                    }),
4842
                });
4843

4844
                for (index, (_, buffer_binding_source)) in event_buffers.iter().enumerate() {
×
4845
                    // @group(3) @binding(2+N) var<storage, read_write> event_buffer_N :
4846
                    // EventBuffer;
4847
                    // FIXME - BufferBindingSource originally was for Events, counting in u32, but
4848
                    // then moved to counting in bytes, so now need some conversion. Need to review
4849
                    // all of this...
4850
                    let mut buffer_binding: BufferBinding = buffer_binding_source.into();
4851
                    buffer_binding.offset *= 4;
4852
                    buffer_binding.size = buffer_binding
4853
                        .size
4854
                        .map(|sz| NonZeroU64::new(sz.get() * 4).unwrap());
×
4855
                    entries.push(BindGroupEntry {
4856
                        binding: 2 + index as u32,
4857
                        resource: BindingResource::Buffer(buffer_binding),
4858
                    });
4859
                }
4860
            }
4861

4862
            let bind_group = render_device.create_bind_group(
2✔
4863
                "hanabi:bind_group:update:metadata@3",
4864
                layout,
2✔
4865
                &entries[..],
2✔
4866
            );
4867

4868
            trace!(
2✔
4869
                "Created new metadata@3 bind group for update pass and buffer index {}: effect_metadata={}",
2✔
4870
                effect_batch.buffer_index,
4871
                effect_metadata_buffer_table_id.0,
4872
            );
4873

4874
            bind_group
2✔
4875
        };
4876

4877
        Ok(&self
1,014✔
4878
            .update_metadata_bind_groups
1,014✔
4879
            .entry(EffectMetadataBindGroupKey {
1,014✔
4880
                buffer_index: effect_batch.buffer_index,
1,014✔
4881
                base_instance: effect_batch.slice.start,
1,014✔
4882
            })
4883
            .and_modify(|cbg| {
2,026✔
4884
                if cbg.key != key {
1,012✔
4885
                    trace!(
×
4886
                        "Bind group key changed for update metadata@3, re-creating bind group... old={:?} new={:?}",
×
4887
                        cbg.key,
4888
                        key
4889
                    );
4890
                    cbg.key = key.clone();
×
4891
                    cbg.bind_group = make_entry();
×
4892
                }
4893
            })
4894
            .or_insert_with(|| {
1,016✔
4895
                trace!(
2✔
4896
                    "Inserting new bind group for update metadata@3 with key={:?}",
2✔
4897
                    key
4898
                );
4899
                CachedBindGroup {
2✔
4900
                    key: key.clone(),
2✔
4901
                    bind_group: make_entry(),
2✔
4902
                }
4903
            })
4904
            .bind_group)
1,014✔
4905
    }
4906

4907
    /// Retrieve the metadata@2 bind group for the render pass, creating it if
4908
    /// needed.
4909
    pub(self) fn get_or_create_render_metadata(
1,014✔
4910
        &mut self,
4911
        effect_batch: &EffectBatch,
4912
        gpu_limits: &GpuLimits,
4913
        render_device: &RenderDevice,
4914
        layout: &BindGroupLayout,
4915
        effect_metadata_buffer: &Buffer,
4916
    ) -> Result<&BindGroup, ()> {
4917
        let DispatchBufferIndices {
1,014✔
4918
            effect_metadata_buffer_table_id,
1,014✔
4919
            ..
1,014✔
4920
        } = &effect_batch.dispatch_buffer_indices;
1,014✔
4921

4922
        let key = RenderMetadataBindGroupKey {
4923
            buffer_index: effect_batch.buffer_index,
1,014✔
4924
            effect_metadata_buffer: effect_metadata_buffer.id(),
1,014✔
4925
            effect_metadata_offset: gpu_limits
1,014✔
4926
                .effect_metadata_offset(effect_metadata_buffer_table_id.0)
4927
                as u32,
4928
        };
4929

4930
        let make_entry = || {
1,016✔
4931
            // @group(3) @binding(0) var<storage, read> effect_metadata :
4932
            // EffectMetadata;
4933

4934
            let bind_group = render_device.create_bind_group(
2✔
4935
                "hanabi:bind_group:render:metadata@2",
4936
                layout,
2✔
4937
                &[BindGroupEntry {
2✔
4938
                    binding: 0,
2✔
4939
                    resource: BindingResource::Buffer(BufferBinding {
2✔
4940
                        buffer: effect_metadata_buffer,
2✔
4941
                        offset: key.effect_metadata_offset as u64,
2✔
4942
                        size: Some(gpu_limits.effect_metadata_aligned_size.into()),
2✔
4943
                    }),
4944
                }],
4945
            );
4946

4947
            trace!(
2✔
4948
                "Created new metadata@2 bind group for render pass and buffer index {}: effect_metadata={}",
2✔
4949
                effect_batch.buffer_index,
4950
                effect_metadata_buffer_table_id.0,
4951
            );
4952

4953
            bind_group
2✔
4954
        };
4955

4956
        Ok(&self
1,014✔
4957
            .render_metadata_bind_groups
1,014✔
4958
            .entry(EffectMetadataBindGroupKey {
1,014✔
4959
                buffer_index: effect_batch.buffer_index,
1,014✔
4960
                base_instance: effect_batch.slice.start,
1,014✔
4961
            })
4962
            .and_modify(|cbg| {
2,026✔
4963
                if cbg.key != key {
1,012✔
NEW
4964
                    trace!(
×
NEW
4965
                        "Bind group key changed for render metadata@3, re-creating bind group... old={:?} new={:?}",
×
4966
                        cbg.key,
4967
                        key
4968
                    );
NEW
4969
                    cbg.key = key;
×
NEW
4970
                    cbg.bind_group = make_entry();
×
4971
                }
4972
            })
4973
            .or_insert_with(|| {
1,016✔
4974
                trace!(
2✔
4975
                    "Inserting new bind group for render metadata@3 with key={:?}",
2✔
4976
                    key
4977
                );
4978
                CachedBindGroup {
2✔
4979
                    key,
2✔
4980
                    bind_group: make_entry(),
2✔
4981
                }
4982
            })
4983
            .bind_group)
1,014✔
4984
    }
4985
}
4986

4987
#[derive(SystemParam)]
4988
pub struct QueueEffectsReadOnlyParams<'w, 's> {
4989
    #[cfg(feature = "2d")]
4990
    draw_functions_2d: Res<'w, DrawFunctions<Transparent2d>>,
4991
    #[cfg(feature = "3d")]
4992
    draw_functions_3d: Res<'w, DrawFunctions<Transparent3d>>,
4993
    #[cfg(feature = "3d")]
4994
    draw_functions_alpha_mask: Res<'w, DrawFunctions<AlphaMask3d>>,
4995
    #[cfg(feature = "3d")]
4996
    draw_functions_opaque: Res<'w, DrawFunctions<Opaque3d>>,
4997
    marker: PhantomData<&'s usize>,
4998
}
4999

5000
fn emit_sorted_draw<T, F>(
2,028✔
5001
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5002
    render_phases: &mut ResMut<ViewSortedRenderPhases<T>>,
5003
    view_entities: &mut FixedBitSet,
5004
    sorted_effect_batches: &SortedEffectBatches,
5005
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5006
    render_pipeline: &mut ParticlesRenderPipeline,
5007
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5008
    render_meshes: &RenderAssets<RenderMesh>,
5009
    pipeline_cache: &PipelineCache,
5010
    make_phase_item: F,
5011
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5012
) where
5013
    T: SortedPhaseItem,
5014
    F: Fn(CachedRenderPipelineId, (Entity, MainEntity), &EffectDrawBatch, &ExtractedView) -> T,
5015
{
5016
    trace!("emit_sorted_draw() {} views", views.iter().len());
4,056✔
5017

5018
    for (visible_entities, view, msaa) in views.iter() {
4,056✔
5019
        trace!(
×
5020
            "Process new sorted view with {} visible particle effect entities",
2,028✔
5021
            visible_entities.len::<CompiledParticleEffect>()
2,028✔
5022
        );
5023

5024
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
1,014✔
5025
            continue;
1,014✔
5026
        };
5027

5028
        {
5029
            #[cfg(feature = "trace")]
5030
            let _span = bevy::log::info_span!("collect_view_entities").entered();
3,042✔
5031

5032
            view_entities.clear();
1,014✔
5033
            view_entities.extend(
1,014✔
5034
                visible_entities
1,014✔
5035
                    .iter::<EffectVisibilityClass>()
1,014✔
5036
                    .map(|e| e.1.index() as usize),
3,042✔
5037
            );
5038
        }
5039

5040
        // For each view, loop over all the effect batches to determine if the effect
5041
        // needs to be rendered for that view, and enqueue a view-dependent
5042
        // batch if so.
5043
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
2,028✔
5044
            #[cfg(feature = "trace")]
5045
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5046

5047
            trace!(
×
5048
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
1,014✔
5049
                draw_entity,
×
5050
                draw_batch.effect_batch_index,
×
5051
            );
5052

5053
            // Get the EffectBatches this EffectDrawBatch is part of.
5054
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
1,014✔
5055
            else {
×
5056
                continue;
×
5057
            };
5058

5059
            trace!(
×
5060
                "-> EffectBach: buffer_index={} spawner_base={} layout_flags={:?}",
1,014✔
5061
                effect_batch.buffer_index,
×
5062
                effect_batch.spawner_base,
×
5063
                effect_batch.layout_flags,
×
5064
            );
5065

5066
            // AlphaMask is a binned draw, so no sorted draw can possibly use it
5067
            if effect_batch
×
5068
                .layout_flags
×
5069
                .intersects(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE)
×
5070
            {
5071
                trace!("Non-transparent batch. Skipped.");
×
5072
                continue;
×
5073
            }
5074

5075
            // Check if batch contains any entity visible in the current view. Otherwise we
5076
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5077
            // the Sprite renderer this is inspired from) we don't expect more than
5078
            // a handful of particle effect instances, so would rather not pay the memory
5079
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5080
            // TODO - Profile to confirm.
5081
            #[cfg(feature = "trace")]
5082
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5083
            let has_visible_entity = effect_batch
×
5084
                .entities
×
5085
                .iter()
5086
                .any(|index| view_entities.contains(*index as usize));
1,014✔
5087
            if !has_visible_entity {
×
5088
                trace!("No visible entity for view, not emitting any draw call.");
×
5089
                continue;
×
5090
            }
5091
            #[cfg(feature = "trace")]
5092
            _span_check_vis.exit();
1,014✔
5093

5094
            // Create and cache the bind group layout for this texture layout
5095
            render_pipeline.cache_material(&effect_batch.texture_layout);
1,014✔
5096

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

5100
            let local_space_simulation = effect_batch
1,014✔
5101
                .layout_flags
1,014✔
5102
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
1,014✔
5103
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
1,014✔
5104
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
1,014✔
5105
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
1,014✔
5106
            let needs_normal = effect_batch
1,014✔
5107
                .layout_flags
1,014✔
5108
                .contains(LayoutFlags::NEEDS_NORMAL);
1,014✔
5109
            let needs_particle_fragment = effect_batch
1,014✔
5110
                .layout_flags
1,014✔
5111
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
1,014✔
5112
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
1,014✔
5113
            let image_count = effect_batch.texture_layout.layout.len() as u8;
1,014✔
5114

5115
            // FIXME - Maybe it's better to copy the mesh layout into the batch, instead of
5116
            // re-querying here...?
5117
            let Some(render_mesh) = render_meshes.get(effect_batch.mesh) else {
2,028✔
5118
                trace!("Batch has no render mesh, skipped.");
×
5119
                continue;
×
5120
            };
5121
            let mesh_layout = render_mesh.layout.clone();
×
5122

5123
            // Specialize the render pipeline based on the effect batch
5124
            trace!(
×
5125
                "Specializing render pipeline: render_shader={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
1,014✔
5126
                effect_batch.render_shader,
×
5127
                image_count,
×
5128
                alpha_mask,
×
5129
                flipbook,
×
5130
                view.hdr
×
5131
            );
5132

5133
            // Add a draw pass for the effect batch
5134
            trace!("Emitting individual draw for batch");
1,014✔
5135

5136
            let alpha_mode = effect_batch.alpha_mode;
×
5137

5138
            #[cfg(feature = "trace")]
5139
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5140
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5141
                pipeline_cache,
×
5142
                render_pipeline,
×
5143
                ParticleRenderPipelineKey {
×
5144
                    shader: effect_batch.render_shader.clone(),
×
5145
                    mesh_layout: Some(mesh_layout),
×
5146
                    particle_layout: effect_batch.particle_layout.clone(),
×
5147
                    texture_layout: effect_batch.texture_layout.clone(),
×
5148
                    local_space_simulation,
×
5149
                    alpha_mask,
×
5150
                    alpha_mode,
×
5151
                    flipbook,
×
5152
                    needs_uv,
×
5153
                    needs_normal,
×
5154
                    needs_particle_fragment,
×
5155
                    ribbons,
×
5156
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5157
                    pipeline_mode,
×
5158
                    msaa_samples: msaa.samples(),
×
5159
                    hdr: view.hdr,
×
5160
                },
5161
            );
5162
            #[cfg(feature = "trace")]
5163
            _span_specialize.exit();
×
5164

5165
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
1,014✔
5166
            trace!(
×
5167
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
1,014✔
5168
                spawner_base={} handle={:?}",
1,014✔
5169
                draw_entity,
×
5170
                effect_batch.buffer_index,
×
5171
                effect_batch.spawner_base,
×
5172
                effect_batch.handle
×
5173
            );
5174
            render_phase.add(make_phase_item(
×
5175
                render_pipeline_id,
×
5176
                (draw_entity, MainEntity::from(Entity::PLACEHOLDER)),
×
5177
                draw_batch,
×
5178
                view,
×
5179
            ));
5180
        }
5181
    }
5182
}
5183

5184
#[cfg(feature = "3d")]
5185
fn emit_binned_draw<T, F, G>(
2,028✔
5186
    views: &Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5187
    render_phases: &mut ResMut<ViewBinnedRenderPhases<T>>,
5188
    view_entities: &mut FixedBitSet,
5189
    sorted_effect_batches: &SortedEffectBatches,
5190
    effect_draw_batches: &Query<(Entity, &mut EffectDrawBatch)>,
5191
    render_pipeline: &mut ParticlesRenderPipeline,
5192
    mut specialized_render_pipelines: Mut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5193
    pipeline_cache: &PipelineCache,
5194
    render_meshes: &RenderAssets<RenderMesh>,
5195
    make_batch_set_key: F,
5196
    make_bin_key: G,
5197
    #[cfg(all(feature = "2d", feature = "3d"))] pipeline_mode: PipelineMode,
5198
    alpha_mask: ParticleRenderAlphaMaskPipelineKey,
5199
    change_tick: &mut Tick,
5200
) where
5201
    T: BinnedPhaseItem,
5202
    F: Fn(CachedRenderPipelineId, &EffectDrawBatch, &ExtractedView) -> T::BatchSetKey,
5203
    G: Fn() -> T::BinKey,
5204
{
5205
    use bevy::render::render_phase::{BinnedRenderPhaseType, InputUniformIndex};
5206

5207
    trace!("emit_binned_draw() {} views", views.iter().len());
4,056✔
5208

5209
    for (visible_entities, view, msaa) in views.iter() {
4,056✔
5210
        trace!("Process new binned view (alpha_mask={:?})", alpha_mask);
2,028✔
5211

5212
        let Some(render_phase) = render_phases.get_mut(&view.retained_view_entity) else {
2,028✔
5213
            continue;
×
5214
        };
5215

5216
        {
5217
            #[cfg(feature = "trace")]
5218
            let _span = bevy::log::info_span!("collect_view_entities").entered();
6,084✔
5219

5220
            view_entities.clear();
2,028✔
5221
            view_entities.extend(
2,028✔
5222
                visible_entities
2,028✔
5223
                    .iter::<EffectVisibilityClass>()
2,028✔
5224
                    .map(|e| e.1.index() as usize),
6,084✔
5225
            );
5226
        }
5227

5228
        // For each view, loop over all the effect batches to determine if the effect
5229
        // needs to be rendered for that view, and enqueue a view-dependent
5230
        // batch if so.
5231
        for (draw_entity, draw_batch) in effect_draw_batches.iter() {
4,056✔
5232
            #[cfg(feature = "trace")]
5233
            let _span_draw = bevy::log::info_span!("draw_batch").entered();
×
5234

5235
            trace!(
×
5236
                "Process draw batch: draw_entity={:?} effect_batch_index={:?}",
2,028✔
5237
                draw_entity,
×
5238
                draw_batch.effect_batch_index,
×
5239
            );
5240

5241
            // Get the EffectBatches this EffectDrawBatch is part of.
5242
            let Some(effect_batch) = sorted_effect_batches.get(draw_batch.effect_batch_index)
2,028✔
5243
            else {
×
5244
                continue;
×
5245
            };
5246

5247
            trace!(
×
5248
                "-> EffectBaches: buffer_index={} spawner_base={} layout_flags={:?}",
2,028✔
5249
                effect_batch.buffer_index,
×
5250
                effect_batch.spawner_base,
×
5251
                effect_batch.layout_flags,
×
5252
            );
5253

5254
            if ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags) != alpha_mask {
×
5255
                trace!(
2,028✔
5256
                    "Mismatching alpha mask pipeline key (batches={:?}, expected={:?}). Skipped.",
2,028✔
5257
                    effect_batch.layout_flags,
×
5258
                    alpha_mask
×
5259
                );
5260
                continue;
2,028✔
5261
            }
5262

5263
            // Check if batch contains any entity visible in the current view. Otherwise we
5264
            // can skip the entire batch. Note: This is O(n^2) but (unlike
5265
            // the Sprite renderer this is inspired from) we don't expect more than
5266
            // a handful of particle effect instances, so would rather not pay the memory
5267
            // cost of a FixedBitSet for the sake of an arguable speed-up.
5268
            // TODO - Profile to confirm.
5269
            #[cfg(feature = "trace")]
5270
            let _span_check_vis = bevy::log::info_span!("check_visibility").entered();
×
5271
            let has_visible_entity = effect_batch
×
5272
                .entities
×
5273
                .iter()
5274
                .any(|index| view_entities.contains(*index as usize));
×
5275
            if !has_visible_entity {
×
5276
                trace!("No visible entity for view, not emitting any draw call.");
×
5277
                continue;
×
5278
            }
5279
            #[cfg(feature = "trace")]
5280
            _span_check_vis.exit();
×
5281

5282
            // Create and cache the bind group layout for this texture layout
5283
            render_pipeline.cache_material(&effect_batch.texture_layout);
×
5284

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

5288
            let local_space_simulation = effect_batch
×
5289
                .layout_flags
×
5290
                .contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
×
5291
            let alpha_mask = ParticleRenderAlphaMaskPipelineKey::from(effect_batch.layout_flags);
×
5292
            let flipbook = effect_batch.layout_flags.contains(LayoutFlags::FLIPBOOK);
×
5293
            let needs_uv = effect_batch.layout_flags.contains(LayoutFlags::NEEDS_UV);
×
5294
            let needs_normal = effect_batch
×
5295
                .layout_flags
×
5296
                .contains(LayoutFlags::NEEDS_NORMAL);
×
5297
            let needs_particle_fragment = effect_batch
×
5298
                .layout_flags
×
5299
                .contains(LayoutFlags::NEEDS_PARTICLE_FRAGMENT);
×
5300
            let ribbons = effect_batch.layout_flags.contains(LayoutFlags::RIBBONS);
×
5301
            let image_count = effect_batch.texture_layout.layout.len() as u8;
×
5302
            let render_mesh = render_meshes.get(effect_batch.mesh);
×
5303

5304
            // Specialize the render pipeline based on the effect batch
5305
            trace!(
×
5306
                "Specializing render pipeline: render_shaders={:?} image_count={} alpha_mask={:?} flipbook={:?} hdr={}",
×
5307
                effect_batch.render_shader,
×
5308
                image_count,
×
5309
                alpha_mask,
×
5310
                flipbook,
×
5311
                view.hdr
×
5312
            );
5313

5314
            // Add a draw pass for the effect batch
5315
            trace!("Emitting individual draw for batch");
×
5316

5317
            let alpha_mode = effect_batch.alpha_mode;
×
5318

5319
            let Some(mesh_layout) = render_mesh.map(|gpu_mesh| gpu_mesh.layout.clone()) else {
×
5320
                trace!("Missing mesh vertex buffer layout. Skipped.");
×
5321
                continue;
×
5322
            };
5323

5324
            #[cfg(feature = "trace")]
5325
            let _span_specialize = bevy::log::info_span!("specialize").entered();
×
5326
            let render_pipeline_id = specialized_render_pipelines.specialize(
×
5327
                pipeline_cache,
×
5328
                render_pipeline,
×
5329
                ParticleRenderPipelineKey {
×
5330
                    shader: effect_batch.render_shader.clone(),
×
5331
                    mesh_layout: Some(mesh_layout),
×
5332
                    particle_layout: effect_batch.particle_layout.clone(),
×
5333
                    texture_layout: effect_batch.texture_layout.clone(),
×
5334
                    local_space_simulation,
×
5335
                    alpha_mask,
×
5336
                    alpha_mode,
×
5337
                    flipbook,
×
5338
                    needs_uv,
×
5339
                    needs_normal,
×
5340
                    needs_particle_fragment,
×
5341
                    ribbons,
×
5342
                    #[cfg(all(feature = "2d", feature = "3d"))]
×
5343
                    pipeline_mode,
×
5344
                    msaa_samples: msaa.samples(),
×
5345
                    hdr: view.hdr,
×
5346
                },
5347
            );
5348
            #[cfg(feature = "trace")]
5349
            _span_specialize.exit();
×
5350

5351
            trace!("+ Render pipeline specialized: id={:?}", render_pipeline_id,);
×
5352
            trace!(
×
5353
                "+ Add Transparent for batch on draw_entity {:?}: buffer_index={} \
×
5354
                spawner_base={} handle={:?}",
×
5355
                draw_entity,
×
5356
                effect_batch.buffer_index,
×
5357
                effect_batch.spawner_base,
×
5358
                effect_batch.handle
×
5359
            );
5360
            render_phase.add(
×
5361
                make_batch_set_key(render_pipeline_id, draw_batch, view),
×
5362
                make_bin_key(),
×
5363
                (draw_entity, draw_batch.main_entity),
×
5364
                InputUniformIndex::default(),
×
5365
                BinnedRenderPhaseType::NonMesh,
×
5366
                *change_tick,
×
5367
            );
5368
        }
5369
    }
5370
}
5371

5372
#[allow(clippy::too_many_arguments)]
5373
pub(crate) fn queue_effects(
1,030✔
5374
    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
5375
    effects_meta: Res<EffectsMeta>,
5376
    mut render_pipeline: ResMut<ParticlesRenderPipeline>,
5377
    mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<ParticlesRenderPipeline>>,
5378
    pipeline_cache: Res<PipelineCache>,
5379
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5380
    sorted_effect_batches: Res<SortedEffectBatches>,
5381
    effect_draw_batches: Query<(Entity, &mut EffectDrawBatch)>,
5382
    events: Res<EffectAssetEvents>,
5383
    render_meshes: Res<RenderAssets<RenderMesh>>,
5384
    read_params: QueueEffectsReadOnlyParams,
5385
    mut view_entities: Local<FixedBitSet>,
5386
    #[cfg(feature = "2d")] mut transparent_2d_render_phases: ResMut<
5387
        ViewSortedRenderPhases<Transparent2d>,
5388
    >,
5389
    #[cfg(feature = "3d")] mut transparent_3d_render_phases: ResMut<
5390
        ViewSortedRenderPhases<Transparent3d>,
5391
    >,
5392
    #[cfg(feature = "3d")] (mut opaque_3d_render_phases, mut alpha_mask_3d_render_phases): (
5393
        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
5394
        ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
5395
    ),
5396
    mut change_tick: Local<Tick>,
5397
) {
5398
    #[cfg(feature = "trace")]
5399
    let _span = bevy::log::info_span!("hanabi:queue_effects").entered();
3,090✔
5400

5401
    trace!("queue_effects");
2,050✔
5402

5403
    // Bump the change tick so that Bevy is forced to rebuild the binned render
5404
    // phase bins. We don't use the built-in caching so we don't want Bevy to
5405
    // reuse stale data.
5406
    let next_change_tick = change_tick.get() + 1;
1,030✔
5407
    change_tick.set(next_change_tick);
1,030✔
5408

5409
    // If an image has changed, the GpuImage has (probably) changed
5410
    for event in &events.images {
1,057✔
5411
        match event {
5412
            AssetEvent::Added { .. } => None,
24✔
5413
            AssetEvent::LoadedWithDependencies { .. } => None,
×
5414
            AssetEvent::Unused { .. } => None,
×
5415
            AssetEvent::Modified { id } => {
×
5416
                trace!("Destroy bind group of modified image asset {:?}", id);
×
5417
                effect_bind_groups.images.remove(id)
×
5418
            }
5419
            AssetEvent::Removed { id } => {
3✔
5420
                trace!("Destroy bind group of removed image asset {:?}", id);
5✔
5421
                effect_bind_groups.images.remove(id)
3✔
5422
            }
5423
        };
5424
    }
5425

5426
    if effects_meta.spawner_buffer.buffer().is_none() || effects_meta.spawner_buffer.is_empty() {
2,050✔
5427
        // No spawners are active
5428
        return;
16✔
5429
    }
5430

5431
    // Loop over all 2D cameras/views that need to render effects
5432
    #[cfg(feature = "2d")]
5433
    {
5434
        #[cfg(feature = "trace")]
5435
        let _span_draw = bevy::log::info_span!("draw_2d").entered();
5436

5437
        let draw_effects_function_2d = read_params
5438
            .draw_functions_2d
5439
            .read()
5440
            .get_id::<DrawEffects>()
5441
            .unwrap();
5442

5443
        // Effects with full alpha blending
5444
        if !views.is_empty() {
5445
            trace!("Emit effect draw calls for alpha blended 2D views...");
2,028✔
5446
            emit_sorted_draw(
5447
                &views,
5448
                &mut transparent_2d_render_phases,
5449
                &mut view_entities,
5450
                &sorted_effect_batches,
5451
                &effect_draw_batches,
5452
                &mut render_pipeline,
5453
                specialized_render_pipelines.reborrow(),
5454
                &render_meshes,
5455
                &pipeline_cache,
5456
                |id, entity, draw_batch, _view| Transparent2d {
×
5457
                    sort_key: FloatOrd(draw_batch.translation.z),
×
5458
                    entity,
×
5459
                    pipeline: id,
×
5460
                    draw_function: draw_effects_function_2d,
×
5461
                    batch_range: 0..1,
×
5462
                    extracted_index: 0, // ???
×
5463
                    extra_index: PhaseItemExtraIndex::None,
×
5464
                    indexed: true, // ???
×
5465
                },
5466
                #[cfg(feature = "3d")]
5467
                PipelineMode::Camera2d,
5468
            );
5469
        }
5470
    }
5471

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

5478
        // Effects with full alpha blending
5479
        if !views.is_empty() {
5480
            trace!("Emit effect draw calls for alpha blended 3D views...");
2,028✔
5481

5482
            let draw_effects_function_3d = read_params
5483
                .draw_functions_3d
5484
                .read()
5485
                .get_id::<DrawEffects>()
5486
                .unwrap();
5487

5488
            emit_sorted_draw(
5489
                &views,
5490
                &mut transparent_3d_render_phases,
5491
                &mut view_entities,
5492
                &sorted_effect_batches,
5493
                &effect_draw_batches,
5494
                &mut render_pipeline,
5495
                specialized_render_pipelines.reborrow(),
5496
                &render_meshes,
5497
                &pipeline_cache,
5498
                |id, entity, batch, view| Transparent3d {
1,014✔
5499
                    distance: view
1,014✔
5500
                        .rangefinder3d()
1,014✔
5501
                        .distance_translation(&batch.translation),
1,014✔
5502
                    pipeline: id,
1,014✔
5503
                    entity,
1,014✔
5504
                    draw_function: draw_effects_function_3d,
1,014✔
5505
                    batch_range: 0..1,
1,014✔
5506
                    extra_index: PhaseItemExtraIndex::None,
1,014✔
5507
                    indexed: true, // ???
1,014✔
5508
                },
5509
                #[cfg(feature = "2d")]
5510
                PipelineMode::Camera3d,
5511
            );
5512
        }
5513

5514
        // Effects with alpha mask
5515
        if !views.is_empty() {
5516
            #[cfg(feature = "trace")]
5517
            let _span_draw = bevy::log::info_span!("draw_alphamask").entered();
1,014✔
5518

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

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

5527
            emit_binned_draw(
5528
                &views,
5529
                &mut alpha_mask_3d_render_phases,
5530
                &mut view_entities,
5531
                &sorted_effect_batches,
5532
                &effect_draw_batches,
5533
                &mut render_pipeline,
5534
                specialized_render_pipelines.reborrow(),
5535
                &pipeline_cache,
5536
                &render_meshes,
5537
                |id, _batch, _view| OpaqueNoLightmap3dBatchSetKey {
×
5538
                    pipeline: id,
×
5539
                    draw_function: draw_effects_function_alpha_mask,
×
5540
                    material_bind_group_index: None,
×
5541
                    vertex_slab: default(),
×
5542
                    index_slab: None,
×
5543
                },
5544
                // Unused for now
5545
                || OpaqueNoLightmap3dBinKey {
×
5546
                    asset_id: AssetId::<Mesh>::invalid().untyped(),
×
5547
                },
5548
                #[cfg(feature = "2d")]
5549
                PipelineMode::Camera3d,
5550
                ParticleRenderAlphaMaskPipelineKey::AlphaMask,
5551
                &mut change_tick,
5552
            );
5553
        }
5554

5555
        // Opaque particles
5556
        if !views.is_empty() {
5557
            #[cfg(feature = "trace")]
5558
            let _span_draw = bevy::log::info_span!("draw_opaque").entered();
1,014✔
5559

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

5562
            let draw_effects_function_opaque = read_params
5563
                .draw_functions_opaque
5564
                .read()
5565
                .get_id::<DrawEffects>()
5566
                .unwrap();
5567

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

5599
/// Prepare GPU resources for effect rendering.
5600
///
5601
/// This system runs in the [`RenderSet::PrepareResources`] render set, after
5602
/// Bevy has updated the [`ViewUniforms`], which need to be referenced to get
5603
/// access to the current camera view.
5604
pub(crate) fn prepare_gpu_resources(
1,030✔
5605
    mut effects_meta: ResMut<EffectsMeta>,
5606
    //mut effect_cache: ResMut<EffectCache>,
5607
    mut event_cache: ResMut<EventCache>,
5608
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5609
    mut sort_bind_groups: ResMut<SortBindGroups>,
5610
    render_device: Res<RenderDevice>,
5611
    render_queue: Res<RenderQueue>,
5612
    view_uniforms: Res<ViewUniforms>,
5613
    render_pipeline: Res<ParticlesRenderPipeline>,
5614
) {
5615
    // Get the binding for the ViewUniform, the uniform data structure containing
5616
    // the Camera data for the current view. If not available, we cannot render
5617
    // anything.
5618
    let Some(view_binding) = view_uniforms.uniforms.binding() else {
2,060✔
5619
        return;
×
5620
    };
5621

5622
    // Create the bind group for the camera/view parameters
5623
    // FIXME - Not here!
5624
    effects_meta.view_bind_group = Some(render_device.create_bind_group(
5625
        "hanabi:bind_group_camera_view",
5626
        &render_pipeline.view_layout,
5627
        &[
5628
            BindGroupEntry {
5629
                binding: 0,
5630
                resource: view_binding,
5631
            },
5632
            BindGroupEntry {
5633
                binding: 1,
5634
                resource: effects_meta.sim_params_uniforms.binding().unwrap(),
5635
            },
5636
        ],
5637
    ));
5638

5639
    // Re-/allocate any GPU buffer if needed
5640
    //effect_cache.prepare_buffers(&render_device, &render_queue, &mut
5641
    // effect_bind_groups);
5642
    event_cache.prepare_buffers(&render_device, &render_queue, &mut effect_bind_groups);
5643
    sort_bind_groups.prepare_buffers(&render_device);
5644
    if effects_meta
5645
        .update_dispatch_indirect_buffer
5646
        .prepare_buffers(&render_device)
5647
    {
5648
        // All those bind groups use the buffer so need to be re-created
5649
        trace!("*** Dispatch indirect buffer for update pass re-allocated; clearing all bind groups using it.");
4✔
5650
        effect_bind_groups.particle_buffers.clear();
2✔
5651
    }
5652
}
5653

5654
/// Read the queued init fill dispatch operations, batch them together by
5655
/// contiguous source and destination entries in the buffers, and enqueue
5656
/// corresponding GPU buffer fill dispatch operations for all batches.
5657
///
5658
/// This system runs after the GPU buffers have been (re-)allocated in
5659
/// [`prepare_gpu_resources()`], so that it can read the new buffer IDs and
5660
/// reference them from the generic [`GpuBufferOperationQueue`].
5661
pub(crate) fn queue_init_fill_dispatch_ops(
1,030✔
5662
    event_cache: Res<EventCache>,
5663
    render_device: Res<RenderDevice>,
5664
    render_queue: Res<RenderQueue>,
5665
    mut init_fill_dispatch_queue: ResMut<InitFillDispatchQueue>,
5666
    mut gpu_buffer_operations: ResMut<GpuBufferOperations>,
5667
) {
5668
    // Submit all queued init fill dispatch operations with the proper buffers
5669
    if !init_fill_dispatch_queue.is_empty() {
1,030✔
5670
        let src_buffer = event_cache.child_infos().buffer();
×
5671
        let dst_buffer = event_cache.init_indirect_dispatch_buffer();
×
5672
        if let (Some(src_buffer), Some(dst_buffer)) = (src_buffer, dst_buffer) {
×
5673
            init_fill_dispatch_queue.submit(src_buffer, dst_buffer, &mut gpu_buffer_operations);
5674
        } else {
5675
            if src_buffer.is_none() {
×
5676
                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());
×
5677
            }
5678
            if dst_buffer.is_none() {
×
5679
                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());
×
5680
            }
5681
        }
5682
    }
5683

5684
    // Once all GPU operations for this frame are enqueued, upload them to GPU
5685
    gpu_buffer_operations.end_frame(&render_device, &render_queue);
1,030✔
5686
}
5687

5688
pub(crate) fn prepare_bind_groups(
1,030✔
5689
    mut effects_meta: ResMut<EffectsMeta>,
5690
    mut effect_cache: ResMut<EffectCache>,
5691
    mut event_cache: ResMut<EventCache>,
5692
    mut effect_bind_groups: ResMut<EffectBindGroups>,
5693
    mut property_bind_groups: ResMut<PropertyBindGroups>,
5694
    mut sort_bind_groups: ResMut<SortBindGroups>,
5695
    property_cache: Res<PropertyCache>,
5696
    sorted_effect_batched: Res<SortedEffectBatches>,
5697
    render_device: Res<RenderDevice>,
5698
    dispatch_indirect_pipeline: Res<DispatchIndirectPipeline>,
5699
    utils_pipeline: Res<UtilsPipeline>,
5700
    update_pipeline: Res<ParticlesUpdatePipeline>,
5701
    render_pipeline: ResMut<ParticlesRenderPipeline>,
5702
    gpu_images: Res<RenderAssets<GpuImage>>,
5703
    mut gpu_buffer_operation_queue: ResMut<GpuBufferOperations>,
5704
) {
5705
    // We can't simulate nor render anything without at least the spawner buffer
5706
    if effects_meta.spawner_buffer.is_empty() {
1,030✔
5707
        return;
16✔
5708
    }
5709
    let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
5710
        return;
×
5711
    };
5712

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

5718
    {
5719
        #[cfg(feature = "trace")]
5720
        let _span = bevy::log::info_span!("shared_bind_groups").entered();
5721

5722
        // Make a copy of the buffer IDs before borrowing effects_meta mutably in the
5723
        // loop below. Also allows earlying out before doing any work in case some
5724
        // buffer is missing.
5725
        let Some(spawner_buffer) = effects_meta.spawner_buffer.buffer().cloned() else {
1,014✔
5726
            return;
×
5727
        };
5728

5729
        // Create the sim_params@0 bind group for the global simulation parameters,
5730
        // which is shared by the init and update passes.
5731
        if effects_meta.indirect_sim_params_bind_group.is_none() {
2✔
5732
            effects_meta.indirect_sim_params_bind_group = Some(render_device.create_bind_group(
2✔
5733
                "hanabi:bind_group:vfx_indirect:sim_params@0",
2✔
5734
                &update_pipeline.sim_params_layout, // FIXME - Shared with init
2✔
5735
                &[BindGroupEntry {
2✔
5736
                    binding: 0,
2✔
5737
                    resource: effects_meta.sim_params_uniforms.binding().unwrap(),
2✔
5738
                }],
5739
            ));
5740
        }
5741

5742
        // Create the @1 bind group for the indirect dispatch preparation pass of all
5743
        // effects at once
5744
        effects_meta.indirect_metadata_bind_group = match (
5745
            effects_meta.effect_metadata_buffer.buffer(),
5746
            effects_meta.update_dispatch_indirect_buffer.buffer(),
5747
        ) {
5748
            (Some(effect_metadata_buffer), Some(dispatch_indirect_buffer)) => {
1,014✔
5749
                // Base bind group for indirect pass
5750
                Some(render_device.create_bind_group(
1,014✔
5751
                    "hanabi:bind_group:vfx_indirect:metadata@1",
1,014✔
5752
                    &dispatch_indirect_pipeline.effect_metadata_bind_group_layout,
1,014✔
5753
                    &[
1,014✔
5754
                        // @group(1) @binding(0) var<storage, read_write> effect_metadata_buffer : array<u32>;
5755
                        BindGroupEntry {
1,014✔
5756
                            binding: 0,
1,014✔
5757
                            resource: BindingResource::Buffer(BufferBinding {
1,014✔
5758
                                buffer: effect_metadata_buffer,
1,014✔
5759
                                offset: 0,
1,014✔
5760
                                size: None, //NonZeroU64::new(256), // Some(GpuEffectMetadata::min_size()),
1,014✔
5761
                            }),
5762
                        },
5763
                        // @group(1) @binding(1) var<storage, read_write> dispatch_indirect_buffer : array<u32>;
5764
                        BindGroupEntry {
1,014✔
5765
                            binding: 1,
1,014✔
5766
                            resource: BindingResource::Buffer(BufferBinding {
1,014✔
5767
                                buffer: dispatch_indirect_buffer,
1,014✔
5768
                                offset: 0,
1,014✔
5769
                                size: None, //NonZeroU64::new(256), // Some(GpuDispatchIndirect::min_size()),
1,014✔
5770
                            }),
5771
                        },
5772
                    ],
5773
                ))
5774
            }
5775

5776
            // Some buffer is not yet available, can't create the bind group
5777
            _ => None,
×
5778
        };
5779

5780
        // Create the @2 bind group for the indirect dispatch preparation pass of all
5781
        // effects at once
5782
        if effects_meta.indirect_spawner_bind_group.is_none() {
2✔
5783
            let bind_group = render_device.create_bind_group(
2✔
5784
                "hanabi:bind_group:vfx_indirect:spawner@2",
5785
                &dispatch_indirect_pipeline.spawner_bind_group_layout,
2✔
5786
                &[
2✔
5787
                    // @group(2) @binding(0) var<storage, read> spawner_buffer : array<Spawner>;
5788
                    BindGroupEntry {
2✔
5789
                        binding: 0,
2✔
5790
                        resource: BindingResource::Buffer(BufferBinding {
2✔
5791
                            buffer: &spawner_buffer,
2✔
5792
                            offset: 0,
2✔
5793
                            size: None,
2✔
5794
                        }),
5795
                    },
5796
                ],
5797
            );
5798

5799
            effects_meta.indirect_spawner_bind_group = Some(bind_group);
2✔
5800
        }
5801
    }
5802

5803
    // Create the per-buffer bind groups
5804
    trace!("Create per-buffer bind groups...");
1,014✔
5805
    for (buffer_index, effect_buffer) in effect_cache.buffers().iter().enumerate() {
1,014✔
5806
        #[cfg(feature = "trace")]
5807
        let _span_buffer = bevy::log::info_span!("create_buffer_bind_groups").entered();
5808

5809
        let Some(effect_buffer) = effect_buffer else {
1,014✔
5810
            trace!(
×
5811
                "Effect buffer index #{} has no allocated EffectBuffer, skipped.",
×
5812
                buffer_index
5813
            );
5814
            continue;
×
5815
        };
5816

5817
        // Ensure all effects in this batch have a bind group for the entire buffer of
5818
        // the group, since the update phase runs on an entire group/buffer at once,
5819
        // with all the effect instances in it batched together.
5820
        trace!("effect particle buffer_index=#{}", buffer_index);
1,014✔
5821
        effect_bind_groups
5822
            .particle_buffers
5823
            .entry(buffer_index as u32)
5824
            .or_insert_with(|| {
2✔
5825
                // Bind group particle@1 for render pass
5826
                trace!("Creating particle@1 bind group for buffer #{buffer_index} in render pass");
4✔
5827
                let spawner_min_binding_size = GpuSpawnerParams::aligned_size(
2✔
5828
                    render_device.limits().min_storage_buffer_offset_alignment,
2✔
5829
                );
5830
                let entries = [
2✔
5831
                    // @group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
5832
                    BindGroupEntry {
2✔
5833
                        binding: 0,
2✔
5834
                        resource: effect_buffer.max_binding(),
2✔
5835
                    },
5836
                    // @group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
5837
                    BindGroupEntry {
2✔
5838
                        binding: 1,
2✔
5839
                        resource: effect_buffer.indirect_index_max_binding(),
2✔
5840
                    },
5841
                    // @group(1) @binding(2) var<storage, read> spawner : Spawner;
5842
                    BindGroupEntry {
2✔
5843
                        binding: 2,
2✔
5844
                        resource: BindingResource::Buffer(BufferBinding {
2✔
5845
                            buffer: &spawner_buffer,
2✔
5846
                            offset: 0,
2✔
5847
                            size: Some(spawner_min_binding_size),
2✔
5848
                        }),
5849
                    },
5850
                ];
5851
                let render = render_device.create_bind_group(
2✔
5852
                    &format!("hanabi:bind_group:render:particles@1:vfx{buffer_index}")[..],
2✔
5853
                    effect_buffer.render_particles_buffer_layout(),
2✔
5854
                    &entries[..],
2✔
5855
                );
5856

5857
                BufferBindGroups { render }
2✔
5858
            });
5859
    }
5860

5861
    // Create bind groups for queued GPU buffer operations
5862
    gpu_buffer_operation_queue.create_bind_groups(&render_device, &utils_pipeline);
5863

5864
    // Create the per-effect bind groups
5865
    let spawner_buffer_binding_size =
5866
        NonZeroU64::new(effects_meta.spawner_buffer.aligned_size() as u64).unwrap();
5867
    for effect_batch in sorted_effect_batched.iter() {
1,014✔
5868
        #[cfg(feature = "trace")]
5869
        let _span_buffer = bevy::log::info_span!("create_batch_bind_groups").entered();
3,042✔
5870

5871
        // Create the property bind group @2 if needed
5872
        if let Some(property_key) = &effect_batch.property_key {
1,023✔
5873
            if let Err(err) = property_bind_groups.ensure_exists(
×
5874
                property_key,
5875
                &property_cache,
5876
                &spawner_buffer,
5877
                spawner_buffer_binding_size,
5878
                &render_device,
5879
            ) {
5880
                error!("Failed to create property bind group for effect batch: {err:?}");
×
5881
                continue;
5882
            }
5883
        } else if let Err(err) = property_bind_groups.ensure_exists_no_property(
1,005✔
5884
            &property_cache,
1,005✔
5885
            &spawner_buffer,
1,005✔
5886
            spawner_buffer_binding_size,
1,005✔
5887
            &render_device,
1,005✔
5888
        ) {
5889
            error!("Failed to create property bind group for effect batch: {err:?}");
×
5890
            continue;
5891
        }
5892

5893
        // Bind group particle@1 for the simulate compute shaders (init and udpate) to
5894
        // simulate particles.
5895
        if effect_cache
1,014✔
5896
            .create_particle_sim_bind_group(
5897
                effect_batch.buffer_index,
1,014✔
5898
                &render_device,
1,014✔
5899
                effect_batch.particle_layout.min_binding_size32(),
1,014✔
5900
                effect_batch.parent_min_binding_size,
1,014✔
5901
                effect_batch.parent_binding_source.as_ref(),
1,014✔
5902
            )
5903
            .is_err()
5904
        {
5905
            error!("No particle buffer allocated for effect batch.");
×
5906
            continue;
×
5907
        }
5908

5909
        // Bind group @3 of init pass
5910
        // FIXME - this is instance-dependent, not buffer-dependent
5911
        {
5912
            let consume_gpu_spawn_events = effect_batch
1,014✔
5913
                .layout_flags
1,014✔
5914
                .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
1,014✔
5915
            let consume_event_buffers = if let BatchSpawnInfo::GpuSpawner { .. } =
1,014✔
5916
                effect_batch.spawn_info
5917
            {
5918
                assert!(consume_gpu_spawn_events);
×
5919
                let cached_effect_events = effect_batch.cached_effect_events.as_ref().unwrap();
×
5920
                Some(ConsumeEventBuffers {
×
5921
                    child_infos_buffer: event_cache.child_infos_buffer().unwrap(),
×
5922
                    events: BufferSlice {
×
5923
                        buffer: event_cache
×
5924
                            .get_buffer(cached_effect_events.buffer_index)
×
5925
                            .unwrap(),
×
5926
                        // Note: event range is in u32 count, not bytes
5927
                        offset: cached_effect_events.range.start * 4,
×
5928
                        size: NonZeroU32::new(cached_effect_events.range.len() as u32 * 4).unwrap(),
×
5929
                    },
5930
                })
5931
            } else {
5932
                assert!(!consume_gpu_spawn_events);
1,014✔
5933
                None
1,014✔
5934
            };
5935
            let Some(init_metadata_layout) =
1,014✔
5936
                effect_cache.metadata_init_bind_group_layout(consume_gpu_spawn_events)
5937
            else {
5938
                continue;
×
5939
            };
5940
            if effect_bind_groups
5941
                .get_or_create_init_metadata(
5942
                    effect_batch,
5943
                    &effects_meta.gpu_limits,
5944
                    &render_device,
5945
                    init_metadata_layout,
5946
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5947
                    consume_event_buffers,
5948
                )
5949
                .is_err()
5950
            {
5951
                continue;
×
5952
            }
5953
        }
5954

5955
        // Bind group @3 of update pass
5956
        // FIXME - this is instance-dependent, not buffer-dependent#
5957
        {
5958
            let num_event_buffers = effect_batch.child_event_buffers.len() as u32;
1,014✔
5959

5960
            let Some(update_metadata_layout) =
1,014✔
5961
                effect_cache.metadata_update_bind_group_layout(num_event_buffers)
5962
            else {
5963
                continue;
×
5964
            };
5965
            if effect_bind_groups
5966
                .get_or_create_update_metadata(
5967
                    effect_batch,
5968
                    &effects_meta.gpu_limits,
5969
                    &render_device,
5970
                    update_metadata_layout,
5971
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5972
                    event_cache.child_infos_buffer(),
5973
                    &effect_batch.child_event_buffers[..],
5974
                )
5975
                .is_err()
5976
            {
5977
                continue;
×
5978
            }
5979
        }
5980

5981
        // Bind group @2 of render pass
5982
        // FIXME - this is instance-dependent, not buffer-dependent#
5983
        {
5984
            let Some(render_metadata_layout) = effect_cache.metadata_render_bind_group_layout()
1,014✔
5985
            else {
NEW
5986
                continue;
×
5987
            };
5988
            if effect_bind_groups
5989
                .get_or_create_render_metadata(
5990
                    effect_batch,
5991
                    &effects_meta.gpu_limits,
5992
                    &render_device,
5993
                    render_metadata_layout,
5994
                    effects_meta.effect_metadata_buffer.buffer().unwrap(),
5995
                )
5996
                .is_err()
5997
            {
NEW
5998
                continue;
×
5999
            }
6000
        }
6001

6002
        if effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
1,014✔
6003
            let effect_buffer = effect_cache.get_buffer(effect_batch.buffer_index).unwrap();
×
6004

6005
            // Bind group @0 of sort-fill pass
6006
            let particle_buffer = effect_buffer.particle_buffer();
×
6007
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6008
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
×
6009
            if let Err(err) = sort_bind_groups.ensure_sort_fill_bind_group(
×
6010
                &effect_batch.particle_layout,
×
6011
                particle_buffer,
×
6012
                indirect_index_buffer,
×
6013
                effect_metadata_buffer,
×
6014
            ) {
6015
                error!(
6016
                    "Failed to create sort-fill bind group @0 for ribbon effect: {:?}",
×
6017
                    err
6018
                );
6019
                continue;
6020
            }
6021

6022
            // Bind group @0 of sort-copy pass
6023
            let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6024
            if let Err(err) = sort_bind_groups
×
6025
                .ensure_sort_copy_bind_group(indirect_index_buffer, effect_metadata_buffer)
×
6026
            {
6027
                error!(
6028
                    "Failed to create sort-copy bind group @0 for ribbon effect: {:?}",
×
6029
                    err
6030
                );
6031
                continue;
6032
            }
6033
        }
6034

6035
        // Ensure the particle texture(s) are available as GPU resources and that a bind
6036
        // group for them exists
6037
        // FIXME fix this insert+get below
6038
        if !effect_batch.texture_layout.layout.is_empty() {
1,014✔
6039
            // This should always be available, as this is cached into the render pipeline
6040
            // just before we start specializing it.
6041
            let Some(material_bind_group_layout) =
×
6042
                render_pipeline.get_material(&effect_batch.texture_layout)
×
6043
            else {
6044
                error!(
×
6045
                    "Failed to find material bind group layout for buffer #{}",
×
6046
                    effect_batch.buffer_index
6047
                );
6048
                continue;
×
6049
            };
6050

6051
            // TODO = move
6052
            let material = Material {
6053
                layout: effect_batch.texture_layout.clone(),
6054
                textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6055
            };
6056
            assert_eq!(material.layout.layout.len(), material.textures.len());
6057

6058
            //let bind_group_entries = material.make_entries(&gpu_images).unwrap();
6059
            let Ok(bind_group_entries) = material.make_entries(&gpu_images) else {
×
6060
                trace!(
×
6061
                    "Temporarily ignoring material {:?} due to missing image(s)",
×
6062
                    material
6063
                );
6064
                continue;
×
6065
            };
6066

6067
            effect_bind_groups
6068
                .material_bind_groups
6069
                .entry(material.clone())
6070
                .or_insert_with(|| {
×
6071
                    debug!("Creating material bind group for material {:?}", material);
×
6072
                    render_device.create_bind_group(
×
6073
                        &format!(
×
6074
                            "hanabi:material_bind_group_{}",
×
6075
                            material.layout.layout.len()
×
6076
                        )[..],
×
6077
                        material_bind_group_layout,
×
6078
                        &bind_group_entries[..],
×
6079
                    )
6080
                });
6081
        }
6082
    }
6083
}
6084

6085
type DrawEffectsSystemState = SystemState<(
6086
    SRes<EffectsMeta>,
6087
    SRes<EffectBindGroups>,
6088
    SRes<PipelineCache>,
6089
    SRes<RenderAssets<RenderMesh>>,
6090
    SRes<MeshAllocator>,
6091
    SQuery<Read<ViewUniformOffset>>,
6092
    SRes<SortedEffectBatches>,
6093
    SQuery<Read<EffectDrawBatch>>,
6094
)>;
6095

6096
/// Draw function for rendering all active effects for the current frame.
6097
///
6098
/// Effects are rendered in the [`Transparent2d`] phase of the main 2D pass,
6099
/// and the [`Transparent3d`] phase of the main 3D pass.
6100
pub(crate) struct DrawEffects {
6101
    params: DrawEffectsSystemState,
6102
}
6103

6104
impl DrawEffects {
6105
    pub fn new(world: &mut World) -> Self {
12✔
6106
        Self {
6107
            params: SystemState::new(world),
12✔
6108
        }
6109
    }
6110
}
6111

6112
/// Draw all particles of a single effect in view, in 2D or 3D.
6113
///
6114
/// FIXME: use pipeline ID to look up which group index it is.
6115
fn draw<'w>(
1,013✔
6116
    world: &'w World,
6117
    pass: &mut TrackedRenderPass<'w>,
6118
    view: Entity,
6119
    entity: (Entity, MainEntity),
6120
    pipeline_id: CachedRenderPipelineId,
6121
    params: &mut DrawEffectsSystemState,
6122
) {
6123
    let (
1,013✔
6124
        effects_meta,
1,013✔
6125
        effect_bind_groups,
1,013✔
6126
        pipeline_cache,
1,013✔
6127
        meshes,
1,013✔
6128
        mesh_allocator,
1,013✔
6129
        views,
1,013✔
6130
        sorted_effect_batches,
1,013✔
6131
        effect_draw_batches,
1,013✔
6132
    ) = params.get(world);
1,013✔
6133
    let view_uniform = views.get(view).unwrap();
1,013✔
6134
    let effects_meta = effects_meta.into_inner();
1,013✔
6135
    let effect_bind_groups = effect_bind_groups.into_inner();
1,013✔
6136
    let meshes = meshes.into_inner();
1,013✔
6137
    let mesh_allocator = mesh_allocator.into_inner();
1,013✔
6138
    let effect_draw_batch = effect_draw_batches.get(entity.0).unwrap();
1,013✔
6139
    let effect_batch = sorted_effect_batches
1,013✔
6140
        .get(effect_draw_batch.effect_batch_index)
1,013✔
6141
        .unwrap();
6142

6143
    let gpu_limits = &effects_meta.gpu_limits;
1,013✔
6144

6145
    let Some(pipeline) = pipeline_cache.into_inner().get_render_pipeline(pipeline_id) else {
2,026✔
6146
        return;
×
6147
    };
6148

6149
    trace!("render pass");
1,013✔
6150

6151
    pass.set_render_pipeline(pipeline);
×
6152

6153
    let Some(render_mesh): Option<&RenderMesh> = meshes.get(effect_batch.mesh) else {
1,013✔
6154
        return;
×
6155
    };
6156
    let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&effect_batch.mesh) else {
1,013✔
6157
        return;
×
6158
    };
6159

6160
    // Vertex buffer containing the particle model to draw. Generally a quad.
6161
    // FIXME - need to upload "vertex_buffer_slice.range.start as i32" into
6162
    // "base_vertex" in the indirect struct...
6163
    pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..));
×
6164

6165
    // View properties (camera matrix, etc.)
6166
    pass.set_bind_group(
×
6167
        0,
6168
        effects_meta.view_bind_group.as_ref().unwrap(),
×
6169
        &[view_uniform.offset],
×
6170
    );
6171

6172
    // Particles buffer
6173
    let spawner_base = effect_batch.spawner_base;
×
6174
    let spawner_buffer_aligned = effects_meta.spawner_buffer.aligned_size();
×
6175
    assert!(spawner_buffer_aligned >= GpuSpawnerParams::min_size().get() as usize);
×
6176
    let spawner_offset = spawner_base * spawner_buffer_aligned as u32;
1,013✔
6177
    pass.set_bind_group(
1,013✔
6178
        1,
6179
        effect_bind_groups
1,013✔
6180
            .particle_render(effect_batch.buffer_index)
1,013✔
6181
            .unwrap(),
1,013✔
6182
        &[spawner_offset],
1,013✔
6183
    );
6184

6185
    // Effects metadata
6186
    let Some(metadata_bind_group) =
1,013✔
6187
        effect_bind_groups
1,013✔
6188
            .render_metadata_bind_groups
1,013✔
6189
            .get(&EffectMetadataBindGroupKey {
1,013✔
6190
                buffer_index: effect_batch.buffer_index,
1,013✔
6191
                base_instance: effect_batch.slice.start,
1,013✔
6192
            })
NEW
6193
    else {
×
NEW
6194
        error!(
×
NEW
6195
            "Failed to find update metadata@3 bind group for buffer index {}",
×
NEW
6196
            effect_batch.buffer_index
×
6197
        );
NEW
6198
        return;
×
6199
    };
NEW
6200
    pass.set_bind_group(2, &metadata_bind_group.bind_group, &[]);
×
6201

6202
    // Particle texture
6203
    // TODO = move
6204
    let material = Material {
UNCOV
6205
        layout: effect_batch.texture_layout.clone(),
×
UNCOV
6206
        textures: effect_batch.textures.iter().map(|h| h.id()).collect(),
×
6207
    };
UNCOV
6208
    if !effect_batch.texture_layout.layout.is_empty() {
×
6209
        if let Some(bind_group) = effect_bind_groups.material_bind_groups.get(&material) {
×
NEW
6210
            pass.set_bind_group(3, bind_group, &[]);
×
6211
        } else {
6212
            // Texture(s) not ready; skip this drawing for now
6213
            trace!(
×
6214
                "Particle material bind group not available for batch buf={}. Skipping draw call.",
×
6215
                effect_batch.buffer_index,
×
6216
            );
6217
            return;
×
6218
        }
6219
    }
6220

6221
    let effect_metadata_index = effect_batch
1,013✔
6222
        .dispatch_buffer_indices
1,013✔
6223
        .effect_metadata_buffer_table_id
1,013✔
6224
        .0;
1,013✔
6225
    let effect_metadata_offset =
1,013✔
6226
        effect_metadata_index as u64 * gpu_limits.effect_metadata_aligned_size.get() as u64;
1,013✔
6227
    trace!(
1,013✔
6228
        "Draw up to {} particles with {} vertices per particle for batch from buffer #{} \
1,013✔
6229
            (effect_metadata_index={}, offset={}B).",
1,013✔
6230
        effect_batch.slice.len(),
1,013✔
6231
        render_mesh.vertex_count,
×
6232
        effect_batch.buffer_index,
×
6233
        effect_metadata_index,
×
6234
        effect_metadata_offset,
×
6235
    );
6236

6237
    // Note: the indirect draw args are the first few fields of GpuEffectMetadata
6238
    let Some(indirect_buffer) = effects_meta.effect_metadata_buffer.buffer() else {
1,013✔
6239
        trace!(
×
6240
            "The metadata buffer containing the indirect draw args is not ready for batch buf=#{}. Skipping draw call.",
×
6241
            effect_batch.buffer_index,
×
6242
        );
6243
        return;
×
6244
    };
6245

6246
    match render_mesh.buffer_info {
×
6247
        RenderMeshBufferInfo::Indexed { index_format, .. } => {
1,013✔
6248
            let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&effect_batch.mesh)
1,013✔
6249
            else {
×
6250
                return;
×
6251
            };
6252

6253
            pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, index_format);
×
6254
            pass.draw_indexed_indirect(indirect_buffer, effect_metadata_offset);
×
6255
        }
6256
        RenderMeshBufferInfo::NonIndexed => {
×
6257
            pass.draw_indirect(indirect_buffer, effect_metadata_offset);
×
6258
        }
6259
    }
6260
}
6261

6262
#[cfg(feature = "2d")]
6263
impl Draw<Transparent2d> for DrawEffects {
6264
    fn draw<'w>(
×
6265
        &mut self,
6266
        world: &'w World,
6267
        pass: &mut TrackedRenderPass<'w>,
6268
        view: Entity,
6269
        item: &Transparent2d,
6270
    ) -> Result<(), DrawError> {
6271
        trace!("Draw<Transparent2d>: view={:?}", view);
×
6272
        draw(
6273
            world,
×
6274
            pass,
×
6275
            view,
×
6276
            item.entity,
×
6277
            item.pipeline,
×
6278
            &mut self.params,
×
6279
        );
6280
        Ok(())
×
6281
    }
6282
}
6283

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

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

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

6350
/// Render node to run the simulation sub-graph once per frame.
6351
///
6352
/// This node doesn't simulate anything by itself, but instead schedules the
6353
/// simulation sub-graph, where other nodes like [`VfxSimulateNode`] do the
6354
/// actual simulation.
6355
///
6356
/// The simulation sub-graph is scheduled to run before the [`CameraDriverNode`]
6357
/// renders all the views, such that rendered views have access to the
6358
/// just-simulated particles to render them.
6359
///
6360
/// [`CameraDriverNode`]: bevy::render::camera::CameraDriverNode
6361
pub(crate) struct VfxSimulateDriverNode;
6362

6363
impl Node for VfxSimulateDriverNode {
6364
    fn run(
1,030✔
6365
        &self,
6366
        graph: &mut RenderGraphContext,
6367
        _render_context: &mut RenderContext,
6368
        _world: &World,
6369
    ) -> Result<(), NodeRunError> {
6370
        graph.run_sub_graph(
1,030✔
6371
            crate::plugin::simulate_graph::HanabiSimulateGraph,
1,030✔
6372
            vec![],
1,030✔
6373
            None,
1,030✔
6374
        )?;
6375
        Ok(())
1,030✔
6376
    }
6377
}
6378

6379
#[derive(Debug, Clone, PartialEq, Eq)]
6380
enum HanabiPipelineId {
6381
    Invalid,
6382
    Cached(CachedComputePipelineId),
6383
}
6384

6385
pub(crate) enum ComputePipelineError {
6386
    Queued,
6387
    Creating,
6388
    Error,
6389
}
6390

6391
impl From<&CachedPipelineState> for ComputePipelineError {
6392
    fn from(value: &CachedPipelineState) -> Self {
×
6393
        match value {
×
6394
            CachedPipelineState::Queued => Self::Queued,
×
6395
            CachedPipelineState::Creating(_) => Self::Creating,
×
6396
            CachedPipelineState::Err(_) => Self::Error,
×
6397
            _ => panic!("Trying to convert Ok state to error."),
×
6398
        }
6399
    }
6400
}
6401

6402
pub(crate) struct HanabiComputePass<'a> {
6403
    /// Pipeline cache to fetch cached compute pipelines by ID.
6404
    pipeline_cache: &'a PipelineCache,
6405
    /// WGPU compute pass.
6406
    compute_pass: ComputePass<'a>,
6407
    /// Current pipeline (cached).
6408
    pipeline_id: HanabiPipelineId,
6409
}
6410

6411
impl<'a> Deref for HanabiComputePass<'a> {
6412
    type Target = ComputePass<'a>;
6413

6414
    fn deref(&self) -> &Self::Target {
×
6415
        &self.compute_pass
×
6416
    }
6417
}
6418

6419
impl DerefMut for HanabiComputePass<'_> {
6420
    fn deref_mut(&mut self) -> &mut Self::Target {
14,140✔
6421
        &mut self.compute_pass
14,140✔
6422
    }
6423
}
6424

6425
impl<'a> HanabiComputePass<'a> {
6426
    pub fn new(pipeline_cache: &'a PipelineCache, compute_pass: ComputePass<'a>) -> Self {
4,056✔
6427
        Self {
6428
            pipeline_cache,
6429
            compute_pass,
6430
            pipeline_id: HanabiPipelineId::Invalid,
6431
        }
6432
    }
6433

6434
    pub fn set_cached_compute_pipeline(
3,028✔
6435
        &mut self,
6436
        pipeline_id: CachedComputePipelineId,
6437
    ) -> Result<(), ComputePipelineError> {
6438
        trace!("set_cached_compute_pipeline() id={pipeline_id:?}");
6,056✔
6439
        if HanabiPipelineId::Cached(pipeline_id) == self.pipeline_id {
3,028✔
6440
            trace!("-> already set; skipped");
×
6441
            return Ok(());
×
6442
        }
6443
        let Some(pipeline) = self.pipeline_cache.get_compute_pipeline(pipeline_id) else {
3,028✔
6444
            let state = self.pipeline_cache.get_compute_pipeline_state(pipeline_id);
×
6445
            if let CachedPipelineState::Err(err) = state {
×
6446
                error!(
×
6447
                    "Failed to find compute pipeline #{}: {:?}",
×
6448
                    pipeline_id.id(),
×
6449
                    err
×
6450
                );
6451
            } else {
6452
                debug!("Compute pipeline not ready #{}", pipeline_id.id());
×
6453
            }
6454
            return Err(state.into());
×
6455
        };
6456
        self.compute_pass.set_pipeline(pipeline);
×
6457
        self.pipeline_id = HanabiPipelineId::Cached(pipeline_id);
×
6458
        Ok(())
×
6459
    }
6460
}
6461

6462
/// Render node to run the simulation of all effects once per frame.
6463
///
6464
/// Runs inside the simulation sub-graph, looping over all extracted effect
6465
/// batches to simulate them.
6466
pub(crate) struct VfxSimulateNode {}
6467

6468
impl VfxSimulateNode {
6469
    /// Create a new node for simulating the effects of the given world.
6470
    pub fn new(_world: &mut World) -> Self {
3✔
6471
        Self {}
6472
    }
6473

6474
    /// Begin a new compute pass and return a wrapper with extra
6475
    /// functionalities.
6476
    pub fn begin_compute_pass<'encoder>(
4,056✔
6477
        &self,
6478
        label: &str,
6479
        pipeline_cache: &'encoder PipelineCache,
6480
        render_context: &'encoder mut RenderContext,
6481
    ) -> HanabiComputePass<'encoder> {
6482
        let compute_pass =
4,056✔
6483
            render_context
4,056✔
6484
                .command_encoder()
6485
                .begin_compute_pass(&ComputePassDescriptor {
4,056✔
6486
                    label: Some(label),
4,056✔
6487
                    timestamp_writes: None,
4,056✔
6488
                });
6489
        HanabiComputePass::new(pipeline_cache, compute_pass)
4,056✔
6490
    }
6491
}
6492

6493
impl Node for VfxSimulateNode {
6494
    fn input(&self) -> Vec<SlotInfo> {
3✔
6495
        vec![]
3✔
6496
    }
6497

6498
    fn update(&mut self, _world: &mut World) {}
1,030✔
6499

6500
    fn run(
1,030✔
6501
        &self,
6502
        _graph: &mut RenderGraphContext,
6503
        render_context: &mut RenderContext,
6504
        world: &World,
6505
    ) -> Result<(), NodeRunError> {
6506
        trace!("VfxSimulateNode::run()");
2,050✔
6507

6508
        let pipeline_cache = world.resource::<PipelineCache>();
1,030✔
6509
        let effects_meta = world.resource::<EffectsMeta>();
1,030✔
6510
        let effect_bind_groups = world.resource::<EffectBindGroups>();
1,030✔
6511
        let property_bind_groups = world.resource::<PropertyBindGroups>();
1,030✔
6512
        let sort_bind_groups = world.resource::<SortBindGroups>();
1,030✔
6513
        let utils_pipeline = world.resource::<UtilsPipeline>();
1,030✔
6514
        let effect_cache = world.resource::<EffectCache>();
1,030✔
6515
        let event_cache = world.resource::<EventCache>();
1,030✔
6516
        let gpu_buffer_operations = world.resource::<GpuBufferOperations>();
1,030✔
6517
        let sorted_effect_batches = world.resource::<SortedEffectBatches>();
1,030✔
6518
        let init_fill_dispatch_queue = world.resource::<InitFillDispatchQueue>();
1,030✔
6519

6520
        // Make sure to schedule any buffer copy before accessing their content later in
6521
        // the GPU commands below.
6522
        {
6523
            let command_encoder = render_context.command_encoder();
1,030✔
6524
            effects_meta
1,030✔
6525
                .update_dispatch_indirect_buffer
1,030✔
6526
                .write_buffers(command_encoder);
1,030✔
6527
            effects_meta
1,030✔
6528
                .effect_metadata_buffer
1,030✔
6529
                .write_buffer(command_encoder);
1,030✔
6530
            event_cache.write_buffers(command_encoder);
1,030✔
6531
            sort_bind_groups.write_buffers(command_encoder);
1,030✔
6532
        }
6533

6534
        // Compute init fill dispatch pass - Fill the indirect dispatch structs for any
6535
        // upcoming init pass of this frame, based on the GPU spawn events emitted by
6536
        // the update pass of their parent effect during the previous frame.
6537
        if let Some(queue_index) = init_fill_dispatch_queue.submitted_queue_index.as_ref() {
1,030✔
6538
            gpu_buffer_operations.dispatch(
6539
                *queue_index,
6540
                render_context,
6541
                utils_pipeline,
6542
                Some("hanabi:init_indirect_fill_dispatch"),
6543
            );
6544
        }
6545

6546
        // If there's no batch, there's nothing more to do. Avoid continuing because
6547
        // some GPU resources are missing, which is expected when there's no effect but
6548
        // is an error (and will log warnings/errors) otherwise.
6549
        if sorted_effect_batches.is_empty() {
1,030✔
6550
            return Ok(());
16✔
6551
        }
6552

6553
        // Compute init pass
6554
        {
6555
            trace!("init: loop over effect batches...");
2,028✔
6556

6557
            let mut compute_pass =
6558
                self.begin_compute_pass("hanabi:init", pipeline_cache, render_context);
6559

6560
            // Bind group simparams@0 is common to everything, only set once per init pass
6561
            compute_pass.set_bind_group(
6562
                0,
6563
                effects_meta
6564
                    .indirect_sim_params_bind_group
6565
                    .as_ref()
6566
                    .unwrap(),
6567
                &[],
6568
            );
6569

6570
            // Dispatch init compute jobs for all batches
6571
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6572
                // Do not dispatch any init work if there's nothing to spawn this frame for the
6573
                // batch. Note that this hopefully should have been skipped earlier.
6574
                {
6575
                    let use_indirect_dispatch = effect_batch
1,014✔
6576
                        .layout_flags
1,014✔
6577
                        .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS);
1,014✔
6578
                    match effect_batch.spawn_info {
1,014✔
6579
                        BatchSpawnInfo::CpuSpawner { total_spawn_count } => {
1,014✔
6580
                            assert!(!use_indirect_dispatch);
1,014✔
6581
                            if total_spawn_count == 0 {
1,014✔
6582
                                continue;
14✔
6583
                            }
6584
                        }
6585
                        BatchSpawnInfo::GpuSpawner { .. } => {
6586
                            assert!(use_indirect_dispatch);
×
6587
                        }
6588
                    }
6589
                }
6590

6591
                // Fetch bind group particle@1
6592
                let Some(particle_bind_group) =
1,000✔
6593
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
1,000✔
6594
                else {
6595
                    error!(
×
6596
                        "Failed to find init particle@1 bind group for buffer index {}",
×
6597
                        effect_batch.buffer_index
6598
                    );
6599
                    continue;
×
6600
                };
6601

6602
                // Fetch bind group metadata@3
6603
                let Some(metadata_bind_group) =
1,000✔
6604
                    effect_bind_groups
6605
                        .init_metadata_bind_groups
6606
                        .get(&EffectMetadataBindGroupKey {
6607
                            buffer_index: effect_batch.buffer_index,
6608
                            base_instance: effect_batch.slice.start,
6609
                        })
6610
                else {
6611
                    error!(
×
6612
                        "Failed to find init metadata@3 bind group for buffer index {}",
×
6613
                        effect_batch.buffer_index
6614
                    );
6615
                    continue;
×
6616
                };
6617

6618
                if compute_pass
6619
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.init)
6620
                    .is_err()
6621
                {
6622
                    continue;
×
6623
                }
6624

6625
                // Compute dynamic offsets
6626
                let spawner_base = effect_batch.spawner_base;
1,000✔
6627
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
1,000✔
6628
                debug_assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
1,000✔
6629
                let spawner_offset = spawner_base * spawner_aligned_size as u32;
1,000✔
6630
                let property_offset = effect_batch.property_offset;
1,000✔
6631

6632
                // Setup init pass
6633
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
1,000✔
6634
                let offsets = if let Some(property_offset) = property_offset {
2,000✔
6635
                    vec![spawner_offset, property_offset]
6636
                } else {
6637
                    vec![spawner_offset]
1,000✔
6638
                };
6639
                compute_pass.set_bind_group(
1,000✔
6640
                    2,
6641
                    property_bind_groups
1,000✔
6642
                        .get(effect_batch.property_key.as_ref())
1,000✔
6643
                        .unwrap(),
1,000✔
6644
                    &offsets[..],
1,000✔
6645
                );
6646
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
1,000✔
6647

6648
                // Dispatch init job
6649
                match effect_batch.spawn_info {
1,000✔
6650
                    // Indirect dispatch via GPU spawn events
6651
                    BatchSpawnInfo::GpuSpawner {
6652
                        init_indirect_dispatch_index,
×
6653
                        ..
×
6654
                    } => {
×
6655
                        assert!(effect_batch
×
6656
                            .layout_flags
×
6657
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
×
6658

6659
                        // Note: the indirect offset of a dispatch workgroup only needs
6660
                        // 4-byte alignment
6661
                        assert_eq!(GpuDispatchIndirect::min_size().get(), 12);
×
6662
                        let indirect_offset = init_indirect_dispatch_index as u64 * 12;
×
6663

6664
                        trace!(
×
6665
                            "record commands for indirect init pipeline of effect {:?} \
×
6666
                                init_indirect_dispatch_index={} \
×
6667
                                indirect_offset={} \
×
6668
                                spawner_base={} \
×
6669
                                spawner_offset={} \
×
6670
                                property_key={:?}...",
×
6671
                            effect_batch.handle,
6672
                            init_indirect_dispatch_index,
6673
                            indirect_offset,
6674
                            spawner_base,
6675
                            spawner_offset,
6676
                            effect_batch.property_key,
6677
                        );
6678

6679
                        compute_pass.dispatch_workgroups_indirect(
×
6680
                            event_cache.init_indirect_dispatch_buffer().unwrap(),
×
6681
                            indirect_offset,
×
6682
                        );
6683
                    }
6684

6685
                    // Direct dispatch via CPU spawn count
6686
                    BatchSpawnInfo::CpuSpawner {
6687
                        total_spawn_count: spawn_count,
1,000✔
6688
                    } => {
1,000✔
6689
                        assert!(!effect_batch
1,000✔
6690
                            .layout_flags
1,000✔
6691
                            .contains(LayoutFlags::CONSUME_GPU_SPAWN_EVENTS));
1,000✔
6692

6693
                        const WORKGROUP_SIZE: u32 = 64;
6694
                        let workgroup_count = spawn_count.div_ceil(WORKGROUP_SIZE);
1,000✔
6695

6696
                        trace!(
1,000✔
6697
                            "record commands for init pipeline of effect {:?} \
1,000✔
6698
                                (spawn {} particles => {} workgroups) spawner_base={} \
1,000✔
6699
                                spawner_offset={} \
1,000✔
6700
                                property_key={:?}...",
1,000✔
6701
                            effect_batch.handle,
6702
                            spawn_count,
6703
                            workgroup_count,
6704
                            spawner_base,
6705
                            spawner_offset,
6706
                            effect_batch.property_key,
6707
                        );
6708

6709
                        compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6710
                    }
6711
                }
6712

6713
                trace!("init compute dispatched");
2,000✔
6714
            }
6715
        }
6716

6717
        // Compute indirect dispatch pass
6718
        if effects_meta.spawner_buffer.buffer().is_some()
1,014✔
6719
            && !effects_meta.spawner_buffer.is_empty()
1,014✔
6720
            && effects_meta.indirect_metadata_bind_group.is_some()
1,014✔
6721
            && effects_meta.indirect_sim_params_bind_group.is_some()
1,014✔
6722
        {
6723
            // Only start a compute pass if there's an effect; makes things clearer in
6724
            // debugger.
6725
            let mut compute_pass =
1,014✔
6726
                self.begin_compute_pass("hanabi:indirect_dispatch", pipeline_cache, render_context);
1,014✔
6727

6728
            // Dispatch indirect dispatch compute job
6729
            trace!("record commands for indirect dispatch pipeline...");
2,028✔
6730

6731
            let has_gpu_spawn_events = !event_cache.child_infos().is_empty();
1,014✔
6732
            if has_gpu_spawn_events {
1,014✔
6733
                if let Some(indirect_child_info_buffer_bind_group) =
×
6734
                    event_cache.indirect_child_info_buffer_bind_group()
×
6735
                {
6736
                    assert!(has_gpu_spawn_events);
6737
                    compute_pass.set_bind_group(3, indirect_child_info_buffer_bind_group, &[]);
×
6738
                } else {
6739
                    error!("Missing child_info_buffer@3 bind group for the vfx_indirect pass.");
×
6740
                    // render_context
6741
                    //     .command_encoder()
6742
                    //     .insert_debug_marker("ERROR:MissingIndirectBindGroup3");
6743
                    // FIXME - Bevy doesn't allow returning custom errors here...
6744
                    return Ok(());
×
6745
                }
6746
            }
6747

6748
            if compute_pass
1,014✔
6749
                .set_cached_compute_pipeline(effects_meta.active_indirect_pipeline_id)
1,014✔
6750
                .is_err()
6751
            {
6752
                // FIXME - Bevy doesn't allow returning custom errors here...
6753
                return Ok(());
×
6754
            }
6755

6756
            //error!("FIXME - effect_metadata_buffer has gaps!!!! this won't work. len() is
6757
            // the size exluding gaps!");
6758
            const WORKGROUP_SIZE: u32 = 64;
6759
            //let total_effect_count = effects_meta.effect_metadata_buffer.len();
6760
            let total_effect_count = effects_meta.spawner_buffer.len() as u32;
6761
            let workgroup_count = total_effect_count.div_ceil(WORKGROUP_SIZE);
6762

6763
            // Setup vfx_indirect pass
6764
            compute_pass.set_bind_group(
6765
                0,
6766
                effects_meta
6767
                    .indirect_sim_params_bind_group
6768
                    .as_ref()
6769
                    .unwrap(),
6770
                &[],
6771
            );
6772
            compute_pass.set_bind_group(
6773
                1,
6774
                // FIXME - got some unwrap() panic here, investigate... possibly race
6775
                // condition!
6776
                effects_meta.indirect_metadata_bind_group.as_ref().unwrap(),
6777
                &[],
6778
            );
6779
            compute_pass.set_bind_group(
6780
                2,
6781
                effects_meta.indirect_spawner_bind_group.as_ref().unwrap(),
6782
                &[],
6783
            );
6784
            compute_pass.dispatch_workgroups(workgroup_count, 1, 1);
6785
            trace!(
6786
                "indirect dispatch compute dispatched: total_effect_count={} workgroup_count={}",
1,014✔
6787
                total_effect_count,
6788
                workgroup_count
6789
            );
6790
        }
6791

6792
        // Compute update pass
6793
        {
6794
            let Some(indirect_buffer) = effects_meta.update_dispatch_indirect_buffer.buffer()
2,028✔
6795
            else {
6796
                warn!("Missing indirect buffer for update pass, cannot dispatch anything.");
×
6797
                render_context
×
6798
                    .command_encoder()
6799
                    .insert_debug_marker("ERROR:MissingUpdateIndirectBuffer");
6800
                // FIXME - Bevy doesn't allow returning custom errors here...
6801
                return Ok(());
×
6802
            };
6803

6804
            let mut compute_pass =
6805
                self.begin_compute_pass("hanabi:update", pipeline_cache, render_context);
6806

6807
            // Bind group simparams@0 is common to everything, only set once per update pass
6808
            compute_pass.set_bind_group(
6809
                0,
6810
                effects_meta
6811
                    .indirect_sim_params_bind_group
6812
                    .as_ref()
6813
                    .unwrap(),
6814
                &[],
6815
            );
6816

6817
            // Dispatch update compute jobs
6818
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6819
                // Fetch bind group particle@1
6820
                let Some(particle_bind_group) =
1,014✔
6821
                    effect_cache.particle_sim_bind_group(effect_batch.buffer_index)
1,014✔
6822
                else {
6823
                    error!(
×
6824
                        "Failed to find update particle@1 bind group for buffer index {}",
×
6825
                        effect_batch.buffer_index
6826
                    );
6827
                    continue;
×
6828
                };
6829

6830
                // Fetch bind group metadata@3
6831
                let Some(metadata_bind_group) = effect_bind_groups.update_metadata_bind_groups.get(
1,014✔
6832
                    &EffectMetadataBindGroupKey {
6833
                        buffer_index: effect_batch.buffer_index,
6834
                        base_instance: effect_batch.slice.start,
6835
                    },
6836
                ) else {
6837
                    error!(
×
6838
                        "Failed to find update metadata@3 bind group for buffer index {}",
×
6839
                        effect_batch.buffer_index
6840
                    );
6841
                    continue;
×
6842
                };
6843

6844
                // Fetch compute pipeline
6845
                if compute_pass
6846
                    .set_cached_compute_pipeline(effect_batch.init_and_update_pipeline_ids.update)
6847
                    .is_err()
6848
                {
6849
                    continue;
×
6850
                }
6851

6852
                // Compute dynamic offsets
6853
                let spawner_index = effect_batch.spawner_base;
1,014✔
6854
                let spawner_aligned_size = effects_meta.spawner_buffer.aligned_size();
1,014✔
6855
                assert!(spawner_aligned_size >= GpuSpawnerParams::min_size().get() as usize);
1,014✔
6856
                let spawner_offset = spawner_index * spawner_aligned_size as u32;
1,014✔
6857
                let property_offset = effect_batch.property_offset;
1,014✔
6858

6859
                trace!(
1,014✔
6860
                    "record commands for update pipeline of effect {:?} spawner_base={}",
1,014✔
6861
                    effect_batch.handle,
6862
                    spawner_index,
6863
                );
6864

6865
                // Setup update pass
6866
                compute_pass.set_bind_group(1, particle_bind_group, &[]);
6867
                let offsets = if let Some(property_offset) = property_offset {
9✔
6868
                    vec![spawner_offset, property_offset]
6869
                } else {
6870
                    vec![spawner_offset]
1,005✔
6871
                };
6872
                compute_pass.set_bind_group(
6873
                    2,
6874
                    property_bind_groups
6875
                        .get(effect_batch.property_key.as_ref())
6876
                        .unwrap(),
6877
                    &offsets[..],
6878
                );
6879
                compute_pass.set_bind_group(3, &metadata_bind_group.bind_group, &[]);
6880

6881
                // Dispatch update job
6882
                let dispatch_indirect_offset = effect_batch
6883
                    .dispatch_buffer_indices
6884
                    .update_dispatch_indirect_buffer_row_index
6885
                    * 12;
6886
                trace!(
6887
                    "dispatch_workgroups_indirect: buffer={:?} offset={}B",
1,014✔
6888
                    indirect_buffer,
6889
                    dispatch_indirect_offset,
6890
                );
6891
                compute_pass
6892
                    .dispatch_workgroups_indirect(indirect_buffer, dispatch_indirect_offset as u64);
6893

6894
                trace!("update compute dispatched");
1,014✔
6895
            }
6896
        }
6897

6898
        // Compute sort fill dispatch pass - Fill the indirect dispatch structs for any
6899
        // batch of particles which needs sorting, based on the actual number of alive
6900
        // particles in the batch after their update in the compute update pass. Since
6901
        // particles may die during update, this may be different from the number of
6902
        // particles updated.
6903
        if let Some(queue_index) = sorted_effect_batches.dispatch_queue_index.as_ref() {
1,014✔
6904
            gpu_buffer_operations.dispatch(
6905
                *queue_index,
6906
                render_context,
6907
                utils_pipeline,
6908
                Some("hanabi:sort_fill_dispatch"),
6909
            );
6910
        }
6911

6912
        // Compute sort pass
6913
        {
6914
            let mut compute_pass =
6915
                self.begin_compute_pass("hanabi:sort", pipeline_cache, render_context);
6916

6917
            let effect_metadata_buffer = effects_meta.effect_metadata_buffer.buffer().unwrap();
6918
            let indirect_buffer = sort_bind_groups.indirect_buffer().unwrap();
6919

6920
            // Loop on batches and find those which need sorting
6921
            for effect_batch in sorted_effect_batches.iter() {
1,014✔
6922
                trace!("Processing effect batch for sorting...");
2,028✔
6923
                if !effect_batch.layout_flags.contains(LayoutFlags::RIBBONS) {
1,014✔
6924
                    continue;
1,014✔
6925
                }
6926
                assert!(effect_batch.particle_layout.contains(Attribute::RIBBON_ID));
×
6927
                assert!(effect_batch.particle_layout.contains(Attribute::AGE)); // or is that optional?
×
6928

6929
                let Some(effect_buffer) = effect_cache.get_buffer(effect_batch.buffer_index) else {
×
6930
                    warn!("Missing sort-fill effect buffer.");
×
6931
                    continue;
×
6932
                };
6933

6934
                let indirect_dispatch_index = *effect_batch
6935
                    .sort_fill_indirect_dispatch_index
6936
                    .as_ref()
6937
                    .unwrap();
6938
                let indirect_offset =
6939
                    sort_bind_groups.get_indirect_dispatch_byte_offset(indirect_dispatch_index);
6940

6941
                // Fill the sort buffer with the key-value pairs to sort
6942
                {
6943
                    compute_pass.push_debug_group("hanabi:sort_fill");
6944

6945
                    // Fetch compute pipeline
6946
                    let Some(pipeline_id) =
×
6947
                        sort_bind_groups.get_sort_fill_pipeline_id(&effect_batch.particle_layout)
6948
                    else {
6949
                        warn!("Missing sort-fill pipeline.");
×
6950
                        continue;
×
6951
                    };
6952
                    if compute_pass
6953
                        .set_cached_compute_pipeline(pipeline_id)
6954
                        .is_err()
6955
                    {
6956
                        compute_pass.pop_debug_group();
×
6957
                        // FIXME - Bevy doesn't allow returning custom errors here...
6958
                        return Ok(());
×
6959
                    }
6960

6961
                    // Bind group sort_fill@0
6962
                    let particle_buffer = effect_buffer.particle_buffer();
×
6963
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
6964
                    let Some(bind_group) = sort_bind_groups.sort_fill_bind_group(
×
6965
                        particle_buffer.id(),
6966
                        indirect_index_buffer.id(),
6967
                        effect_metadata_buffer.id(),
6968
                    ) else {
6969
                        warn!("Missing sort-fill bind group.");
×
6970
                        continue;
×
6971
                    };
6972
                    let effect_metadata_offset = effects_meta.gpu_limits.effect_metadata_offset(
6973
                        effect_batch
6974
                            .dispatch_buffer_indices
6975
                            .effect_metadata_buffer_table_id
6976
                            .0,
6977
                    ) as u32;
6978
                    compute_pass.set_bind_group(0, bind_group, &[effect_metadata_offset]);
6979

6980
                    compute_pass
6981
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
6982
                    trace!("Dispatched sort-fill with indirect offset +{indirect_offset}");
×
6983

6984
                    compute_pass.pop_debug_group();
6985
                }
6986

6987
                // Do the actual sort
6988
                {
6989
                    compute_pass.push_debug_group("hanabi:sort");
6990

6991
                    if compute_pass
6992
                        .set_cached_compute_pipeline(sort_bind_groups.sort_pipeline_id())
6993
                        .is_err()
6994
                    {
6995
                        compute_pass.pop_debug_group();
×
6996
                        // FIXME - Bevy doesn't allow returning custom errors here...
6997
                        return Ok(());
×
6998
                    }
6999

7000
                    compute_pass.set_bind_group(0, sort_bind_groups.sort_bind_group(), &[]);
×
7001
                    compute_pass
×
7002
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
×
7003
                    trace!("Dispatched sort with indirect offset +{indirect_offset}");
×
7004

7005
                    compute_pass.pop_debug_group();
7006
                }
7007

7008
                // Copy the sorted particle indices back into the indirect index buffer, where
7009
                // the render pass will read them.
7010
                {
7011
                    compute_pass.push_debug_group("hanabi:copy_sorted_indices");
7012

7013
                    // Fetch compute pipeline
7014
                    let pipeline_id = sort_bind_groups.get_sort_copy_pipeline_id();
7015
                    if compute_pass
7016
                        .set_cached_compute_pipeline(pipeline_id)
7017
                        .is_err()
7018
                    {
7019
                        compute_pass.pop_debug_group();
×
7020
                        // FIXME - Bevy doesn't allow returning custom errors here...
7021
                        return Ok(());
×
7022
                    }
7023

7024
                    // Bind group sort_copy@0
7025
                    let indirect_index_buffer = effect_buffer.indirect_index_buffer();
×
7026
                    let Some(bind_group) = sort_bind_groups.sort_copy_bind_group(
×
7027
                        indirect_index_buffer.id(),
7028
                        effect_metadata_buffer.id(),
7029
                    ) else {
7030
                        warn!("Missing sort-copy bind group.");
×
7031
                        continue;
×
7032
                    };
7033
                    let effect_metadata_offset =
7034
                        effects_meta.effect_metadata_buffer.dynamic_offset(
7035
                            effect_batch
7036
                                .dispatch_buffer_indices
7037
                                .effect_metadata_buffer_table_id,
7038
                        );
7039
                    compute_pass.set_bind_group(0, bind_group, &[effect_metadata_offset]);
7040

7041
                    compute_pass
7042
                        .dispatch_workgroups_indirect(indirect_buffer, indirect_offset as u64);
7043
                    trace!("Dispatched sort-copy with indirect offset +{indirect_offset}");
×
7044

7045
                    compute_pass.pop_debug_group();
7046
                }
7047
            }
7048
        }
7049

7050
        Ok(())
1,014✔
7051
    }
7052
}
7053

7054
impl From<LayoutFlags> for ParticleRenderAlphaMaskPipelineKey {
7055
    fn from(layout_flags: LayoutFlags) -> Self {
3,042✔
7056
        if layout_flags.contains(LayoutFlags::USE_ALPHA_MASK) {
3,042✔
7057
            ParticleRenderAlphaMaskPipelineKey::AlphaMask
×
7058
        } else if layout_flags.contains(LayoutFlags::OPAQUE) {
3,042✔
7059
            ParticleRenderAlphaMaskPipelineKey::Opaque
×
7060
        } else {
7061
            ParticleRenderAlphaMaskPipelineKey::Blend
3,042✔
7062
        }
7063
    }
7064
}
7065

7066
#[cfg(test)]
7067
mod tests {
7068
    use super::*;
7069

7070
    #[test]
7071
    fn layout_flags() {
7072
        let flags = LayoutFlags::default();
7073
        assert_eq!(flags, LayoutFlags::NONE);
7074
    }
7075

7076
    #[cfg(feature = "gpu_tests")]
7077
    #[test]
7078
    fn gpu_limits() {
7079
        use crate::test_utils::MockRenderer;
7080

7081
        let renderer = MockRenderer::new();
7082
        let device = renderer.device();
7083
        let limits = GpuLimits::from_device(&device);
7084

7085
        // assert!(limits.storage_buffer_align().get() >= 1);
7086
        assert!(limits.effect_metadata_offset(256) >= 256 * GpuEffectMetadata::min_size().get());
7087
    }
7088

7089
    #[cfg(feature = "gpu_tests")]
7090
    #[test]
7091
    fn gpu_ops_ifda() {
7092
        use crate::test_utils::MockRenderer;
7093

7094
        let renderer = MockRenderer::new();
7095
        let device = renderer.device();
7096
        let render_queue = renderer.queue();
7097

7098
        let mut world = World::new();
7099
        world.insert_resource(device.clone());
7100
        let mut buffer_ops = GpuBufferOperations::from_world(&mut world);
7101

7102
        let src_buffer = device.create_buffer(&BufferDescriptor {
7103
            label: None,
7104
            size: 256,
7105
            usage: BufferUsages::STORAGE,
7106
            mapped_at_creation: false,
7107
        });
7108
        let dst_buffer = device.create_buffer(&BufferDescriptor {
7109
            label: None,
7110
            size: 256,
7111
            usage: BufferUsages::STORAGE,
7112
            mapped_at_creation: false,
7113
        });
7114

7115
        // Two consecutive ops can be merged. This includes having contiguous slices
7116
        // both in source and destination.
7117
        buffer_ops.begin_frame();
7118
        {
7119
            let mut q = InitFillDispatchQueue::default();
7120
            q.enqueue(0, 0);
7121
            assert_eq!(q.queue.len(), 1);
7122
            q.enqueue(1, 1);
7123
            // Ops are not batched yet
7124
            assert_eq!(q.queue.len(), 2);
7125
            // On submit, the ops get batched together
7126
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7127
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7128
        }
7129
        buffer_ops.end_frame(&device, &render_queue);
7130

7131
        // Even if out of order, the init fill dispatch ops are batchable. Here the
7132
        // offsets are enqueued inverted.
7133
        buffer_ops.begin_frame();
7134
        {
7135
            let mut q = InitFillDispatchQueue::default();
7136
            q.enqueue(1, 1);
7137
            assert_eq!(q.queue.len(), 1);
7138
            q.enqueue(0, 0);
7139
            // Ops are not batched yet
7140
            assert_eq!(q.queue.len(), 2);
7141
            // On submit, the ops get batched together
7142
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7143
            assert_eq!(buffer_ops.args_buffer.len(), 1);
7144
        }
7145
        buffer_ops.end_frame(&device, &render_queue);
7146

7147
        // However, both the source and destination need to be contiguous at the same
7148
        // time. Here they are mixed so we can't batch.
7149
        buffer_ops.begin_frame();
7150
        {
7151
            let mut q = InitFillDispatchQueue::default();
7152
            q.enqueue(0, 1);
7153
            assert_eq!(q.queue.len(), 1);
7154
            q.enqueue(1, 0);
7155
            // Ops are not batched yet
7156
            assert_eq!(q.queue.len(), 2);
7157
            // On submit, the ops cannot get batched together
7158
            q.submit(&src_buffer, &dst_buffer, &mut buffer_ops);
7159
            assert_eq!(buffer_ops.args_buffer.len(), 2);
7160
        }
7161
        buffer_ops.end_frame(&device, &render_queue);
7162
    }
7163
}
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